Skip to content

finding(spec/runtime): GET /packages rows are the ASSEMBLED package body, but InstalledPackageSchema types them as the AUTHORING manifest — the #14242 stage mismatch, one layer up on the read API #17431

Description

@os-justin

Found while landing #16781 (deliverable 2). Filed rather than fixed: the remedy is a packages/spec declaration, and #16781 is explicitly routed away from packages/spec.

The contract, verbatim

ListInstalledPackagesResponseSchema (packages/spec/src/api/package-api.zod.ts) types every row of data.packages as InstalledPackageSchema, whose manifest is ManifestSchema. ManifestSchema declares:

objects: z.array(z.string()).optional().describe('Glob patterns for ObjectQL schemas files'),

— i.e. the authoring stage, where manifest.objects names file patterns. ManifestSchema is additionally a strictObject.

What the door actually serves

SchemaRegistry.installPackage records what it is handed, and ObjectQL.registerApp is handed manifest.objects as object definitions — it iterates them and calls registerObject(objDef, …). That is the assembled body, and it is what a defineStack() host produces (examples/app-showcase/objectstack.config.ts: objects: [...Object.values(objects), ExternalCustomer, ExternalOrder]). GET /packages projects those rows through toPackageResponse and serves them.

Measured

Driven against the real SchemaRegistry + the real dispatcher door, after #16781 added the missing hasMore:

package authored as ListInstalledPackagesResponseSchema.safeParse(body)
glob patterns (objects: ['./src/objects/*.object.yml']) success
defineStack() shape (objects: [{ name, fields }]) failure, one issue: data.packages.0.manifest.objects.0 — expected string, received object

So the shipped open-core path serves a payload its own declared response schema refuses, and the single surviving reason is the manifest stage.

Why this is the same thing as #14242, not a new class

#14242 identified exactly this authoring-vs-assembled split one layer down, and the ruling is quoted in packages/spec/src/stack.zod.ts at ArtifactPackageSchema:

This key is read at LOAD time, so the authoring-time ArtifactPackageEntrySchema (whose manifest.objects are glob patterns) cannot describe it: that mismatch was #14242, and the maintainer's decision (2026-09-02) was to declare the assembled stage rather than widen the authoring one.

#14242 declared the assembled stage for the artifact load path. The read API never got the same treatment: InstalledPackageSchema still wraps the authoring manifest, so the API contract inherits the mismatch.

Consequence today

packages/runtime/src/route-ledger.ts's GET /packages row is left with no responseSchema by #16781, deliberately, and the blank is a measured verdict. The ledger header forbids filling a row without conformance coverage, and a name there would promise conformance the door keeps only for glob-authored packages. The boundary is pinned in both directions in packages/runtime/src/domains/packages-read-delete-response-conformance.test.ts:

  • the glob row parses end to end;
  • the assembled row does not, and the test asserts the surviving issue list is exactly ['data.packages.0.manifest.objects.0'].

⇒ Whoever declares the assembled stage for this surface gets a red test telling them the ledger row has become fillable. That is the intended pickup path.

Suggested shape (not a ruling)

Follow #14242's ruling one layer up: an assembled-stage counterpart of InstalledPackageSchema in @objectstack/spec/api whose manifest is the assembled body, bound to the GET /packages and GET /packages/:id response declarations. ⛔ Not widening ManifestSchema — that is the option #14242 already rejected.

Related: #14242 (the ruling) · #16781 (where this was measured) · #16628 · #3877 (the responseSchema programme).

Activity

  1. os-litant commented on Sep 10, 2026

    @os-litant
    Collaborator

    Triage: lands in packages/spec/src/api/package-api.zod.ts; domain:spec; priority:p2.

    ListInstalledPackagesResponseSchema types every row of data.packages as InstalledPackageSchema, whose manifest is ManifestSchema — the AUTHORING manifest. But GET /packages rows are the ASSEMBLED package body. ⇒ the read API is typed at the wrong stage: the #14242 stage mismatch, one layer up.

    ⇒ p2: a consumer typed against ManifestSchema writes code for authoring-stage shapes and receives assembled-stage data. The mismatch is in the type system, so it is invisible until a field is the wrong shape at runtime — and typed consumers are precisely the ones who will not defend against it.

    ⇒ Declare the assembled shape for this response. ⚠️ This is a published API type change — declare Clause-②, and say in the PR whether it widens, narrows, or replaces. ⭐ Read #14242's resolution first and follow its convention: two layers describing the same mismatch differently is how this becomes three cards.

    ⭐ Correctly filed rather than fixed: #16781 is explicitly routed away from packages/spec, and the remedy is a packages/spec declaration. The fence held.

    Size/model suggestion: M.

    分诊席位 · session_017VGfRocA8VjczSe84fgjY3 · R+166 · 2026-09-10T14:39Z · 本评论来自分诊座位


    Generated by Claude Code

  2. added theissue type on Sep 10, 2026
  3. self-assigned this
    on Sep 10, 2026
  4. os-bill commented on Sep 10, 2026

    @os-bill
    Collaborator

    Claim: session_01MkQhmuuJAVDjmeWNixwDDH · branch claude/issue-17431-assembled-stage-package-response
    Clause-②: yes — declared at dispatch and confirmed by triage: this is a published API type change. needs:contract-review is hung on this card with this claim; your PR carries it too, and an at-tier verdict is owed on the head that lands. ⚠️ Triage asks you to say in the PR whether it widens, narrows, or replaces — answer that explicitly, ⛔ not just "yes".

    File face declared — packages/spec/src/api/package-api.zod.ts and its tests, packages/runtime/src/route-ledger.ts (the GET /packages row), packages/runtime/src/domains/packages-read-delete-response-conformance.test.ts, and a changeset. ⭐ If your face grows past this list, report it — the SEAT amends this comment.

    ⚠️ Note packages/runtime/** normally sits in the domain:cli lane. Triage routed this card domain:spec because the remedy is the spec declaration and the runtime files are its consumer — that is the cross-domain exception path, and this claim declares the file face for it. ⛔ Do not widen into anything else under packages/runtime/.

    ⭐ The direction is already ruled — follow it, do not re-derive it

    #14242, maintainer 2026-09-02, quoted in packages/spec/src/stack.zod.ts at ArtifactPackageSchema:

    This key is read at LOAD time, so the authoring-time ArtifactPackageEntrySchema (whose manifest.objects are glob patterns) cannot describe it: that mismatch was #14242, and the maintainer's decision (2026-09-02) was to declare the assembled stage rather than widen the authoring one.

    ⇒ ⛔ Do NOT widen ManifestSchema. That is the option #14242 already rejected, and ManifestSchema is a strictObject whose objects are glob patterns by design. The remedy is an assembled-stage counterpart of InstalledPackageSchema, bound to the GET /packages and GET /packages/:id response declarations.

    ⭐ Triage adds one instruction worth obeying literally: "Read #14242's resolution first and follow its convention: two layers describing the same mismatch differently is how this becomes three cards." ⇒ read how #14242 named and shaped its assembled-stage schema, and mirror it. If you find yourself inventing a different convention, stop and say why.

    ⭐ The acceptance is already built, and it is a red test — the best kind

    packages/runtime/src/domains/packages-read-delete-response-conformance.test.ts pins the boundary in both directions today:

    • the glob-authored row parses end to end;
    • the assembled row does not, and the test asserts the surviving issue list is exactly ['data.packages.0.manifest.objects.0'].

    ⇒ the card's own words: "Whoever declares the assembled stage for this surface gets a red test telling them the ledger row has become fillable. That is the intended pickup path."

    ⭐ So your discriminator exists before you write a line: that pin must go red, and you make it green by declaring the stage — ⛔ not by editing the assertion to match. And route-ledger.ts's GET /packages row currently has no responseSchema deliberately — #16781 left it blank as a measured verdict, because the ledger header forbids filling a row without conformance coverage. Filling that row is part of this deliverable, and it is only legitimate once the conformance test is green for both shapes.

    Falsify the premises FIRST

    Re-run the card's own two-row table on the current tree, driven against the real SchemaRegistry + the real dispatcher door:

    package authored as expected
    glob patterns (objects: ['./src/objects/*.object.yml']) success
    defineStack() shape (objects: [{ name, fields }]) failure, one issue at data.packages.0.manifest.objects.0, expected string, received object

    ⭐ The failure being exactly one issue at exactly that path is what makes this a stage mismatch rather than a general shape problem — check the issue list, ⛔ not just success === false. A measured "already fixed" or "the issue set differs" is a good outcome; ⛔ closing the card is the seat's act, never yours.


    ⭐ A count or a zero is not a reading until you look at what it matched. Lit control (> 0) AND dark control (0) on every absence claim. Traps confirmed in this lane today: grep -c counts LINES not occurrences; grep -E's [ \t] is the character SET — use grep -P; a probe both sides pass is not a discriminator; a red for the wrong reason is NOT MEASURED; ⭐ a probe that reads 0 for a structural reason is not a measurement of absence; ⚠️ the shared checkout /home/user/objectstack sits on another agent's branch — verify against origin/main via git show origin/main:PATH, ⛔ never the working tree.

    ⛔ Never capture an exit code through a pipe — cmd > log 2>&1; EXIT=$?.
    ⛔ Run a tool's own predicate; never re-implement it.
    ⚠️ Exit 3 = a gate's own PREREQUISITE NOT MET ⇒ NOT MEASURED, not red.
    ⚠️ check:migration-registry / check:spec-changes / check:upgrade-guide / check:generated are NOT root scripts — bare invocation exits 254 = NOT MEASURED. Use pnpm --filter @objectstack/spec check:….
    ⚠️ check:react-declaration-parity: try MANIFEST="$PWD/sdui.manifest.json" … --baseline react-declaration-parity.baseline.json --strict, and report the exit code you got and which manifest you used (#17405).
    ⚠️ Verify-lock: ⛔ re-read bash scripts/pm/os-verify-lock.sh --status before your first heavy run; exit 99 is NOT MEASURED, not red.

    ⛔ Fenced out, held elsewhere: packages/spec/src/ui/view.zod.ts + view.test.ts (#17360, in flight) · the repeater carriers under packages/spec/src/** that #17232 is surveying · packages/spec/src/security/permission.zod.ts (#17485) · ui/component.zod.ts (#17473) · ui/dashboard.zod.ts (#17474) · data/field.zod.ts (#17477) · packages/spec/scripts/** · docs/adr/** (governed — read only).
    ⚠️ Whether this needs an ADR-0087 disposition depends on your widens/narrows/replaces answer — if the published response type moves in a way an existing consumer must react to, it does. migrations/registry.ts is currently free; re-check at write time and ⛔ never hand-edit between the os-generated markers.

    Deliverable: a changeset, the conformance test green in both directions, the filled ledger row, and a draft PR with Fixes #17431.
    ⚠️ The body MUST carry a line that STARTS with Clause-②: yes — a ⇒ prefix or a leading backtick makes it a near miss and Check Changeset goes red.
    ⛔ The card relation is declared ONCE, in the PR BODY. Commit messages carry NO card relation — no Fixes, no Part of, no Refs. A trailer in a pushed commit message is a permanent red on Part-of PR must not also close its card that no author action can clear, because the repair would be the forbidden history rewrite. That happened on a PR of mine today; ⛔ it does not happen twice.
    ⚠️ Attribution: session id in body prose; the platform appends its own footer.
    ⛔ Do NOT flip ready, enqueue, or arm auto-merge. ⛔ The at-tier verdict is the seat's to commission.

    Commit trailers — ⛔ model-free, exactly:

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

    Report: structured JSON — what you measured with what each probe matched, what you changed, gate exits with NOT MEASURED called out separately from red, PR number and head sha, CI as newest-run-per-check-NAME plus the combined commit status, your clause-② reading with the widens/narrows/replaces answer, open questions, and out-of-scope findings each with a named carrier.

    Claimed by the domain:spec execution seat for an os-dev subagent, which inherits this claim and this assignee — ⛔ it posts no second Claim: and ⛔ never writes the assignee field.

    Batch independence: face measured disjoint from the two in-flight rounds (#17360 ui/view.zod.ts; #17232, surveying the repeater carriers) and from every open PR at claim time. Read at 2026-09-10T17:10Z.

    domain:spec 执行席 · session_01MkQhmuuJAVDjmeWNixwDDH · 2026-09-10T17:10Z


    Generated by Claude Code

  5. os-bill commented on Sep 10, 2026

    @os-bill
    Collaborator

    os-dev-report

    {
      "issue": 17431,
      "status": "needs_decision",
      "branch": "claude/issue-17431-assembled-stage-package-response",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/17517",
      "pr_number": 17517,
      "head_sha": "d137951efea63759078f91bff066dd9cec982859",
      "premise_still_valid": true,
      "summary": "The card's DEFECT premises hold exactly, re-measured on origin/main against a real SchemaRegistry and the real dispatcher door: glob-authored parses; defineStack()-authored fails with exactly one issue at data.packages.0.manifest.objects.0 (expected string, received object) on GET /packages, and exactly one at data.manifest.objects.0 on GET /packages/:id. The card's SUGGESTED SHAPE is refuted by measurement: binding AssembledPackageBodySchema to the two published read responses trips two independent published-surface constraints, both of which need a maintainer call. (1) The assembled body has no JSON Schema form -- exactly 2 of its 55 shape members (functions, hooks) are unrepresentable -- so both response schemas DISAPPEARED from json-schema/api/ and the build's disappearance ratchet refuses that. (2) The assembled body IS the stack tree, which reaches DatasourceSchema and the Node-only pg-connection-string, so the browser-reachable ./api entry goes red; established by ablation with a clean dark control. PR #17517 implements the ruled direction with the generator's own preferred remedy for (1) and leaves (2) untouched, is DRAFT, is red on check:browser-reachable-entries by construction, and its body states both calls. It must not land until they are ruled. Everything else is green: spec build, 41 spec tests, 17 runtime conformance tests green in BOTH directions on BOTH doors, and the two /packages read ledger rows filled.",
      "decision_frame_missing": "The dispatch word carried no four-axis decision frame (the rules/dev-template.md {decision_frame} slot was not filled). Per the standing clause I did not invent axes. Please hand me the frame and I will re-analyse both questions below axis by axis.",
      "measurements": {
        "premise_two_row_table": {
          "harness": "real SchemaRegistry + real HttpDispatcher door, JSON round-tripped, run from a worktree at origin/main fa23d69875",
          "GET /packages, glob-authored": "success",
          "GET /packages, defineStack": "success=false; issue list EXACTLY ['data.packages.0.manifest.objects.0']; code invalid_type; expected 'string'; message 'Invalid input: expected string, received object'",
          "GET /packages/:id, glob-authored": "success",
          "GET /packages/:id, defineStack": "success=false; issue list EXACTLY ['data.manifest.objects.0']; same code and message",
          "verdict": "stage mismatch confirmed on BOTH read doors, not a general shape problem"
        },
        "both_stages_really_reach_the_table": "PackageInstallRequestSchema declares manifest: ManifestSchema (AUTHORING) and packages.ts POST /packages passes `body.manifest || body` straight to registry.installPackage with no parse; registerApp installs the ASSEMBLED body. So the read row is genuinely bi-stage -- which is why the remedy names both stages rather than replacing one with the other.",
        "json_schema_obstruction": {
          "lit": "binding AssembledPackageBodySchema verbatim: build FAILS -- '2 previously published schema(s) disappeared from this build: json-schema/api/GetInstalledPackageResponse.json, json-schema/api/ListInstalledPackagesResponse.json'",
          "per_key_probe": "z.toJSONSchema(member, {io:'input'}) over all 55 keys of AssembledPackageBodySchema.shape matched EXACTLY 2: functions ('Function types cannot be represented'), hooks ('Custom types cannot be represented')",
          "dark_controls": "omit(functions) still FAILS (custom); omit(hooks) still FAILS (function); omit(both) OK; InstalledPackageSchema alone emits OK -- so the 2-key set is the whole cause, measured in both directions",
          "corroboration": "no ArtifactPackage.json and no ObjectStackDefinition.json exist under packages/spec/json-schema/ (dark control: 1519 sibling definitions do)"
        },
        "browser_entry_obstruction": {
          "lit": "this branch: check:browser-reachable-entries exit 1 -- './api': ./dist/api/index.mjs links the Node-only specifier pg-connection-string'",
          "dark": "packages/spec reverted to merge base fa23d69875 and rebuilt (build exit 0): SAME gate exit 0 -- 'the browser-resolvable surface links no Node builtin and no server-only package (#11072: 4 browser-conditioned subpath(s), 8 server-only link(s) on their Node side -- positive control held)'",
          "restoration_proof": "git hash-object of packages/spec/src/api/package-api.zod.ts back to e4b821324ef0dc6cba3d711f90032177b516a93c == its HEAD blob; git diff HEAD over packages/spec empty",
          "first_attempt_discarded": "an earlier ablation reverted only the one file, the dark build then failed at gen:schema and the gate reported itself NOT MEASURED (stale bundles). That reading was thrown away, not used; the table above is the re-run.",
          "cause": "AssembledPackageBodySchema IS the stack tree; the tree reaches DatasourceSchema, which reaches data/driver/pg-url-grammar.server.ts, the ONE module allowed to import pg-connection-string. ./api declares no browser condition."
        }
      },
      "changed": [
        "packages/spec/src/api/package-api.zod.ts -- new exports AssembledInstalledPackageSchema and InstalledPackageAtEitherStageSchema (+ their Parsed aliases); both read responses bound to the union; a module-local AssembledPackageRecordBodySchema leaves `functions`/`hooks` unchecked so the two responses keep emitting JSON Schema",
        "packages/spec/src/api/package-api.test.ts -- both stages proven DISTINCT (each refuses the other, by issue path), the union proven to admit both and refuse a mixed-objects row and an unknown manifest key, and the override set pinned as MEASURED (exactly the members with no JSON form) with a lit control",
        "packages/runtime/src/domains/packages-read-delete-response-conformance.test.ts -- the #14242 pin flipped to green by declaration, a neither-stage refusal added through the real door with a lit control, the per-row `writable` residue named, and GET /packages/:id covered in both directions",
        "packages/runtime/src/route-ledger.ts -- GET /packages and GET /packages/:id carry a responseSchema again, each note recording what its declaration does not carry",
        ".changeset/plain-donkeys-repeat.md -- minor on @objectstack/spec",
        "regenerated: api-surface/, export-origins/, declaration-map/, content/docs/references/**, json-schema.manifest/api.json, authorable-surface/api.json, authorable-defaults/api.json, test-typecheck-debt.json"
      ],
      "tests": "pnpm --filter @objectstack/spec build -- VERDICT command-exit 0 (both response JSON Schemas emit again; 2 new ones added). pnpm --filter @objectstack/spec exec vitest run src/api/package-api.test.ts -- 41 passed. pnpm --filter @objectstack/runtime exec vitest run src/domains/packages-read-delete-response-conformance.test.ts -- 17 passed (the assembled row now parses end to end on both doors; the glob row still does; a mixed-objects row is still refused, with a lit control on the same door in the same test). All heavy runs went through scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-17431; verdicts read from its own 'VERDICT command-exit' line, never a bare $?. ABLATION (browser entry): committed first, mutated by reverting packages/spec to the merge base, rebuilt, gate re-run, restored under an EXIT/INT/TERM trap with absolute paths and verified by blob hash + empty git diff HEAD -- both legs in `measurements.browser_entry_obstruction`. No permanent test file was left behind; the two throwaway probe files were removed and their absence verified.",
      "gates": {
        "derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (accepted the --repo assertion) -- 87 commands, all run",
        "red": [
          "pnpm --filter @objectstack/spec run check:browser-reachable-entries -- exit 1, CAUSED BY THIS CHANGE (dark control exit 0). This is open question 2."
        ],
        "not_measured_exit_3_PREREQUISITE_NOT_MET": [
          "node packages/lint/scripts/check-reference-carrier-shape.mjs",
          "node scripts/check-plugin-teardown-shape.mjs (and its --self-test)",
          "node scripts/check-system-context-census.mjs",
          "pnpm check:doc-authoring",
          "pnpm check:dual-build-cjs-loads",
          "pnpm check:logger-receiver-detach",
          "pnpm check:type-check-debt"
        ],
        "red_then_repaired": "check:route-ledger-census, check:query-options-erasure and check:slot-lookup were red on an unescaped apostrophe inside a single-quoted ledger note (a real syntax error at route-ledger.ts:385) -- escaped, all three now exit 0. check:api-surface / check:docs / check:export-origins / check-comment-mask-corpus were red on stale generated artifacts -- regenerated with check:generated --fix, all now exit 0. check:test-typecheck asked for a re-record because the ratchet TIGHTENED (two TS6133 'declared but never read' errors on package-api.test.ts vanished: this PR gives those two imports a use) -- gen:test-typecheck-debt run, exit 0.",
        "green_and_load_bearing": [
          "pnpm --filter @objectstack/spec run check:api-surface -- exit 0; surface diff is 6 ADDED names, nothing removed or narrowed",
          "node scripts/check-adr-0087-registration.mjs --base origin/main -- exit 0",
          "pnpm check:route-envelope, check:nul-bytes, check:spec-parsed-alias, check:pm-widening-tells, check:cross-package-test-inputs, check:test-source-alias, check:type-check-coverage -- all exit 0",
          "node scripts/pm/check-clause2-carriers.mjs --pair 17517 -- exit 0, 'the clause-(2) declaration is readable in the fixed spelling and both carriers agree'"
        ],
        "not_run_here": "The derivation named, outside those 87: 45 artifact-roster families whose silence is a fact about a LIST and not a clearance, 10 declared-wide-population families, 5 families taking a value from the workflow, and 5 path-scheduled CI jobs running 27 steps with no local invocation. Those are CI's."
      },
      "ci": {
        "read_at": "2026-09-10T18:35Z, newest run per check NAME on head d137951efea63759078f91bff066dd9cec982859",
        "names": 31,
        "completed_success": 10,
        "completed_skipped": 3,
        "in_progress": 18,
        "failure": 0,
        "combined_commit_status": "success (1 context: filter)",
        "notable_completed": "Part-of PR must not also close its card = success; No other open PR may claim the same issue = success; No other open PR may claim the same single-writer path = success; Check Documentation Links = success",
        "caveat": "18 names were still in_progress at report time and Lint & Repo Gates is one of them -- it is the job that runs check:browser-reachable-entries, so it is EXPECTED to go red. Reported as in_progress rather than green; CI convergence is the seat's read, not mine."
      },
      "clause_2": {
        "declaration": "yes -- a published API type change, declared at dispatch, confirmed by triage; needs:contract-review hung on PR #17517 and readable in the fixed spelling (--pair 17517 exit 0)",
        "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 accepted became refused",
        "static": "a widening a reader must react to: ListInstalledPackagesResponse['data']['packages'][number] and GetInstalledPackageResponse['data'] become a union, so reading .manifest.version off one requires narrowing 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, untouched here",
        "not_a_replacement": "no declaration removed, renamed or tightened; ManifestSchema untouched (still strictObject, still globs)",
        "adr_0087": "NOT owed by the widening itself -- nothing retired, renamed or made lossy; check:adr-0087-registration --base origin/main exit 0; migrations/registry.ts untouched. It WOULD be owed under open question 1's alternative (retiring the two published JSON-Schema defs), which is one reason that alternative is the maintainer's."
      },
      "open_questions": [
        {
          "question": "The assembled body has no JSON Schema form (exactly functions + hooks of its 55 members). Binding it verbatim UNPUBLISHES api/ListInstalledPackagesResponse.json and api/GetInstalledPackageResponse.json. Which price does the read API pay?",
          "options": [
            "A -- leave the two collections UNCHECKED on the read-API record body only (what PR #17517 does). Cost: two keys accepted without being checked on this surface; they are refused outright today while the door really can serve them, so the declaration moves from wrong to incomplete, never from checked to tolerant. The override set is measured, not hand-picked, and pinned key-by-key so a new non-serialisable collection reddens by name. Benefit: nothing is unpublished, every other key including objects is checked at the assembled stage, and this is the remedy build-schemas.ts itself calls preferred ('make it emit -- narrow the unrepresentable member').",
            "B -- retire the two published JSON-Schema defs: delete the manifest keys plus RETIRED_DEFS_BY_MAJOR entries (the disappearance ratchet's own named remedy, and it explicitly refuses the unemitted-schemas ledger for a previously-published schema). Cost: two published JSON Schemas and their content/docs/references pages go away; an ADR-0087 D3 disposition becomes owed; consumers generating clients from JSON Schema lose these two. Benefit: the Zod declaration is exactly strict with no unchecked key.",
            "C -- fix it at the source: narrow functions/hooks on AssembledPackageBodySchema itself, so no future surface pays this tax. Cost: moves a declaration #14242 ruled on and changes the artifact load gate. Filed separately as #17518 with the measurements."
          ],
          "recommendation": "A for this card, C as the real repair. A loses nothing a reader has today and keeps the stage declaration honest where it matters (objects, datasources, permissions); B trades documentation and codegen for strictness on two keys that a JSON record cannot carry in their declared shape anyway. If C lands first, A's override collapses to nothing and the pin in package-api.test.ts is what will say so."
        },
        {
          "question": "The assembled body IS the stack tree, so declaring it in @objectstack/spec/api drags the Node-only pg-connection-string into ./api, an entry with no browser condition. check:browser-reachable-entries goes red (lit exit 1 / dark exit 0). May ./api gain a browser condition?",
          "options": [
            "A -- apply #11072 Option A to ./api: a browser export condition pointing at the build that swaps pg-url-grammar.server for its browser arm (the seam and swapServerOnlyGrammarArm already exist; 4 subpaths carry it today). Cost: browser consumers of @objectstack/spec/api start resolving the DEGRADED driver-config validators -- a consumer-visible change to a published entry, and it edits packages/spec/tsup.config.ts and package.json, both outside the file face this card declared. Benefit: the ruled remedy lands; the mechanism is already blessed and roughly one commit.",
            "B -- keep ./api Node-free and do not declare the assembled stage there. Cost: this card cannot be delivered as ruled -- the read API stays typed at the wrong stage and the ledger rows go back to blank. Benefit: no published entry changes shape.",
            "C -- move the Node-only import out of the datasource tree entirely so the stack tree is browser-safe. Cost: a different, larger surface (data/driver) and it reopens #11072's ruling. Benefit: every future declaration of the assembled stage is free of this."
          ],
          "recommendation": "A. #11072's maintainer ruling already prescribes exactly this when an entry becomes affected ('the affected entries gain a browser export condition pointing at a build with the driver-config validators excluded'), so this is applying a ruling rather than inventing one -- but it changes what browsers resolve for a published entry and grows the declared file face, so I did not do it unasked. Say the word and it is one commit."
        }
      ],
      "out_of_scope_findings": [
        "filed as #17518: AssembledPackageBodySchema declares a z.function() branch (functions) and a z.custom() branch (hooks) that ADR-0130 D4's own docblock says an inert-JSON artifact can never hold, and the measured consequence is that every published export embedding it loses its JSON Schema and its reference page. Carrier: #17518, back-linked to #14242 (the ruling that created the schema) and to this card.",
        "noted, not filed: POST /packages accepts a manifest declared as ManifestSchema (authoring, globs) and stores it with no assembly step, so a glob-authored package can sit in the installed-packages table with objects that name files nobody will ever read. This is the producer half of the same stage question and is arguably by design; it is what makes the read row bi-stage and therefore what shapes this card's remedy. Carrier named: whoever takes open question 1 or 2 above touches packages.ts's install door next -- if neither is taken, carrier: NONE, and it should be a card of its own before anyone assumes the read row is single-stage.",
        "noted, not filed: both /packages read doors serve a per-row `writable` key (this door's own computed verdict, #14375) that no declared record field carries, so a declared parse strips it. Named by assertion in packages-read-delete-response-conformance.test.ts and in both ledger notes rather than fixed -- deleting it is a wire removal and declaring it is a decision about what the response promises. Carrier: the same #3877 responseSchema increment that next touches the /packages family; it is now written down in two places it cannot be missed."
      ],
      "mcp_calls": "1 -- one targeted mcp__github__search_issues for the #17518 dedup (lit control: it returned #14242, so the channel was live). Everything else went through repo-scoped REST (probe green at open) and git.",
      "face_growth_reported": "The claim named packages/spec/src/api/package-api.zod.ts + its tests, route-ledger.ts's GET /packages row, the runtime conformance test and a changeset. Grown by: (i) the GET /packages/:id ledger row, same file, same defect class, same gate family, coverage added in the same test file; (ii) the regenerated spec artifacts + content/docs/references/** that check:generated --fix proved stale. NOT grown into packages/spec/tsup.config.ts or package.json -- that is open question 2 and is the seat's to authorise."
    }

    Generated by Claude Code

  6. os-bill commented on Sep 10, 2026

    @os-bill
    Collaborator

    os-dev-report
    {
    "issue": 17431,
    "status": "done",
    "branch": "claude/issue-17431-assembled-stage-package-response",
    "pr": "#17517",
    "premise_still_valid": true,
    "summary": "Applied both seat answers. Q2=A: ./api gains a browser export condition by the SAME mechanism the four existing entries use — src/api/index.ts added to browserConditionedEntries in packages/spec/tsup.config.ts, and a browser branch added to ./api in packages/spec/package.json pointing at dist/browser/api/*. No second mechanism; the four existing entries are untouched and the diff is purely additive (zero removed lines). Q1=A: no change — the measured key-by-key-pinned unknown override stands exactly as it was. Falsified the seat's measurement rather than trusting it: FOUR entries carry the condition ('.', './data', './system', './kernel') on both origin/main and this branch, and './cloud' is not an export at all — my earlier report's 'five' was wrong (it copied #11072's dev report). Seam confirmed at tsup.config.ts:133 wired at :217, as stated. Separately fixed a red I had introduced earlier on this branch: check:doc-authoring refuses internal tracker ids in runtime string prose, and my earlier commit had moved five id references out of // comments into the /packages route-ledger note: strings (5 file/id pairs above the shrink-only baseline). Ids returned to the adjacent comment; gate back to exit 0. Changeset extended to record the exports-map change, since it is published surface. PR body rewritten to record both answers, that Q2 rides the standing #11072 ruling, that C is filed as #17518, and the four-vs-five correction. PR left as a DRAFT — not flipped ready, not enqueued, no auto-merge, no review submitted; the at-tier contract review on the final head is the seat's.",
    "tests": "Final head c93e1f4. Heavy runs via scripts/pm/os-verify-lock.sh (slot issue-17431-spec-build); every verdict read from its own VERDICT command-exit line, and every gate exit captured before any pipe. GATE, BOTH DIRECTIONS: before (no browser condition on ./api) exit 1 — '2 problem(s)', both halves: "'./api': ./dist/api/index.mjs links the Node-only specifier 'pg-connection-string' ... This subpath declares NO browser condition" and the same for index.js. After: exit 0 — "...the browser-resolvable surface links no Node builtin and no server-only package (#11072: 5 browser-conditioned subpath(s), 10 server-only link(s) on their Node side — positive control held); exports map fully classified." DARK CONTROL (post-fix ablation): deleted ONLY the browser condition from the exports map, leaving the browser bundles on disk; gate exit 1 for the RULE's own reason ('declares NO browser condition'), explicitly NOT a freshness/NOT-MEASURED refusal (asserted by grepping for both strings). On-disk mutation proven, not assumed: dist/browser/api target lines 2 -> 0; JSON re-validated after mutation. Restore proven by blob hash (180e392f774053c2faae7b226659a414186d4506 == HEAD blob) AND empty git diff HEAD, via git checkout HEAD -- ABS_PATH in a trap on EXIT/INT/TERM with an absolute REPO_ROOT path; gate re-run green after restore. BROWSER PASS REALLY BUILDS (not a silent no-op): dist/browser/api/index.js 1.80 MB, index.mjs 1.77 MB, emitted for the first time. NODE SIDE UNTOUCHED: dist/api/index.mjs 1.77 MB and index.js 1.81 MB identical in the builds before and after the tsup change. RESOLUTION verified both directions/both halves: default conditions -> dist/api/index.{js,mjs}; --conditions=browser -> dist/browser/api/index.{js,mjs}. WHAT BROWSER CONSUMERS NOW RESOLVE (measured, not restated): loaded BOTH bundles and parsed identical payloads through the same AssembledInstalledPackageSchema, comparing issue sets on datasources[0].config.url. 'postgresql://h1:5432,h2:5433/db' (multi-host) -> Node REFUSES, browser ACCEPTS. 'host=x dbname=y' (libpq keyword/value, relative-to-base arm) -> Node REFUSES, browser ACCEPTS. 'postgresql://db.example.com/app?sslcert=/etc/x.pem' -> BOTH REFUSE, byte-identical message. '/var/run/postgresql' (unix-socket short-circuit) -> both accept. 'postgresql://db.example.com:5432/app' -> both accept. That is EXACTLY the degradation the ruling describes — the postgres URL refinement falls back to the shape-only checks it already performs before parse (fs-reading query-param refusal and unix-socket short-circuit kept; only the 'is this a URL pg can open' arm answers no-findings). NO STOP CONDITION: the ruling's grounds do cover this entry. Nothing else moved — credentialFreeUrl, placeholderFree and the object-level strict close return byte-identical issue sets from both bundles. The two remaining 'pg-connection-string' occurrences in the browser bundle were checked and are both inside comment blocks (prose), which is why the gate's comment-mask reports the bundle clean. BUILDS: pnpm --filter @objectstack/spec build exit 0 (146s, then 142s); pnpm --filter '@objectstack/runtime^...' build exit 0 (301s); @objectstack/objectql and @objectstack/client-react built to convert two prerequisite refusals into real measurements. TESTS: 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. (First attempt read '2 failed / no tests' — 'Failed to resolve entry for package @objectstack/observability|@objectstack/objectql', an unbuilt closure: NOT MEASURED, nothing ran. Re-run after the closure build.) GATE FAMILIES: scripts/pm/dispatch-gates.mjs (no hand-made path list; three-dot vs merge-base fa23d69), reconciled with --ran carrying COMMAND :: exit CODE per family: '118 derived famil(ies) accounted for — 116 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)'. The 2 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) — declared to CI, NOT recorded as failures. check:lean-entry-closure and check:skill-examples were also exit 3 at first and were converted to real greens by targeted builds. Ratchet families re-run ON the final head after the last commit: check:doc-authoring 0, check:type-check-coverage 0, check:browser-reachable-entries 0, check:nul-bytes 0, check-empty-changeset 0. LINT: no narrowing needed — ran the whole repo, pnpm lint (eslint . --no-inline-config) exit 0 in 88s. BYTES: control-byte sweep grep -naP over every changed file — no hits.",
    "mcp_calls": "1 — one mcp__github__pull_request_read to fetch the PR body before rewriting it; the PR body PATCH, the read-backs and the report comment all went through the REST channel (probed first: HTTP 200), and issue/PR reads came from the local git tree.",
    "open_questions": [],
    "out_of_scope_findings": [
    "noted, not filed: nothing new to file this round. The one red I hit (check:doc-authoring, 5 file/id pairs in packages/runtime/src/route-ledger.ts) was IN scope — my own earlier commit on this branch caused it — so it is fixed here rather than filed.",
    "noted, not filed: check:browser-reachable-entries first read as 'Outside this card's derivation' in the --ran reconciliation. I checked before treating it as a roster gap and it is NOT one — the gate IS derived (via packages/spec/src/**, artifact-roster bucket); I had recorded the pnpm --filter ... check:X spelling while the roster names pnpm --filter ... run check:X. A spelling mismatch in my record, not a defect in the tool. Carrier: none — no PR or person is blocked by it."
    ]
    }


    Generated by Claude Code

  7. os-bill commented on Sep 10, 2026

    @os-bill
    Collaborator

    os-dev-report

    {
      "issue": 17431,
      "status": "done",
      "branch": "claude/issue-17431-assembled-stage-package-response",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/17517",
      "premise_still_valid": true,
      "summary": "One change requested by the at-tier verdict, applied: the changeset claim, not the code. I verified the review's measurement myself before writing the correction and it holds in full. The sentence 'Consumers ... now see a union at `manifest` and narrow at the point of use' is replaced by the measured account: the runtime parse now accepts both stages (the real gain), while the assembled branch carries no field typing because its body is annotated as a plain string-keyed record of unknown in stack.zod.ts (inherited, untouched by this PR) - so a plain .manifest.version read yields unknown where it used to yield string, narrowing toward the AUTHORING branch restores ManifestSchema in full, and narrowing away from it yields the opaque record with every manifest field unknown. Nothing else moved: no code, no schema, no exports map, no ledger row, and none of advisories 1-5 were touched. New head 5253cc3729c94ae64060d31a4996b9934e4d74a0. The PR body was updated to carry the same measured statement and the new head sha, read back byte-for-byte (only GitHub's trailing-newline normalisation differs; single footer, no angle-bracket fragments). PR left as a DRAFT - not flipped ready, not enqueued, no auto-merge, no review submitted.",
      "tests": "VERIFICATION OF THE CLAIM (independent, on the built .d.ts at head c93e1f456a, spec dist already built at that head; probe files removed afterwards). Static probe, 5 assignments, `tsc --ignoreConfig --noEmit --strict --skipLibCheck --module esnext --moduleResolution bundler --target es2022` = exit 0 (captured into EXIT before any pipe): a garbage manifest {totally:'unrelated'} on GET, the same on LIST, an objects array MIXING a glob with an object definition, and the authoring row assigned to AssembledInstalledPackage all typecheck CLEAN. DARK CONTROL, 5 cases, same compiler flags = exit 2, all five errored, so the instrument is live: success:'yes' TS2322 string/boolean; enabled:'nope' TS2322; `const c3: string = assembled.manifest.version` TS2322 'unknown' is not assignable to 'string'; `const c4: unknown[] = assembled.manifest.objects` TS2322 'unknown' is not assignable to 'unknown[]'; `const c5: never = assembled` TS2322 printing the narrowed branch as { manifest: Record[string, unknown]; status?; enabled?; ... } - i.e. NOT never, and the row-level keys ARE still typed, only `manifest` is opaque. NARROWING probe = exit 2 with exactly one error, at the intended line: an unnarrowed `.manifest.version` read is unknown (error), while a real type guard toward the authoring branch and `Extract` both give `version: string` and `objects: string[]` with no error. RUNTIME probe against the built dist, node exit 0: garbage => REFUSED, mixed-stage => REFUSED; positive control, authoring (globs) => ACCEPTED, assembled (defs) => ACCEPTED. So the static and runtime accept-sets really do disagree in the direction the review named, and my measurement AGREES with the review's. GATES ON THE NEW HEAD 5253cc3729 (each exit code captured into a variable before any pipe): `node scripts/check-changeset-no-major.mjs --base origin/main` exit 0 ('no major bump'; LEVEL AXIS reported NOT APPLICABLE - no pull_request payload in a local run, by construction); `--self-test` exit 0, 265 assertions. `node scripts/check-empty-changeset.mjs --base origin/main` exit 0 ('1 declaring changeset(s) added'); `--self-test` exit 0, 118 assertions. `node scripts/check-changeset-fixed.mjs` exit 0. `check:doc-authoring` BOTH legs: `--self-test` exit 0 (16 batteries, 198 cases, every battery at or above its floor) and the guard exit 0 (401 files clean, 44 published skill files clean, 15022 customer-facing spec strings clean, sibling-package prose-id baseline 821 pinned sites with no growth). `node scripts/check-nul-bytes.mjs` exit 0 (8291 text files). Control-byte sweep over the one changed file, `grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]'`, exit 1 = no hits. FIRST doc-authoring attempt read exit 3 = PREREQUISITE NOT MET ('Fix: pnpm install') in the fresh worktree - NOT MEASURED, not a finding; re-run green after `pnpm install` (exit 0). No build/test run was needed or taken: the diff is one markdown file, so `pnpm --filter` scopes are empty by construction and eslint does not read .changeset/*.md. Gate families re-derived on the new head with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (exit 0); it flags the tree as at least 23 commits behind origin/main, which is a staleness note on the derivation, not a finding, and the derived families for this diff are exactly the changeset + doc-authoring + nul-bytes ones I ran.",
      "mcp_calls": "2 - one pull_request_read get, one pull_request_read get_comments. Every write (PR body PATCH, this comment) went through repo-scoped REST with GH_TOKEN, probed first and answering http=200.",
      "open_questions": [],
      "out_of_scope_findings": [
        "noted, not filed: verdict advisories 1 (unclaimed browser-bundle growth on ./api, +444,960 bytes raw / +131,398 gzipped, sitting in browser-reachable-entries.json's `unjudged` list so the weight rule asserts nothing) and 2 (@objectstack/client's packages.get/list still declare the authoring stage while the door is now declared at either stage) - the verdict states cards are owed for both and that the SEAT files them; carrier: the seat, named in the verdict. I filed nothing and touched nothing.",
        "noted, not filed: verdict advisories 3-5 (degradation set representative not exhaustive; both newly-filled responseSchema rows declare a strict subset of the wire with `writable` stripped, pinned by name and deliberately unfixed; #16628 paraphrased rather than cited in the route-ledger note) - explicitly out of scope for this round per the dispatch. Carrier: none needed, all three are recorded in the verdict comment on the PR."
      ]
    }

    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions