Repository navigation
Commit b43a814
Fixes #20507
Clause-②: no
## What this changes
The layered view, `GET /meta/:type/:name/layers` and the deprecated
`?layers=true` flag, answered a name with nothing behind it `200` with
`code`, `overlay` and `effective` all `null`. It answered a member's
read of an unpublished app `404 RESOURCE_NOT_FOUND`. The difference told
a member which unpublished apps exist. ADR-0045 §3 says "Hidden" means
externally unobservable, consistently across every surface, and the
plain read already kept that promise.
`createMetaLayeredAnswer` in `packages/rest/src/meta-item-read-gate.ts`
is the one chain both transports call after the store read
(`RestServer`, and the runtime dispatcher's `/meta` domain). It now
answers a layered read with no layer present as `absent`, before the
per-caller gate runs. That is the same place the plain read judges
absence (`createMetaItemAnswer` step 1). Both transports already write
`absent` in their own absence envelope, the one they use for an
unpublished app. So there is one change, and no transport edit:
`rest-server.ts` and `runtime/src/domains/meta.ts` are untouched.
## The four measurements the dispatch asked for
**H0, the reader: nothing depends on the 200.** Measured at objectui's
pinned `.objectui-sha` `dd3f7e1be3561d63267d7162f3fc0ac52e72834d`, by
`git grep` over the tree at that commit.
- Every wire read of the layered view goes through one method:
`MetadataClient.layered`
(`packages/data-objectstack/src/metadata-client.ts`, lines 1289 to
1304). It reads `/layers`. On `404` it already returns `{ code: null,
overlay: null, overlayScope: null, effective: null }`, the same four
null layers the old `200` carried.
- There is no `?layers=` spelling in objectui's non-test source. As a
control, the same pattern hits the client's own `/layers` URL at line
1301.
- The client has 15 non-test call sites. All are Studio or
metadata-admin authoring screens, used by callers who hold an authoring
capability:
- `ResourceEditPage.tsx`: 1017, 1510, 1649, 1689 and 1723. This is the
metadata edit page. Its load effect skips the layered read in create
mode.
- `StudioDesignSurface.tsx`: 1820 (app navigation), 1949 (leaf
designer), 2962 (object designer) and 4015 (flow designer).
- `EmbeddedItemEditor.tsx`: 110, the parent re-read before a save.
- `PermissionMatrixEditor.tsx`: 512, 841 and 885 (the Access pillar).
- `PackageOwdOverviewPanel.tsx`: 155 and 249.
- Each site reads `effective ?? code` and takes the draft through
`getDraft`. It reads the protection verdicts defensively: `editable !==
false`, `deletable !== false`, `lock && lock !== 'none'`, and
`resettable ?? isArtifactItem`. So the four-null result from a `404`
renders exactly as the old `200` did. `PermissionMatrixEditor.tsx` line
834 already relies on it in words: a record the server does not hold
"answers the 404 shape".
- No member-facing caller exists.
- The SDK (`packages/client/src`, `packages/client-react/src`) has zero
layered reads. As a control, `packages/client/src/index.ts` has 26
`/meta` hits. The route ledger records this route as `server-only`,
consumed by objectui over plain HTTP.
**H1, re-measured on `main` before the fix** (`fb194c70e5`). The new
pins ran red against the unfixed chain. As a member, on both spellings
and both transports, `no_such_app` answered `200` with every layer
`null` (plus `lock`, `editable`, `deletable` and `resettable`).
`launchpad`, the unpublished app, answered `404 RESOURCE_NOT_FOUND`. The
four published-app controls stayed green. The reading: 6 failed and 4
passed.
**H2, what reaches the chain and what a builder now receives.** An
absent name reaches `createMetaLayeredAnswer` with every layer `null`.
The protocol documents that `effective` is never null while another
layer is present, so "no layer present" means the name resolves to
nothing. The chain answers `{ kind: 'refuse', refusal: { reason:
'absent' } }` before any layer is judged. `RestServer` writes it with
`sendMetaItemAbsent`: `404` `{ error: { code: "RESOURCE_NOT_FOUND",
message } }`. The dispatcher writes it as `deps.error('Not found',
404)`. These are the bytes each transport already sends for an
unpublished app.
- **A builder** (`studio.access`) now receives `404 RESOURCE_NOT_FOUND`
for an absent name, on both spellings and both transports. So do the
holder, the member and the author. This is the plain read's absence,
which does not depend on who asks.
- A builder is still served the unpublished `launchpad` (`200`).
**H3, ablation.** I deleted the new branch through
`scripts/ablation-replace.mjs` in WRAP mode. The tool recorded: anchor
count 1 to 0, blob `662d87187124` to `af0967c88051`. Under the mutation
I ran the full `@objectstack/runtime` and `@objectstack/rest` suites.
`@objectstack/rest` resolves from source in both suites: the runtime
vitest config aliases it to `../rest/src/index.ts`. So `dist` is not on
the path.
- Runtime: 6 failed, 4885 passed. The 6 are exactly the absent-name
pins: the four "absent name and unpublished app answer the same" cases,
the "whoever asks" case, and the `?package=` emptied-scope case. The
four published-app controls stayed green.
- Rest: 221 files passed.
- Restore: the tool's own verdict was that the blob after restore equals
HEAD (`662d87187124`) and `git diff HEAD` is empty. `git status
--porcelain` was empty afterwards.
## Tests
The new pins are in
`packages/runtime/src/domains/meta-list-projection-parity.test.ts`. That
file drives `RestServer` and the dispatcher over the same fixtures:
- As a member, for each of `/layers` and `?layers=true` on each
transport, an absent name and an unpublished app answer the same status,
code, body, `Vary`, `Cache-Control` and `Deprecation`. The flag's `Link`
also matches once the requested name is masked, because it names the
caller's own path.
- Control: a published app is still served to the member, with every
layer pruned.
- Every caller gets the same `404` for an absent name, and a builder is
still served the unpublished app.
Two existing fixtures pinned the old answer, and I triaged them:
- **The parity `?package=` case** asserted `200` with all-null layers
for `payroll?package=crm`, which is exactly the answer this change
removes. Its purpose, showing that `?package=` scopes the code layer, is
kept by `crm?package=elsewhere`: `200`, `code: null`, and the overlay
still answers. `payroll?package=crm` is now an absence pin.
- **The execctx census's protocol double**
(`packages/rest/src/execctx-consumer-census.test.ts`) answered
`getMetaItemLayered` with a generic object that carried no layer. Its
`/layers` row read `200` only because the chain served an all-absent
body. The double now answers the layered read in its own shape, so the
row still measures "serves on its own reading".
Results on the final head `32de5e64fc` (`origin/main` merged):
- `pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2`: 222
files and 4239 tests passed (40 skipped). This covers both projects.
- `pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2`:
289 files and 4897 tests passed (1 skipped). This covers both projects.
- `pnpm --filter @objectstack/rest --filter @objectstack/runtime
typecheck`: exit 0, and both packages print `check:test-typecheck: OK`.
- Before the tests, the dependency closure was built with `pnpm --filter
'@objectstack/runtime...' build` after the merge.
## Gates
- `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands`, derived at `32de5e64fc`: 61 families.
- 60 exited 0.
- `pnpm check:dual-build-cjs-loads` exited 3, PREREQUISITE NOT MET: 36
packages have no `dist/`. That is NOT MEASURED, and a whole-repo `pnpm
build` was not run on this shared host. As a narrowed probe of the one
package whose `dist` this diff changes,
`require('packages/rest/dist/index.cjs')` loads and exports
`createMetaLayeredAnswer` as a function. The diff changes no import,
export or module format.
- `--ran` reconciliation: "61 derived famil(ies) accounted for — 60 run,
1 NOT-MEASURED".
- `node scripts/check-issue-citations.mjs --base origin/main`, run after
the merge: every citation this change adds resolves (5 of 5).
- `pnpm lint` at `32de5e64fc`: exit 0, 35s.
Every build and test command above ran through
`scripts/pm/os-verify-lock.sh`, which printed this for each run. The
block below is from the final `@objectstack/rest` run:
**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 --filter @objectstack/rest exec vitest run --maxWorkers=2
## Acceptance notes
- **Spec text drift, for the spec seat.**
`GetMetaItemLayeredResponseSchema.effective` in
`packages/spec/src/api/protocol.zod.ts` says its value is `null` "when
the item resolves to nothing at all". Through the REST route, that case
now answers `404` instead of a body. The text still describes the
protocol method's in-process return, which is unchanged. It is outside
this card's file surface (`packages/spec/**` is read-only here), so I
have not filed or edited it. Carrier: the spec seat.
- **Consumers not run.** `packages/qa/http-conformance` and
`packages/qa/dogfood` were not run. By `git grep`, the conformance suite
makes no `/layers` or `?layers=` HTTP read. The dogfood suites read
`/layers` only for items that exist (`crm_order`, `showcase_account`).
CI runs both.
- **ADR anchor.**
`scripts/adr-anchors/packages__rest__src__meta-item-read-gate.ts.json`
already anchors ADR-0045 §3 on this file. The code comment carries the
ADR id.
---
_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>
1 parent fb38607 commit b43a814
4 files changed
Lines changed: 117 additions & 15 deletions
File tree
- .changeset
- packages
- rest/src
- runtime/src/domains
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
163 | 163 | | |
164 | 164 | | |
165 | 165 | | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
166 | 175 | | |
167 | 176 | | |
168 | 177 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2516 | 2516 | | |
2517 | 2517 | | |
2518 | 2518 | | |
2519 | | - | |
2520 | | - | |
| 2519 | + | |
| 2520 | + | |
| 2521 | + | |
| 2522 | + | |
2521 | 2523 | | |
2522 | 2524 | | |
2523 | 2525 | | |
| |||
2558 | 2560 | | |
2559 | 2561 | | |
2560 | 2562 | | |
2561 | | - | |
2562 | | - | |
2563 | | - | |
| 2563 | + | |
| 2564 | + | |
| 2565 | + | |
| 2566 | + | |
| 2567 | + | |
| 2568 | + | |
| 2569 | + | |
| 2570 | + | |
| 2571 | + | |
| 2572 | + | |
| 2573 | + | |
| 2574 | + | |
2564 | 2575 | | |
2565 | 2576 | | |
2566 | 2577 | | |
| |||
2571 | 2582 | | |
2572 | 2583 | | |
2573 | 2584 | | |
2574 | | - | |
| 2585 | + | |
2575 | 2586 | | |
2576 | 2587 | | |
2577 | 2588 | | |
2578 | 2589 | | |
2579 | 2590 | | |
2580 | 2591 | | |
2581 | | - | |
2582 | | - | |
2583 | | - | |
2584 | | - | |
| 2592 | + | |
| 2593 | + | |
2585 | 2594 | | |
2586 | 2595 | | |
2587 | 2596 | | |
| |||
2590 | 2599 | | |
2591 | 2600 | | |
2592 | 2601 | | |
| 2602 | + | |
| 2603 | + | |
| 2604 | + | |
| 2605 | + | |
| 2606 | + | |
2593 | 2607 | | |
2594 | | - | |
| 2608 | + | |
2595 | 2609 | | |
2596 | 2610 | | |
2597 | | - | |
2598 | 2611 | | |
2599 | 2612 | | |
2600 | 2613 | | |
| |||
2605 | 2618 | | |
2606 | 2619 | | |
2607 | 2620 | | |
2608 | | - | |
| 2621 | + | |
2609 | 2622 | | |
2610 | 2623 | | |
2611 | 2624 | | |
| |||
Lines changed: 69 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1022 | 1022 | | |
1023 | 1023 | | |
1024 | 1024 | | |
1025 | | - | |
1026 | | - | |
| 1025 | + | |
| 1026 | + | |
| 1027 | + | |
| 1028 | + | |
| 1029 | + | |
| 1030 | + | |
| 1031 | + | |
| 1032 | + | |
| 1033 | + | |
1027 | 1034 | | |
1028 | 1035 | | |
1029 | 1036 | | |
| |||
1058 | 1065 | | |
1059 | 1066 | | |
1060 | 1067 | | |
| 1068 | + | |
| 1069 | + | |
| 1070 | + | |
| 1071 | + | |
| 1072 | + | |
| 1073 | + | |
| 1074 | + | |
| 1075 | + | |
| 1076 | + | |
| 1077 | + | |
| 1078 | + | |
| 1079 | + | |
| 1080 | + | |
| 1081 | + | |
| 1082 | + | |
| 1083 | + | |
| 1084 | + | |
| 1085 | + | |
| 1086 | + | |
| 1087 | + | |
| 1088 | + | |
| 1089 | + | |
| 1090 | + | |
| 1091 | + | |
| 1092 | + | |
| 1093 | + | |
| 1094 | + | |
| 1095 | + | |
| 1096 | + | |
| 1097 | + | |
| 1098 | + | |
| 1099 | + | |
| 1100 | + | |
| 1101 | + | |
| 1102 | + | |
| 1103 | + | |
| 1104 | + | |
| 1105 | + | |
| 1106 | + | |
| 1107 | + | |
| 1108 | + | |
| 1109 | + | |
| 1110 | + | |
| 1111 | + | |
| 1112 | + | |
| 1113 | + | |
| 1114 | + | |
| 1115 | + | |
| 1116 | + | |
| 1117 | + | |
| 1118 | + | |
| 1119 | + | |
| 1120 | + | |
| 1121 | + | |
| 1122 | + | |
| 1123 | + | |
| 1124 | + | |
| 1125 | + | |
| 1126 | + | |
| 1127 | + | |
1061 | 1128 | | |
1062 | 1129 | | |
1063 | 1130 | | |
| |||
0 commit comments