Skip to content

fix(rest): REST refusals, notes and the OpenAPI text state each decision in words instead of a tracker number (stage 2) - #21188

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-20752-stage2-rest-strings
Oct 1, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-20752-stage2-rest-strings

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #20752
Clause-②: no

Stage 2 of 5 of the domain:cli lane under the maintainer's A / A ruling (5902360492): the packages/rest strings. The card stays open for stages 3-5, so this PR carries no closing keyword. Text only: no status, error code, field, route, export or control flow moves.

What this does

The REST layer's refusal envelopes, a boot warning, the served OpenAPI descriptions, a /discovery capability description and the route ledger's notes sent the reader to a tracker number for the reason behind them. In form D, as stage 1 (PR #21172) and the engine lane's stages applied it, the number goes. Where the sentence already said what was decided, only the citation goes. Where it leaned on the number, it now says the decision in words.

All 34 ledgered occurrences in packages/rest (claim 5933139597): rest-route-ledger.ts 29, openapi-builtin-paths.ts 2, import-mapping.ts 1, rest-api-plugin.ts 1, rest-server.ts 1 (the :4830 string only).

Rewritten in words

Where (head line) Cited The text now says Decision read from
import-mapping.ts:93 UNSUPPORTED_TRANSFORM message 2611 "...which the import path does not execute (there is no server-side sandbox), so the import is refused rather than run with that transform skipped" landing commit fce8ff4 (javascript: no server-side sandbox, never silently skipped)
openapi-builtin-paths.ts:152 response description 5588 "This section is built from the routes this server actually mounts ... Per-route payload schemas are not derived here and are deliberately not invented." ruling C, comment 5200114550 (rest produces the built-in section from its own route table) and ACCEPT 5201374820 (no invented schema or status)
openapi-builtin-paths.ts:157 request-body description 5588 "Its shape is route-specific; this document leaves it undescribed rather than invent one." same
rest-route-ledger.ts:178 discovery note 5682 (a PR) "...through the double assertion: the live body parses against DiscoverySchema, and it carries no key the protocol does not declare" PR 5682 body and its reverse-verification comment 5198848731
rest-route-ledger.ts:221 _migrate-stored note 4327 "ADR-0087 stored-row canonicalization, the route form of os migrate meta --stored: it rewrites stored sys_metadata rows in place to their canonical form" card body; commits 83cf2d3 and 8aacf94
rest-route-ledger.ts:228 book-tree note 12038 ("ruling 5A") "...re-exported into /api, never declared there a second time" ruling 5434804846, item 5A
rest-route-ledger.ts:249 GET /meta/:type/:name note 5950 "...the ADR-0010 protection envelope this schema now declares, every key optional because the cached branch never publishes it" landing commit 361bd5b
rest-route-ledger.ts:251 PUT /meta/:type/:name note 12702 adds "so a tenant org admin authors their own org's overlays without platform-wide manage_metadata" card body; ACCEPT 5439334711
rest-route-ledger.ts:267 audit note 11678 (twice) "The schema predates this row: it joined the spec when MetadataProtocol gained its optional auditMetaItem member ... conformance: the audit-door capture suite" card option B; os-dev-report 5405323732 and ACCEPT 5405332618
rest-route-ledger.ts:290 legal-next-state note 9180 "Step 2 of the singular-segment ruling retired the plural ... twin" (the sentence already ends "the /meta type segment is singular, always") ruling in the card body; re-weigh 5311434183
rest-route-ledger.ts:290 same note, last clause 10179 (404) "...meta-state-plural-tolerance.test.ts, which pins both halves as behaviour so this note cannot quietly stop being true" landing commit 53a48c9 (PR #10613)
rest-route-ledger.ts:293 published-snapshot note 7526 "...the fall-through into the compound-name route, before this path had a registration of its own, structurally could not do" card body; ACCEPT 5251269251
rest-route-ledger.ts:313 GET /ui/view note 3611 "the client was moved to the path form both surfaces accept, rather than this server registering the query dialect as a second spelling" card option A
rest-route-ledger.ts:379 search note 8140 "...a near miss that would compile here and be false" card thread (bind only a type verified against the route's actual emit)

Citation only (the sentence already stated the decision)

  • rest-route-ledger.ts:213, :216, :225, :228, :267, :272, :275 (12038, the bracketed prefix): each note goes on to say the schema is a transcription of the producer's declared return, with its conformance suite.
  • rest-route-ledger.ts:221 (12038 ruling 2C): "DELIBERATELY UNBOUND — ... a second declaration in spec would drift against the CLI rendering the same report."
  • rest-route-ledger.ts:293 (12038 ruling 1C): "The named schema is DELIBERATELY OPAQUE (z.unknown()) ... never a union frozen against the type registry."
  • rest-route-ledger.ts:251 (6603) and :253 (7019): "Gated on manage_metadata ... a session alone is no longer enough" and the DELETE reasoning.
  • rest-route-ledger.ts:253, :269, :272 (12702): each already names the shared verdict and the caller's own org partition.
  • rest-route-ledger.ts:362, :379 (11924): "Filled with its conformance coverage", the ruled condition.
  • rest-route-ledger.ts:462 (3610, 7563): "Moved off the bare POST /packages to this path: ..." and "Mounted UNCONDITIONALLY — ... answers an honest 404 on a deployment that composes none."
  • rest-api-plugin.ts:530 (3963): "api.requireAuth was removed and is IGNORED — anonymous access to object data is always denied."
  • rest-server.ts:4830 (1604): the transactionalBatch description keeps ADR-0034, a customer-resolvable reference the gate keeps.

Every cited card was read (REST, open or closed) before its string was rewritten. One answers 404: 10179, read through its landing commit 53a48c9. rest-route-ledger.ts:290's "(10179)" was the string the dead-citation sweep left for this card's form-D stage (commit 04b202e, its Acceptance notes).

Ledger (scripts/doc-authoring-prose-id.baseline.json)

Recomputed with node scripts/check-doc-authoring.mjs --census-ledger (exit 0, no growth refusal) into a scratch file, then copied into place. The diff deletes 30 lines and adds none: exactly the five packages/rest rows. Every other row is byte-identical. After merging origin/main (454bbb6866) the recomputed ledger is byte-identical to the committed one.

before (b9087d77e9) after
packages/rest 34 occurrences, 20 pairs, 5 files 0
whole ledger 584 occurrences, 395 pairs, 152 files 550 occurrences, 375 pairs, 147 files

pnpm check:doc-authoring: before, "487 pinned site(s) across 152 file(s) ... no growth, no burn-down unrecorded"; after, "463 pinned site(s) across 147 file(s) ... no growth, no burn-down unrecorded". No gate is added or loosened; scripts/check-doc-authoring.mjs is untouched.

Changeset

.changeset/20752-rest-strings-state-the-decision.md: patch for @objectstack/rest. Measured after the build: the four new non-ledger sentences are each in dist/index.js and dist/index.cjs, and each old spelling (see framework plus the number, invented ( plus the number, removed ( plus the number, the batch description's number) is in 0 files. The route ledger does not ship: REST_ROUTE_LEDGER and the new ledger sentences are in 0 files under packages/rest/dist.

Text-only proof

A TypeScript-AST skeleton of each changed .ts file, where every string literal and template text is one placeholder, consecutive literal operands of a + chain merge, and comments and JSDoc are never read. b9087d77e9 against the fix commit, and again 454bbb6866 (the merged origin/main) against the head: 5 of 5 SAME, with token and literal-slot counts identical per file. Control: the same tool reports DIFF on import-mapping.ts across 8368f1c005, a real code change.

Pins

No test, fixture or snapshot asserts any of the old strings with its number. Each old fragment was searched repo-wide; the only hits outside the five files are comments, the runtime ledger's twin notes (stage 3) and a checklist anchor. So nothing is re-pinned and there is no ablation to run. The one pin on a rewritten string, rest-config-parse-not-cast.test.ts:407 (toContain of "api.requireAuth was removed"), asserts a substring the rewrite keeps, and runs green in the suite below.

Tests

All on the merged head, through scripts/pm/os-verify-lock.sh, every verdict VERDICT command-exit 0:

  • Build: turbo run build --filter=@objectstack/rest..., 25/25 tasks.
  • @objectstack/rest, both vitest projects (local and repo): 259 files passed (259), 4989 tests passed, 254 skipped. Before the merge: 258 files, 4946 passed, 254 skipped.
  • @objectstack/rest typecheck, including check:test-typecheck: OK.
  • The three @objectstack/client suites that import the ledger as source (client-url-conformance, rest-route-ledger-coverage, route-ledger-response-schema): 3 files, 9 tests passed.

Gates

  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths) at d2b6ede4e9: 66 commands, run one at a time from the worktree, every exit code recorded before any pipe. 66 of 66 exit 0. --ran: "66 derived famil(ies) accounted for — 66 run, 0 NOT-MEASURED (a DERIVED zero — all 66 recorded an exit code and none of them is 3)".
    • check:dual-build-cjs-loads first answered PREREQUISITE NOT MET (exit 3: only the rest closure was built). After a full turbo run build of ./packages/* and ./packages/*/* (71/71 tasks), it and the other four dist-reading gates were re-run, all exit 0: 105 require entry points across 66 packages load; check:dts-closure 71 built packages, 167/167 declaration files present; check:sourcemap-no-sources-content 68 packages, 522 maps; check:lean-entry-closure and check:published-files green.
    • check:doc-authoring: "463 pinned site(s) across 147 file(s) ... no growth, no burn-down unrecorded". check:issue-citations: "no issue citations added against 454bbb6 (5 file(s) read)". check:nul-bytes: OK, 9832 files.
  • Outside the derived set: check:meta-type-normalized (a declared wide-population family whose scan root is packages/rest/src), exit 0, "OK (27 file(s), no raw :type param decisions)". The other declared wide-population families, the artifact roster, the path-scheduled CI jobs and the type-check lanes are CI's. NOT MEASURED: check-issue-citations.mjs --census and the shard-attestation and test-completeness steps, reason: their argv takes values that exist only inside a CI run.
  • pnpm lint (eslint . --no-inline-config, repo-wide, not narrowed) at d2b6ede4e9: exit 0, 119 s under the lock.

Acceptance notes

  • docs/qa/platform-checklist/areas/records-forms.json:4068: an acceptance clause says the UNSUPPORTED_TRANSFORM message "names the missing server-side sandbox and framework" plus the number. The message still names the missing sandbox, and no longer the number. Editing a checklist clause is a semantic edit that bumps the item's revision and history, so it is outside this claim; noted for the checklist's next pass, not filed.
  • The runtime ledger twins of several rewritten notes (packages/runtime/src/route-ledger.ts:497, :507, :509) still carry their numbers. They are stage 3.

Generated by Claude Code

claude added 3 commits October 1, 2026 14:17
…ion in words instead of a tracker number (stage 2)

The 34 ledgered tracker numbers in packages/rest leave runtime strings in
form D: where a sentence leaned on the number it now says what was
decided; where it already said so, only the citation goes. Text only:
no code, status, field, route or control flow moves.

- rest-route-ledger.ts: 29 occurrences across 19 notes
- import-mapping.ts: the javascript-transform refusal
- openapi-builtin-paths.ts: the two OpenAPI description notes
- rest-api-plugin.ts: the retired api.requireAuth warning
- rest-server.ts: the transactionalBatch capability description

The doc-authoring ledger is recomputed with --census-ledger: the rest
rows go to zero, no other row moves (584 -> 550 occurrences).

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

The rewritten refusal, warning and OpenAPI description strings ship in
the package's dist; the route-ledger notes do not (package-internal).

Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/s label Oct 1, 2026
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/rest, touching 10 documentable anchor(s).

11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via packages.uninstall (sdk, the route ledger binds it to DELETE /packages/:id))
  • content/docs/api/environment-routing.mdx (via /packages/:id (route, a path literal in note))
  • content/docs/api/metadata-api.mdx (via /packages/:id (route, a path literal in note))
  • content/docs/data-modeling/formulas.mdx (via /packages/:id (route, a path literal in note))
  • content/docs/deployment/publish-and-preview.mdx (via /packages/:id (route, a path literal in note))
  • content/docs/kernel/contracts/metadata-service.mdx (via /packages/:id (route, a path literal in note))
  • content/docs/permissions/authentication.mdx (via createRestApiPlugin (symbol, a top-level function))
  • content/docs/permissions/permission-sets.mdx (via /packages/:id (route, a path literal in note))
  • content/docs/permissions/system-context.mdx (via /packages/:id (route, a path literal in note))
  • content/docs/plugins/packages.mdx (via createRestApiPlugin (symbol, a top-level function))
  • content/docs/protocol/kernel/error-handling.mdx (via /packages/:id (route, a path literal in note))

⛔ 5 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx (via registerDiscoveryEndpoints (symbol, a method of class RestServer))
  • content/docs/releases/v15.mdx (via /packages/:id (route, a path literal in note))
  • content/docs/releases/v17/17-0.mdx (via /packages/:id (route, a path literal in note))
  • content/docs/releases/v17/17-4.mdx (via /packages/:id (route, a path literal in note))
  • content/docs/releases/v17/17-5.mdx (via packages.get (sdk, the route ledger binds it to GET /packages/:id))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 15 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 2c1cef3345738b5f2486eed5b0608687dba36f11 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 8e5ee098366b1cc0571f06a1a35c8d306f1d0f77 — the merge of head d2b6ede4e9858af25bb72f2c9f211d2823faaf23 into base 2c1cef3345738b5f2486eed5b0608687dba36f11, 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 8e5ee098366b1cc0571f06a1a35c8d306f1d0f77 && git checkout 8e5ee098366b1cc0571f06a1a35c8d306f1d0f77
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2c1cef3345738b5f2486eed5b0608687dba36f11 d2b6ede4e9858af25bb72f2c9f211d2823faaf23 && git checkout -B drift-repro 2c1cef3345738b5f2486eed5b0608687dba36f11 && git merge --no-ff d2b6ede4e9858af25bb72f2c9f211d2823faaf23

node scripts/docs-audit/affected-docs.mjs --json 2c1cef3345738b5f2486eed5b0608687dba36f11

⚠️ 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 2c1cef3345738b5f2486eed5b0608687dba36f11 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: d2b6ede4e9858af25bb72f2c9f211d2823faaf23
Local-runs: none

Reviewed against card #20752 (body and all ten comments), the governing ruling 5902360492 on #20513, triage 5903805251, the seat's staging 5930275362, stage 1's landing 5933112569, the stage-2 claim 5933139597, the os-dev-report 5934217094, the stage-1 precedent (PR #21172 at b9087d77e9 and its PASS record 5932559935), the PR body as re-read from the API, the 7-file net diff git diff 454bbb6866 refs/review/pr-21188 (+42 / −56, matching the API file list), every cited card or, for the one 404, its landing commit, and the head's check-runs. ⛔ Not the dispatch order, not the seat's conclusions.

Check-runs on the head, converged: 34 distinct names (34 runs, one per name, collapsed latest-per-name), all completed, all on d2b6ede4e9: 31 success, 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in)), 0 failed. Lint & Repo Gates — the job that runs pnpm check:doc-authoring (lint.yml:2180) and check:issue-citations — was the last to land, success at 15:32:51Z, after the Test Core rollup and all six shards; Governed Surface Queue Guard, Check Changeset, Part-of PR must not also close its card and The card this PR closes must claim this branch are green.

Merge-tree: git merge-tree --write-tree origin/main refs/review/pr-21188 against a freshly fetched origin/main (ebdb6f2aca) is clean: tree 32b61c208b, exit 0. origin/main is 2 commits past the PR's merged base 454bbb6866; neither touches any of the 7 files, and the ledger at origin/main is byte-identical to the base's.

① Derived judgments

(a) Form D, string by string — holds. All 34 ledgered occurrences were read against their cards (19 cards and one PR, all answering), or for 10179 (404) against its landing commit 53a48c93f4. Each rewritten sentence states what was decided, without inventing a decision:

  • import-mapping.ts:93 (2611): the landing changeset of fce8ff4384 records "UNSUPPORTED_TRANSFORM for javascript (no server-side sandbox — never silently skipped)", and the hunk's own context returns ok: false, status: 400. "there is no server-side sandbox, so the import is refused rather than run with that transform skipped" is accurate.
  • openapi-builtin-paths.ts:152/:157 (5588): ruling C (5200114550) moved the built-in section to rest's own route facts, and the ACCEPT (5201374820) records it is produced from the same table the router matches and that no schema or status is invented. At the head buildBuiltinPaths(this.getRoutes(), basePath) is the call, so "built from the routes this server actually mounts" and "leaves it undescribed rather than invent one" are true of the ruling and of the code.
  • rest-route-ledger.ts:178 (5682): the PR body and its reverse-verification comment 5198848731 define the producer gate as two assertions, DiscoverySchema.parse() on the live body plus a key-set check that nothing undeclared is emitted. The new clause states exactly those two.
  • :221 (4327): the card title and 83cf2d3082 / 8aacf9456c — os migrate meta --stored rewrites sys_metadata rows in place to canonical form, and POST /meta/_migrate-stored is its route form. Accurate.
  • :228 (12038 item 5A): 5434804846 rules "re-export the correct existing schemas into @objectstack/spec/api — never a second copy". "re-exported into /api, never declared there a second time" is that ruling in words.
  • :249 (5950): 361bd5b753 declares the ADR-0010 envelope on GetMetaItemResponseSchema with every key optional because the cached (default) branch never consults the lock resolver; at the head the schema spreads MetadataProtectionEnvelopeFields, each .optional(). Accurate.
  • :251 (12702): the card's title and contract and the ACCEPT 5439334711 — manage_org_presentation admitted by the shared metaWriteCapabilityVerdict only for an allowOrgOverride type, org-scoped to the caller's own active organization, so a tenant org admin authors their own org's overlays without platform manage_metadata. Accurate.
  • :267 (11678, twice): the os-dev-report 5405323732 and ACCEPT 5405332618 — option B, MetadataProtocol gains an optional auditMetaItem member with AuditMetaItemResponseSchema; protocol.test.ts carries the audit-door capture suite. Accurate.
  • :290 (9180): the ruling card's step ② (singular twin, plural registration deleted) and "the /meta type segment is singular, always"; the re-weigh 5311434183 item 3 the sentence goes on to quote is unchanged. Accurate. :290 (10179, 404): 53a48c93f4 adds meta-state-plural-tolerance.test.ts so the note is "checkable rather than trusted" and cannot "quietly stop being true" — the new clause states that, and the dispatcher twin already said the same.
  • :293 (7526): symptom 2 of the card — /published was never registered, fell through to the compound-name route and answered a protection-envelope stub that could not 404; the fix registered it. "before this path had a registration of its own" is accurate.
  • :313 (3611): option A, the client switched to the path dialect, option B (REST registering the query form) not taken; the row's disposition: 'sdk' shows A landed. Accurate.
  • :379 (8140): the near-miss as recorded on Four client SDK routes answer a shape no published contract declares — automation.create / automation.update / search / data.clone #11924 and in the 8140 review 5403917015 — SearchResult types the per-object service and "binding it would typecheck and ship a false declaration". Accurate.
  • Citation-only sites, each already stating the decision (the stage-1 standard, PR fix(driver-sql): refusals, drift reports and log lines state each decision in words instead of a tracker number (stage 1) #20795: "where the sentence already stated what was decided, only the citation goes"): the eight [#12038] prefixes (:213, :216, :221 2C, :225, :267, :272, :275, :293 1C) sit on notes that say "describe-only transcription of X's declared return; conformance: spec api/protocol.test.ts", "DELIBERATELY UNBOUND — a second declaration in spec would drift" (2C) and "DELIBERATELY OPAQUE (z.unknown()) — never a union frozen against the type registry" (1C), which are the ruled items verbatim in substance; :251 (6603) "gated on manage_metadata … a session alone is no longer enough" is ruling B (5225531464, reaffirmed 5229988826); :253 (7019) states the DELETE door's gate and its reason (5231332057); :253/:269/:272 (12702) each name the shared verdict and the caller's own org partition; :362/:379 (11924) "Filled with its conformance coverage" is the ruled condition of 5406810394; :462 (3610, 7563) state the move to /packages/publish and the unconditional mount with an honest 404 (5251781389); rest-api-plugin.ts:530 (3963) states decision A1 — the opt-out removed, anonymous deny unconditional, public surfaces by declaration; rest-server.ts:4830 (1604) states the cross-object atomic batch with intra-batch $ref parent references (4637721260), keeping ADR-0034, a customer-resolvable reference the gate keeps.

(b) Text only — holds. Every hunk in the five .ts files is inside a string literal: the error: template in import-mapping.ts, the two const concatenations in openapi-builtin-paths.ts (re-split with the joins spaced correctly), the logger.warn literal, the description concatenation in rest-server.ts, and 19 note: values in rest-route-ledger.ts. Checked mechanically on the ledger hunks: all 38 changed lines begin note: ', and every route / family / source / disposition / client / responseSchema / authz value on a changed line appears identically on its - and + side — no path, method, schema name, disposition or gate field moves. No #NNNN token remains on any added line. The ledger's own hygiene test reads !e.note (presence) only, unchanged.

(c) The ledger — holds. Computed from the JSON at both ends: base 454bbb6866 (= b9087d77e9 = origin/main for this file) 584 occurrences / 395 pairs / 152 files, packages/rest 34 / 20 / 5; head 550 / 375 / 147, packages/rest 0. Row-level: exactly the five packages/rest rows removed, 0 rows added, 0 counts changed; the diff is 30 deleted lines and none added, and the file stays sorted. The runtime row is untouched at 67. No gate added or loosened: scripts/check-doc-authoring.mjs is not in the diff.

(d) Pins and consumers — holds. Grep of every old fragment across the whole head tree outside the five files (tests, fixtures, snapshots, content/docs/**, docs/qa/**): no test, fixture or snapshot asserts an old string with its number. The only pins on rewritten strings assert substrings the rewrite keeps: rest-config-parse-not-cast.test.ts:407 (toContain('api.requireAuth was removed')) and rest.test.ts:2800 (toContain('POST {basePath}/batch')). The @objectstack/client suites that import REST_ROUTE_LEDGER as source (client-url-conformance, rest-route-ledger-coverage, route-ledger-response-schema) read no .note; neither do the qa dogfood matrices. Residual quotes are comments (client/src/index.ts, runtime/api-mapping.ts:97), the runtime ledger twins (stage 3), test titles in other packages' lanes, and checklist text (③ below).

② Semver level

@objectstack/rest patch, Clause-②: no in the changeset and the PR body — right. The route ledger does not ship — verified from the import graph: src/index.ts is the only tsup entry (root tsup.config.ts), it never mentions the ledger, and the only in-package importers of ./rest-route-ledger are six test files; outside the package only client/qa tests import it. The four non-ledger strings do ship: index.ts exports RestServer and createRestApiPlugin; rest-server.ts imports buildBuiltinPaths (openapi-builtin-paths.ts) and prepareImportRequest (import-prepare.ts, which imports resolveNamedMapping from import-mapping.ts). Caller-visible text only, no status, code, field, route or export moves, so patch is the right level; the changeset's consumer note on the old UNSUPPORTED_TRANSFORM suffix is the fair note for it.

③ Boundary flags

  • Deviations, all four: (1) the stray /build-all.pid at the container's filesystem root is outside the repo and the diff — nothing in the PR; for the user to delete, no action here. (2) no re-pin and no ablation — verified above, no pin asserted an old string. (3) check:dual-build-cjs-loads exit 3 then 0 after a full build — a local-run detail; the head's Build Core is the measurement of record. (4) records-forms.json:4068 left untouched — a revisioned checklist edit outside the claimed surface, correct to leave.
  • Out-of-scope finding 1, real: docs/qa/platform-checklist/areas/records-forms.json:4068 says the message "names the missing server-side sandbox and framework#2611"; at the head the message names the sandbox and not the number, so a checklist run marks that half false. :4103 (source, "the javascript refusal naming defineMapping artifacts are registered but never consumed — wire named mappings into REST import or de-scope from the stack #2611") and :4051 (the steps line, which the dev did not list) also still carry the number. api-backend.json:1013 likewise quotes the old in #3610 note text in a source pointer. None is a pin; all are for the checklist's next authoring pass, as the dev notes.
  • Out-of-scope finding 2, confirmed: packages/runtime/src/route-ledger.ts:497 ([#7019], [#12702]), :507 ((#4327), (#12038 ruling 2C)) and :509 (#9180, (#10179)) are the dispatcher twins; every one of those numbers is in the ledger's runtime row at the head (#7019 1, #12702 1, #4327 1, #12038 11, #9180 2, #10179 1), which is stage 3 by the seat's staging.
  • Closing keyword: the body opens Part of #20752, no closing keyword; Part-of PR must not also close its card and The card this PR closes must claim this branch are green. Right, since stages 3–5 remain.
  • Governed paths: none of the 7 files is under docs/adr/, .claude/, skills/, AGENTS.md, CLAUDE.md or docs/NORTH-STAR.md (the GOVERNED_SURFACES register at the head); Governed Surface Queue Guard is green. Draft, base main, mergeable_state: blocked, as expected before this record.

Implemented-by: claude/issue-20752-stage2-rest-strings
Reviewed-by: session_01VvcEokUG1tvVxkceYfR5XB

VERDICT: PASS

@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 36885837931 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Temporal Conformance (live PG + MySQL) — 失败步骤: Run the non-SQL temporal backends under the skewed process zone

    packages/services/service-analytics test:  FAIL  src/__tests__/caller-content-admission-door.test.ts > [#21177] caller-content admission — the /analytics/query door > 'NativeSQLStrategy' > 'no securit
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    packages/services/service-analytics test:  FAIL  src/__tests__/caller-content-admission-door.test.ts > [#21177] caller-content admission — the /analytics/query door > 'NativeSQLStrategy' > 'admin — pr
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    packages/services/service-analytics test:  FAIL  src/__tests__/caller-content-admission-door.test.ts > [#21177] caller-content admission — the /analytics/query door > 'NativeSQLStrategy' > 'non-admin 
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    packages/services/service-analytics test:  FAIL  src/__tests__/caller-content-admission-door.test.ts > [#21177] caller-content admission — the /analytics/query door > 'ObjectQLStrategy' > 'no security
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    packages/services/service-analytics test:  FAIL  src/__tests__/caller-content-admission-door.test.ts > [#21177] caller-content admission — the /analytics/query door > 'ObjectQLStrategy' > 'admin — pro
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    packages/services/service-analytics test:  FAIL  src/__tests__/caller-content-admission-door.test.ts > [#21177] caller-content admission — the /analytics/query door > 'ObjectQLStrategy' > 'non-admin —
      ↳ 失败原因: packages/services/service-analytics test: AssertionError: expected Error: [Analytics] Access denied: member … { …(4) } to match object { code: 'INVALID_FIELD', …(3) }
    packages/services/service-analytics test:  FAIL  src/__tests__/field-read-admission-gate.test.ts > [#20965] the field-level read gate — a member that names no field is refused, never stood down > 'Nat
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    packages/services/service-analytics test:  FAIL  src/__tests__/field-read-admission-gate.test.ts > [#20965] the field-level read gate — a member that names no field is refused, never stood down > 'Obj
      ↳ 失败原因: packages/services/service-analytics test: AssertionError: expected Error: [Analytics] Access denied: member … { …(4) } to match object { code: 'INVALID_FIELD', …(2) }
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 2 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

The merge-queue build for this PR (run 36885837931) failed in Temporal Conformance (live PG + MySQL), and the failure is not this PR's. The seat re-queues it once.

domain:cli seat · session_01VvcEokUG1tvVxkceYfR5XB · 2026-10-01T15:59Z


Generated by Claude Code

Merged via the queue into main with commit f115b1f Oct 1, 2026
36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20752-stage2-rest-strings branch October 1, 2026 16:38
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…e policies — double accumulation, the PostgreSQL boolean cast and the empty-sum fold, hoisted into core (objectstack-ai#21042) (objectstack-ai#21209)

Fixes objectstack-ai#21042
Clause-②: yes (widening)

## What this changes

The analytics native-SQL strategy (`NativeSQLStrategy`, the default on a
SQL driver) skipped three aggregate policies that
`SqlDriver.aggregate()` applies. So one route answered different
numbers, or a `500`, depending on which strategy served it. This PR
follows the route ruling on objectstack-ai#21042 (comment `5925613967`): the operand
policies are hoisted into `@objectstack/core`, beside
`AGGREGATE_ANSWER_KIND`, and both faces read them from there.

- **`packages/core/src/utils/aggregate-answer.ts`**
(`@objectstack/core`, `minor`). It takes:
- `AGGREGATE_ACCUMULATION`, moved from `driver-sql` with its docblock
(the docblock and the table are byte-identical to the base, apart from
the `export` keyword);
- `aggregandColumnClass({ type, multiple })`, the one column-class
predicate (`'fractional'`, `'integral'`, `'boolean'`, or none);
- `POSTGRES_BOOLEAN_AGGREGAND_CAST`, the objectstack-ai#11635 cast, as a `Record` over
`AggregationFunction`: `sum` / `avg` / `min` / `max` are cast, the
counts never are;
- `doubleAccumulationOperand(operand, dialect)`, the double operand with
the dialect as a parameter;
- `aggregandOperandSql(func, columnClass, dialect, operand)`, the one
composition both faces emit (the cast inside, the double operand around
it).
  No `index.ts` line was added: the module is already exported.
- **`packages/drivers/driver-sql/src/sql-driver.ts`** (`patch`), in the
ruled regions only. The `AGGREGATE_ACCUMULATION` table becomes a pointer
plus an import. `isFractionalNumericType` is deleted. The two registry
fills fill `fractionalNumericFields` through the predicate.
`accumulatesInDouble` / `doubleAccumulationOperand` are replaced by
`aggregandColumnClassOf`, which maps the driver's registries onto the
predicate's classes. In `aggregate()`, the private boolean-cast
condition and the accumulation call become one
`aggregandOperandSql(funcName, class, this.dialectName, '??')`.
-
**`packages/services/service-analytics/src/strategies/native-sql-strategy.ts`**
(`patch`), in two places.
- **`resolveMeasureSql`** wraps the column it hands `AGGREGATE_SQL` /
`CONDITIONAL_AGGREGATE_SQL` in `aggregandOperandSql`. The column class
comes from the declaration the host already relays
(`declaredValueShape`), on the object the column lives on
(`columnObjectOf`, the one hop resolver). The dialect comes from
`sqlDialect`, which the strategy already reads.
- **The `execute` shaping point** that PR objectstack-ai#21040 added now folds a
`null` measure answer to `emptyGroupValueFor(measure.type)`
(`@objectstack/spec`). It does this for every measure, measure-scoped
ones included, before the number presenter, in `driver-sql`'s order. The
dataset door's `DatasetExecutor` fill stays, and it is idempotent on a
folded row.
- The one line outside those two regions is the `generateSql` call site,
which now passes `ctx` to `resolveMeasureSql`.
- No native copy of any policy, and no runtime hook asks the driver: the
rejected (C) route was not taken. `canHandle`, `buildFieldMeta`, the
hop-object sites, the filter / text-match rendering,
`analytics-service.ts` and `field-read-admission.ts` are untouched.

## The card's table, before and after

Measured through `AnalyticsService.query` (the cube door, which `POST
/api/v1/analytics/query` relays verbatim) and
`AnalyticsService.queryDataset` (the dataset door), on
`AnalyticsServicePlugin` over a real ObjectQL engine and `SqlDriver`.
**Native** is the plugin's own composition (`NativeSQLStrategy`
answered, with one raw statement and no engine aggregate). **ObjectQL**
is the same composition narrowed to `engine.aggregate`. "Before" is the
strategy file at the base `d34aa58a2a`; "after" is this branch at
`61aab5013a`. Neither merge since then touches the aggregate code paths,
and the pins below are green at `ef1f9d8484`.

Fixture:

- group `f`: `frac` (a `number` column) holds 0.1 and 0.2, and `flag`
holds true and false;
- group `i`: `stars` (a `rating` column) holds seven 1s and two 2s, and
`flag` holds 7 trues and 2 falses;
- group `n`: every aggregand is NULL in all three rows.

The measure-scoped measures filter on `tag = x`, which only group `f`
holds.

**PostgreSQL 16.13** (a private local server; the ObjectQL column is the
same before and after):

| measure | group | door | native before | native after | ObjectQL |
|:--|:--|:--|:--|:--|:--|
| `sum(frac)` | f | cube, dataset | `0.3` | `0.30000000000000004` |
`0.30000000000000004` |
| `avg(frac)` | f | cube, dataset | `0.15` | `0.15000000000000002` |
`0.15000000000000002` |
| `avg(stars)`, an integer column | i | cube, dataset |
`1.222222222222222` | `1.2222222222222223` | `1.2222222222222223` |
| `sum(flag)` | i | cube, dataset | `500 DATABASE_ERROR` | `7` | `7` |
| `avg(flag)` | i | cube, dataset | `500 DATABASE_ERROR` |
`0.7777777777777778` | `0.7777777777777778` |
| `min(flag)` / `max(flag)` | i | cube, dataset | `500 DATABASE_ERROR` |
`0` / `1` | `0` / `1` |
| `sum(frac)`, all-NULL group | n | cube | `null` | `0` | `0` |
| `sum(frac)`, all-NULL group | n | dataset | `0` (executor fill) | `0`
| `0` |
| `sum(flag)`, all-NULL group | n | cube | `500 DATABASE_ERROR` | `0` |
`0` |
| `avg(frac)`, all-NULL group | n | cube, dataset | `null` | `null` |
`null` |
| measure-scoped `sum(frac)`, no admitted row | i | cube | `null` | `0`
| `0` |
| measure-scoped `sum(frac)`, no admitted row | i | dataset | `0`
(executor fill) | `0` | `0` |
| measure-scoped `avg(frac)`, no admitted row | i | cube | `null` |
`null` | `null` |
| `count` control | n | cube, dataset | `3` | `3` | `3` |
| measure-scoped `count` control | i | cube, dataset | `0` | `0` | `0` |

**SQLite** (better-sqlite3): accumulation and the boolean answers
already agreed on every face (`0.30000000000000004`,
`0.15000000000000002`, `1.2222222222222223`, `7`, `0.7777777777777778`,
`0` / `1`). The fold is the policy that diverged there:

| measure | group | door | native before | native after | ObjectQL |
|:--|:--|:--|:--|:--|:--|
| `sum(frac)` / `sum(stars)` / `sum(flag)`, all-NULL group | n | cube |
`null` | `0` | `0` |
| `sum(frac)` / `sum(stars)` / `sum(flag)`, all-NULL group | n | dataset
| `0` (executor fill) | `0` | `0` |
| measure-scoped `sum(frac)`, no admitted row | i, n | cube | `null` |
`0` | `0` |
| measure-scoped `sum(frac)`, no admitted row | i, n | dataset | `0`
(executor fill) | `0` | `0` |

After the fix, the native and ObjectQL faces **differ in 0 of 144
cells** (2 drivers × 2 doors × 12 measures × 3 groups).

**MySQL is NOT MEASURED**: there is no MySQL server in this container.
The MySQL operand text is pinned offline: by `core`'s
`aggregate-answer.test.ts`, and by the `driver-sql` move proof for the
driver's own statements.

## The move proof

`driver-sql`'s aggregate statements were dumped at the base, before any
consumer changed. The dump covered `SqlDriver.aggregate()` for every
function (`count`, `count_distinct`, `sum`, `avg`, `min`, `max`, and
`count(*)`), aliased and unaliased, over 23 columns: every fractional,
integral and boolean type, the `float` / `integer` / `int` aliases,
multi-valued and untyped columns, and text / date / lookup / formula. It
ran on SQLite, PostgreSQL and MySQL, through both registration paths
(`registerObjectMetadata` and `registerExternalObject`), offline (knex
`toSQL()`).

The policies were then hoisted, `driver-sql` was switched to the
imports, and the same dump was run again:

- base dump: 1668 entries, 0 errors, md5
`8eee668372a28a7568f3eb1cc5a2bc9b`;
- after dump (at `bc8aa0cc2f`): 1668 entries, md5
`8eee668372a28a7568f3eb1cc5a2bc9b`. `cmp` printed nothing: the two dumps
are **byte-identical**.

`sql-driver.ts` and `aggregate-answer.ts` are unchanged between
`bc8aa0cc2f` and `61aab5013a`.

The committed move-proof pin,
`packages/drivers/driver-sql/src/sql-driver-21042-aggregate-policy-move.test.ts`,
holds the captured expressions for one column of each class, on each
dialect and through each registration path. It passed at the base
(`192fc0010b`: 54 / 54) and passes after (54 / 54).

## Pins (committed red first, then the fix)

| file | at the pins commit (`192fc0010b`, base code) | after |
|:--|:--|:--|
| `core` `aggregate-answer.test.ts` | 16 red (the exports did not exist)
| 22 / 22 |
| `service-analytics` `native-sql-aggregate-policies.test.ts` (each
measure on both faces at both doors, against the engine's arithmetic;
SQLite and live PostgreSQL cells) | SQLite: the cube-door folds red.
PostgreSQL: accumulation, boolean `500`, folds red | 49 / 49 |
| `service-analytics` `cube-measure-field-type-door.test.ts`, **the
lifted skip** | PostgreSQL native `max(boolean)` red (`500`) | 23 / 23 |
| `rest` `analytics-dataset-aggregate-policies-door.test.ts` (the route,
both strategies) | PostgreSQL: 7 red (accumulation and booleans). SQLite
green (that door already folded) | 19 / 19 |
| `driver-sql` move proof | 54 / 54 | 54 / 54 |

**The lifted skip:** `it.skipIf(cell.id === 'pg' && face === 'native')`
in `cube-measure-field-type-door.test.ts` (from PR objectstack-ai#21128) is gone. Its
comment now says why the cell runs on every cell and face. The
PostgreSQL native `max_flag` cell answers `1`.

`native-sql-measure-number-presentation.test.ts` gets a comment-only
edit: its header said the native statement does not carry objectstack-ai#20387's
accumulation, and it now points at the new pin.

## Ablations

There was one ablation per policy, each predicted in writing before it
ran. Each mutation was planted through `scripts/ablation-replace.mjs`,
which checks that the anchor hit and that the blob changed. Each
mutation was confirmed in the built `dist/`
(`ablation-dist-preflight.mjs`: marker present). Each restore ran by
absolute path (`git checkout HEAD`), and the file's blob was proven
equal to its `HEAD` blob with `git diff HEAD` empty. After each restore
the package was rebuilt, and the marker was proven absent from `dist/`
with the tree clean. Every prediction held exactly.

| ablation | mutation | predicted red | observed red |
|:--|:--|:--|:--|
| A1 accumulation | `core` `accumulatesInDouble`'s dialect gate never
admits PostgreSQL or MySQL | `core` 3; `driver-sql` move proof 24 (pg
and mysql × both fills × the six numeric / boolean columns);
`service-analytics` 10, PostgreSQL only (both doors × `sum` /
`avg(frac)`, `avg(stars)`, measure-scoped `sum` / `avg`); `rest` 3,
PostgreSQL only | the same 3 / 24 / 10 / 3; SQLite cells green;
`avg(flag)` green as predicted |
| A2 boolean cast | `core` `aggregandOperandSql` never casts | `core` 1;
move proof 4 (pg × both fills × `boolean` / `toggle`);
`service-analytics` 8, PostgreSQL only; the lifted cell, PostgreSQL
native and ObjectQL, 2; `rest` 4, PostgreSQL only | the same 1 / 4 / 8 /
2 / 4 |
| A3 fold | the native shaping point never folds | `service-analytics`
8, cube door only (SQLite and PostgreSQL × the three all-NULL `sum`s and
the measure-scoped `sum`); everything else green, the `rest`
dataset-door route included, because the executor fill folds there | the
same 8; `rest` 19 / 19 green |

A1 and A2 show one policy reaching both faces. Each one turned
`driver-sql`'s own statements red. Under A2 the ObjectQL face's
`max(boolean)` cell failed too, with the driver's refusal. Under A1 the
ObjectQL face answered the same exact decimal as the native face for the
plain measures: the failing assertion was the engine's number, while
native and ObjectQL still agreed.

A **reverse type check** also ran. Passing a dialect the new type
rejects (`'oracle'`) to `aggregandOperandSql` turned
`service-analytics`' typecheck red (`TS2345 ... not assignable to
parameter of type 'AggregandSqlDialect'`), which shows the rebuilt
`core` `.d.ts` was read. The file was restored byte-identical.

## Verification (at `ef1f9d8484`, after merging `origin/main` at
`cb45469e67`, which carries PR objectstack-ai#21170 and PR objectstack-ai#21173)

Everything below ran as one locked script, at `ef1f9d8484`, with each
exit code captured before any pipe. The live PostgreSQL 16.13 server ran
at `timezone = Asia/Shanghai`, and the `driver-sql` suite ran under
`TZ=America/New_York`, which are its own non-vacuity preconditions.

- **Refresh after the merge:** `pnpm turbo run build
--filter='!@objectstack/docs' --concurrency=1` exit 0, and `pnpm
--filter @objectstack/spec check:generated` exit 0.
- **Gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 67 commands from the real diff (10
paths). All 67 exited 0. Reconciliation with `--ran` and the recorded
exit codes printed: `Run reconciliation — 67 derived, 67 run, 0
NOT-MEASURED, 0 UNRUN.`
- **Typecheck:** `pnpm --filter … typecheck` exit 0 for
`@objectstack/core`, `@objectstack/driver-sql`,
`@objectstack/service-analytics` and `@objectstack/rest`.
- **Tests, every vitest project of every touched package** (`vitest run
--maxWorkers=2`, with `OS_TEST_POSTGRES_URL` set so the live cells ran):

| package / project | files | tests |
|:--|:--|:--|
| `core` local | 74 passed | 2120 passed |
| `core` repo | 3 passed | 48 passed |
| `driver-sql` | 216 passed, 3 skipped | 4300 passed, 96 skipped |
| `service-analytics` | 161 passed | 3765 passed |
| `rest` local | 256 passed | 5027 passed, 127 skipped |
| `rest` repo | 5 passed | 179 passed, 1 skipped |

- **The five pin files, run explicitly:** move proof 54 / 54, `core` 22
/ 22, policies 49 / 49 (24 SQLite + 24 PostgreSQL + the oracle),
field-type door 23 / 23, `rest` route 19 / 19 (9 SQLite + 9 PostgreSQL +
the oracle).
- **Lint, narrowed and proven:** `eslint --no-inline-config --format
json` over the 9 changed code files answered 9 results, 0 errors and 0
warnings. The population is read from eslint's own config:
`ESLint.isPathIgnored` answers `false` for each of the 9. The narrowing
excludes nothing that could move, because `eslint.config.mjs` never
enables type-aware linting (no `parserOptions.project`, no typed rules),
so this diff cannot change the verdict on an untouched file. The
repo-wide `pnpm lint` is CI's.
- **The `driver-sql` live preconditions:** the first gate run used a
private server at UTC, and the suite's four timezone non-vacuity cells
failed by design (`… start it with timezone=Asia/Shanghai`). With the
server at `Asia/Shanghai` and the process at `America/New_York` the
suite is green, as listed above.
- **`main` after the final merge:** `origin/main` moved 4 commits past
`cb45469e67` before this PR opened (objectstack-ai#21149, objectstack-ai#21188, objectstack-ai#21195, objectstack-ai#21192).
None of them touches `core`, `driver-sql`, `service-analytics` or the
analytics `rest` tests. The one `packages/spec` file in the analytics
area, `ui/dataset.zod.ts`, changes a comment only. They are not merged
here; CI runs on the merge ref.

## Acceptance notes

- **MySQL is NOT MEASURED** (no server in this container). The MySQL
operand text is pinned offline in `core` and in the `driver-sql` move
proof.
- **The live PostgreSQL cells are not run in CI.** No CI step sets
`OS_TEST_POSTGRES_URL` for `service-analytics` or `rest`. The cells
above ran against a private PostgreSQL 16.13 started for this run and
removed afterwards. In CI the SQLite cells run, and so do the offline
`core` / move-proof pins.
- **Phase 0's note on the dataset door, which is no divergence:** a
measure-scoped `avg` is **absent** from a row its supplementary query
reported no row for. That is `x_avg_frac` for groups `i` and `n`, on
both strategies and before and after. It is not `null`. The new service
pin holds this cell only to "both faces agree", not to a value.
Relatedly, a dataset-door selection made only of measure-scoped measures
reports only the groups their filter admits, so the pin asks each one
beside the base count.
- **Residual, as stated in the ruling:** a host that relays no field
declarations (`declaredValueShape`), or names no SQL dialect, gets no
column class or no policy. It keeps the native arithmetic it had, and a
PostgreSQL boolean `sum` there still answers `500`. The plugin's own
composition wires both.
- **How `driver-sql` reads the class:** it reads its own registries
rather than calling the predicate per column. `fractionalNumericFields`
is filled by the predicate. `booleanFields` and `numericFields` are
filled by the driver's coercion rules, whose populations equal the
predicate's `'boolean'` and `'fractional'` ∪ `'integral'` classes. The
move proof pins that equality per column class. Asking the predicate per
column through the driver's `valueShapeFields` would retire
`fractionalNumericFields`, but its declaration and shard-alias regions
are outside the ruled surface, so this PR does not do it.
- **The scan-order residual is unchanged.** On PostgreSQL and MySQL the
double sums are added without compensation (`AGGREGATE_ACCUMULATION`'s
docblock), so three or more fractions can still differ in the last place
from SQLite and the rows path. The pins use two addends.
- objectstack-ai#21129 (the presenter for a relationship-path `min` / `max`) is not
addressed here.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
… each decision in words instead of a tracker number (stage 3) (objectstack-ai#21219)

Part of objectstack-ai#20752
Clause-②: no

**Stage 3 of 5 of the `domain:cli` lane under the maintainer's A / A
ruling (5902360492): the `packages/runtime` strings.** The card stays
open for stages 4 and 5, so this PR carries no closing keyword. Text
only: no status, error `code`, field, route, export or control flow
moves, and the dispatcher error vocabulary keeps every key and code.

## What this does

Refusals, boot errors, log lines, endpoint hints, the dispatcher error
vocabulary's `why` text and the route ledger's notes in
`packages/runtime` sent the reader to a tracker number for the reason
behind them. In form D, as stages 1 and 2 (PR objectstack-ai#21172, PR objectstack-ai#21188) applied
it, the number goes. Where the sentence already said what was decided,
only the citation goes. Where it leaned on the number, it now says the
decision in words.

All 67 ledgered occurrences in `packages/runtime` (claim `5935998841`),
in 55 string sites: `route-ledger.ts` 42,
`dispatcher-error-vocabulary.ts` 10, `standalone-stack.ts` 3,
`sandbox/body-runner.ts` 3, `endpoint-executor.ts` 2,
`action-execution.ts` 2, and one each in `resolve-project-database.ts`,
`endpoint-policy.ts`, `domains/auth.ts`, `domains/activation-gate.ts`
(the fold site from the dead-citation sweep) and `app-plugin.ts`.

Every cited card was read first. Three answer 404 and were read through
the commit that decided them: 8885 (`30b1c636a2`, which registered the
nine template-generated approvals codes), 10243 (`266436a7f`, which made
flow enablement an authoring write) and 10179 (`53a48c93f4`, through
stage 2's wording). Two are objectui cards (6593, 5933) that this
session cannot open; they were read through the records in this
repository that cite them (see Acceptance notes).

### Rewritten in words

| Where (head line) | Cited | The text now says | Decision read from |
|---|---|---|---|
| `action-execution.ts:2476` doubled-navigation warning | 11519 |
"...two post-success destinations for one success, a pair the contract
refuses rather than ranks." | ruling 5393507405 (refuse the doubled
channel, no precedence field) |
| `action-execution.ts:2477` same warning | objectui 5933 | "...is
ignored (the interim precedence the console renderer applies, which no
contract promises)" | card 11519 body and ruling 5393507405; the spec
refusal's own wording, "the interim precedence" |
| `app-plugin.ts:1781` seed tenancy warning | 8686 | "seed tenancy
handoff failed: the seed rows were not stamped with the new
organization, so seed and API writes stay on separate autonumber
counters until the next boot's migration repairs it" | ruling 5299880350
(seed writes carry the organization the way API writes do; one counter
per object) |
| `dispatcher-error-vocabulary.ts:349` `APPROVAL_*_FAILED` row | 8885
(404) | "All nine codes the family produces are registered in the
ledger, and this row's pin is what keeps that true" | landing commit
`30b1c636a2` |
| `dispatcher-error-vocabulary.ts:356` same row | 9223 | "the scan
reports a template-spelled code under its family identity rather than
dropping it, so it SEES the template" | os-dev-report 5312437754, ACCEPT
5312453178 (`objlittemplate` shape) |
| `dispatcher-error-vocabulary.ts:520` `YOU_CANNOT_IMPERSONATE_ADMINS`
row | 9968 | "The in-repo re-implementation of the vendor's
impersonation handler, which admits an ADR-0068 platform admin, makes it
reachable for the first time" | rulings 5353915975, 5380748215; contract
review 5363785802 |
| `dispatcher-error-vocabulary.ts:597` `OS_METADATA_CONVERTED` row |
12772 | "the artifact-ingestion forward-conversion policy, which runs
the ADR-0087 conversions over an artifact built by older tooling before
its strict parse" | os-dev-report 5448148598, review 5447759290 |
| `dispatcher-error-vocabulary.ts:680` `owd_widening_forbidden` row |
9232 | "it rides the wire in TWO fields because the flat REST door
narrows like every other door" | ruling 5315734845 (the flat door
demotes too) |
| `dispatcher-error-vocabulary.ts:690` same row | 9460 | "Invisible to
BOTH vocabulary gates until the scan learned the code-carrying helper
shape" | ACCEPT 5327577494, landing note 5327948790 (`codehelper` shape)
|
| `dispatcher-error-vocabulary.ts:696` same row | 9106, 9460 half (2) |
"the call between renaming it and keeping the demote (the closed member
in `code`, this spelling in `declaredCode`) is the `packages/spec`
lane's; until that lane registers a code the standing demote answers
this spelling, and the call is NOT decided here" | ruling 5307569301
(demote to `declaredCode`); triage 5326829216 (half 2 settled by the
standing rule, registration is the spec lane's) |
| `domains/activation-gate.ts:279` enablement refusal | 10243 (404) |
"...for as long as it stays off, and the switch is not scoped to the
caller's organization." | commit `266436a7f` (no organization wall
scopes the enabled bit; one activation row per deployment) |
| `domains/auth.ts:143` sanitised-500 log line | 5085 | "...answered
with a sanitised 500: the message is withheld unconditionally, and this
line is where the original error is read" | ACCEPT 5200514639 |
| `resolve-project-database.ts:314` legacy-file notice | 6469 | "Reading
legacy database file ... — dev, start and migrate now share one default,
...; migrate with: ..." | ruling 5225112344 (one default, legacy files
read with a loud notice) |
| `sandbox/body-runner.ts:126` missing-logger warning | 7448 | "Pass
`logger` to ...BodyRunnerFactory({ … }): the capability writes only to
that logger, never to `console`, so the host's level and sinks apply." |
report 5251672873 (serve the capability from the factory's logger) and
the function's own docblock |
| `standalone-stack.ts:675` no-dispatch-arm guard | 3276 | "falling
through to SQLite would hand the caller a database engine they never
selected." | card body; commit `cfb549db8` |
| `route-ledger.ts:321` discovery note | 5682 (a PR) | "...holds to the
double assertion: the value parses against `DiscoverySchema`, and it
carries no key the protocol does not declare" | PR 5682 body, comment
5198848731; stage 2's REST wording |
| `route-ledger.ts:330`, `:332` analytics notes | 3584 | "...so the
client moved to this route rather than the dispatcher growing an alias"
/ "...so the client aligned to the dispatcher rather than the dispatcher
growing an alias" | landing commit `0bab8bb454` (analytics: client
aligns to the dispatcher) |
| `route-ledger.ts:402` publish note | 12038 ("ruling 5A") |
"...re-exported into `/api`, never declared there a second time" |
ruling 5434804846 item 5A; stage 2's wording |
| `route-ledger.ts:405` publish-drafts note | 9406 ruling | "`probes` is
deliberately opaque in the declaration, upgraded to a modeled schema
only when a consumer needs a field of it" | ruling 5322875103 |
| `route-ledger.ts:427` duplicate note | objectui 6593 | "(a console
that read the envelope `success` reported a partial or empty duplicate
as done)" | `DuplicatePackageResponseSchema` docblock and the client's
`duplicate` comment, which record that defect |
| `route-ledger.ts:435`, `:441` automation notes | 10243 ruling | "since
the ruling that enablement is an authoring write" / "since the
2026-08-23 ruling that enablement is an authoring write" | commit
`266436a7f` |
| `route-ledger.ts:441` toggle note | 10145 | "The definition-write gate
deliberately left this one out as engine state and filed the question" |
landing commit `128684d500` ("toggle ... filed separately rather than
folded into a security fix") |
| `route-ledger.ts:435`, `:441` | 5519 | "the domain-wide anonymous
floor" | card 5519 (anonymous 401 gate before dispatch, ACCEPT
5195955348); the clone row's existing wording |
| `route-ledger.ts:497` dispatcher PUT note | 12702 | stage 2's twin
wording: "...own active organization, so a tenant org admin authors
their own org's overlays without platform-wide `manage_metadata`" |
ACCEPT 5439334711 |
| `route-ledger.ts:507` migrate-stored note | 4327 | stage 2's twin
wording: "ADR-0087 stored-row canonicalization, the route form of `os
migrate meta --stored`: it rewrites stored `sys_metadata` rows in place
to their canonical form" | card body; commits `83cf2d3082`, `8aacf9456c`
|
| `route-ledger.ts:509` state/:field note | 9180 (x2), 10179 (404) |
stage 2's twin wording: "Step 2 of the singular-segment ruling ...",
"the maintainer re-weigh of the singular-segment ruling", "pins BOTH
halves as behaviour so this note cannot quietly stop being true" | card
9180 ruling; landing commit `53a48c93f4` |
| `route-ledger.ts:545` object-less action note | 3913 | "the
object-less spelling of the global-action call, routed rather than
refused" | comment 5117039474 (`'global'` canonical; `/actions//:action`
routes instead of 400-ing) |
| `route-ledger.ts:549` activation note | 7526 | "(the live-mount parity
gate, which asks the running router, caught it)" | ACCEPT 5251269251 |
| `route-ledger.ts:578` apps note | 5040 E5b | "on a match runs the full
chain: the policy keys ..., then target delegation" (the program's stage
labels go with the citation) | card 5040 program table |
| `route-ledger.ts:584` apps note | 5040 E7 | "LIVE since publish
flipped from refusing to executing" | card 5040 comment 5176642864 (E7
flip) |

### Citation only (the sentence already stated the decision)

- `route-ledger.ts`: 12038 at `:408`, `:411`, `:414`, `:417` (ruling
3A), `:421` (ruling 4A), `:424`, `:427`, `:500` (ruling 1C), `:503`,
`:507` (ruling 2C, stage 2's twin wording); 9406 at `:405`; 10145 at
`:435`, `:470`, `:472`; 3801 and 5561 at `:447`; 3656 at `:476`; 3718 at
`:480` (x2); 7019 at `:497` (stage 2's twin wording); 7526 at `:537`;
12160 at `:548`.
- `dispatcher-error-vocabulary.ts:437`, `:456` (5085).
- `endpoint-executor.ts:232`, `:255` and `endpoint-policy.ts:263` (5040
E7, section 7-3, section 3.3): the hints and the warning already say
publish rejects the form, that `script` and `proxy` wait for their own
rulings, and that `cacheTtlSeconds` is GET-only.
- `sandbox/body-runner.ts:391` (4352), `:469` (4345).
- `standalone-stack.ts:152`, `:423` (3276).

## Text only, proven on the AST

A skeleton of each changed file's TypeScript AST, with every maximal
string expression (a literal, a template, or a `+` chain of them)
collapsed to its list of embedded non-literal expressions, is identical
before and after for all 14 changed files (BASE `5e5ce48cef` against
`a67666a600`; the later merge of `origin/main` touches none of them).
Re-wording or re-splitting literal text cannot move it; an identifier,
operator, property name, embedded expression or statement change does.
Control: changing `ACTIVATION_DENY_STATUS` to `ACTIVATION_DENY_STATUS +
1` in a copy of `activation-gate.ts` moves the hash.

A literal can still hide a code or a route, so every changed string
chain was also paired against its old twin and located: 48 chains
changed, and each sits in prose: 28 `note:` values and 6 `why:` values,
2 `hint:` values, 1 `notice:`, 6 log-call messages, 1 `deps.error`
message argument (the code argument is an identifier, untouched), 2 `new
Error` messages and 2 message builders' `return` values, plus 4 test
assertions. Control: flipping one `disposition: 'sdk'` to
`'server-only'` in a copy is reported as `prop:disposition`.

## The ledger

`node scripts/check-doc-authoring.mjs --census-ledger`, written to a
scratch file first so its no-growth check reads the committed baseline,
then installed:

| | occurrences | (file, id) pairs | files |
|---|--:|--:|--:|
| `packages/runtime` before | 67 | 41 | 11 |
| `packages/runtime` after | 0 | 0 | 0 |
| whole ledger before | 550 | 375 | 147 |
| whole ledger after | 483 | 334 | 136 |

63 lines deleted, 0 added; every other row is byte-identical. After
merging `origin/main` (`e18fea6dcd`) the recomputed ledger is
byte-identical to the committed one. `pnpm check:doc-authoring`: before
"463 pinned site(s) across 147 file(s) ... no growth, no burn-down
unrecorded", after "408 pinned site(s) across 136 file(s) ... no growth,
no burn-down unrecorded".

## Pins, and that they can fail

Three tests asserted an id. Each now asserts the words that carry the
decision, and each was ablated through `scripts/ablation-replace.mjs`
(anchor must hit, blob must change, restore proven by blob equal to HEAD
and an empty `git diff HEAD`). The tests import the source by relative
path, so no build sits between the mutation and the run.

| Pin | Asserts now | Ablation (source text removed) | Result |
|---|---|---|---|
| `http-dispatcher.actions-doubled-redirect.test.ts:101` | "the interim
precedence the console renderer applies" | that phrase | 1 failed / 13
passed |
| `http-dispatcher.actions-doubled-redirect.test.ts:102` | "a pair the
contract refuses rather than ranks" | that phrase | 1 failed / 13 passed
|
| `endpoint-executor.test.ts:212` | "rejected at publish pending their
own rulings" | that phrase | 2 failed / 48 passed |
| `sandbox/body-runner.test.ts:369` | "`body` only runs for `type:
'script'`" | "only runs for" | 5 failed / 31 passed |

Restored, the three files run 3 passed (3), 100 tests passed. Three of
the first ablation attempts were refused by the tool before any test ran
(the replacement text was a substring of the anchor, so its count could
not rise, and the file was restored); they were re-run with distinct
markers, and only the re-runs are reported above.

## What ships

Measured on the built `dist/index.js` and `dist/index.cjs`: every
rewritten non-ledger sentence is present once in each, and none of the
old spellings is. `ROUTE_LEDGER` and `UNREGISTERED_CODE_SITES` are
absent from both: the route ledger and the vocabulary table are
runtime-internal and do not ship. So the changeset,
`@objectstack/runtime` `patch`, covers the refusal, error, warning and
notice text, and the ledger and vocabulary notes ride along unpublished.

## Verification

Heavy runs went through `scripts/pm/os-verify-lock.sh` (slot
`issue-20752-s3`); each verdict line reads `VERDICT command-exit 0`.

- Build: `turbo run build --filter=@objectstack/runtime...` 30/30 before
the merge; after merging `origin/main` (`e18fea6dcd`), the whole
workspace except docs, 72/72.
- `@objectstack/runtime`, both vitest projects (`local` + `repo`): 305
files passed (305), 5080 passed / 11 skipped, at `7eca8ebff6` and again
at the merged head `5a3cfda26d`. 305 is every `*.test.ts` in the
package.
- `@objectstack/runtime` typecheck (`tsc --noEmit` and
`check:test-typecheck`): exit 0, before and after the merge.
- Consumers that import the route ledger as source:
`@objectstack/client` `client-url-conformance`, `route-ledger-coverage`,
`route-ledger-response-schema`: 3 files, 9 tests passed;
`@objectstack/dogfood` `route-ledger-live-mount-parity.dogfood.test.ts`:
1 file, 8 tests passed (after the full build). None of them reads a
`note`; `route-ledger.conformance.test.ts` (in the runtime run) checks
that every non-`sdk` row still carries one.
- `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` (no paths), derived at `a67666a600`: 75 commands, each run
at the merged head `5a3cfda26d` from the worktree with its exit code
recorded before any pipe, 75/75 exit 0. The dist-reading nine
(`check:dts-closure`, `check:dual-build-cjs-loads`, `check:entry-guard`,
`check:lean-entry-closure`, `check:published-files`,
`check:sourcemap-no-sources-content`, `check:test-source-alias`,
`check:type-check-coverage`, `check:type-check-debt`) ran after the full
build. `--ran`, which recomputed the same 75 at `5a3cfda26d`: "75
derived famil(ies) accounted for — 75 run, 0 NOT-MEASURED (a DERIVED
zero — all 75 recorded an exit code and none of them is 3)".
- `check:dispatcher-error-vocabulary`: "OK — 55 unregistered
code-stamping site(s), all classified". `check:issue-citations`: "no
issue citations added against e18fea6 (11 file(s) read)".
`check:nul-bytes`: OK, 9854 files.
- `pnpm lint` (`eslint . --no-inline-config`, repo-wide, not narrowed)
at `5a3cfda26d`: exit 0, 127 s.

## Acceptance notes

- **Two cited cards live in objectui and could not be opened here.**
`objectui#6593` and `objectui#5933` answer 403 to this session (the
repository is not enabled for it). Their decisions were read through the
records in this repository that cite them:
`DuplicatePackageResponseSchema`'s docblock and the client's `duplicate`
comment (6593: a console that read the envelope `success` reported a
partial or empty duplicate as done), and card 11519's body, ruling
5393507405 and the spec refusal message in `ui/action.zod.ts` (5933: the
console renderer's interim declared-wins precedence). The rewritten
sentences say no more than those records do.
- **Comments keep their numbers.** Only strings are in the ledger; the
`//` and docblock citations in these files are the sanctioned home for
an id and are untouched.
- **The checklist still reads true.**
`docs/qa/platform-checklist/areas/access-security.json:2195` and
`api-backend.json:1567` quote "functionally equivalent to deleting it"
from the enablement refusal; that phrase is kept, so neither clause
changes truth.
- **Out of this stage's files:**
`packages/lint/src/validate-action-body-writes.test.ts:316` pins `objectstack-ai#4345`
in the lint rule's own message. That string is in the ledger's
`packages/lint` row, not this stage's; it rides whichever stage takes
that row. Noted, not filed.
- **Stages left on the card:** 4 (`verify`, `plugin-dev`,
`plugin-hono-server`, 14 occurrences) and 5 (`qa`, 102).

---
_Generated by [Claude
Code](https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
… tenancy refusals and no-API warning state each decision in words instead of a tracker number (stage 4) (objectstack-ai#21231)

Part of objectstack-ai#20752
Clause-②: no

**Stage 4 of 5 of the `domain:cli` lane under the maintainer's A / A
ruling (5902360492): the `packages/verify`, `plugin-dev` and
`plugin-hono-server` strings.** The card stays open for stage 5 (`qa`),
so this PR carries no closing keyword. Text only: no status, error
`code`, exit code, route, field, export, control flow, or `verify`
verdict or count moves.

## What this does

The `objectstack verify --rls` report, its persona-provisioning refusals
and the records its probe writes, the verify harness's organizations
remedy, the dev plugin's tenancy refusals and no-auth warning, and the
Hono server's no-API warning sent the reader to a tracker number for the
reason behind them. In form D, as stages 1 to 3 applied it (PR objectstack-ai#21172,
PR objectstack-ai#21188, PR objectstack-ai#21219), the number goes. Where the sentence already said
what was decided, only the citation goes. Where it leaned on the number,
it now says the decision in words.

All 14 ledgered occurrences in the claim's four files (claim
`5938321786`), at 13 string sites: `rls.ts` 9 (1994 x3, 7685 x3, 7978
x3), `dev-plugin.ts` 3 (4818 x2, 3963 x1), `harness.ts` 1 (4719),
`hono-plugin.ts` 1 (4073). Every cited card was read first; all seven
answer, and each decision was cross-read against its landing commit.

### Rewritten in words

| Where (head line) | Cited | The text now says | Decision read from |
|---|---|---|---|
| `rls.ts:908` report header | 1994 | `=== objectstack verify (RLS /
cross-owner by-id-write invariant) — APP ===` | card 1994 (a by-id
update or delete must pass the row-level write filter: a member can no
longer change a record it cannot see), and this module's header, which
names that invariant |
| `rls.ts:768` `rls-hole` detail | 1994 | "...by-id write bypassed RLS,
and a caller that cannot read a record must not be able to write it" |
card 1994; the module header's "A user who CANNOT READ a record must not
be able to WRITE it" |
| `rls.ts:431` probe-persona refusal | 1994 | "...masked by the object
gate and a by-id write that bypasses RLS is unreachable." | card 1994
for the class; the 7685 half is citation-only (below) |
| `rls.ts:943` position-persona report line | 7978 | `── position
personas (each holds one declared position and nothing else) — N of M
declared position(s) probed` | card 7978 and landing commit `f5434b0ea4`
(one persona per declared position, each holding that position and
nothing else) |
| `dev-plugin.ts:934-935` no-auth warning | 3963 | "...anonymous access
to object data is always denied, with no setting that turns that off." |
card 3963 decision A1 and landing commit `3c628ce647` (the
`api.requireAuth` opt-out is retired; anonymous data access is denied
unconditionally); stage 1's twin wording in `os serve` |

### Citation only (the sentence already stated the decision)

- `rls.ts:357` probe RLS policy description and `rls.ts:464` probe
`sys_permission_set` description (7685): each already says the probe
holds object read+edit plus an owner-scoped narrowing so a refusal is
the record gate's. Read from card 7685 item (i) and landing commit
`be37f859bc`. The 7685 citation at `rls.ts:431` goes the same way.
- `rls.ts:553` position-persona refusal and `rls.ts:575`
`sys_user_position` reason (7978): each already says position-gated
policies only apply to a persona holding the position.
- `dev-plugin.ts:874` construct-stage refusal and `dev-plugin.ts:1000`
init-stage refusal (4818): each already says `OS_ALLOW_DEGRADED_TENANCY`
covers an absent multi-org runtime, not a present one that declined.
Read from card 4818 and landing commit `29326f8eea`; stage 1 dropped the
twin citation in `os serve` the same way.
- `harness.ts:626` `bootStack` remedy (4719): it already says the app's
declaration is what is checked and that a package reachable only through
NODE_PATH or a hoisted store is not accepted. Read from card 4719 and
landing commit `02dc076927`.
- `hono-plugin.ts:683` no-API boot warning (4073): it already says the
plugin is a transport adapter that serves neither API. Read from card
4073 and landing commit `e5a4d26901`.

The `rls.ts` report header was on the ledger, so it is rewritten to name
the invariant it proves rather than losing its anchor.

## Text only, proven on the AST

A scratch script (not committed) parses each changed TypeScript file at
BASE `7c5a311a58` and at head `36281b515a`, folds every `+` chain made
only of string literals into one value, blanks every string value and
template span, and compares the remaining node sequence (kinds,
identifiers, numerals). Result: identical skeleton in all six files,
string-value counts equal (dev-plugin 220, hono-plugin 126, harness 117,
rls 212, the two tests 105 and 24), and 15 changed values, each of them
prose: 2 `description:` values, 1 `reason:`, 1 `detail:`, 2 report
lines, 4 `new Error` messages, 2 logger `warn` messages, 1 remedy
string, and the 2 test assertions. The later merge of `origin/main`
touches none of these files.

The dev plugin's no-auth warning first gained a fifth literal; commit
`03a490204b` re-wrapped the sentence across the original four so the
skeleton stays equal.

## The ledger

`node scripts/check-doc-authoring.mjs --census-ledger`, written to a
scratch file first so its no-growth check reads the committed baseline,
then installed:

| | occurrences | (file, id) pairs | files |
|---|--:|--:|--:|
| the four rows before | 14 | 7 | 4 |
| the four rows after | 0 | 0 | 0 |
| whole ledger before (`7c5a311a58`, also `d6d6e872e5`) | 399 | 279 |
107 |
| whole ledger after | 385 | 272 | 103 |

15 lines deleted, 0 added; every other row is byte-identical. After
merging `origin/main` (`d6d6e872e5`) the recomputed ledger is
byte-identical to the committed one. `pnpm check:doc-authoring`: before
"338 pinned site(s) across 107 file(s) ... no growth, no burn-down
unrecorded", after "325 pinned site(s) across 103 file(s) ... no growth,
no burn-down unrecorded".

## Pins, and that they can fail

Two tests asserted an id. Each now asserts the words that carry the
decision, and each was ablated through `scripts/ablation-replace.mjs` on
the committed fix (anchor must hit once, blob must change, restore
proven by blob equal to HEAD and an empty `git diff HEAD`). Both tests
import the source by relative path, so no build sits between the
mutation and the run.

| Pin | Asserts now | Ablation | Result |
|---|---|---|---|
| `plugin-dev/src/dev-plugin-optional-load-failure.test.ts:283` |
"always denied, with no setting that turns that off" | "turns that off"
to "turns that on" in `dev-plugin.ts` | 1 failed / 9 passed |
| `plugin-hono-server/src/hono-transport-only.test.ts:92` | "transport
adapter and serves neither" | "serves neither" to "serves nothing" in
`hono-plugin.ts` | 1 failed / 4 passed |

Restored, the two files run 10 passed and 5 passed.

No other pin asserts any of the old strings: the whole repository was
searched (`packages/**` including `qa` and dogfood, `examples`,
`docs/qa/**`, snapshots and JSON). The dogfood RLS suites print
`formatRlsReport` only as an assertion message, and
`docs/qa/platform-checklist/RUNNER.md` quotes the summary lines and `N
of M declared position(s) probed`, which are unchanged.

## What ships

Measured on the built `dist/` of each package: every rewritten sentence
is present (in both the ESM and CJS bundles) and no old spelling is. All
three packages publish `dist`, so the changeset carries `patch` for
`@objectstack/verify`, `@objectstack/plugin-dev` and
`@objectstack/plugin-hono-server`.

## Verification (head `36281b515a`, after merging `origin/main`
`d6d6e872e5`)

Heavy runs went through `scripts/pm/os-verify-lock.sh` (slot
`issue-20752-s4`); each verdict line reads `VERDICT command-exit 0`
unless stated.

- Build: `pnpm turbo run build` over the three packages and their
dependency closures, 38/38 tasks.
- Tests: `@objectstack/plugin-hono-server` 27 files / 324 tests,
`@objectstack/plugin-dev` 9 / 86, `@objectstack/verify` 16 / 120, all
passed (before and after the merge). The dogfood RLS runner oracle
(`packages/qa/dogfood/test/rls-runner.test.ts`, which drives the
`rls-hole` path against the rebuilt `@objectstack/verify` dist): 17
passed.
- Typecheck: `typecheck` of all three packages, each `tsc --noEmit` plus
`check:test-typecheck` (the test layer, the two pins included) OK.
- Gates: `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` derived 71 commands; all 71 run
with exit codes recorded; `--ran` reconciliation: "71 derived famil(ies)
accounted for — 71 run, 0 NOT-MEASURED". `check:dual-build-cjs-loads`
first answered PREREQUISITE NOT MET (exit 3, no dist for unrelated
packages); after a full workspace build (72/72 tasks, 71 cached) it
passed and that rerun is the recorded result.
- Lint, a proven narrowing: eslint's own config places 6 of the 8
changed paths in its population (`isPathIgnored` false and a matching
config object; the changeset and the JSON ledger are outside it).
`--no-inline-config` with the json formatter: 6 file results, 0 errors,
0 warnings. Invariance: this repo's config enables no type-aware linting
(`eslint.config.mjs`, the note at lines 326-328) and its only disk reads
are two baselines this diff does not touch, so no untouched file's
verdict can move. The full `pnpm lint` is CI's.

## Acceptance notes

- The seat's staging comment (5930275362) listed a dead tracker number
in the `plugin-dev` test title
`dev-plugin-security-enforcement-warning.test.ts:121` for this stage.
Claim `5938321786` names four files and not that one, and a test title
is not on the ledger, so it is not touched here. Carrier: the seat, when
it stages what remains.
- Code comments in the same four files still cite these cards. They are
not on the ledger, and the claim keeps them out of scope.
- `packages/qa/dogfood/test/enterprise-organizations.ts:184` carries the
4719 twin of the harness remedy. It sits in the `qa` row, which stage 5
owns.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…rmance ledgers and harness refusals state each decision in words instead of a tracker number (stage 5a) (objectstack-ai#21250)

Part of objectstack-ai#20752
Clause-②: no

**Stage 5a of the `domain:cli` lane under the maintainer's A / A ruling
(5902360492): every ledgered tracker number in the `packages/qa` strings
outside the authz conformance matrix, plus the `plugin-dev` test title
folded in from the dead-citation sweep.** The matrix file (76
occurrences) is stage 5b, which carries the closing keyword, so this PR
has none. Text only: no expected status, error `code`, verdict, route,
field, export or control flow moves.

## What this does

The dogfood harness refusals (`assertArmed`, the build stand-in, the
showcase security helper, the multi-org remedy), the expression and
search conformance ledgers' summary and enforcement cells, nine fixture
manifests and one permission-set label, and the downstream-contract
manifest sent the reader to a tracker number for the reason behind them.
In form D, as stages 1 to 4 applied it (PR objectstack-ai#21172, PR objectstack-ai#21188, PR objectstack-ai#21219,
PR objectstack-ai#21231), the number goes. Where the sentence already said what was
decided, only the citation goes. Where it leaned on the number, it now
says the decision in words.

All 26 ledgered occurrences in the claim's 16 files (claim
`5940325318`), at 25 string sites, plus the one `plugin-dev` `describe`
title the claim adds (not on the ledger). Every cited card was read
first. Three answer 404 or cannot be read here and were read through
their landing commits instead (below).

### Rewritten in words

| Where (head line) | Cited | The text now says | Decision read from |
|---|---|---|---|
| `dogfood/test/armed.ts:136-138` empty-declaration refusal | 8074 |
"...the exact defect class this helper exists to close, a fixture that
passes while the control it measures is not engaged." | card 8074 (an
org-less fixture cannot observe the gated write floor, so a real 403
records as a passing cell; direction 2: a helper that refuses) |
| `dogfood/test/armed.ts:162` DISARMED refusal | 8074 | the bracketed
tracker tag becomes `assertArmed():`, the same lead the
empty-declaration refusal already uses; the rest of the sentence already
said the assertions would pass without testing anything | card 8074 |
| `expression-conformance.ledger.ts:144` `sharing-condition` summary |
1887 | "(ADR-0058 D3: compiled from the authored CEL, a faithful
lowering rather than a divergent hand-written filter)" | card 1887 (the
spec condition was never compiled; enforce or remove) and ADR-0058 D3
(the CEL condition compiles to `criteria_json`, a faithful lowering of
the authored CEL) |
| `expression-conformance.ledger.ts:293` `settings-visibility`
enforcement | 7327 | "The spec DECLARES that same grammar
(`SettingsVisibilityInputSchema`) rather than CEL, so it is refused at
publish/parse too" | card 7327 (narrow the declaration to the grammar
the save-time evaluator implements, measured 1 against 93) |
| `expression-conformance.ledger.ts:303`
`cel-action-param-option-visible` summary | 5016 |
"(params[].options[].visibleWhen, the same per-option key a field's
option list declares)" | card 5016, maintainer ruling B (reuse the field
option vocabulary), landed as commit `f6609e6ae2`; `ui/action.zod.ts`
records the per-key outcome (`visibleWhen` opened) |
| `fixtures/attachments-fixture.ts:140` manifest | 2755 | "...exercising
the non-admin attachment permission matrix: ..." | card 2755 item 2
(dogfood the non-admin attachment permission matrix) |
| `fixtures/comments-fixture.ts:139` manifest | 4630 | "...exercising
the record-level comment permission matrix: ..." | card 4630
(`sys_comment` gains record-level authorization, mirroring attachments)
|
| `fixtures/label-scope-fixture.ts:55` manifest | 3602 | "Deal → vendor
lookup exercising the dimension-label read scope: a vendor the reader
cannot read is shown by raw id, never by name." | card 3602's first
residual (the per-record label read behind a grouped lookup carried no
read scope); the fixture's own header |
| `fixtures/rls-owner-fixture.ts:57` manifest | 1994 | "...exercising
the cross-owner by-id-write invariant: a caller that cannot read a
record must not be able to write it." | PR 1994 (a by-id write must pass
the row-level write filter); stage 4's wording of the same invariant in
`verify --rls` |
| `fixtures/rls-owner-fixture.ts:105` permission-set label | 1994 | `RLS
Fixture Member — owner-scoped reads only (no write policy: the
by-id-write hole shape)` | PR 1994; the fixture header (owner policy on
SELECT only, the hole class's authoring shape) |
| `dogfood/test/showcase-security.ts:66` refusal | 5491 | "...the CLI
wiring these fixtures model cannot be reproduced, and the platform
baseline alone grants a member no object access" | card 5491, maintainer
ruling of 2026-08-07 (the wildcard grant leaves `member_default`; the
baseline is explicit-allow) |
| `downstream-contract/src/stack.ts:19` manifest | 2035 | "Frozen
third-party consumer gating spec backward compatibility: a spec change
that needs this fixture edited to stay green is breaking." | landing
commit `92647c13aa` (the downstream-consumer contract, frozen: a spec
change that requires editing it is breaking), the work done under card
2035; the package README states the same contract |

### Citation only (the sentence already stated the decision)

- `dogfood/test/build-shaped-artifact.ts:192`, `:225`, `:265` (6293,
answers 404): the three refusals already say what a stand-in must do
instead of `JSON.stringify` (fix the walk, never the assertion; a
headless husk is what a plain stringify leaves; a function left in the
artifact would be dropped without a sound). Read from landing commit
`c39a911ae6` (fixtures get the real lowering, and the stand-in throws
naming what went missing).
- `dogfood/test/enterprise-organizations.ts:184` multi-org remedy
(4719): the twin of the `verify` harness remedy that stage 4 rewrote
citation-only. It already says the app's own declaration is what counts
and a transitive reach is not enough. Card 4719 (option 2: the host
declaration decides).
- `expression-conformance.ledger.ts:293` (7310): "Fail-closed since"
plus the card becomes "Fail-closed:"; the sentence goes on to say a
predicate outside the grammar refuses the save. PR 7310.
- `expression-conformance.ledger.ts:319` (objectui card 3067):
"selection-bar bulk action per-record eligibility
(bulkActionDefs[].visible)". This session has no read access to
`objectstack-ai/objectui` (403) and the dispatch forbids attaching it,
so the decision was read from this repository's record:
`ui/bulk-action.zod.ts`'s `visible` describe (evaluated once per
selected record; the button is offered when at least one passes) and the
row's own enforcement cell, both unchanged.
- `expression-conformance.ledger.ts:343`, `:350` (objectui card 2614):
the two summaries already say "per-record visibility" and "per-record
disabling" of the built-in row Edit/Delete. Read from this repository's
landing commit `627f225f2c` (`userActions.edit/delete` accept per-record
CEL predicates).
- `fixtures/email-template-materialization-fixture.ts:59` (4509) and
`fixtures/webhook-materialization-fixture.ts:55` (3461): each already
says the stack entries materialize into the rows the runtime reads;
`ADR-0054` stays.
- `fixtures/flow-durable-suspend-fixture.ts:101` (4470): already says
the flow suspends, persists and resumes after a cold boot.
- `fixtures/flow-function-effect-fixture.ts:69` (4396): already says the
declared effect reaches the run summary.
- `fixtures/flow-runas-fixture.ts:125` (1888): already says `flow.runAs`
identity is enforced.
- `dogfood/test/search-conformance.ledger.ts:49` (4254): already says a
name outside the set is a 400 at the REST ingress, not silently dropped.
- `plugin-dev/src/dev-plugin-security-enforcement-warning.test.ts:121`
`describe` title (10036, answers 404; the fold site from the
dead-citation sweep, ACCEPT `5939681859`): the bracketed tag goes; the
title already says the warning must fire when `SecurityPlugin.start()`
bailed. Read from landing commit `7552e03375` (the warning probes the
published `security` service in `start()`).

## Text only, proven on the AST

A scratch script (not committed) parses each changed TypeScript file at
BASE `a7d9768ecd` and at `75e33f4c45` (the stage commit; the later merge
of `origin/main` touches none of these files), blanks every string value
and template span, and compares the remaining node sequence (kinds,
identifiers, numerals). Result: identical skeleton in all 17 files,
equal node and string-value counts per file, and 28 changed string
values, all prose: the 26 sites above plus two neighbouring literals of
the `armed.ts:136-138` refusal, re-wrapped so the message keeps its four
literals.

## The ledger

`node scripts/check-doc-authoring.mjs --census-ledger`, written to a
scratch file first so its no-growth check reads the committed baseline,
then installed:

| | occurrences | (file, id) pairs | files |
|---|--:|--:|--:|
| the 16 stage rows before | 26 | 21 | 16 |
| the 16 stage rows after | 0 | 0 | 0 |
| `authz-conformance.matrix.ts` (stage 5b) | 76 | 46 | 1 |
| whole ledger before (`a7d9768ecd`) | 385 | 272 | 103 |
| whole ledger after | 359 | 251 | 87 |

53 lines deleted, 0 added; every other row is byte-identical, the matrix
row included. After merging `origin/main` (`ef96c9ede7`) the recomputed
ledger is byte-identical to the committed one. `pnpm
check:doc-authoring`: before "325 pinned site(s) across 103 file(s) ...
no growth, no burn-down unrecorded", after "300 pinned site(s) across 87
file(s) ... no growth, no burn-down unrecorded".

## Pins

No test asserts any of the old strings: every rewritten fragment and
every cited number inside an assertion was searched across the
repository, with no hit outside the sites themselves. The two existing
pins that read rewritten messages, `armed.dogfood.test.ts` matching
`this fixture is DISARMED` and `arming declaration is EMPTY`, still
match, and pass. With nothing re-pointed there was no pin to ablate. The
`merge-queue-triage` job-log fixtures under `scripts/fixtures/` quote
the old `describe` title as recorded CI output; they are history and
stay as they are.

The run itself shows one rewritten string live: the multi-org skip line
in the dogfood run now prints "...being reachable as somebody else's
transitive dependency is not enough. Set
OS_TEST_MULTI_ORG_ENABLED=1...".

## What ships

Nothing. `@objectstack/dogfood` and `@objectstack/downstream-contract`
are `private: true`. `@objectstack/plugin-dev` publishes `dist`, and its
built `dist/` holds the new `describe` title 0 times; the control, the
warning's own `NOT enforced` text, is found in `dist/index.js`. So no
changeset, and the PR takes `skip-changeset`.

The downstream-contract fixture is frozen against spec-driven edits (its
README). This edit is prose in the manifest description, made for this
ruling and not to make a spec change pass.

## Verification (head `3efe6499b8`, after merging `origin/main`
`ef96c9ede7`)

Heavy runs went through `scripts/pm/os-verify-lock.sh` (slot
`issue-20752-s5a`); each verdict line reads `VERDICT command-exit 0`.

- Build: `pnpm turbo run build` over the dependency closures of
`@objectstack/dogfood`, `@objectstack/downstream-contract` and
`@objectstack/plugin-dev`, 63/63 tasks, before and after the merge.
- Tests, before and after the merge: `@objectstack/plugin-dev` 9 files /
86 tests passed (the renamed `describe` ran under the verbose reporter
with its 4 tests green); `@objectstack/downstream-contract` 3 / 31
passed; `@objectstack/dogfood`, every test file that imports a changed
module directly (46 files, run in two batches): 45 passed, 1 skipped
(`rls-multitenant`, which needs the enterprise organizations package
this repository does not ship), 388 tests passed, 3 skipped. The full
dogfood suite is CI's `Dogfood Regression Gate`.
- Typecheck: `@objectstack/dogfood` (`tsc --noEmit`; `--listFiles` shows
all 15 changed dogfood files in its program),
`@objectstack/downstream-contract`, and `@objectstack/plugin-dev` (`tsc
--noEmit` plus `check:test-typecheck`, which compiles the test layer:
OK).
- Gates: `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` derived 67 commands; all 67 run
with exit codes recorded; `--ran` reconciliation: "67 derived famil(ies)
accounted for — 67 run, 0 NOT-MEASURED". `check:dual-build-cjs-loads`
first answered PREREQUISITE NOT MET (exit 3, no dist for unrelated
packages); after a full workspace build (72/72 tasks, 71 cached) it
passed, and that rerun is the recorded result.
- Lint: the full `pnpm lint` (`eslint . --no-inline-config`) at
`3efe6499b8`: exit 0, no findings.

## Acceptance notes

- `expression-conformance.ledger.ts:322` is a comment that says the bulk
row "reached the ledger in" one card "not" another, both spelled bare.
The second is objectui's card, and a bare number reads as this
repository's. It is a comment, not on the ledger, so it is untouched
here. Carrier: the dead-citation sweep.
- `packages/qa/downstream-contract/package.json`'s `description` and its
README still cite card 2035, and dogfood test titles and `it` names
still carry tracker numbers. None of these is on the ledger or the
claim. Carrier: the seat, when it stages what remains after 5b.
- Code comments in the 17 files still cite these cards. They are not on
the ledger, and the claim keeps them out of scope.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB)_

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…n words instead of a tracker number (stage 5b) (objectstack-ai#21265)

Fixes objectstack-ai#20752
Clause-②: no

**Stage 5b, the last stage of the `domain:cli` lane under the
maintainer's A / A ruling (5902360492): the authz conformance matrix.**
Stages 1 to 5a (PR objectstack-ai#21172, PR objectstack-ai#21188, PR objectstack-ai#21219, PR objectstack-ai#21231, PR objectstack-ai#21250)
emptied every other row of this lane; this PR empties the last one, so
the lane's share of the ledger burn-down is zero once it lands. Text
only: no row id, `state`, `covers` key, `proof` file or enforcement site
moves, and the file's code comments are untouched.

## What this does

The `summary`, `enforcement` and `note` strings of
`packages/qa/dogfood/test/authz-conformance.matrix.ts` sent the reader
to a tracker number for the reason behind a row. In form D, as the
earlier stages applied it, the number goes. Where the sentence already
said what was decided, only the citation goes. Where it leaned on the
number, it now says the decision in words.

All 76 ledgered occurrences of 46 cards, at 43 string sites (claim
`5942403129`, ledger read at `a23be7498e`): 16 `summary`, 10
`enforcement`, 17 `note` strings. 22 sites now say something in words;
21 already stated the decision and only lose the citation.

Every cited card was read first. Six answer 404 and were read through
the commit that decided them: 10243 (`266436a7f`), 12176 (`7986d973f`),
11757 (`4d25d22d4`), 8710 (`04d03c3a0`), 8711 (`2ce1eb41b`) and 8811
(`d6e793507`). The 10145 and 10243 phrases on the automation row reuse
stage 3's `route-ledger.ts` wording for the same decisions, and the 5519
floor is named the way stage 3 named it (the domain-wide anonymous
floor, refused before dispatch).

Two notes cite `describe` titles in other packages that carry a
bracketed tag. Those titles are quoted without the tag, so each quote
stays a literal substring of the real title and is still findable by
search.

### The 43 sites (line numbers are the same at base and head)

| Site | Cited | Form | The text now says (new fragment) | Decision read
from |
|---|---|---|---|---|
| `:161` `rls-by-id-write` summary | 1994 | citation only | by-id write
enforcement | PR 1994 (RLS re-checked on the pre-image of a by-id
update/delete) |
| `:162` `rls-by-id-write` enforcement | 7665 | citation only |
write-scope DERIVATION: when no update/delete-class policy applies |
card 7665 via PR 7792 (an empty write-class policy set derives its scope
from the caller's select narrowing); the sentence after the colon
already says it |
| `:164` `rls-by-id-write` note | 7685, 7665, 7665, 7792, 7685 | words
(tag dropped; three phrases worded) | Re-verified ... — that was the
hole through which a contributor PATCHed records it could not read, and
... since the fix that derives a missing write scope from the select
narrowing, that file carries ... whose probe persona holds object
read+edit narrowed by select-only RLS and so reaches this class — | card
7665 (a by-id write was not gated by record visibility under select-only
RLS); PR 7792 (option A: derive the write scope from the select
narrowing); card 7685 comment 5264791326 (measurement first: a probe
persona with object read+edit narrowed by select-only RLS; both rows
stay enforced) |
| `:174` `controlled-by-parent` note | 7685, 7665 | tag dropped; one
phrase worded | Re-verified as `enforced` on its OWN evidence ... when
the select-derived write-scope derivation its master depends on is
ablated. | card 7685 comment 5264791326 (re-verified by measurement, not
downgraded); card 7665 / PR 7792 |
| `:177` `multi-tenant-write-postimage` summary | 2937 | words | (forged
INSERT / Finding 1 re-point — a forged OR re-pointed organization_id
cannot cross the tenant wall) | card 2937 (an INSERT carrying a forged
organization_id crossed the tenant wall; Layer 0 gains an insert
post-image check) |
| `:179` `multi-tenant-write-postimage` note | 2937, 2937, 2937 |
citation only + suite titles named in words | INSERT a forged
cross-tenant organization_id or UPDATE ... (the "Layer 0 insert
post-image tenant guard" suite + the Finding 1 "Layer 0 update
post-image tenant guard (cross-tenant re-point)" suite) | card 2937; the
two describe titles in plugin-security/authz-matrix-gate.test.ts, quoted
without their bracket tags so each stays a substring of the real title |
| `:181` `multi-tenant-exemption-posture` enforcement | 2956 | citation
only | reads the carried ctx.posture rung (ADR-0099 D1) | PR 2956 (carry
the derived posture rung on ExecutionContext); the phrase 'the carried
ctx.posture rung' already says it |
| `:182` `multi-tenant-exemption-posture` note | 2937 | suite title
named in words | (the Finding 2 "Layer 0 cross-tenant exemption requires
the platform posture" suite + "ADR-0099 P1 ...") | the describe title in
plugin-security/authz-matrix-gate.test.ts |
| `:195` `org-write-validation` note | 2937 | words | — the
forged-organization_id INSERT defect one call site down. | card 2937 |
| `:213` `anonymous-deny-meta` summary | 2567 | words | (uniform
anonymous posture, surface 1) | card 2567 (the anonymous-deny posture
must be uniform across every HTTP surface that reaches ObjectQL) |
| `:228` `anonymous-deny-meta` note | 11373, 12176 | citation only +
words | For most of this row's life ... five since the retirement of
slash-bearing metadata item names un-mounted the compound save | card
11373 (measure first; the note goes on to state the measured refusal);
card 12176 answers 404, read through landing commit 7986d97 (stage 3
of the ruled retirement of slash-bearing metadata item names: un-mounts
the compound arities) |
| `:236` `anonymous-deny-actions` summary | 2567, 5519 | words |
(uniform anonymous posture, surface 2: refused 401 before dispatch, as
`/data` and `/meta` are) | card 2567; card 5519 (anonymous /actions and
/automation requests are refused 401 before dispatch, the same baseline
as /data and /meta) |
| `:244` `anonymous-deny-actions` note | 5519 | words | — before the
gate, an anonymous `POST /actions/showcase_task/showcase_mark_done/:id`
was measured answering 200 with the update applied. | card 5519 |
| `:245` `anonymous-deny-automation` summary | 2567, 5519 | words |
(uniform anonymous posture, surface 3: refused 401 before dispatch, as
`/data` and `/meta` are) | card 2567; card 5519 |
| `:246` `anonymous-deny-automation` enforcement | 10145, 10243, 7900,
3801, 5561 | words (stage 3 route-ledger twin wording) | DELETE /:name`,
the definition writes on the metadata plane, plus enablement `POST
/:name/toggle` since the 2026-08-23 ruling that enablement is an
authoring write, ... the run-state reads (the `sys_automation_run` read
grant) and `resume` (keyed on the node the run is suspended on,
fail-closed for a node that declares no resumeAuthority) carry their own
| card 10145 (flow definition writes are authored metadata, so
manage_metadata gates them); card 10243 answers 404, read through
landing commit 266436a (enablement is an authoring write, ruling of
2026-08-23); card 7900 via commit 627e65a (run-state reads consult
the sys_automation_run read grant); card 3801 (resume gated on the
suspended node); card 5561 (no declared resumeAuthority fails closed);
the 10145 and 10243 phrases reuse stage 3's route-ledger.ts wording |
| `:253` `anonymous-deny-automation` note | 5519 | words | which the
original anonymous-surface report did not record. | card 5519 |
| `:263` `anonymous-deny-packages` summary | 7033, 7023 | citation only
| anonymous-deny on the package-management surface | cards 7033 and 7023
(the /packages domain carried no authorization predicate; the row's
enforcement already states the domain-wide gate); same shape as the
sibling analytics row |
| `:300` `realtime-delivery-authz` summary | 2992 | words | (a latent
surface: identity admission is owed before any client transport ships) |
card 2992 (GraphQL and realtime must satisfy identity admission before a
client transport ships) |
| `:302` `realtime-delivery-authz` note | 9083, 9083 | tag dropped +
words | Clearing that red ... Before that admission rule landed, this
note promised a gate that did not exist | card 9083 (a TRANSPORT-WIRED
key may be classified only by an enforced row); the sentence after the
tag already says it |
| `:310` `mcp-http-identity` enforcement | 2698 | citation only | (403
on none) | card 2698 (OAuth 2.1 for /api/v1/mcp, scope-gated tool
families); the sentence already says it |
| `:320` `mcp-http-identity` note | 3167 | citation only | proven
end-to-end: the proof boots | card 3167 (identity admission first); the
sentence after the colon states what the proof drives |
| `:332` `readonly-static-write` summary | 2948, 3003, 3043 | citation
only + words | UPDATE AND INSERT (first at the data-write ingress;
in-engine ...) | cards 2948 and 3003 (strip static readonly on
non-system UPDATE); card 3043 (tighten the INSERT exemption, first
enforced at the data-write ingress) |
| `:333` `readonly-static-write` enforcement | 2948, 5591 | citation
only | (caller-supplied VALUES only — ... the caller also sent) | card
2948; card 5591 (strip the caller-supplied value, never a hook stamp);
the parenthetical already says both |
| `:335` `readonly-static-write` note | 3003, 3043, 3003 | words | The
originating field report: ... The INSERT face followed: ... a step
SHORTER than the draft-then-PATCH route, | card 3003 (readonly was
UI-only; a non-admin self-approved by PATCH); card 3043 (the INSERT
exemption let the same caller POST an approved record) |
| `:340` `declarative-rbac-seeding` summary | 2077 | citation only |
seeded at boot | card 2077 (activate declarative roles + sharingRules at
runtime) |
| `:355` `ownership-anchor-guard` summary | 3004 | citation only |
without the transfer grant | card 3004 (owner_id is system-managed for
non-privileged writers) |
| `:358` `bulk-write-owner-scoping` summary | 2982 | citation only | not
just single-id writes | card 2982 (bulk writes owner-scoped on
OWD-private objects) |
| `:361` `public-form-managed-anchors` summary | 3022 | citation only |
(owner_id / organization_id / audit / id) | card 3022 (a public-form
submit cannot supply owner_id or other server-managed anchors) |
| `:362` `public-form-managed-anchors` enforcement | 3004 | words |
complements the step 3.5 owner-anchor guard | card 3004 |
| `:378` `hierarchy-widening` enforcement | 7807 | citation only | the
runtime was narrowed to the declaration | card 7807 (business_unit
expands exactly one unit, as declared) |
| `:381` `rls-compiler-fail-closed` enforcement | 4983 | citation only |
hoisted out of plugin-security so lint/... | card 4983 (wire the
ADR-0056 D4 authoring gate to the one predicate definition) |
| `:385` `secure-by-default-posture` enforcement | 11757 | words | (the
gate's other carrier, sys_scim_provider, retired once the stable SCIM
line stopped deriving a provider model) | card 11757 answers 404, read
through landing commit 4d25d22 (retire the rc.1-era sys_scim_provider;
stable @better-auth/scim derives no scimProvider model) |
| `:387` `flow-run-as` summary | 1888 | citation only | under the run's
effective identity | card 1888 (enforce runAs) |
| `:390` `flow-run-as` note | 1888 | words | but its enforce-or-remove
decision chose ENFORCE and implemented it for flow data nodes | card
1888 (decision required: enforce or remove; enforced) |
| `:420` `permission-set-active` summary | 8613 | citation only |
(ADR-0049) | card 8613 (the active flag on both grant catalogues is
enforced) |
| `:422` `permission-set-active` note | 8613 | citation only | "the
`active` flag on the grant catalogues (ADR-0049)" | card 8613; the
describe title in core/security/resolve-authz-context.test.ts, quoted
without its tag so it stays a substring of the real title |
| `:423` `position-active` summary | 8613 | citation only | (ADR-0049) |
card 8613 |
| `:443` `grant-validity-window` enforcement | 10982 | citation only |
accessible_org_ids and the org-administration role projection | card
10982 (window-filter the role projection too); the present-tense list
already says it |
| `:445` `grant-validity-window` note | 8811, 8711, 8710, 10982, 11089,
10982, 9377, 7976 | tags dropped + words | NO `covers` ... Per the
2026-08-15 maintainer ruling ... by the 2026-08-15 ruling that
access-conferring paths filter and addressing paths do not, because
approval ROUTING ... `sys_member` ... last-admin-guard.ts's ... the role
projection as window-filtered now; ... is now cited: ... the
mutual-attribution contract (a cited proof file names the rows it
proves) is satisfied. | 8811 (404) via d6e7935 (adds this row); 8711
(404) via 2ce1eb4 (ruled narrowing of the completeness claim to
routes); 8710 (404) via 04d03c3 (ruling 2026-08-15: access-conferring
paths filter deactivated positions, addressing paths do not); card
10982; card 11089 (the last-admin-guard note went stale after 10982);
card 9377 (cite delegation-of-duty as this row's proof); card 7976 (a
proof file names the rows it proves, checked both ways) |
| `:452` `agent-visibility` summary | 1901 | citation only | listing
scope | card 1901 (agent visibility not enforceable without owner/org
anchors; removed per D8) |
| `:453` `agent-visibility` note | 1901, 1884 | citation only |
`visibility` deleted) ... at the chat route; | card 1901; card 1884
(enforce access/permissions at the chat route) |
| `:462` `requireAuth-removed` note | 3963, 7976 | words + tag dropped |
ADR-0056 D2, completed by deleting the switch once every session-less
surface was declared: the `requireAuth: false` opt-out is RETIRED ...
The `showcase-anonymous-deny.dogfood.test.ts` CITATION WAS DROPPED |
card 3963 (step 1: public as a declared capability; step 2: delete
api.requireAuth, an auth-less stack fails at boot); card 7976 (mutual
attribution, already named in the sentence) |
| `:466` `allow-transfer-restore-purge` note | 1883, 3004, 12497, 1883 |
words + citation only | ADR-0049 → roadmap M2, which builds the
lifecycle ops and their RBAC bits as one batch: the ops still do not
exist ... owner_id door. ... RETIRED 2026-08-26 (maintainer ruling:
retire the two bits now rather than carry them unenforceable until M2):
| card 1883 (M2: build undelete/purge and their RBAC bits in one batch);
card 3004; card 12497 (ruled 2026-08-26: retire allowRestore/allowPurge,
the keys return with M2) |

## The ledger

`scripts/doc-authoring-prose-id.baseline.json`, recomputed with `node
scripts/check-doc-authoring.mjs --census-ledger` (refuses any growth):

| | occurrences | (file, id) pairs | files | matrix row |
|---|--:|--:|--:|--:|
| base `434c6c7cab` | 359 | 251 | 87 | 76 occurrences / 46 cards |
| head | 283 | 205 | 86 | absent |

- Exactly the matrix row leaves: 48 lines deleted, 0 added. The other 86
rows compare equal as JSON.
- Recomputed again after merging `origin/main` (`8dea55d314`):
byte-identical.
- `pnpm check:doc-authoring`: exit 0 before and after. Its cross-package
line reads 300 pinned sites across 87 files at base and 257 across 86 at
head, "no growth, no burn-down unrecorded".
- The census's other 257 sites are the same before and after (file, line
and ids).

## Text only

A scratch TypeScript-AST comparison of the file at base `434c6c7cab` and
head: 1695 skeleton nodes on both sides, identical; 280 string literals
on both sides, 43 values changed, owned by `summary` 16, `enforcement`
10, `note` 17 and nothing else; 255 comment trivia blocks on both sides,
byte-identical. Its three controls each behave: a renamed property key
reads "skeleton DIFFERS", a changed `state` value is reported as owned
by `state`, and an edited comment reads "not identical".

## Pins

None. Nothing mechanical reads these three fields: the companion test
imports the rows for `id`, `state`, `proof`, `covers` and `enforcement`
presence, and the census and blind-spot test read only the header
docblock and the `covers` arrays. A whole-repo fixed-string search for
the text around each of the 76 removed ids (224 fragments, `docs/qa/**`
included) found 12 fragments with hits, every one a code comment, a
release-owned CHANGELOG entry, a `describe` title or a liveness note,
none asserting this file's text. So no pin moved and no ablation is
owed.

## Verification

- Build: `pnpm turbo run build --filter='@objectstack/dogfood^...'
--concurrency=2`: 63/63 tasks.
- Tests: `pnpm --filter @objectstack/dogfood exec vitest run
--maxWorkers=2 test/authz-conformance.test.ts
test/authz-probe-blind-spot.test.ts`: 2 files, 90 tests passed. These
are the only consumers of the module and of its text.
- Typecheck: `pnpm --filter @objectstack/dogfood typecheck` exit 0;
`--listFilesOnly` lists the matrix file and the companion test.
- Gates: `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` at `3005a8b6d1` derives 54
commands; all 54 run, exit 0. `check:dual-build-cjs-loads` first
answered exit 3 (PREREQUISITE NOT MET: 8 unrelated packages without
`dist/`); after a full workspace build (72/72) it exits 0 and that rerun
is the recorded result. `--ran` reconciliation: 54 derived, 54 run, 0
NOT-MEASURED, 0 UNRUN.
- Lint: full `pnpm lint` at `3005a8b6d1`: exit 0, no findings.
- Tests and typecheck ran at `0dbdbd759b`; the merge after it brought
one `docs/adr` file and no package input.

## Changeset

None, with `skip-changeset`: `@objectstack/dogfood` is `private: true`,
and the ledger is a repository script file. No `.changeset/*.md` is
touched.

## Acceptance notes

- **Code comments** in the matrix file still cite cards (33 comment
lines, the bracketed tracker tags and the block-comment headers among
them). They are not runtime strings and not on the ledger, so they are
outside ruling 5902360492 and this claim. Noted, not filed.
- **Two `describe` titles the notes quote** carry a bracketed tag:
`plugin-security/src/authz-matrix-gate.test.ts` (the Layer 0 insert,
update and exemption suites) and
`core/src/security/resolve-authz-context.test.ts` (the `active`-flag
suite). Test bodies are outside the gate's reading and belong to those
packages, not to this lane. Noted, not filed.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…ion run proved stale (objectstack-ai#21339)

Docs-only checklist revision from the 17.6.0 release-verification run
objectstack-ai#21330 (subject `617f25f8`, Console pin `31971ff1e28f`). Every change
follows the checklist README's lifecycle rule: the item's `revision`
bumps and one `history` entry says what changed, why, and cites the run.
No product code, no `content/docs/**`, no generated file.

## Stale clauses the run proved (each a FAIL in objectstack-ai#21330 with disposition
stale-clause / assertion-defect)

| item | rev | evidence | changed by |
|---|---|---|---|
| `access-security.audit-log-browser` | 2 → 3 | admin `GET
/data/sys_audit_log?filter={"action":"delete"}` → 0 rows; the row is
stored with correct attribution | `30c530e5` (objectstack-ai#21194): the ledger serves
a non-system reader, admins included, only rows about records it can
read |
| `api-backend.filter-comparand-conformance` | 2 → 3 | POST `/query` →
400 `VALIDATION_FAILED` at `query.where.f_number.$eq`; GET `$filter` and
engine → 400 `INVALID_FILTER`; no door returns rows | objectstack-ai#20116 (`cfc3bcf1`
objectstack-ai#20247, `dd1b8031` objectstack-ai#20325) — the split query-contract-matrix rev 3
already records |
| `api-backend.date-range-preset-matrix` | 1 → 2 | equality
`{"signed_on":"today"}` → 400 `INVALID_FILTER` (temporal door);
`$gte:"this_week"` → 400 with `bareDateRangePresetComparandMessage` | by
design: `18.filter-preset-ordering-comparand-refused.ts` judges ordering
positions only |
| `records-forms.import-transform-matrix` | 1 → 2 | 400
`UNSUPPORTED_TRANSFORM` names the missing sandbox, 0 rows — but no
`framework#2611` | `f115b1f` (objectstack-ai#21188): refusals state decisions in
words, not tracker numbers |
| `studio-authoring.view-authoring-live` | 1 → 2 | `GET
/meta/view?object=repair_asset` serves `repair_asset.default` /
`repair_asset.form` with the authored config; container name 0 hits | by
design: `expandViewContainer` (objectstack-ai#7163, objectstack-ai#7736, objectstack-ai#13407) |

## Expected-fail notes 17.6.0 has made pass (clauses held in objectstack-ai#21330;
only their framing was stale)

| item | rev | measured | fixed by |
|---|---|---|---|
| `automation.packaged-flow-subflow-disable-refusal` | 1 → 2 | caller
off → child's disable retry 200, ledger `active=false`; caller-first
enable 409 `RESOURCE_CONFLICT` | `36d043b` objectstack-ai#20724, `0d9349f` objectstack-ai#20759; the
enable guard is `679f95e` objectstack-ai#20711 (step 6 now enables the child first) |
| `automation.packaged-flow-clone-contract` | 1 → 2 | clone survives a
cold restart and fires; still unreachable from Studio | durability
`cb4c31d` objectstack-ai#20907; reachability now filed as objectstack-ai#21332 (clause unchanged,
still expected to fail) |
| `access-security.packaged-flow-write-door-parity` | 1 → 2 | `PUT` /
`DELETE /automation/showcase_urgent_task_alert` → 403 `NOT_OVERRIDABLE`,
flow unchanged | `4b45afae` (objectstack-ai#20817); knownGap names the existing pin
`packaged-flow-write-door-parity.dogfood.test.ts` |

No clause was weakened: each still refuses the original failure mode
(rows returned, a served delete row, a 200-with-zero-rows), and the
clone clause keeps its expected fail.

## Validation

- `node scripts/check-platform-checklist.mjs` → `OK — 15 areas, 269
items (265 active, 2 planned)`; symbol anchors and line-citation sweep
green.
- `api-backend.json` is re-serialized in its existing canonical 2-space
form; the other four files are edited in place in their existing mixed
formatting.

Not in this PR (listed on objectstack-ai#21330's close-out instead): the other
checklist-accuracy findings the run collected, and the two `planned`
picklist items, which can only be promoted by a run in which they pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants