Skip to content

fix(plugin-hono-server): a Hono door's 409 DELETE_RESTRICTED, and every structured refusal, carries RestServer's arm fields (#23587) - #23616

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-23587-hono-delete-restricted-fields
Oct 15, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-23587-hono-delete-restricted-fields

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Part of #23587
Clause-②: no

This lands the card's three structured fields on both Hono doors, under one rule that covers every arm of RestServer's classification. It also corrects the published error pages this answer changes (patch rounds 1 and 2, the seat's reviews 6185187566 and 6186145904). What stays on the card is one location, folded there as its remainder by the seat's pointer 6185197160: declaredEnvelopeForThrow answers the engine's DuplicateRecordError as 409 DUPLICATE_RECORD on both Hono doors, where RestServer answers UNIQUE_VIOLATION with developerMessage, field and object. The governing text is the maintainer ruling recorded at commit 65846bc46: one wire spelling on every route. This PR's agreement rule leaves that code alone on purpose.

What this does

The engine refuses the delete of a record that a deleteBehavior: 'restrict' lookup still references with 409 DELETE_RESTRICTED. RestServer's single-record DELETE answers it through mapDataError, whose first step is structuredCodeAnswer (@objectstack/types). That arm carries the engine's developerMessage, dependentObject, dependentCount and the route's object beside the code. Both Hono doors answered the same throw with code and message only. They are @objectstack/hono's createHonoApp (its dispatch() catch-all, through thrownJson) and a route mounted on HonoHttpServer (wrap(), and the transport onError seam). All of them render through declaredEnvelopeForThrow in packages/plugins/plugin-hono-server/src/adapter.ts.

Measured on a real createHonoApp over the real engine and protocol (SQLite WASM), at 16abea884:

  • FROM 409 {"success":false,"error":{"code":"DELETE_RESTRICTED","message":"This Parent is still referenced by 1 Child record(s) through “Parent”. Delete or reassign them first."}}
  • TO the same, plus "details":{"developerMessage":"Cannot delete parent (ID): 1 dependent child record(s) reference it via parent. Delete or reassign them first, or set deleteBehavior:'cascade' on child.parent.","dependentObject":"child","dependentCount":1}

PR #23582 added a helper beside the recogniser that read the arm for CONCURRENT_UPDATE only. This PR widens it from one code to every arm that agrees with the envelope already decided, and renames it from declaredConflictContext to declaredArmContext, since it no longer reads only conflicts:

Status, code and message do not move on any answer. The REST door's answer is unchanged. @objectstack/hono's source is unchanged too: createHonoApp renders through declaredEnvelopeForThrow.

Every arm, checked

Each arm of structuredCodeAnswer is listed below, with what RestServer sends and what the Hono doors send before and after. Readings marked "measured" come from a real createHonoApp at 16abea884 (before) and d1a39b380 (after). The rest come from the producer-shaped rows in the unit file and the code read.

code (status) RestServer (mapDataError) Hono doors before Hono doors after not forwarded, and why
DELETE_RESTRICTED (409) developerMessage, dependentObject, dependentCount, object none (measured) developerMessage, dependentObject, dependentCount (measured) object: the arm takes it from the route only
CONCURRENT_UPDATE (409) currentVersion, currentRecord, object currentVersion currentVersion (unchanged) currentRecord: held, #23586. object: route only
METADATA_CONFLICT (409) currentVersion, object none currentVersion, on a route mounted on HonoHttpServer object: route only. On createHonoApp the /meta save is answered by the dispatcher's own errorFromThrown, so it never reaches the recogniser. Measured unchanged there; see Acceptance notes (#22222)
INVALID_FIELD (400) field, object none (measured) field, object (measured) none
OBJECT_NOT_FOUND (404) object none (measured) object (measured) none. The arm's fixed sentence is not taken: the envelope keeps the thrown message
FEEDS_DISABLED, FILES_DISABLED, ATTACHMENT_PARENT_ACCESS, ATTACHMENT_DELETE_DENIED, RECORD_NOT_ACCESSIBLE (403) object none object none
VALIDATION_FAILED (400) fields, object fields (the resolver's) fields, the same array object: route only. validationFailureDetails and the arm read fields alike, so the merge is byte-identical
DUPLICATE_RECORD, answered by REST as UNIQUE_VIOLATION (409) developerMessage, field, object none, code DUPLICATE_RECORD (measured) unchanged (measured) the arm answers a different code, so it is not consulted. This is the card's remainder (opening paragraph)
ERR_DATASOURCE_UNAVAILABLE (503) datasource, reason, object the recogniser declines unchanged its one producer (DatasourceUnavailableError, @objectstack/objectql) declares no status, so the recogniser never answers it
OBJECT_API_DISABLED, OBJECT_API_METHOD_NOT_ALLOWED object, allowed none unchanged the arm fires only when it is given a route object that differs from the error's own, and the recogniser gives none

System-context reads (#23586's question, asked of every arm). currentRecord is the only field read with a system context and left unnarrowed. Every other field is one of these:

  • a version token (currentVersion);
  • an API name (object, dependentObject, field);
  • the caller's own input;
  • a sentence that names the same API names (developerMessage).

DELETE_RESTRICTED's dependents probe is elevated. But the engine discloses the count, in dependentCount and in developerMessage, only when the caller's own identity would have read the same rows (dependentCountIsDisclosable, @objectstack/objectql). It names the dependent object unconditionally, by that ruling's constraint 2. RestServer already sends all of it to the same caller. So currentRecord stays the one exclusion, by name. The helper's docblock says that an arm which grows such a field must be held there by name in the same way.

Sandboxed producers. A SandboxError carries only code, fields, status, userMessage and innerMessage (runtime/src/sandbox/quickjs-runner.ts). So no arm adds a field from one. VALIDATION_FAILED's fields is the resolver's own array, and INVALID_FIELD and OBJECT_NOT_FOUND decline a sandbox origin themselves.

What stays off these doors

Tests

  • Pins. Both files keep their existing controls, and the hono: a 409 CONCURRENT_UPDATE rendered by #16545's recogniser (createHonoApp, the direct-mount adapter) carries no currentVersion or currentRecord, which the wire contract and objectui's conflict bar read #23531 blocks are unchanged.
    • packages/plugins/plugin-hono-server/src/handler-throw-declared-envelope.test.ts, the new [#23587] block, 14 rows on direct-mount routes:
      • DELETE_RESTRICTED with the count, with the count withheld, and in the subflow guard's shape, which states no field and keeps the two-key envelope;
      • METADATA_CONFLICT with a token and with null;
      • INVALID_FIELD, OBJECT_NOT_FOUND, and the five 403 access codes;
      • two guard rows: an arm whose status, or whose code, differs from the declared one is not consulted.
      • Each refusal row asserts status, code and error.details exactly.
    • packages/qa/http-conformance/src/hono-declared-envelope.conformance.test.ts, the new [#23587] block, 3 rows on a real createHonoApp:
      • a restricted DELETE (account and contact, restrict): error.details equals the developerMessage, dependentObject and dependentCount of the throw the engine really raised, read off the deleteData call. The row also asserts that throw stated a string developerMessage, so the equality cannot hold between two absences, and that the parent row is still stored;
      • GET /data/task?nosuchfield=1: 400 INVALID_FIELD with { field, object };
      • GET /data/ghost: 404 OBJECT_NOT_FOUND with { object }.
  • Red before the fix (pins only, 90574de90, on the unfixed recogniser): unit Tests 11 failed | 26 passed (37), conformance Tests 3 failed | 15 passed (18). Every red row failed at its error.details assertion with expected undefined to deeply equal …. The three guard rows pass on the unfixed tree by construction; they are the regression half.
  • Ablation, on d1a39b380. Two legs, each run with node scripts/ablation-replace.mjs in wrap mode, with an outer trap … EXIT INT TERM restore against the absolute path. Each leg rebuilt @objectstack/plugin-hono-server, and ablation-dist-preflight.mjs found the marker in dist/index.js and dist/index.mjs before either suite ran.
    • Leg A narrowed the helper back to one code. The anchor const arm = structuredCodeAnswer(thrown); became a read guarded on resolved.code === 'CONCURRENT_UPDATE', carrying the marker ABLATION_23587_ONE_CODE (blob 7fd1f090b0 to 11a3ca30af). Every new [#23587] refusal row went RED, and the [#23531] rows stayed green: unit Tests 11 failed | 26 passed (37), conformance Tests 3 failed | 15 passed (18), each at expected undefined to deeply equal ….
    • Leg B dropped the held field. The anchor && key !== 'currentRecord'), became a key no body carries, ABLATION_23587_HELD_FIELD (blob to 1f5ac3cbb6). Exactly the four currentRecord absence pins went RED: unit Tests 2 failed | 35 passed (37), expected { …(2) } to deeply equal { Object (currentVersion) }; conformance Tests 2 failed | 16 passed (18), to not have property "currentRecord". The conformance body then carried the system-context row the real protocol states (organization_id, owner_id, created_by and the rest). That is the disclosure the exclusion holds back.
    • Restore: the blob equals HEAD (7fd1f090b0), git diff HEAD is empty and git status --porcelain is clean. After a rebuild, preflight --absent found both markers gone from all 6 built files, with a clean tree. Both pin files were green again: unit 37 passed (37), conformance 18 passed (18).
  • Packages, at d1a39b380, after a rebuild of @objectstack/plugin-hono-server (the conformance file reads its dist/):
    • pnpm --filter @objectstack/plugin-hono-server typecheck (tsc, tsconfig.typecheck.json, check:test-typecheck OK, 0 debt) and test: Test Files 29 passed (29), Tests 354 passed (354).
    • pnpm --filter @objectstack/http-conformance typecheck (check:test-typecheck OK, ledger unchanged) and test: Test Files 13 passed (13), Tests 142 passed (142).
  • Patch rounds 1 and 2 (docs only, heads 83c418436 and 9675127f3). No package file changed since d1a39b380, so the package suites above stand. The docs readings were taken with scratch files that were not committed: R1 on the createHonoApp rig, and R2 on one kernel, comparing a composed host (@objectstack/rest's routes registered, then createHonoApp mounted) with createHonoApp alone. See "Docs corrected (patch rounds 1 and 2)".
  • @objectstack/hono's own suite is not run locally: its source is untouched, and its vitest config stubs @objectstack/runtime. The composed-host pins above cover its door. It is declared to CI.

Gates

Local, at the final head 9675127f3:

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, run with no paths, derived 95 commands from the 7 changed paths (merge base 16abea884); the 3 docs pages add 29 families to round 0's 66. All 95 ran, each exit code captured before any pipe.
    • 93 exited 0 on the first pass.
    • pnpm --filter @objectstack/spec run check:skill-examples and pnpm check:dual-build-cjs-loads first exited 3, PREREQUISITE NOT MET: the recreated worktree had no packages/client-react/dist and no workspace dist/.
    • Under the lock, after a cache-backed pnpm turbo run build --concurrency=2 (76 successful, 75 cached), both measured exit 0: 263 prose examples type-check across 3 surface(s) and 110 published require entry point(s) across 68 package(s) load.
  • --ran reconciliation: 95 derived, 95 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero).
  • The docs gates on the touched pages, among them, all exit 0: check:doc-anchors, @objectstack/spec check:docs, check:docs-transcript-drift, check:corpus-claim-drift, check:docs-redirects, check:docs-single-h1, check:docs-spec-enumerations, check:doc-frontmatter, check:docs-section-name and check:section-landing-index.
  • pnpm check:route-envelope and pnpm check:dispatcher-error-vocabulary (derived), and pnpm check:durability-log-level (run separately): exit 0.
  • The four artifact-roster gates under touched directories, all exit 0: node scripts/check-changeset-fixed.mjs, pnpm check:authz-resolver, pnpm check:error-code-casing, pnpm check:filter-alias-parity.
  • ⚠️ The derivation reports a stale tree: this branch is 16 commits behind origin/main (7c6b7f4a3), and 11 gate files changed across that range. So the local run read this branch's copies of those scripts. None of those commits touches the 7 changed paths, and git merge-tree --write-tree HEAD origin/main is clean, so the branch is not merged (after a PR opens, main is merged only on a conflict, a main-caused red or a queue removal). CI runs the merge ref, which carries the current scripts.
  • CI on 9675127f3, read once: 41 check-runs, 36 success, 5 skipped, none failed or in progress.
  • Round 1 (83c418436) gave the same 95-family result. Round 0 (code head d1a39b380): 66 derived, 66 run, 0 NOT-MEASURED; eslint --no-inline-config over the two touched packages exit 0. The patch rounds change no lintable source.

Changeset

.changeset/23587-hono-structured-arm-fields.md: @objectstack/plugin-hono-server patch. @objectstack/hono is not named, because its source does not change, and both packages are in one fixed version group. The body gives FROM and TO for DELETE_RESTRICTED, names the other arms the rule now covers, and says which two fields stay off and why.

Docs corrected (patch rounds 1 and 2)

Head 9675127f3: round 1's commit 83c418436, then round 2's 9675127f3, which scopes the sentences that assumed every createHonoApp host serves /data through the dispatcher (the seat's review 6186145904). Line numbers are given on that head, with the origin/main lines in brackets; those three files are unchanged on main since the merge base. Readings:

R1 measured, per code, on the Hono door and then the REST twin:

  • DELETE_RESTRICTED: nested, details {developerMessage, dependentObject, dependentCount} / flat with those three and object.
  • CONCURRENT_UPDATE: nested, details {currentVersion} / flat with currentVersion, currentRecord, object.
  • INVALID_FIELD: nested, {field, object} / flat with both.
  • OBJECT_NOT_FOUND: nested, {object}, message Object 'ghost' not found / flat, error Object 'ghost' is not registered.
  • VALIDATION_FAILED (a missing required field): nested, {fields} / flat with fields and object.
  • A duplicate insert: nested DUPLICATE_RECORD, no details / flat UNIQUE_VIOLATION with developerMessage, field, object.
  • None of the six Hono bodies carries httpStatus.

R2 measured, on the hosted composition and then on createHonoApp alone:

  • the same restricted DELETE /api/v1/data/account/ID: 409 flat {error, code: DELETE_RESTRICTED, developerMessage, dependentObject, dependentCount: 1, object: "account"} / 409 nested with those three under error.details;
  • GET /api/v1/data/account?nosuchfield=1: 400 flat INVALID_FIELD with field and object / 400 nested with {field, object} under error.details.

content/docs/protocol/kernel/error-handling.mdx:

Lines (main) Change Reading
:75-:77 (:75-:78) Corollary 1 keeps UNIQUE_VIOLATION "always flat" and drops DELETE_RESTRICTED / INVALID_FIELD from it R1 (UNIQUE_VIOLATION answered by no Hono door)
:81-:84 (:82) Corollary 2 gains DELETE_RESTRICTED, INVALID_FIELD, OBJECT_NOT_FOUND: flat on the @objectstack/rest /data door, nested on a Hono door with their fields under error.details R1, R2
:107 (:105) "two writers" → "three writers" R1
:113-:114 (new) the third writer, declaredEnvelopeForThrow, the Hono doors' renderer R1, U
:133 (:129) error.httpStatus: the Hono doors' renderer never writes it R1 (0 of 6 bodies)
:134-:137 (:130-:132) error.details: "and nowhere else" removed; the Hono doors put the per-condition siblings there R1, U
:183 (:178) the {base}/data/* row is scoped to the @objectstack/rest door R1, R2
:193 (new; round 2 scoped) route-family row: a declared refusal thrown out of the dispatcher on a createHonoApp host, with /data included only where no @objectstack/rest route is registered ahead of its catch-all; or out of a HonoHttpServer route. Nested R1, R2, U
:196-:206 (:190-:194; round 2) the split-by-door callout now covers /data as well as /meta. A createHonoApp host can be either: REST routes registered before the mount (the hosted composition) answer flat on the REST door; with none ahead of the catch-all, /data and /meta answer through the dispatcher, nested R2 (/data), 6110136480 (/meta)
:218-:261 (new; round 2 scoped) ### Hono doors: who renders (the /data refusals leave the dispatcher only on a host whose catch-all serves /data, with a pointer to the table), the DELETE_RESTRICTED example (captioned "whose catch-all serves /data"), the per-code details table, and what a Hono door never sends (httpStatus, currentRecord, a route-taken object; the thrown message) The example and the DELETE_RESTRICTED, CONCURRENT_UPDATE, INVALID_FIELD, OBJECT_NOT_FOUND, VALIDATION_FAILED rows: R1, R2. The five 403 rows and "dependentCount when the flat door would send it": U
:406-:407 (:350) VALIDATION_FAILED envelope: flat on the rest door, nested on a Hono door (in-place, below) R1
:409-:410 (:352-:353) VALIDATION_FAILED: fields is error.details.fields on a Hono door (in-place) R1
:598-:600 (:541-:542) INVALID_FIELD envelope: nested on a Hono door, error.details.field / .object R1, R2
:655-:656 (:597) OBJECT_NOT_FOUND: nested on a Hono door, object in error.details R1
:695-:697 (:636-:637) DELETE_RESTRICTED envelope: nested on a Hono door, the three fields in error.details, no object R1, R2
:1052-:1054 (:992-:993) readError reads err.details?.fields on a nested body (in-place) R1
:1103-:1105 (:1042-:1043) field-validation guidance: error.details.fields on a Hono door; "no details.fields bag on either envelope" removed (in-place) R1

content/docs/api/wire-format.mdx:

Lines (main) Change Reading
:328 (:328; round 2 scoped) "Data route errors" scoped to the kernel REST server. A createHonoApp host answers nested when its catch-all serves /data; one that registers the REST routes before it mounts createHonoApp (the hosted composition) answers flat R1, R2
:485-:507 (new; round 2 caption) ### Hono hosts nest the extras: nested, no httpStatus; the classification's extras in error.details, with currentRecord and a route-taken object not sent; the CONCURRENT_UPDATE example, on a host "whose catch-all serves /data" R1

content/docs/api/index.mdx:

Lines (main) Change Reading
:221-:222 (new; round 2 scoped) On a createHonoApp host or a HonoHttpServer route, a declared refusal is nested without httpStatus, with the classification's fields in details except currentRecord and a route-taken object. The /data refusals are among them only where the catch-all serves /data; the hosted composition answers those flat R1, R2

The bounded in-place rule, for VALIDATION_FAILED. This PR leaves that code's Hono-door body byte-identical. Four lines were false in the same way and are corrected: :406-:407, :409-:410, :1052-:1054 and :1103-:1105. Each condition was checked:

  1. Same defect class: a published statement that a refusal is never nested, or has no details, which is false at the Hono doors.
  2. Mechanical, with the shape pinned: R1 measured error.details.fields, and handler-throw-declared-envelope.test.ts already pins it on a direct-mount route.
  3. No other claim holds the file: the seat's pointer 6185197160 read all 15 open PRs.
  4. Same gate family: the docs gates on the same page, with no new verification surface.

Read and left unchanged:

  • content/docs/api/error-handling-server.mdx:10: it is scoped to the kernel REST server, and true there.
  • content/docs/api/data-api.mdx:45-:52: the INVALID_FIELD example is the REST server's body and claims no exclusivity.
  • content/docs/protocol/objectql/types.mdx:717 ("the response carries dependentObject and dependentCount"): true on both doors.
  • error-handling.mdx:1399-:1401 (DELETE_RESTRICTED withholding): true on both doors.
  • The RECORD_NOT_FOUND callout at :642-:658: it names the /data door and makes no exclusivity claim.
  • No published statement names METADATA_CONFLICT's or the five 403 codes' body shape. The 403 codes appear only in status tables (permissions/attachments-access.mdx).
  • ⛔ content/docs/releases/ is untouched.

Found, not fixed here

  • The published duplicate-record statements, which this PR does not change. The bounded in-place rule fails its second condition here: the shape is not settled, because the card's remainder changes the code these doors answer. Carrier: hono: a 409 DELETE_RESTRICTED answered by createHonoApp carries no developerMessage, dependentObject, dependentCount or object, which RestServer's structuredCodeAnswer arm carries (the #23531 family beyond CONCURRENT_UPDATE) #23587's remainder. On a createHonoApp host a duplicate insert was measured as 409 {"success":false,"error":{"code":"DUPLICATE_RECORD","message":"Duplicate record refused on 'task': …"}}, at 16abea884 and again at d1a39b380. These lines say otherwise, and they are left as they stand:
    • content/docs/protocol/kernel/error-handling.mdx:668 (:609 on origin/main): UNIQUE_VIOLATION "flat … so it is never nested".
    • :680-:683 (:621-:624 on origin/main): "every route answers the wire code UNIQUE_VIOLATION and the in-process spelling never crosses HTTP".
    • content/docs/api/error-catalog.mdx:507-:510: the same sentence.
    • :75-:76 of the corrected corollary keeps UNIQUE_VIOLATION "always flat". That holds for every reading taken: no Hono door answered UNIQUE_VIOLATION. It moves with the remainder.

Acceptance notes


Generated by Claude Code

…oth Hono doors (#23587)

Pins first, against the unfixed recogniser: a direct-mount route and a real
createHonoApp host answer 409 DELETE_RESTRICTED, 400 INVALID_FIELD,
404 OBJECT_NOT_FOUND, 409 METADATA_CONFLICT and the 403 access arms with the
structured fields RestServer's classification carries, in error.details.
Guard rows: an arm that disagrees on status or code is not consulted, and a
DELETE_RESTRICTED stating no field keeps the two-key envelope.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FLwgbTSmGMrqsXkBDsiwfG
…tServer's arm fields (#23587)

declaredEnvelopeForThrow asked RestServer's classification (structuredCodeAnswer)
for CONCURRENT_UPDATE only, so a 409 DELETE_RESTRICTED reached a caller of
createHonoApp or a HonoHttpServer route with code and message only, while the
REST server's answer carries developerMessage, dependentObject and
dependentCount. The helper, renamed declaredConflictContext ->
declaredArmContext, now forwards the context of any arm that agrees with the
envelope already decided (same status, same code), into error.details, under
the arm's own rule (each field only when the producer stated it).

currentRecord stays held by name until #23586. An arm that disagrees on status
or code (DuplicateRecordError's UNIQUE_VIOLATION) is not consulted. The
route's object is not substituted from the error's own object.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FLwgbTSmGMrqsXkBDsiwfG
@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-hono-server, touching 4 documentable anchor(s).

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

  • content/docs/api/error-handling-server.mdx (via CONCURRENT_UPDATE (literal, a string literal in declaredConflictContext))
  • content/docs/api/index.mdx (via CONCURRENT_UPDATE (literal, a string literal in declaredConflictContext))
  • content/docs/api/wire-format.mdx (via CONCURRENT_UPDATE (literal, a string literal in declaredConflictContext))
  • content/docs/protocol/kernel/error-handling.mdx (via declaredEnvelopeForThrow (symbol, a top-level function), CONCURRENT_UPDATE (literal, a string literal in declaredConflictContext))

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

  • content/docs/releases/v15.mdx (via CONCURRENT_UPDATE (literal, a string literal in declaredConflictContext))

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
  • 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 — 9 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 dd29bbe60df87727664424521c3a0b9eed37e985 → packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 dd29bbe60df87727664424521c3a0b9eed37e985 → 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

🔁 REWORK, round 1 of 2: PR #23616 at d1a39b380c (#23587). One item: the published error page this PR makes false

domain:cli seat 1 (#6024) · os-bill · session session_01FLwgbTSmGMrqsXkBDsiwfG · 2026-10-15T05:30Z · holder of claim 6183773313. Report: os-dev-report 6185043669. Thread-read: 6185023118.

席位核对

Everything else holds; it is recorded here so the patch round does not reopen it.

  1. Shape, read on GitHub:

    • Draft against main, head d1a39b380c. Line 1 is Part of #23587; line 2 is a bare, line-initial Clause-②: no. Assignee os-bill.
    • Files: 4, +349 / −44: adapter.ts, the two pin files, and .changeset/23587-hono-structured-arm-fields.md.
    • check-governed-merges --pr 23616: NOT governed, 393 changed lines. No content/docs/releases/ change.
  2. The hunk, read at source:

    • declaredArmContext drops the one-code guard and keeps the agreement guard (arm.status and arm.body.code against the decided envelope) and the by-name currentRecord exclusion.
    • The seat read every arm of structuredCodeAnswer on origin/main (packages/types/src/data-error-classification.ts:1011 onward). Every body key other than error, code and currentRecord is a version token, an API name, a count or the caller's input. DELETE_RESTRICTED's count is withheld from both dependentCount and developerMessage by the producer when dependentCountIsDisclosable says no (packages/objectql/src/engine.ts near :18699-:18738). So the hard constraint from triage 6183354733 holds, and ablation leg B proves the exclusion carries weight.
    • The wider scope (OBJECT_NOT_FOUND and the five 403 arms, beyond triage's three named codes) passes the four in-place conditions. It is the same defect class, mechanical under the agreement rule, claimed by no one, and in the same gate family. It sits inside the claimed function, and the PR body names it with one pin per arm.
    • Seat 2's Release note 6183630660 ("Do not widen the helper's code guard while metadata-protocol: a 409 CONCURRENT_UPDATE carries currentRecord as read with a system context (probeRecord), so a caller who sends a stale version may receive a row, or fields, that row- or field-level security hides from them #23586 is open") against triage's grade 6183354733, which permits a widening that keeps currentRecord out: the seat follows the grade. The note's concern is the currentRecord disclosure, and this diff keeps it held.
    • Placement in error.details: ApiErrorSchema.details is declared as z.unknown().optional(), "Additional error context" (packages/spec/src/api/errors.zod.ts:450). ADR-0112 retires details only as a code carrier (D5), and PR fix(plugin-hono-server): a Hono door's 409 CONCURRENT_UPDATE carries currentVersion (#23531) #23582 already landed currentVersion there.
  3. Tests: 14 unit rows and 3 conformance rows assert status, code and error.details exactly. They were red before the fix, at 90574de90. Two ablation legs went red as expected, and the restore was proven by blob.

  4. Gates:

    • Local: 66 derived, 66 run.
    • CI on d1a39b380c, read in this act: 38 check-runs, 25 success, 6 skipped, 7 in_progress (Test Core 1–6/6), none failed. Lint & Repo Gates and TypeScript Type Check both succeeded.
  5. Changeset, read sentence by sentence against the hunk:

    • the patch bump on @objectstack/plugin-hono-server;
    • FROM/TO for DELETE_RESTRICTED;
    • the arm list (INVALID_FIELD, OBJECT_NOT_FOUND, the five 403 codes, and METADATA_CONFLICT on a direct-mount route);
    • "a duplicate insert gains nothing";
    • the two fields held off.

    Each sentence matches.

The rework item

content/docs/** ships to readers outside this repo. A published statement that this round makes false, or that describes an answer this round changes, is fixed in this PR (review-checklist.md, 〈范围与 changeset〉). On origin/main, content/docs/protocol/kernel/error-handling.mdx says three things this diff contradicts:

  • :130-:132, the nested envelope's error.details bullet: "present only where the emitting route passes one. Not a general-purpose bag: it appears on a handful of refusals (rate limiting, settings validation, partial-delete reports) and nowhere else." After this PR, both Hono doors carry structured-arm fields there for every code in your table. PR fix(plugin-hono-server): a Hono door's 409 CONCURRENT_UPDATE carries currentVersion (#23531) #23582 already made this bullet false for CONCURRENT_UPDATE.
  • :541-:542, INVALID_FIELD: "Envelope: flat … so it is never nested. field and object are top-level siblings, not a details bag." On a createHonoApp host your own rig measured the answer nested, with error.details: { field, object }.
  • :636-:637, DELETE_RESTRICTED: "Envelope: flat … so it is never nested." The card's measured answer is nested, and after this PR it carries error.details: { developerMessage, dependentObject, dependentCount }.

What to do:

  • Correct every statement in content/docs/** about the answer to a code whose Hono-door body this PR changes. That covers DELETE_RESTRICTED, INVALID_FIELD, OBJECT_NOT_FOUND, CONCURRENT_UPDATE, METADATA_CONFLICT and the five 403 codes.
  • The docs-drift comment 6185023118 names content/docs/api/error-handling-server.mdx, content/docs/api/index.mdx and content/docs/api/wire-format.mdx as leads. Read them, and list every line you change, with its number, in the PR body.
  • The criterion is your measured readings, not the code read. Each corrected sentence must agree with a reading in your table, or with one you take on the same rig.
  • Codes this PR does not change (VALIDATION_FAILED's :1043 "no details.fields bag on either envelope", the duplicate record's :609): if they are false in the same way, the bounded in-place rule applies, checked condition by condition and named in the PR body. Otherwise list them under "Found, not fixed here".
  • ⛔ content/docs/releases/ is not touched, though the drift comment names v15.mdx.
  • Gates: re-run the derived gates for the new paths (dispatch-gates --commands, then --ran). The Writes: budget is unchanged: pushes, plus one relay PR-body patch and one relay report comment.

File surface: the claim's surface is revised in the same round, by the pointer on #23587, to add the content/docs/** pages you change, ⛔ content/docs/releases/ excepted.

Rulings on the report

Landing: no contract review is owed. No packages/spec path is touched, Clause-②: no stands (declared context is added and nothing is refused), the PR is not governed, and no accept set narrows. Once the patch round's head reads ACCEPT and every check on it is green, the seat flips the PR ready and arms auto-merge. After the merge the card goes back to pm:queue, unassigned, with the remainder named.


Generated by Claude Code

…heir fields in error.details (#23587)

The error-handling page said DELETE_RESTRICTED, INVALID_FIELD and
OBJECT_NOT_FOUND are always flat, that error.details appears on a handful of
refusals and nowhere else, and that VALIDATION_FAILED has no details.fields
bag on either envelope. On a createHonoApp host, and on a route mounted on
HonoHttpServer, each of those refusals is answered nested, with the
structured fields under error.details and no httpStatus (measured on a real
createHonoApp host). The page now names that door, its writer and its fields
per code; wire-format and the API overview scope their flat-envelope
statements to the REST server and point at it. readError reads
error.details.fields.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FLwgbTSmGMrqsXkBDsiwfG
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

🔁 REWORK, round 2 of 2: PR #23616 at 83c4184368 (#23587). One item: the new docs assume every createHonoApp host serves /data through the dispatcher

domain:cli seat 1 (#6024) · os-bill · session session_01FLwgbTSmGMrqsXkBDsiwfG · 2026-10-15T06:40Z · holder of claim 6183773313. Report: os-dev-report 6186043664. Round 1: REWORK 6185187566.

席位核对

What round 1 delivered holds, read sentence by sentence on the head.

  • Scope: the patch is docs only, three pages. adapter.ts, the pins and the changeset are unchanged since d1a39b380.

  • error-handling.mdx sentences read and accepted:

    • corollary 1 (UNIQUE_VIOLATION only) and corollary 2 (the three thrown codes);
    • "three writers", with declaredEnvelopeForThrow as the third;
    • httpStatus never written by the Hono renderer;
    • the error.details bullet;
    • the /data table row scoped to the @objectstack/rest door;
    • the ### Hono doors section: its example, its per-code table, the five 403 rows, and the "never sends" list (httpStatus, currentRecord, a route-taken object, the thrown message);
    • the VALIDATION_FAILED, INVALID_FIELD, OBJECT_NOT_FOUND and DELETE_RESTRICTED envelope lines;
    • readError reading err.details?.fields;
    • the field-validation guidance.

    Each matches the code and the dev's R1/U readings. The VALIDATION_FAILED corrections pass the bounded in-place rule, all four conditions. Leaving the duplicate-record statements under "Found, not fixed here", carried by this card's remainder, is right: their shape moves with that remainder.

  • CI on 83c4184368: 41 check-runs, 36 success, 5 skipped, none failed.

The rework item

The problem. Three new sentences say a createHonoApp host answers the /data refusals nested. That holds only where the runtime dispatcher serves /data. A host that registers @objectstack/rest's routes on the same Hono app before mounting createHonoApp answers those routes on the REST door, flat. The plugin routes win the match against the catch-all by registration order.

The governing reading. That is the hosted composition. The maintainer's hosted measurement on #22222 (6110136480) reads it for PUT /meta: apps/objectos/server/index.ts registers the plugin routes first, then mounts createHonoApp. The /data routes are registered by the same plugin, so the same order applies to them.

The three sentences:

  • content/docs/api/wire-format.mdx:328, the added sentence: "A host built with createHonoApp answers the same refusals nested instead".
  • content/docs/api/index.mdx:221: "(the /data routes' refusals among them)".
  • content/docs/protocol/kernel/error-handling.mdx: the new table row's "(/data included)", and ### Hono doors's "(the /data routes' refusals do)".

What to do:

  • Scope each one to where it holds: a createHonoApp host whose /data routes reach the runtime dispatcher, which means no @objectstack/rest routes ahead of the catch-all.
  • Say what the other composition answers: flat, from the REST door.
  • The page already has the model: its "The /meta family is split by door, not by path … Read the body, not the path" callout. Widening that callout to /data, and pointing the three sentences at it, is one way. The wording is yours.
  • Evidence:
    • If a composed rig is cheap (a Hono app with RestServer's routes registered, then createHonoApp mounted), measure one /data refusal on it and cite the reading.
    • Otherwise cite 6110136480's structural reading, and say the /data half is read in source, not measured.

Mechanics:

This is the last REWORK round. A third would go to the maintainer.


Generated by Claude Code

…atch-all serves /data (#23587)

Round 1's sentences assumed every createHonoApp host serves /data through
the runtime dispatcher. A host that registers @objectstack/rest's routes on
the same Hono app before mounting createHonoApp, the hosted composition,
answers those routes on the REST door, flat: the routes registered first win
the match. Measured on one kernel: with the REST routes registered first, a
restricted DELETE and an unknown field filter answered flat; with
createHonoApp alone, the same requests answered nested with error.details.
The three sentences are scoped to a catch-all that serves /data, and the
split-by-door callout now covers /data as well as /meta.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FLwgbTSmGMrqsXkBDsiwfG
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

✅ ACCEPT: PR #23616 at 9675127f30 (#23587, after REWORK rounds 1 and 2). It queues now

domain:cli seat 1 (#6024) · os-bill · session session_01FLwgbTSmGMrqsXkBDsiwfG · 2026-10-15T07:19Z · holder of claim 6183773313. Reports: os-dev-report 6186043664 (round 1) and 6186642817 (round 2). Reviews: REWORK 6185187566 and 6186145904.

席位核对

  1. Round 2, read sentence by sentence on the head.
    • What changed: one docs-only commit on 83c418436. Each sentence that assumed every createHonoApp host serves /data through the dispatcher is now scoped to where it holds: a catch-all that serves /data, with no @objectstack/rest route registered ahead of it. Each also says the hosted composition answers those routes flat, on the REST door.
    • The sentences:
      • wire-format.mdx:328, and its example caption at :492;
      • api/index.mdx:221;
      • in error-handling.mdx: the route-table row at :193, the ### Hono doors lead at :220-:224 and its example caption at :230-:231.
    • The callout: the page's split-by-door callout (:196-:206) now covers /meta and /data, and names both compositions of a createHonoApp host. Each sentence matches the readings below.
  2. The evidence is measured, not cited (R2, a scratch rig, not committed).
    • The rig: one kernel with HonoServerPlugin and createRestApiPlugin, with @objectstack/rest's routes registered, then createHonoApp mounted (the ALL /api/v1/* catch-all present).
    • The hosted composition: a restricted DELETE /api/v1/data/account/ID answered 409 flat, with developerMessage, dependentObject, dependentCount and object. GET ?nosuchfield=1 answered 400 flat INVALID_FIELD.
    • createHonoApp alone: both nested, with the fields under error.details.
    • /meta: that half rests on the maintainer's hosted measurement 6110136480.
  3. Round 1 stands as read in 6186145904. Both rounds are docs only: adapter.ts, the pins and the changeset are unchanged since d1a39b380.
  4. Body:
  5. Gates:
    • Local: 95 derived, 95 run, 0 NOT-MEASURED.
    • CI on 9675127f30, read in this act: 36 success, 5 skipped, none failed. The edited-event checks the body write re-triggered are read again before the flip.
    • check-governed-merges --pr 23616: NOT governed, 536 changed lines. No content/docs/releases/ change.

Landing:


Generated by Claude Code

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/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants