You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 6981abf
Browse filesBrowse the repository at this point in the historyBrowse files
authored
fix(cli)!: the JSX page gate reads the project's sdui.manifest.json beside the config, not in the invoker's cwd (#20675)
Fixes#20166
Clause-②: no
## What this changes
`os validate`, `os build` (`compile`) and `os lint` arm the JSX page
gate with an SDUI component manifest: the project's own
`sdui.manifest.json` first, then the copy `@objectstack/console` ships.
The project leg was read from `process.cwd()`, while every other
project-relative lookup of the same run reads the directory of the
config the command was given (the capability preflight's `projectDir:
dirname(absolutePath)`, the access-matrix snapshot beside the config).
So `os validate path/to/app/objectstack.config.ts`, run from anywhere
else, never read `path/to/app/sdui.manifest.json`, and a manifest in the
invoker's directory judged a project it does not belong to.
- `resolveJsxGateManifest(stack, projectDir, resolution?)`
(`packages/cli/src/utils/sdui-manifest.ts`) now takes the project
directory as a **required** argument, and its default resolution is
`resolveSduiManifest(projectDir)`. There is no working-directory default
left for a caller to fall into.
- The three callers (`validate.ts`, `compile.ts`, `lint.ts`) hand it
`dirname()` of the config path `loadConfig` resolved: triage's execution
note 1, for all three commands.
- `resolveSduiManifest(cwd = process.cwd(), consoleOrigin?)` keeps its
signature and its working-directory default, as the order requires.
#20542 relies on it, and `os init`'s scaffold check
(`scaffold-validate.ts`, unchanged) reads the invoker's directory by its
own recorded decision. Its docblock now says that the parameter is the
project directory whenever a command judges a project.
- Changeset: `@objectstack/cli` **minor**, `Clause-②: no (narrowing)`,
BREAKING, ADR-0087 `not-required (no-migration-prescription)`. See
*Direction* below and the deviation note.
## Pins
- **Per-PR (unit), `src/utils/sdui-manifest.test.ts`.** The invoker's
directory is played by a `process.cwd()` spy on a foreign directory that
carries its own manifest. Lit control: the working-directory default
reads that manifest. `resolveJsxGateManifest(stack, projectDir)` reads
the manifest beside the config instead. The control is a spy on the
project directory itself, which gives the same answer. A manifest-less
project does not borrow the foreign manifest, and a malformed foreign
manifest refuses nothing. A seam pin checks that each of the three
command files calls `resolveJsxGateManifest` once, with
`dirname(absolutePath)` taken from `loadConfig`. 8 new cases, and the
existing calls now pass a project directory.
- **Command level (nightly `.e2e`),
`test/jsx-gate-manifest-notice.e2e.test.ts`.** This is triage note 3.
Each of `validate` / `build` / `lint` is given an explicit config path
from three places: the config's own directory (the control), a foreign
directory carrying its own manifest that refuses the page, and a bare
directory. A relative-path spelling is added for `validate`. A lit
control shows the foreign manifest really refuses the page when it is
the project's own. 11 new cases.
## Measurements (PM hypotheses)
All runs go through `bin/run-dev.js` (the source entry), not
`dist/index.js`. The fixture's project manifest declares `div`. The
foreign directory's manifest declares `span` only, and the page is a div
tag.
**H0: confirmed on `main` `6bff748bbd`.** From the project directory
(default config and explicit path), all three commands exit 0. From a
foreign directory carrying its own manifest, `validate`, `build` and
`lint` each exit 1 with `jsx-forbidden-tag` + `jsx-unknown-component`:
the foreign manifest judged the project. From a bare directory, all
three exit 0 with the parse-level notice naming
`BARE/sdui.manifest.json` then
`packages/console/dist/sdui.manifest.json`. The project's own manifest
is never named.
**H1: confirmed on `8a85dbb583`.** The same 12 runs all exit 0, with no
`jsx-*` finding and no notice. The same-directory controls are
unchanged, and the foreign directory's manifest does not win.
**H3: confirmed.** The ablation is reported under *Tests* below.
**H2: both directions, measured.** See the table in *Acceptance notes*:
runs are newly refused **and** newly admitted, but only runs whose
config path names a directory other than the one they run in.
## Tests
The union of gates was run after the final commit, at `ff4d8e7c37`:
- `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands`, run with no paths, derived **63** commands. All 63 exit 0.
`--ran` reconciliation: `63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN (a
DERIVED zero — all 63 recorded an exit code and none of them is 3)`. On
the first run, `pnpm check:dual-build-cjs-loads` answered `PREREQUISITE
NOT MET` (8 packages outside the CLI closure had no `dist/`). After
those 8 were built (turbo, 41/41 cached), it exits 0.
- Artifact-roster rows that could apply were run too:
`check-changeset-fixed`, `check:authz-resolver`,
`check:error-code-casing`, `check:filter-alias-parity`,
`release-pending-publish --self-test`, `check-sdui-manifest`,
`check:cli-examples-parity`, `check:scaffold-emission-policy`,
`check-published-list-mirrors`, `check:stack-collection-maps` and
`check:console-injection` all exit 0. `check-closing-target-claim`,
`check-partof-closing-keyword` and `check-single-claim-paths` need this
PR's context. NOT MEASURED locally, reason: they exit 2 `NOT WIRED`
without a PR number or body. Their workflows run them on this PR.
- `pnpm lint` (`eslint . --no-inline-config`, the whole repository)
exits 0 at `ff4d8e7c37`.
- `node scripts/check-issue-citations.mjs --base origin/main`, after
merging `origin/main` (`6c11ef9ecb`): 5 citations judged, 5 resolve.
- `pnpm --filter @objectstack/cli typecheck` exits 0. `tsc --noEmit
--listFiles` includes `src/utils/sdui-manifest.test.ts`, and
`check:test-typecheck` over `tsconfig.test.json` lists both touched
`test/` files.
- `pnpm --filter @objectstack/cli exec vitest run --project unit
--maxWorkers=2`: 232 files passed, 3350 tests passed, 5 failed. All 5
are in `test/published-subpath-console.pin.test.ts` and
`test/published-subpath-hook-body.pin.test.ts`, and they are a host
artefact. This Mac's `TMPDIR` is a symlink (`/var` to `/private/var`).
Rerun with `TMPDIR` set to its real path, both files pass (29/29).
Neither file, nor anything it reads, is in this diff.
- `--project integration`, `OS_TEST_TIERS=nightly`,
`test/jsx-gate-manifest-notice.e2e.test.ts`: 43/43 passed, nothing
skipped, because the console copy is absent in this checkout. The diff
touches that file, so it ran locally.
- **H3 ablation.** The three callers were reverted to the cwd default,
which is `main`'s exact call text; with `projectDir` undefined, the
default parameter reads `process.cwd()`. The mutation went through
`scripts/ablation-replace.mjs`: each anchor went x1 to x0, and the blobs
changed (`validate.ts` 5c311e8 to a95825d1ee2c, `compile.ts`
0dd6cd8 to 74f020b8dd81, `lint.ts` 56422ee to 3769869f163b).
On disk, per file, the `dirname(absolutePath)` call count is 0 and the
bare-call count is 1. Results:
- Unit: `3 failed | 45 passed`. The 3 failures are exactly the seam
pins.
- E2E: `7 failed | 36 passed`. The 7 failures are exactly the
foreign-cwd, bare-cwd and relative-path pins. The lit control and the
three same-directory controls stay green.
- Restore: a trap on EXIT/INT/TERM ran `git checkout HEAD --` on
absolute paths. Proof: each file's blob equals its HEAD blob, and `git
diff HEAD` is empty.
- Nothing here resolves through `dist/`: the unit file imports
`./sdui-manifest.js` from `src/`, and the e2e spawns `bin/run-dev.js`
through tsx over `src/`.
**Declared narrowing — verification ran UNLOCKED.**
`scripts/pm/os-verify-lock.sh`
could not take the shared verify lock on this host: no usable `flock`.
The shared
verify lock is declared Linux-only (`flock` is util-linux, and a stock
macOS does
not ship it), so the command below was run directly, without the lock —
a declared narrowing, not a silent one. No serialization guarantee held
for this
run, nor for any sibling agent in this container while it ran.
pnpm turbo run build --filter='@objectstack/cli^...' --concurrency=2
pnpm turbo run build --filter='@objectstack/example-showcase^...'
--concurrency=2
pnpm --filter @objectstack/cli exec vitest run --project unit
--maxWorkers=2 src/utils/sdui-manifest.test.ts
test/validate-build-gate-parity.test.ts
OS_TEST_TIERS=nightly pnpm --filter @objectstack/cli exec vitest run
--project integration --maxWorkers=2 --reporter=verbose
test/jsx-gate-manifest-notice.e2e.test.ts
bash ablate.sh (the H3 ablation above: ablation-replace x3, both test
files, the direction matrix, restore)
pnpm turbo run build --filter=(8 packages named by
check:dual-build-cjs-loads) --concurrency=2
pnpm --filter @objectstack/cli typecheck
pnpm --filter @objectstack/cli exec vitest run --project unit
--maxWorkers=2
pnpm lint (twice: at 7c21afd and at ff4d8e7)
## Acceptance notes
**Triage note 2: the repo-root showcase invocation, both ways.** The
command is `os CMD examples/app-showcase/objectstack.config.ts --json`,
run from the repository root. Before = `main` `6bff748bbd`, after =
`8a85dbb583`. The repository tracks a root `sdui.manifest.json` (sha256
`d0666ac5…`), and the showcase carries none. The console copy was
measured in both states. When built, the copy is placed by
`scripts/build-console.sh`'s own `cp` of the root file, byte-identical
(same sha256). When not built, the copy is absent, as in CI and in this
worktree.
| run | manifest read | validate | build | lint |
|:--|:--|:--|:--|:--|
| before, console copy absent | repo-root `sdui.manifest.json` (project
leg, from cwd) | exit 0 · 0 errors · 83 warnings · 0 `jsx-*` · no notice
| same as validate | exit 0 · 511 issues (0 errors, 484 warnings, 27
suggestions) · 0 `jsx-*` |
| before, console copy built | repo-root file (project leg) | exit 0 ·
83 warnings · 0 `jsx-*` | same | exit 0 · 511 issues (0 / 484 / 27) |
| **after, console copy built** | console copy (same bytes) | exit 0 ·
83 warnings · 0 `jsx-*` · no notice | same | exit 0 · 511 issues (0 /
484 / 27) |
| **after, console copy absent** | none: parse level | exit 0 · 84
warnings (the +1 is the `sdui/jsx-parse-level-only` notice: 3 html
pages) · 0 `jsx-*` | same | exit 0 · 512 issues (0 / 484 / 28, the +1 is
the notice) |
In each state, the "after" reading equals what a same-directory run
(from `examples/app-showcase`) already gives on `main`. No exit code
moves. The 200-error arming measured on #19922 is gone from `main`: the
regenerated root manifest now declares the html-tier vocabulary. So
#20112's reading no longer depends on which directory this run starts
in.
**H2: which runs move, and which way.** These runs use `validate` with
an explicit absolute config path. N1 also ran `build` and `lint`, which
gave the same answers. "Before" is the H3 ablation, i.e. `main`'s
resolution path. "After" is `8a85dbb583`.
| class (config path names another directory) | console copy | before |
after | direction |
|:--|:--|:--|:--|:--|
| N1: the project's manifest refuses the page, run from a bare directory
| absent | exit 0 + notice | exit 1, `jsx-forbidden-tag` | **newly
refused** |
| N1 | built | exit 1 (the console copy refuses a div too) | exit 1 |
unchanged in this fixture; a tag the console declares but the project's
manifest does not is newly refused |
| N2: the project's manifest is malformed, run from a bare directory |
absent | exit 0 + notice | exit 1, the unusable-manifest refusal naming
the project's file | **newly refused** |
| N3: no project manifest, run from a directory whose manifest admits
the page | built | exit 0 | exit 1 (judged by the console copy) |
**newly refused** |
| W1: no project manifest, run from a directory whose manifest refuses
the page | absent | exit 1 | exit 0 + notice | **newly admitted** |
| W2: no project manifest, run from a directory whose manifest is
malformed | absent | exit 1 (refusal naming the other directory's file)
| exit 0 + notice | **newly admitted** |
| H1: the project's manifest admits the page, run from a directory whose
manifest refuses it | either (the project leg is read first) | exit 1 |
exit 0 | **newly admitted** |
| U: neither directory carries a manifest | either | unchanged |
unchanged | only the notice's first path moves, from cwd to the project
directory |
| any run from the project's own directory | either | unchanged |
unchanged | control |
A run is newly refused, so the changeset is `minor`, `Clause-②: no
(narrowing)`, BREAKING. Runs are also newly admitted: in per-run terms
that is a widening, and it is reported for the seat to judge. This body
carries the claim's bare `Clause-②: no` line, as ordered. The arm is in
the changeset, the way PR #20589 carried it.
**Deviation: no `## FROM → TO` heading in the changeset.** The order
asked for a FROM/TO. `check-adr-0087-registration` reads a `FROM → TO`
label as a rewrite prescription (`from-to-label`), and that refuses the
honest disposition, `not-required (no-migration-prescription)`: no
metadata changes shape, and `objectstack migrate meta` has nothing to
rewrite. The only other disposition open is `registered`, which needs a
`packages/spec` ledger entry, the spec seat's surface, and no ledger
entry can move a file. So the before/now mapping ships as a table under
`## Which manifest each run reads`. With that heading the gate reads no
prescription and passes.
**Notes, not filed:**
- The pending `.changeset/19922-console-manifest-fallback.md` says these
commands "look first for the `sdui.manifest.json` in the directory the
command runs in". This PR makes that sentence false before it ships.
That file is outside this claim's file surface, so this PR's changeset
carries a correction paragraph. A one-line edit to the 19922 entry would
make the release notes read cleanly.
- `resolveSduiManifest`'s first parameter is still named `cwd`, although
every command caller now passes the project directory. The rename was
outside the claim's surface (docblocks and pass-through only). The
docblock now says so.
- Comments describe the repo-root artefact as what
`resolveSduiManifest()` picks up "from the repo root":
`packages/lint/src/validate-jsx-pages.production-witness.test.ts` and
`scripts/cross-package-test-inputs.mjs` (the `#12924` entry). For a
repo-root run against an example's config, the artefact now arrives
through the console copy (`build-console.sh` copies the same bytes), so
the witness still witnesses production bytes, by the console leg. Stale
comments; carrier: none.
- The `published-subpath-*.pin.test.ts` failures on a macOS host with a
symlinked `TMPDIR` (above) are a portability observation; carrier: none.
---
_Generated by [Claude
Code](https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289)_
---------
Co-authored-by: Jack Zhuang <50353452+hotlong@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
fix(cli)!: `objectstack validate`, `objectstack build` and `objectstack lint` read the project's `sdui.manifest.json` beside the config they were given, not in the directory they were run from (#20166)
6
+
7
+
Clause-②: no (narrowing)
8
+
9
+
<!-- adr-0087: not-required (no-migration-prescription) No metadata changes shape and nothing an author wrote is renamed or removed, so `objectstack migrate meta` has nothing to rewrite. What moves is which manifest file judges a run whose config path names another directory. -->
10
+
11
+
**BREAKING for runs given a config path in another directory.**
12
+
13
+
**What changed.** These commands check the `source` of each `kind: 'html'` page
14
+
against an SDUI component manifest: the project's own `sdui.manifest.json` first,
15
+
then the copy `@objectstack/console` ships. They located everything else about a
16
+
project from the directory of its config, but looked for the project's own
17
+
manifest in the directory the command was run from. So
18
+
`objectstack validate path/to/app/objectstack.config.ts`, run from anywhere else,
19
+
never read `path/to/app/sdui.manifest.json`, and a manifest that happened to sit in
20
+
the directory it was run from judged a project it does not belong to. They now read
21
+
the manifest beside the config.
22
+
23
+
## Which manifest each run reads
24
+
25
+
| the run | the project manifest it read | the project manifest it reads now |
26
+
|:--|:--|:--|
27
+
|`objectstack validate` / `build` / `lint` with no config path, in the project's directory |`./sdui.manifest.json`|`./sdui.manifest.json` (unchanged) |
28
+
| the same commands given `path/to/app/objectstack.config.ts`, run from another directory | that other directory's `sdui.manifest.json`|`path/to/app/sdui.manifest.json`|
29
+
30
+
When the project carries no manifest of its own, both rows then fall back to the
31
+
copy `@objectstack/console` ships, as before.
32
+
33
+
**Which runs change, and which way.** Only runs whose config path names a directory
34
+
other than the one they run in. For those, the verdict can move in both directions:
35
+
36
+
- A page the project's own manifest does not declare is now refused
37
+
(`jsx-forbidden-tag`, `jsx-unknown-component`, `jsx-unknown-prop`, exit 1), where
38
+
the other directory's manifest, or the console's copy, used to admit it.
39
+
- A project manifest that is present but not usable is now refused (exit 1), naming
40
+
that file, where the run used to read some other file.
41
+
- A project with no manifest of its own is now checked against the console's copy,
42
+
where the other directory's manifest used to decide.
43
+
- In the other direction, a page the other directory's manifest refused, and that
44
+
the project's own manifest (or the console's copy) declares, is now admitted.
45
+
46
+
If such a run now fails, the manifest that belongs to the project is the one to
47
+
keep beside its config.
48
+
49
+
**What is not affected.** A run in the project's own directory, with or without a
50
+
config path, reads the same file as before. `objectstack init`'s check of a freshly
51
+
generated scaffold keeps reading the directory it was run from.
52
+
53
+
**A correction to this release's console-fallback entry.** That entry says these
54
+
commands "look first for the `sdui.manifest.json` in the directory the command runs
55
+
in". From this release they look first beside the config the command was given,
56
+
which is the same directory whenever the command runs in the project.
0 commit comments