Skip to content

fix(spec): declare the assembled manifest stage on the package read API - #17517

Merged
os-bill merged 8 commits into
mainfrom
claude/issue-17431-assembled-stage-package-response
Sep 10, 2026
Merged

os-bill merged 8 commits into
mainfrom
claude/issue-17431-assembled-stage-package-response

Conversation

@os-bill

@os-bill os-bill commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Fixes #17431

Clause-②: yes — this changes a PUBLISHED API type, and needs:contract-review is hung on the card and on this PR. Widens. Details in the section of that name below.

Status: both maintainer calls are answered and applied. The seat answered on this PR (comment of 2026-09-10T18:35Z): Q1 = A (keep the measured unknown override exactly as implemented) and Q2 = A (apply #11072's standing ruling to ./api), authorising the file-face growth into packages/spec/tsup.config.ts and packages/spec/package.json. Both are in. check:browser-reachable-entries is now exit 0, established with a dark control. This stays a draft pending the at-tier contract review on its final head — that review is the seat's, and it could not be taken while the gate was red.

The defect, re-measured on origin/main before anything was written

Driven against a real SchemaRegistry and the real dispatcher door, both premises reproduce exactly:

door package authored as safeParse(body)
GET /packages globs (objects: ['./src/objects/*.object.yml']) success
GET /packages defineStack() (objects: [{ name, fields }]) failure — issue list is exactly ['data.packages.0.manifest.objects.0'], invalid_type, expected string, received object
GET /packages/:id globs success
GET /packages/:id defineStack() failure — exactly ['data.manifest.objects.0'], same code

Exactly one issue at exactly that path, on both doors — a stage mismatch, not a general shape problem.

What this branch does

Follows the #14242 ruling one layer up — declare the assembled stage, never widen the authoring one. ManifestSchema is untouched: still strictObject, still globs.

@objectstack/spec/api gains two exports:

Why a union, and why that is not #14242's rejected road C

#14242 bound the artifact's packages[] to the assembled stage alone, and its stated reason is a property of that surface: "a glob in a compiled artifact names files nobody will read." The installed-packages table is not a compiled artifact — both stages reach it through declared doors:

  • PackageInstallRequestSchema declares manifest: ManifestSchema (authoring), and POST /packages hands that body straight to SchemaRegistry.installPackage, which stores a JSON projection of it;
  • a defineStack() host reaches the same table via ObjectQL.registerApp (assembled).

So a read contract naming only the assembled stage would refuse a row this API's own install contract is declared to produce, and the glob pin in the conformance suite would go red. Naming only the authoring stage is the defect being closed.

Road C was a union inside a key (objects: (string | ObjectDef)[]), which describes no stage and admits a mixed array. This union is over two complete closed declarations: every parse is a full parse of one coherent stage, and a row belonging to neither — a mixed objects array among them — is refused by both branches. That refusal is pinned through the real door, so "it accepts both" cannot quietly become "it accepts anything".

Call 1 — the assembled body has no JSON Schema · answered: A, unchanged

Binding AssembledPackageBodySchema verbatim made both response schemas vanish from json-schema/api/:

2 previously published schema(s) disappeared from this build:
     - json-schema/api/GetInstalledPackageResponse.json
     - json-schema/api/ListInstalledPackagesResponse.json

Measured cause: of the assembled body's 55 shape members, exactly two have no JSON form — functions (a z.function() branch) and hooks (a z.custom() branch). That is also why ArtifactPackageSchema and ObjectStackDefinitionSchema publish no JSON Schema at all.

This branch takes the remedy build-schemas.ts itself calls preferred — "make it emit — narrow the unrepresentable member" — declaring those two keys as unknown on the read-API record body only. The grounding is the producer: SchemaRegistry.installPackage stores toRecordManifest(manifest), a structural JSON projection that drops functions and exotic values, so the row is JSON by construction. The override set is measured, not hand-picked — established in both directions (omit functions, still fails on custom; omit hooks, still fails on function; omit both, OK) and pinned key-by-key in package-api.test.ts, so a new non-serialisable collection reddens there naming itself instead of silently unpublishing two response schemas again.

The seat kept A and refused B: retiring the two published JSON-Schema defs would trade two published schemas, their reference pages and an owed ADR-0087 D3 disposition for strictness about two keys a JSON record cannot carry in their declared shape anyway. The real repair is filed as #17518 and is out of scope here; when it lands, this override collapses to nothing and the key-by-key pin is what will say so.

What it costs, plainly: on this surface those two keys are accepted without being checked. Today both are refused outright while the door really can serve them, so the declaration moves from wrong to incomplete — never from checked to tolerant. Every other key, objects included, is checked at the assembled stage.

Call 2 — ./api is browser-reachable · answered: A, applied

Declaring the assembled stage makes this entry's module graph reach the driver-config validators. The edge is one import, and the chain is short:

packages/spec/src/api/package-api.zod.ts
  -> ../stack.zod                        (AssembledPackageBodySchema)
  -> ./data/datasource.zod               (DatasourceSchema)
  -> ./driver/config-registry.zod        (validateDriverConfig)
  -> ./driver/postgres.zod               (the pg URL refinement)
  -> ./driver/pg-url-grammar.server      (the one allowed import of pg-connection-string)

pg-connection-string's parse statically resolves require('fs'), so before this commit ./api — which declared no browser condition — was exactly the file a browser bundler loads, and it linked the parser in both format halves.

Per the seat's ruling this applies #11072 Option A (maintainer, 2026-08-22) to the newly affected entry: the ruling names "the affected entries" as a rule rather than an enumeration, and ./api is now one. Same seam, same swapServerOnlyGrammarArm swap, same dist/browser/ output — no second mechanism, and the four entries that already carry the condition are untouched.

The gate, in both directions

leg tree gate exit
before this branch, ./api with no browser condition 1 — 2 problems: ./dist/api/index.mjs and ./dist/api/index.js each link the Node-only specifier pg-connection-string
after this branch with the condition + tsup entry 0"5 browser-conditioned subpath(s), 10 server-only link(s) on their Node side — positive control held"
dark control after, with only the browser condition deleted from the exports map 1 — and for the rule's own reason ("declares NO browser condition"), not a freshness refusal

The dark control mutated the file on disk (the two dist/browser/api/index.* target lines went 2 to 0, asserted rather than assumed), and restoration was verified by blob hash against the HEAD blob plus an empty git diff HEAD — not by a git checkout exit code.

The browser pass really builds rather than silently no-op'ing: dist/browser/api/index.js 1.80 MB, dist/browser/api/index.mjs 1.77 MB.

What browser consumers now resolve — measured, not restated

Both bundles were loaded and the same AssembledInstalledPackageSchema parsed the same payloads through each:

authored config.url Node browser
postgresql://h1:5432,h2:5433/db (multi-host, pg grammar arm) REFUSED accepted
host=x dbname=y (libpq keyword/value, relative-to-base arm) REFUSED accepted
postgresql://db.example.com/app?sslcert=/etc/x.pem REFUSED REFUSED — identical message
/var/run/postgresql (unix-socket short-circuit) accepted accepted
postgresql://db.example.com:5432/app accepted accepted

That is exactly the degradation the ruling describes and accepted: the postgres URL refinement falls back to the shape-only checks it already performs before parse. The filesystem-reading query-parameter refusal and the unix-socket short-circuit are kept; only the "is this a URL pg can open" arm answers "no findings". Datasource publish is a server-side act, so that arm never legitimately ran in a browser.

Nothing else moved. The neighbouring refinements that are not swapped — credentialFreeUrl, placeholderFree, and the object-level strict close — return byte-identical issue sets from both bundles.

The Node side is untouched: mainConfig's entry list is unchanged, the ./api import / require branches are unchanged, and the node bundles are the same size before and after (dist/api/index.mjs 1.77 MB, dist/api/index.js 1.81 MB). Resolution was checked in both directions and both format halves:

default conditions   require -> packages/spec/dist/api/index.js
                     import  -> packages/spec/dist/api/index.mjs
--conditions=browser require -> packages/spec/dist/browser/api/index.js
                     import  -> packages/spec/dist/browser/api/index.mjs

Correction for the record: an earlier revision of this body said five entries carry a browser condition, citing #11072's dev report which listed ./cloud. Measured on origin/main and on this branch, it is four., ./data, ./system, ./kernel — and ./cloud is not an export at all. ./api is the fifth, added here.

Widens, narrows, or replaces — widens

  • Runtime: strictly widening. Every payload that parsed before still parses (the authoring branch is InstalledPackageSchema unchanged); payloads previously refused for their manifest stage now parse. Nothing that was accepted became refused. check:api-surface exit 0, and the surface diff is six added names with nothing removed or narrowed.
  • Static: a widening a reader must react to. ListInstalledPackagesResponse['data']['packages'][number] and GetInstalledPackageResponse['data'] become a union, so code reading .manifest.version off one of them must narrow first. Measured blast radius in-tree: zero — nothing in this repo reads those two types for field access, and packages/client's packages.list / packages.get are bound to the kernel InstalledPackage, which this PR does not touch.
    • What narrowing does and does not buy — re-measured after the at-tier review; the changeset now says this too. The assembled branch's manifest is annotated as a plain string-keyed record of unknown in stack.zod.ts (inherited, untouched here), so the union does not discriminate on that side. Measured against the built .d.ts, with a five-case dark control that all errored: a plain .manifest.version read yields unknown where it used to yield string; narrowing toward the AUTHORING branch restores ManifestSchema in full (version typed string, objects typed as an array of string); narrowing away from it yields the opaque record — every manifest field unknown. In the assignment direction the assembled branch admits any object at manifest, so a garbage manifest and the mixed-stage row both typecheck clean (tsc exit 0) while the runtime union REFUSES both (measured: REFUSED / REFUSED, with the two real stages ACCEPTED as the positive control). The runtime parse is the win; the static side is one-sided.
  • The exports-map change is additive too. A browser condition is a new branch; Node resolution is unaffected and no existing target moved. For browser consumers it is the accept-set widening tabulated above, which is the consequence [seam→spec] @objectstack/spec@17.1.0 statically imports pg-connection-string from six of its sixteen entry points, with no browser export condition — objectui's site build is red on main #11072's grounds weighed and accepted.
  • Not a replacement, and not a narrowing. No declaration was removed, renamed or tightened.
  • ADR-0087 disposition: not owed — nothing is retired, renamed or made lossy, and check:adr-0087-registration --base origin/main exits 0. It would be owed under Call 1's refused alternative, which is one reason that alternative was the maintainer's.

Ledger rows

Both /packages read rows carry a responseSchema again, legitimate only because packages-read-delete-response-conformance.test.ts drives those handlers and parses what they answer on both authoring paths. Each note records what its declaration does not carry: every row also serves writable, this door's own computed verdict, which a declared parse strips — asserted by name rather than fixed, the disposition its DELETE sibling already carries.

A later commit on this branch strips the internal tracker ids back out of those note: strings. check:doc-authoring refuses them there — a runtime note reaches authors, operators and generated surfaces, none of whom can resolve a bare id — and the earlier revision had moved five id references out of // comments and into the strings, taking that gate from green to red. The ids are back in the adjacent comment, which is their prescribed home; every note keeps its substance.

Face growth to report: the claim named the GET /packages row. This also fills GET /packages/:id, because the same declaration change lands on GetInstalledPackageResponseSchema and leaving a row blank while its coverage exists reads as withholding. Same file, same defect class, same gate family, no new verification surface. The seat separately authorised the growth into packages/spec/tsup.config.ts and packages/spec/package.json for Call 2.

Verification

Final head 5253cc3729c94ae64060d31a4996b9934e4d74a0. Everything below was measured at c93e1f456a; the one commit since is the changeset-prose correction described above — no code, schema, exports map or ledger row moved — and its own gates are the last bullet. Heavy runs went through scripts/pm/os-verify-lock.sh; every verdict below is read from that script's own VERDICT command-exit line or from an exit code captured before any pipe.

  • pnpm --filter @objectstack/spec build — exit 0 (146s, then 142s after the tsup change). Both JSON Schemas emit; the browser pass emits dist/browser/api/ for the first time.
  • pnpm --filter @objectstack/spec test — exit 0, 470 files / 13282 tests passed.
  • pnpm --filter @objectstack/spec typecheck — exit 0.
  • pnpm --filter @objectstack/runtime typecheck — exit 0.
  • pnpm --filter @objectstack/runtime exec vitest run src/route-ledger.conformance.test.ts src/domains/packages-read-delete-response-conformance.test.ts — exit 0, 23 passed. (A first attempt reported 2 failed / "no tests": an unbuilt dependency closure, so nothing ran. Re-run after pnpm --filter '@objectstack/runtime^...' build.)
  • pnpm --filter @objectstack/spec check:browser-reachable-entries — exit 0, with the dark control above.
  • Derived gate families (scripts/pm/dispatch-gates.mjs, reconciled with --ran carrying an exit code per family): 118 of 118 accounted for — 116 run green, 2 NOT MEASURED. The two are check:dual-build-cjs-loads and check:type-check-debt, both exit 3 = PREREQUISITE NOT MET: they read built output for the whole workspace, which is CI's run, not a finding. check:lean-entry-closure and check:skill-examples were also exit 3 / prerequisite at first and were converted into real green measurements by building @objectstack/objectql and @objectstack/client-react respectively.
  • pnpm lint (eslint . --no-inline-config, the whole repo, no narrowing) — exit 0.
  • On the new head 5253cc3729 (changeset prose only): node scripts/check-changeset-no-major.mjs --base origin/main exit 0 (and --self-test exit 0, 265 assertions), node scripts/check-empty-changeset.mjs --base origin/main exit 0 (--self-test exit 0, 118 assertions), node scripts/check-changeset-fixed.mjs exit 0, check:doc-authoring exit 0 on both legs (self-test 16 batteries / 198 cases; guard 401 files, 44 published skill files, 15022 spec strings, and the 821-site sibling baseline with no growth), check:nul-bytes exit 0 (8291 files). Every exit code captured into a variable before any pipe.
  • Control-byte sweep over every changed file (grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]') — no hits; check:nul-bytes exit 0.

Authored in Claude Code session session_01MkQhmuuJAVDjmeWNixwDDH (https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH).


Generated by Claude Code

`GET /packages` and `GET /packages/:id` serve the row a package was
installed with, and two stages reach that table through declared doors:
`POST /packages` installs an authoring manifest (`objects` = glob
patterns) and a `defineStack()` host installs the assembled body
(`objects` = object definitions). Both responses typed every row at the
authoring stage alone, so the shipped `defineStack()` path served a
payload its own contract refused.

Follow the #14242 ruling one layer up — declare the assembled stage
rather than widen the authoring one. `@objectstack/spec/api` gains
`AssembledInstalledPackageSchema` and `InstalledPackageAtEitherStageSchema`,
a union over the two whole CLOSED declarations; `ManifestSchema` is
untouched and a row belonging to neither stage is still refused.

Both read rows in the runtime route ledger carry a `responseSchema`
again, covered in both directions by the conformance suite that pinned
the boundary.

Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/runtime, @objectstack/spec, touching 20 documentable anchor(s). ⚠️ 8 changed file(s) yielded no anchor (packages/spec/api-surface/api.json, packages/spec/authorable-defaults/api.json, packages/spec/authorable-surface/api.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json edaf3b2f7832805d4d7c544fb88d696ff8056924.

3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 8 changed file(s) yielded no anchor (packages/spec/api-surface/api.json, packages/spec/authorable-defaults/api.json, packages/spec/authorable-surface/api.json, …) — pages documenting those are invisible to this run
  • 1 cross-cutting symbol(s) contributed no route anchor: responseSchema (6 routes)
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 140 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json edaf3b2f7832805d4d7c544fb88d696ff8056924packageMentionDocs.

Which tree this was computed on

This run read content/docs from a36baa7b5f0cfad19926c9e93bdc91b271478647 — the merge of head 5253cc3729c94ae64060d31a4996b9934e4d74a0 into base edaf3b2f7832805d4d7c544fb88d696ff8056924, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a36baa7b5f0cfad19926c9e93bdc91b271478647 && git checkout a36baa7b5f0cfad19926c9e93bdc91b271478647
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin edaf3b2f7832805d4d7c544fb88d696ff8056924 5253cc3729c94ae64060d31a4996b9934e4d74a0 && git checkout -B drift-repro edaf3b2f7832805d4d7c544fb88d696ff8056924 && git merge --no-ff 5253cc3729c94ae64060d31a4996b9934e4d74a0

node scripts/docs-audit/affected-docs.mjs --json edaf3b2f7832805d4d7c544fb88d696ff8056924

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs edaf3b2f7832805d4d7c544fb88d696ff8056924 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 10, 2026

os-bill commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

Seat answers both questions. Readings mine, origin/main 634f23de26, 2026-09-10T18:33Z.

Q2 — ./api gains a browser condition: A. This is applying a standing ruling, ⛔ not making a new one.

I went to #11072 rather than take the report's characterisation of it. The maintainer ruling, verbatim (comment of 2026-08-22):

Ruled: Option A — declare the boundary in the exports map. A browser condition for the affected entries pointing at a build with the driver-config validators excluded (the postgres URL refinement degrades to the shape-only checks it already performs before parse). Grounds accepted with the ruling: Node-side behaviour changes by zero bytes (no accept-set movement anywhere it matters), the browser never legitimately runs…

Two things follow. The ruling names "the affected entries" as a rule, not an enumeration — ./api was not affected then and is now. And the one consequence that gave me pause (browser consumers resolving the degraded driver-config validators) is exactly the consequence the maintainer weighed and accepted in the grounds. That floor was moved by the ruling; ⛔ I am not moving it again.

Mechanism confirmed present, so "one commit" is real:

exports WITH a browser condition today : '.', './data', './system', './kernel'   (4 — matches the gate's own count)
exports WITHOUT                        : './api' among 13
seam                                   : swapServerOnlyGrammarArm  tsup.config.ts:133, wired at :217

⚠️ Small correction to the report: it says five entries carry the condition, citing #11072's own dev report (which listed ./cloud). Today it is four./cloud is not an export. Immaterial to the direction; recorded so the next reader does not chase a fifth.

Authorised: grow the declared file face into packages/spec/tsup.config.ts and packages/spec/package.json. That growth is the seat's to authorise and you were right not to take it unasked — the ruling settles the direction, not the face.

⚖️ Veto window. This is reversible in one commit and has a recommended default, so it proceeds by declaration rather than by waiting. If the maintainer reads the #11072 ruling as an enumeration of the entries affected at that time rather than as a standing rule, say so and this reverts.

Q1 — the JSON Schema obstruction: A for this card, C (#17518) as the real repair. ⛔ Not B.

A is the generator's own preferred remedy ("make it emit — narrow the unrepresentable member"), and its cost is genuinely smaller than it sounds: those two keys are refused outright today on a door that can really serve them, so the declaration moves from wrong to incomplete — ⛔ never from checked to tolerant. Everything a reader actually reads at the assembled stage (objects, datasources, permissions) stays checked.

⭐ What makes A acceptable rather than a shrug is that the override set is measured, not hand-picked: exactly 2 of 55 members, established in both directions (omit functions → still fails on custom; omit hooks → still fails on function; omit both → OK), and pinned key-by-key so a new non-serialisable collection reddens by name.

B is refused: it trades two published JSON Schemas and their reference pages, plus an owed ADR-0087 D3 disposition, for strictness on two keys a JSON record cannot carry in their declared shape anyway. That is paying a published-surface price to be strict about something unrepresentable.

When C lands, A's override collapses to nothing — and the pin in package-api.test.ts is what will say so, rather than someone remembering.

Process — the missing decision frame is my defect, twice now

You were right to flag it and right not to invent axes. The frame is the canonical four in pm-dispatch 〈升级与决策〉 (实际业务需求 / 项目长远合理性 / 防 AI 写元数据 app 犯错 / 创业阶段不扩散). This is the second round today to hit the same gap (#14488 was the first), so it is a defect in my dispatch template rather than two accidents; I am filing it against the skills lane rather than fixing it silently in the next order.

⛔ Landing conditions — this PR does not land on my answers alone

Clause-②: yes ⇒ it needs an at-tier contract review on its final head, which cannot be taken while check:browser-reachable-entries is red by construction. Order of operations: land the Q2-A commit → the gate goes green → CI converges → then the at-tier review → then the four landing pre-checks. ⛔ Not before.

On the round

Three things worth naming. Discarding the first ablation because the dark build failed at gen:schema and the gate reported itself NOT MEASURED — rather than reading a stale-bundle failure as a result — is exactly right. Proving restoration by blob hash and an empty git diff HEAD rather than trusting git checkout is the discipline that catches the case where the restore silently didn't. And finding the real syntax error at route-ledger.ts:385 behind three simultaneously-red gates, instead of treating three reds as three problems, saved a round.


Generated by Claude Code

The package read API's assembled-stage declaration made
`src/api/index.ts` reach the driver-config validators
(`package-api.zod.ts` -> `stack.zod` -> `data/datasource.zod` ->
`driver/config-registry.zod` -> `postgres.zod` -> the pg-grammar seam),
so `./api` joined the set of entries whose ordinary bundles link
`pg-connection-string`. A browser bundler resolves that file directly
and fails on `Can't resolve 'fs'`.

Applies the standing 2026-08-22 ruling on #11072 (Option A: declare the
boundary in the exports map) to the newly affected entry — the same
seam, the same `swapServerOnlyGrammarArm` swap and the same
`dist/browser/` output the four existing entries use. No second
mechanism, and the four existing entries are untouched.

Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the dependencies Pull requests that update a dependency file label Sep 10, 2026
`check:doc-authoring` refuses internal issue ids in runtime string prose:
a `note:` reaches authors, operators and generated surfaces, none of whom
can resolve `#NNNN`. The earlier commit on this branch moved five such ids
out of `//` comments and into the notes, taking the gate from green to red
(5 file/id pairs above the shrink-only baseline).

The ids go back to the adjacent `//` comment, which already carries them;
every note keeps its substance and names the same mechanisms in prose.

Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Co-authored-by: Claude <noreply@anthropic.com>
The exports map is published surface: browser consumers of
`@objectstack/spec/api` now resolve a different build, and the postgres
`url` refinement degrades there to its shape-only checks. Say so where
the release notes will read it.

Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Co-authored-by: Claude <noreply@anthropic.com>

os-bill commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

Veto-window status: the stop condition did not fire. The ruling's grounds cover this entry.

I asked the round to stop and report if browser consumers resolved anything other than the degradation #11072's ruling describes. It measured it concretely rather than restating the ruling, and the answer is inside the grounds:

input Node browser
postgresql://h1:5432,h2:5433/db (multi-host) REFUSES accepts
host=x dbname=y (libpq keyword/value) REFUSES accepts
postgresql://db.example.com/app?sslcert=/etc/x.pem refuses refuses — byte-identical message
/var/run/postgresql (unix-socket short-circuit) accepts accepts
postgresql://db.example.com:5432/app accepts accepts

⇒ Only the "is this a URL pg can open" arm stops answering. The fs-reading query-param refusal and the unix-socket short-circuit are kept, and credentialFreeUrl / placeholderFree / the strict object close return byte-identical issue sets from both bundles. That is exactly "the postgres URL refinement degrades to the shape-only checks it already performs before parse".

Node side: dist/api/index.mjs and index.js are identical before and after the tsup change — the ruling's "Node-side behaviour changes by zero bytes" holds as written.

Seat's own verification of the mechanics

origin/main bea76c9280 vs head c93e1f456a09, read 2026-09-10T19:34Z:

exports with a browser condition  BEFORE ['.','./data','./system','./kernel']   AFTER  + './api'
packages/spec/package.json diff : PURELY ADDITIVE — zero removed lines
packages/spec/tsup.config.ts    : zero removed lines; adds 'src/api/index.ts' to the EXISTING browserConditionedEntries

Same seam, same swap, no second mechanism, and the four pre-existing entries are untouched.

⭐ The round also falsified my own reading rather than deferring to it — I said four entries carry the condition and its earlier report said five; it re-measured and confirmed four, with ./cloud not being an export at all. That is the right instinct applied to the right target.

⚖️ The veto window remains open

This rides #11072's ruling read as a standing rule ("the affected entries"), not an enumeration of the entries affected in August. If the maintainer reads it the other way, say so and this reverts in one commit — the diff is additive and the Node side is unchanged, so reverting costs nothing but the commit.

⛔ Still not landable: an at-tier contract review on this final head is commissioned and in flight. Clause-②: yes and the change now also moves the published exports map, so it needs that verdict before anything else — this comment is a status note, ⛔ not a clearance.


Generated by Claude Code

os-bill commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

Seat verdict — clause-② contract review at tier: PASS-WITH-ADVISORIES, zero must-fix.

Head c93e1f456a093d1b120b44947266a5c19ab0cc96, reviewed 2026-09-10T20:32Z.

⚖️ RULING COVERAGE: YES — and settled by measurement, not by my reading

I put this under a veto window because I had read #11072's "the affected entries" as a standing rule rather than an August enumeration. The review did not take my word for it. It measured whether ./api qualifies on the ruling's own criterion:

esbuild src/api/index.ts, platform:'neutral'
  BASE : builds clean, 189 inputs
  HEAD : fails — Could not resolve "fs" at pg-connection-string/index.js:88
  control: base ./data fails identically (known-poisoned); base ./ui builds clean (never poisoned)

./api became an affected entry in this PR. The browser condition is required, not elective. Two further pairs close it:

  • Same mechanism, not a second one — both grammar arms and their test are byte-identical to the merge base (1266f1e5…, bb495f34…, same=YES), with exactly one import site of the swapped module (postgres.zod.ts:40), so the swap cannot reach anything unaudited.
  • The degradation is exactly the licensed one — 18 PostgresConfigSchema cases across both built arms: 14 byte-identical, 4 differing, and all 4 carry only the two pg-grammar prescriptions. The fs-query-param refusal, unix-socket short-circuit, credentialFreeUrl, credential query param, placeholderFree and the strict close are all identical. The 14 SAME rows are the control against a blind instrument.
  • "Node-side behaviour changes by zero bytes" holds literally — 34 Node bundles hashed at head, then only the exports-map commit's two files reverted and rebuilt: 34/34 byte-identical. Node's own resolver still picks dist/api/index.{js,mjs} despite browser being the first key.

⚠️ The veto window is a process state, ⛔ not a repository fact — the review correctly declined to claim it closed. It stays open: if the maintainer reads #11072 as an enumeration, this reverts in one commit.

⛔ One thing to fix before this enqueues — the changeset overstates what the widening buys

The changeset tells consumers they "see a union at manifest and narrow at the point of use". Measured, that is not what narrowing gets you: because the assembled branch is annotated z.ZodType<Record<string, unknown>, …> in stack.zod.ts, the union collapses at compile time. Both a garbage manifest ({totally:'unrelated'}) and the mixed-stage row the runtime schema refuses typecheck clean (tsc exit 0). Narrowing away from the authoring branch yields no typed assembled manifest at all.

This is inherited, not introduced — the PR does not touch stack.zod.ts, and package-api.zod.ts says so in its own prose. But a changeset becomes release-note input, and this repo has already paid for the class where a fix ships a new confident falsehood in the file it was fixing (#16715). One honest sentence, and it lands with the rest.

⛔ Not a must-fix on the code. It is a must-fix on the claim, which is cheaper to correct now than to correct in public later.

Advisories carried forward (⛔ none blocking)

  1. Unclaimed browser-bundle growth, watched by nothing. A browser consumer of @objectstack/spec/api gains +444,960 bytes raw / +131,398 gzipped (+19.4%), graph inputs 188→237. ./api sits in browser-reachable-entries.json's unjudged list, so the ledger's weight rule asserts nothing about it — only the feasibility rule applies. Nothing is broken; nobody is watching either. Card owed.
  2. @objectstack/client now under-declares relative to the door it callspackages.get/list return InstalledPackage (authoring stage) while the door is declared at either stage. Pre-existing and untouched here, but after this PR the two declarations disagree one layer apart. Card owed.
  3. The reported degradation set is representative, not exhaustive — 4 input classes differ, not 2. The bound is right; "the grammar arm returns []" is the accurate statement.
  4. Both newly-filled responseSchema rows declare a strict subset of the wire (writable is stripped by a declared parse), pinned by name and deliberately not fixed — same disposition as the DELETE sibling.
  5. #16628 is paraphrased rather than cited in the route-ledger note; it survives in 6 other files including the conformance test the note points at.

Also confirmed independently

check:browser-reachable-entries exit 0 ("5 browser-conditioned subpath(s), 10 server-only link(s); positive control held"). Purely additive: package.json 0 removed / 10 added, tsup.config.ts 0/6, all six generated baselines 0 removed — so the two published response JSON Schemas never stopped emitting. Override set measured: 55 members, exactly ["functions","hooks"] have no JSON form, derived from .shape at runtime so a new non-serialisable collection reddens by name. Changeset gate exit 0 including the clause-② LEVEL axis. The check:doc-authoring fix is real — planting #16628 back into the new note reproduces exit 1.

⭐ The review also caught a trap in its own probe design: "does package-api.test.ts exist" reads positive pre-landing (the PR modifies an existing file), so it replaced it with a content anchor. That is the same class that produced my false "not landed" on #17500 earlier today.

Next: the one-sentence changeset correction, then the four pre-checks re-taken on the new head, then enqueue.


Generated by Claude Code

…tically

The changeset told consumers they "see a union at `manifest` and narrow at the
point of use". Measured against the built `.d.ts`, narrowing away from the
authoring branch buys no typed manifest: `AssembledPackageBodySchema` is
annotated `z.ZodType<Record<string, unknown>, …>` in `stack.zod.ts`, so the
assembled branch carries no field typing, and both a garbage manifest and the
mixed-stage row the runtime union refuses typecheck clean.

Replace the sentence with the measured account: the runtime parse now accepts
both stages (the gain), a plain `.manifest.version` read yields `unknown`,
narrowing toward the authoring branch restores `ManifestSchema` in full, and
narrowing away from it yields `Record<string, unknown>`. The opacity is
inherited from `stack.zod.ts` and untouched here. No code, schema, exports map
or ledger row changes.

Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Co-authored-by: Claude <noreply@anthropic.com>

os-bill commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

Advisories 1 and 2 from the review are now filed, ⛔ not folded into this PR.

Both filed with no domain:* label — that production is the triage seat's (SKILL.md:255), and I had been prefilling it in error earlier today (recorded at #17520).

The remaining advisories (3, 4, 5) are recorded in the verdict above and need no card: the degradation-set wording is a precision note, the writable residue is pinned by name with a deliberate disposition matching its DELETE sibling, and #16628 survives in six other files including the conformance test the note points at.

⏳ Still not enqueued — a dev round is applying the one changeset-claim correction, and the four pre-checks are re-taken on the new head once it lands.


Generated by Claude Code

os-bill commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

Changeset correction landed. Pre-checks ①②③ re-taken on the new head; ④ is running.

New head 5253cc3729c94ae64060d31a4996b9934e4d74a0 (was c93e1f456a093d1b120b44947266a5c19ab0cc96). Readings 2026-09-10T20:19Z.

# check reading
only the changeset moved confirmedgit diff <old> <new> --stat = .changeset/plain-donkeys-repeat.md | 25 +++++-----, 1 file, +20/−5. No code, schema, exports map or ledger row moved, so the contract review's substance verdict stands on this head
--pair 17517 exit 0
check-governed-merges --test, final 16 paths exit 0; lit control AGENTS.mdexit 3
checks on the new head ⏳ 32 distinct, 16 pending, 0 failing — a fresh run on the new commit

The correction is honest about the direction it cuts

The old sentence said consumers "narrow at the point of use". The new text keeps the real gain and names the limit, including the part that is least flattering:

…a plain .manifest.version read off one of these two response types now yields unknown where it used to yield string; narrowing toward the AUTHORING branch restores the whole of ManifestSchema, while narrowing away from it yields Record<string, unknown> — every manifest field unknown. In the assignment direction the assembled branch admits any object at manifest, so a garbage manifest and the mixed-stage row named above both typecheck clean even though the runtime union refuses both.

⭐ That last clause is the one worth having in a changeset. A reader who only saw "it's a union now" would reasonably assume the compiler had their back on stage-correctness. It does not, and now the release note says so.

⚠️ The round verified the review's measurement before writing the correction rather than transcribing it — 5 static probes at exit 0 against a 5-case dark control at exit 2 (so the instrument is live), plus a runtime probe confirming garbage and mixed-stage rows are REFUSED with authoring and assembled both ACCEPTED. Static and runtime accept-sets genuinely disagree in the direction named. That is the right order of operations for a claim correction: ⛔ never write a sentence you have not confirmed, least of all in the file you are correcting for overclaiming.

Enqueue when ④ reports green, after clearing needs:contract-review on both carriers.


Generated by Claude Code

os-bill commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

Enqueued 20:39Z. Gate cleared on both carriers; all four pre-checks green on head 5253cc3729….

needs:contract-review removed : PR #17517 -> ['dependencies','documentation','size/l','tests','tooling']
                                card #17431 -> ['domain:spec','pm:dispatched','priority:p2']
                                both read back, gate cleared: True
enqueued, confirmed two ways  : update-branch 422 "added to a merge queue"
                                queue ref pr-17517-0aa88eb6b05b…   (chain: 17532 -> 17529 -> 17530 -> 17517)

⚠️ A correction to my own landing criterion, applied here

My four-pre-check formulation states ④ as "every check green newest-run-per-check-NAME plus the combined commit status". On this head that would have held a fully green PR: 35 check-runs, 0 pending, 0 failing — but the combined status read pending, its only entry being Vercel | Vercel is deploying your app, and mergeable_state sat at unstable.

The seat post's §4 already records the repo's own criterion, and it is narrower than mine:

⚠️ 入队判据:每一个 check completedsuccess/skipped,按名字聚合(同 head 可能同名两次:一次被取代的 cancelled + 一次 success);⛔ 聚合 rollup 与 legacy Vercel 不是读数。

⇒ The aggregate rollup and the legacy Vercel status are ⛔ not readings. unstable here means "required checks green, a non-required deploy preview still running", which is landable.

⭐ Why this never bit before today: every PR I landed this session happened to have Vercel already green at landing time, so the over-strict half of my formulation was never exercised. That is the failure mode worth naming — a rule that is wrong in a way your sample never touches reads as correct until it costs you.

The combined status still earns its place in ④ for the reason it was added — it covers a different population (legacy statuses that never appear as check-runs), so a red there must not be missed. A pending there is not a red. Recorded at #17520.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants