Repository navigation
Commit 252a582
Fixes #23073
Clause-②: yes (widening: a new declared response key)
Completes ruling 6116165299 (letter A) on #22877 for the two list doors:
each bundle member is addressable by name, and now each list entry says
which member it is. Stage 2 (PR #23070, `038d4c958`) gave the by-name
`/meta` doors the `member` selector; the lists still answered two
languages of one `email_template` as two entries with the same `type`
and `name`, so a reader could not tell them apart or address the second
one.
## What changed
### Contract (`@objectstack/spec`, `domain:spec`)
- `ListDraftsResponseSchema` (`GET /meta/_drafts`): each draft entry
gains `member: z.string().optional()`.
- `ListPackageCommitsResponseSchema` (`GET /packages/:id/commits`): each
commit item gains `member: z.string().optional()`.
- Both describes say the same thing: the bundle member the entry is, for
a type whose identity is a pair (today only `email_template`, `(name,
locale)`); its discriminator value (`zh-CN`), with the canonical member
spelled out (`en-US`); present on every entry of such a type; absent for
a type whose identity is its name alone; it is the by-name doors'
`member` selector (`?member=`) for that entry.
- Regenerated: `content/docs/references/api/protocol.mdx` and
`content/docs/references/api/package-lifecycle.mdx` (`check:generated`
proved only `check:docs` stale; `--fix` regenerated that one and
re-checked it green). No `authorable-surface`, `api-surface` or
`json-schema` artifact moved: these are response shapes, and no export
was added.
### Serving (`@objectstack/metadata-protocol`, `domain:engine`,
cross-domain exception per triage 6129686435)
- `listDrafts`: serves the repository header's stored `member`
(`SysMetadataRepository.listDrafts`, the `bundle_member` column through
`storedRowMemberHeader`), which it used to strip.
- `listCommits`: serves the stored revert-plan item's `member`
(`CommitPlanItem`, the member `revertCommit` acts on), which it used to
strip.
- Both go through `bundleMemberDiscriminator(type, storedMember)`, the
stage-2 function every by-name door already uses to turn a selector into
a discriminator value. It is keyed on `ITEM_KEY_DISCRIMINATORS`, the one
declaration of which types are discriminated. There is no second list of
types, and nothing is read off the body.
- Stored, the canonical member is NULL (the column) or an absent key
(the plan). This is the ruling's "NULL means the canonical member". The
list spells it out as `en-US` rather than omitting it. The reasons:
- Key presence then answers "is this type discriminated" for every
entry: triage's direction is "carry `member` for a discriminated type,
and nothing for a name-only type".
- Two languages read as two distinct values, not one value beside an
absence.
- `?member=en-US` is the same request as no selector (stage 2's
`resolveBundleMemberSelector`), so the spelled-out value round-trips
through every by-name door.
- Only these two serving regions of `protocol.ts` changed, plus one
comment-only docblock (`CommitPlanItem`, near L2093) whose sentence "the
commit timeline serves the declared four keys only" this PR makes false.
The three open PRs editing `protocol.ts` (#23085 landed since; #23075,
#22900) are hunk-disjoint from these.
## Premises measured on `origin/main` (`a22e67b3c`)
- The repository draft header carries `member` when it is non-canonical
(`storedRowMemberHeader`, `sys-metadata-repository.ts`), and the
protocol stripped it: `drafts.map(({ member: _member, ...header }) =>
header)`. **Holds.**
- The stored plan item carries `member` when it is non-canonical
(`CommitPlanItem`), and `listCommits` stripped it the same way.
**Holds.**
- Re-declarations of either list shape, by grep over `packages/**/src`
(excluding `dist`):
- `@objectstack/client`: `meta.listDrafts` and `packages.listCommits`
return `ListDraftsResponse` / `ListPackageCommitsResponse` imported from
`@objectstack/spec/api`. This is derived, not a re-declaration, so it
widens with the schema. Its `return-type-precision.test.ts` pins
`toEqualTypeOf` against those same imported types.
- `@objectstack/rest`: `GET /meta/_drafts` answers `res.json(result)`, a
pass-through. The route ledger names the schema by string.
- `@objectstack/runtime`: the `/metadata/_drafts` and
`/packages/:id/commits` dispatcher arms pass the protocol answer through
(`success({ commits })`). The route ledger names the schemas by string.
- `@objectstack/cli`: zero reads of either list.
- Result: **no re-declaration exists**, so none was updated.
- The one discriminated-type predicate: stage 2's
`resolveBundleMemberSelector` and `bundleMemberDiscriminator` both read
`ITEM_KEY_DISCRIMINATORS` (`@objectstack/metadata-core`). This PR reuses
`bundleMemberDiscriminator`.
## Pins
-
`packages/objectql/src/environment-overlay-email-locale-restart.test.ts`
(stage 1/2's real-engine harness, real `ObjectQL` and protocol):
- New: `en-US` and `zh-CN` drafts of one template, bound to a package,
read from `listDrafts` as two entries `[email_template, acme.welcome,
en-US]` and `[…, zh-CN]`. Each listed `member` is handed back to the
by-name door (`getMetaItem({ state: 'draft', member })`) and reads its
own draft's wording. After `publishPackageDrafts`, `listCommits` reads
the commit's two items with the same two members, and each item's
`member` reads its own published wording through `getMetaItem`.
- Inverted: stage 2's pin that a commit item carries no `member` key now
asserts `zh-CN` on both the publish commit and its revert. The stage-2
comment "the public draft list keeps its declared header (no `member`)"
is removed, and its assertion (`drafts` empty after a discard) is kept.
-
`packages/metadata-protocol/src/sys-metadata-repository-14938-list-drafts-updated-at.test.ts`
§D: two `email_template` draft rows (`bundle_member` NULL and `zh-CN`)
whose bodies BOTH say `en-US`, so the served member is shown to come
from the stored column, not the body. They read as `en-US` / `zh-CN`. An
`object` draft's keys are exactly the six declared ones, with no
`member` key. The response parses as `ListDraftsResponseSchema` with
every member preserved.
-
`packages/metadata-protocol/src/protocol-14038-list-commits-created-at-iso.test.ts`:
a stored plan of an `email_template` item without `member`, one with
`member: 'zh-CN'`, and an `object` item serves `en-US`, `zh-CN`, and no
key. It parses as `ListPackageCommitsResponseSchema` with every member
preserved.
- `packages/spec/src/api/protocol.test.ts` and
`package-lifecycle.test.ts`: two members of one template plus a
name-only entry parse, the parse keeps `member`, and the name-only entry
has no `member` property.
## Reverse verification (ablation)
One-off, from the committed fix at `fe5c3a5e3`, through
`scripts/ablation-replace.mjs`. In each leg the anchor hit exactly once
and the blob changed. Each restore was proven by `blob == HEAD` and an
empty `git diff HEAD`, and the final tree diff was 0 lines. Predicted
direction for each leg: red, exactly the new case.
| leg | mutation | result |
|:---|:---|:---|
| A1 | `listDrafts` serves `header` (no member) |
`sys-metadata-repository-14938…`: `1 failed \| 18 passed (19)`: the
#23073 case |
| A2 | `listCommits` serves `item` (no member) | `protocol-14038…`: `1
failed \| 4 passed (5)`: the #23073 case |
| B1 | `ListDraftsResponseSchema` key renamed away from `member` | spec
`protocol.test.ts`: `1 failed \| 184 passed (185)`: the #23073 case |
| B2 | commit-item key renamed away from `member` | spec
`package-lifecycle.test.ts`: `1 failed \| 19 passed (20)`: the #23073
case |
The objectql harness leg was not ablated. It reads
`@objectstack/metadata-protocol` from `dist`
(`KNOWN_UNALIASED_TEST_IMPORTS`), so an ablation there needs a rebuild
per leg. The serving it exercises is the code that A1 and A2 ablate at
source level.
## Tests and gates (each reading names the tree it was taken on)
Targeted pins and the ablation ran at `fe5c3a5e3`. The merge that
followed, to `f735cffa6`, brought only `packages/rest`,
`packages/runtime` and two changesets, none of which these packages
depend on.
- **Targeted:** spec `protocol.test.ts` and `package-lifecycle.test.ts`:
`Test Files 2 passed (2)`, `Tests 205 passed (205)`. metadata-protocol's
two edited files: `2 passed`, `24 passed`. The objectql harness: `1
passed`, `20 passed`.
- **`@objectstack/spec`:** `typecheck` (tsc, scripts and the test
program) exit 0, at `fe5c3a5e3`. `check:generated` proved only
`check:docs` stale. `--fix` regenerated it and re-checked it green, and
the other 13 artifacts were green.
- **`@objectstack/metadata-protocol`:** `typecheck` exit 0. Its `tsc`
program includes both edited test files (`--listFiles`: 2 of 2). The
full `test` ran at `fe5c3a5e3`: `Test Files 228 passed | 3 skipped
(231)`, `Tests 28207 passed | 19 skipped (28226)`.
- **`@objectstack/objectql`:** `typecheck` exit 0,
`check:test-typecheck` ledger held, at `fe5c3a5e3`. The full `test` ran
at `f735cffa6`: `Test Files 408 passed (408)`, `Tests 7975 passed
(7975)`.
- **`@objectstack/client`** (consumer: its two list methods return the
spec types), at `f735cffa6`:
- `tsc --noEmit` (build program): exit 0.
- `return-type-precision.test.ts`, `unwrap-misfire.pin.test.ts` and
`meta-automation-descriptors.test.ts`: `3 passed`, `27 passed`.
- The test program (`tsconfig.test.json`) is a narrowed reading. 0
errors fall in those three files. All 35 errors elsewhere are `TS2307`
cannot-find-module for unbuilt devDependency `dist`
(`@objectstack/runtime`, `objectql`, `plugin-hono-server`, …). So the
whole-program verdict is NOT MEASURED here and is left to CI.
- **`@objectstack/rest`, `@objectstack/runtime`:** pass-throughs with no
re-declaration (see the premises section). No test owed: the public byte
surface they touch is the spec type, already read above.
- **`@objectstack/cli`:** zero reads of either list, so no `unit` run is
owed.
- **Lint, targeted:** `eslint --no-inline-config --format json` over the
9 changed `.ts` files: `files_linted=9 errors=0 warnings=0`.
- All 9 files fall in the config's own
`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` object; the json lists all 9, so
none is ignored.
- The narrowing excludes nothing untouched: the config enables no
type-aware linting (no `parserOptions.project`, stated in
`eslint.config.mjs`), and its plugins are per-file rules. A diff cannot
move an untouched file's verdict.
- **Derived gate families,** at `f735cffa6`: `node
scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 114, and each ran with its exit code
recorded before any pipe. `--ran` printed: `114 derived famil(ies)
accounted for — 113 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit
3)`.
- Three first runs refused their prerequisite, and each was re-run at
the same head after its prerequisite was met. All three re-runs exited
0:
- `check-engine-split-ratio`: shallow clone. Re-run after `git fetch
--shallow-since=2026-07-07 origin main`.
- `check:skill-examples`: needs the client and client-react `dist`.
Re-run after building them.
- `check:lean-entry-closure`: needs the objectql `dist`. Re-run after
building it.
- NOT MEASURED: `check:dual-build-cjs-loads`, reason: PREREQUISITE NOT
MET. It needs every package's `dist`, with 59+ unbuilt here, and a
whole-monorepo build is CI's.
## Acceptance notes
- **Same family, not fixed here (read, not measured at a door).** The
package-batch result arrays name two members of one template identically
too: `publishPackageDrafts`' `published[]` / `failed[]`,
`discardPackageDrafts`' `discarded[]` / `failed[]`, and `revertCommit`'s
`reverted[]` / `failed[]` carry `{ type, name }` only. That is the same
shape this card closes for the two lists. Carrier: none.
- **A pending changeset sentence this PR makes false.**
`.changeset/22877-member-selector.md` (stage 2, unreleased) says "The
protocol's draft list (`GET /meta/_drafts`) does not include it." That
file is outside this claim's file surface, so it is not edited here.
This PR's changeset says the earlier sentence is superseded. The
one-line amendment belongs to the seat: delete that sentence, or replace
it with "The protocol's draft list serves it too."
- **Comment-only edits outside the two serving regions.** Two docblocks
whose sentences this PR made false: `CommitPlanItem` in `protocol.ts`
and the `member` header doc of `SysMetadataRepository.listDrafts`.
- **The objectql harness** is the serving's real-engine test, and it
held stage 2's opposite pin. It is edited as the serving's test.
- Studio's half (`DraftChangesPanel` addressing each entry with
`?member=`) is objectstack-ai/objectui#12271.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01GZSPSQ5ArPjSJdxxuqwGoZ)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4a87d09 commit 252a582
13 files changed
Lines changed: 215 additions & 20 deletions
File tree
- .changeset
- content/docs/references/api
- packages
- metadata-protocol/src
- objectql/src
- spec/src/api
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
46 | 46 | | |
47 | 47 | | |
48 | 48 | | |
49 | | - | |
| 49 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
184 | 184 | | |
185 | 185 | | |
186 | 186 | | |
187 | | - | |
| 187 | + | |
188 | 188 | | |
189 | 189 | | |
190 | 190 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2108 | 2108 | | |
2109 | 2109 | | |
2110 | 2110 | | |
2111 | | - | |
| 2111 | + | |
2112 | 2112 | | |
2113 | 2113 | | |
2114 | 2114 | | |
2115 | 2115 | | |
2116 | 2116 | | |
2117 | 2117 | | |
2118 | 2118 | | |
| 2119 | + | |
2119 | 2120 | | |
2120 | 2121 | | |
2121 | 2122 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
79 | 79 | | |
80 | 80 | | |
81 | 81 | | |
| 82 | + | |
82 | 83 | | |
83 | 84 | | |
84 | 85 | | |
| |||
212 | 213 | | |
213 | 214 | | |
214 | 215 | | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2183 | 2183 | | |
2184 | 2184 | | |
2185 | 2185 | | |
2186 | | - | |
2187 | | - | |
| 2186 | + | |
| 2187 | + | |
| 2188 | + | |
2188 | 2189 | | |
2189 | 2190 | | |
2190 | 2191 | | |
| |||
22922 | 22923 | | |
22923 | 22924 | | |
22924 | 22925 | | |
| 22926 | + | |
| 22927 | + | |
| 22928 | + | |
| 22929 | + | |
| 22930 | + | |
| 22931 | + | |
| 22932 | + | |
22925 | 22933 | | |
22926 | 22934 | | |
22927 | 22935 | | |
| |||
22930 | 22938 | | |
22931 | 22939 | | |
22932 | 22940 | | |
| 22941 | + | |
| 22942 | + | |
22933 | 22943 | | |
22934 | 22944 | | |
22935 | 22945 | | |
| |||
22944 | 22954 | | |
22945 | 22955 | | |
22946 | 22956 | | |
22947 | | - | |
22948 | | - | |
22949 | | - | |
22950 | | - | |
| 22957 | + | |
| 22958 | + | |
| 22959 | + | |
| 22960 | + | |
| 22961 | + | |
| 22962 | + | |
| 22963 | + | |
| 22964 | + | |
| 22965 | + | |
22951 | 22966 | | |
22952 | 22967 | | |
22953 | 22968 | | |
| |||
25514 | 25529 | | |
25515 | 25530 | | |
25516 | 25531 | | |
25517 | | - | |
| 25532 | + | |
| 25533 | + | |
| 25534 | + | |
| 25535 | + | |
| 25536 | + | |
| 25537 | + | |
25518 | 25538 | | |
25519 | 25539 | | |
25520 | 25540 | | |
| |||
25544 | 25564 | | |
25545 | 25565 | | |
25546 | 25566 | | |
25547 | | - | |
25548 | | - | |
25549 | | - | |
| 25567 | + | |
| 25568 | + | |
| 25569 | + | |
| 25570 | + | |
| 25571 | + | |
| 25572 | + | |
| 25573 | + | |
| 25574 | + | |
25550 | 25575 | | |
25551 | 25576 | | |
25552 | 25577 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
508 | 508 | | |
509 | 509 | | |
510 | 510 | | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
511 | 537 | | |
512 | 538 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1514 | 1514 | | |
1515 | 1515 | | |
1516 | 1516 | | |
1517 | | - | |
1518 | | - | |
| 1517 | + | |
| 1518 | + | |
| 1519 | + | |
1519 | 1520 | | |
1520 | 1521 | | |
1521 | 1522 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
525 | 525 | | |
526 | 526 | | |
527 | 527 | | |
528 | | - | |
529 | | - | |
530 | | - | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
531 | 533 | | |
532 | 534 | | |
533 | 535 | | |
| |||
537 | 539 | | |
538 | 540 | | |
539 | 541 | | |
540 | | - | |
541 | | - | |
| 542 | + | |
542 | 543 | | |
543 | 544 | | |
544 | 545 | | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
545 | 572 | | |
546 | 573 | | |
547 | 574 | | |
| |||
0 commit comments