Skip to content

fix(types,runtime): a declared capability absence is reported once per route per process, at warn - #17854

Merged
claude[bot] merged 4 commits into
mainfrom
claude/issue-14656-declared-capability-absence-warn-once
Sep 12, 2026
Merged

fix(types,runtime): a declared capability absence is reported once per route per process, at warn#17854
claude[bot] merged 4 commits into
mainfrom
claude/issue-14656-declared-capability-absence-warn-once

Conversation

@claude

@claude claude Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Fixes #14656

Clause-②: no

A declared capability absence — a 5xx the platform chose because the deployment did not install an optional service — is now reported once per route per process at warn, naming the missing service. Everything else reaching logServerFault keeps the per-request error line #14310 shipped.

This is the execution of the maintainer ruling recorded at 5528582503 (decision batch #23, 2026-09-03, verbatim 「同意」 to B + C), released for dispatch by the director seat at 5644019323.

The file face — five paths, and packages/spec is not one of them

path what changes
packages/types/src/server-fault-log.ts the only non-test source change. The predicate, the (route, process) registry, and the warn branch inside logServerFault.
packages/types/src/server-fault-log.test.ts 11 new pins on the funnel; one existing fixture repointed off the absence family (it was 503 SERVICE_UNAVAILABLE, which is now the ruled exception — the behaviour that case is about is unchanged and belongs to the fault branch).
packages/runtime/src/declared-capability-absence-warn-once.test.ts new. The door-level pins, the two-door agreement pin, and the wire-bytes block.
packages/runtime/src/dispatcher-5xx-always-logged.test.ts the returned-exit fixture repointed to a genuine fault, plus one door-level guard so this file cannot go green believing /notifications still costs an error line.
.changeset/14656-declared-capability-absence-warn-once.md patch on @objectstack/types and @objectstack/runtime — the packages whose logging changes, which is what the ruling asked for.

Nothing under packages/spec/ is touched. unavailable.ts imports serviceUnavailableMessage from @objectstack/spec/system and this change needed no edit there. The one new spec reference is a type-only import of StandardErrorCode into packages/types, which proves at compile time that the two code spellings are catalogued ADR-0112 codes.

One predicate, one place — and why that is stronger than one import

The ruling's constraint is «⛔ Not spelled once per door: the REST door and the dispatcher read the same predicate». The delivered design applies the predicate inside the shared funnel rather than exporting it for two doors to call:

  • sendError (packages/types/src/response-envelope.ts:227) is the single exit for every nested-envelope 5xx in packages/rest;
  • the runtime dispatcher calls logServerFault at its three exits (dispatcher-plugin.ts :318, :394, :656).

Both already pass through the funnel, so there is no per-door spelling to drift and no door can opt out by forgetting a call. Being unable to spell it twice is stronger than agreeing to spell it once.

It reuses the declared-5xx vocabulary that already exists rather than inventing one: declaresServerFault (packages/types/src/error-leak.ts:289) is the same "the producer declared this shape" read that @objectstack/rest's declaredServerFaultAnswer gates on, and the absence family is the ADR-0112 code the producer already declared (NOT_IMPLEMENTED / SERVICE_UNAVAILABLE).

Three boundaries, all fail-loud, all pinned:

  • A thrown 5xx keeps its error line even when it declared 501. The thrown exit hands the funnel the throw and no envelope code, so it is not an absence here. The half that carries a stack stays loud.
  • A door that supplies no route coordinates is demoted to warn but never suppressed. An un-keyed bucket would collapse every unnamed route into one entry — the same shape as the global "first N" the ruling forbids for hiding the second route.
  • The registry has a ceiling (512) and at the ceiling stops adding rather than evicting, so an unrecorded route reports every time. Eviction would silently re-quiet whichever route was pushed out. The live key space is bounded anyway: instrumentRouteHandler parks the route PATTERN, not the raw URL.

⚠️ The contract-review declaration — measured, and it says no

The claim declared Clause-②: yes when it was dispatched, and the dispatch order explained why: the seat read the ruling's own no as right about the wire but incomplete, because «the same ruling puts the predicate in @objectstack/types where two packages must read it … so it has to cross a package boundary, and a symbol exported from that package's entry is a new published symbol».

That premise does not hold for the delivered design, so the ruling's no is right and the seat's reading was wrong. The predicate never crosses the boundary — it is applied inside the funnel both packages already call — and every symbol this change adds is module-private. Measured on this branch:

  • git diff BASE..HEAD -- packages/types/src/server-fault-log.ts | grep -E '^[+-][^+-].*\bexport\b'no output: not one export line added or removed.
  • packages/types/src/index.ts and packages/types/package.jsonuntouched (0 paths in the diff), so the entry and the exports map are byte-identical.
  • The seven new identifiers, grepped against the rebuilt packages/types/dist/index.d.ts: six score 0; the single hit for isDeclaredCapabilityAbsence is a {@link} inside a doc comment at line 1392, and grep -E "declare (function|const) …" finds no declaration for any of them.

Corrected at review, which is the seat's act and not the dev's. The Clause-②: line at the top of this body now reads no, and needs:contract-review is off both carriers — this PR and card #14656. The record is the contract review at 5646183631, written on head 44e14e367, which re-measured the surface independently of the reading above and reached the same answer the maintainer ruling had already written down: «Clause-②: no (re-read on the final diff; if any response byte moves, stop and report)».

⛔ No wire change — measured, not asserted

The ruling's condition is «if any response byte moves, stop and report». The #14656 — the wire does not move block captures the full status + JSON.stringify(body) for a declared absence (over three requests) and for a fault, and asserts the exact strings.

That block is written to be runnable on the pre-change tree: it names no warn, no count and nothing else this card introduces. Run against the base funnel (packages/types/src/server-fault-log.ts restored to 758ac4097, everything else this branch):

  • wire block on the base funnel: 2 passed. The bytes this branch asserts are the bytes main already answered — a before/after measurement, not a claim.
  • whole new suite on the base funnel: 4 failed / 3 passed. The four behavioural pins discriminate; the three that pass are the two wire-byte tests and the undeclared-500 control, whose behaviour is unchanged by design.
  • Restore proved byte-identical by blob hash (7ed86cc3…), git diff HEAD empty, whole-tree git status --porcelain empty.

The ablation — making the two doors disagree

The two-door pin is only worth something if it can fail. Mutation: gate the demotion on input.request !== undefined, which the dispatcher supplies and sendError does not, so the doors disagree about one envelope.

  • On-disk proof the mutation landed: the anchor line count went 1 → 0, the injected marker 0 → 2, and the blob hash moved off HEAD's.
  • @objectstack/types rebuilt, then node scripts/ablation-dist-preflight.mjs @objectstack/types 'ABLATION_DOOR_SPLIT'exit 0, marker live in dist/index.js and dist/index.mjs.
  • Ablation run: 1 failed / 6 passed — exactly the two-door pin, and the failure names what it lost: door 2 must not disagree with door 1 about the same envelope: expected [] to have a length of 1 but got +0.
  • Restore: blob hash back to 7ed86cc3…, whole-tree git status clean, rebuild, --absent preflight exit 0 (marker absent from all 12 built files).

A first attempt planted the marker inside a /* comment */, which esbuild strips, so the plant-leg preflight exited 1 on a marker-choice artifact; the run above replaces it with a marker that survives bundling. Both runs went red on the same assertion.

Verification

All at 44e14e367 (this branch merged with origin/main at 3c86008e2). Exit codes captured before any pipe; each verdict read from the command's own printed line.

  • Derived gates: 58/58 green. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, reconciled with exit codes recorded: 58 derived, 58 run, 0 NOT-MEASURED, 0 UNRUN — «a DERIVED zero — all 58 recorded an exit code and none of them is 3».
    • Two of them first exited 3 — PREREQUISITE NOT MET, not a finding: check:dual-build-cjs-loads and check:type-check-debt both read built output. After turbo run build --filter='./packages/*' --filter='./packages/*/*' (72/72 successful) both exit 0, the second reporting «5 ledger entr(ies) re-measured in 76.5s, 55 raw tsc error(s) total, none above its recorded number».
  • @objectstack/types: test 22 files / 666 tests passed · test:repo 1 file / 7 passed · typecheck clean.
  • @objectstack/runtime: test 261 files / 3631 tests passed · test:repo 2 files / 69 passed · typecheck clean (which for this package also runs check:test-typecheck).
  • Repo-wide pnpm lint: exit 0, the full eslint . --no-inline-config run — no narrowing to declare.
  • check:nul-bytes green, plus an independent control-character scan of all five changed paths: 0 hits.
  • Heavy runs serialised through scripts/pm/os-verify-lock.sh; every verdict quoted above is the wrapper's VERDICT command-exit line.

NOT MEASURED, named rather than left to read as green: the five CI jobs dispatch-gates reports as scheduled by these paths but having no local invocation (Test Core, Temporal Conformance, Dogfood Regression Gate, Dogfood Verify CLI, Build Core), the five families whose argv takes a value only CI supplies, the 11 declared-wide-population families and the 50 artifact-roster families. No browser/dogfood verification was done: the diff moves no rendered surface and no response byte.

The order's line readings — all four held

The dispatch flagged its own re-derived positions as timestamped readings rather than coordinates. Checked by symbol on this tree, all four are correct: declaredServerFaultAnswer at error-response.ts:606, boundedDeclaredRefusalMessage at :775, logServerFault in packages/types/src/server-fault-log.ts, and capabilityUnavailable at unavailable.ts:60-61. Nothing to charge back.

Acceptance notes

  • The REST door names no route to the funnel, so its declared absences are demoted to warn but not deduped. sendError passes only requestId; instrumentRouteHandler — the one producer of the parked route coordinates — is applied solely by the dispatcher's server proxy, and rest-api-plugin mounts its direct-mount registrars on the raw server, so nothing parks coordinates for that door today. Reading them in sendError would therefore be dead code. The reachable population is one route (external-datasource-routes.ts:383, a 503), which is not polled. Noted, not filed — it is an observation about a door's coordinates, not a reproducible defect, a contract violation, or a metadata-authoring trap.
  • vi.setConfig({ testTimeout: 30_000 }) in the new runtime file is paid for the per-test isolation, not hidden slowness: each test re-executes the dispatcher's module graph so the per-process registry starts empty, and the first one measured over 5s on this shared box. Noted, not filed.

Generated by Claude Code


Generated by Claude Code

…rocess

Maintainer ruling 2026-09-03 (decision batch #23, B + C): a 5xx the platform
chose because an optional service is not installed is a configuration fact, not
a fault — it is reported once per route per process at `warn`, naming the
missing service. Everything else reaching `logServerFault` keeps its
per-request `error` line.

The predicate is applied inside the shared funnel rather than spelled at each
door, so the REST writer and the runtime dispatcher read the same answer by
construction. It reuses `declaresServerFault`, the repo's existing declared-5xx
vocabulary, and the ADR-0112 code the producer already declared.

Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c
Co-authored-by: Claude <noreply@anthropic.com>
…t must not move

Three pins the ruling names: a declared 501 reported once across N requests at
`warn`; a second, different route still reporting (the shape a global "first N"
throttle would hide); and an undeclared 500 on the same door still loud, once
per request.

The two-door pin reads the envelope the real dispatcher answers off the wire
and hands it to `sendError` — the exit every nested-envelope 5xx in
`packages/rest` takes — so both doors must classify one fixture envelope the
same way.

`vi.resetModules()` per test is load-bearing: the dedupe registry is module
state, which is the ruling's "per process" half.

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/ai/agents.mdx (via /api/v1/ai/* (route, a path literal on a changed line))
  • content/docs/ai/index.mdx (via /api/v1/ai/* (route, a path literal on a changed line))
  • content/docs/ai/knowledge-rag.mdx (via /api/v1/ai/* (route, a path literal on a changed line))
  • content/docs/ai/natural-language-queries.mdx (via /api/v1/ai/* (route, a path literal on a changed line))
  • content/docs/api/client-sdk.mdx (via NOT_IMPLEMENTED (literal, a string literal in CAPABILITY_ABSENCE_CODES; a string literal on a changed line), SERVICE_UNAVAILABLE (literal, a string literal in CAPABILITY_ABSENCE_CODES))
  • content/docs/api/data-api.mdx (via NOT_IMPLEMENTED (literal, a string literal in CAPABILITY_ABSENCE_CODES; a string literal on a changed line))
  • content/docs/api/error-catalog.mdx (via NOT_IMPLEMENTED (literal, a string literal in CAPABILITY_ABSENCE_CODES; a string literal on a changed line), SERVICE_UNAVAILABLE (literal, a string literal in CAPABILITY_ABSENCE_CODES))
  • content/docs/api/index.mdx (via SERVICE_UNAVAILABLE (literal, a string literal in CAPABILITY_ABSENCE_CODES))
  • content/docs/api/wire-format.mdx (via NOT_IMPLEMENTED (literal, a string literal in CAPABILITY_ABSENCE_CODES; a string literal on a changed line))
  • content/docs/automation/flows.mdx (via NOT_IMPLEMENTED (literal, a string literal in CAPABILITY_ABSENCE_CODES; a string literal on a changed line))
  • content/docs/data-modeling/queries.mdx (via NOT_IMPLEMENTED (literal, a string literal in CAPABILITY_ABSENCE_CODES; a string literal on a changed line))
  • content/docs/permissions/authentication.mdx (via NOT_IMPLEMENTED (literal, a string literal in CAPABILITY_ABSENCE_CODES; a string literal on a changed line))
  • content/docs/protocol/kernel/error-handling.mdx (via SERVICE_UNAVAILABLE (literal, a string literal in CAPABILITY_ABSENCE_CODES))
  • content/docs/protocol/objectql/query-syntax.mdx (via NOT_IMPLEMENTED (literal, a string literal in CAPABILITY_ABSENCE_CODES; a string literal on a changed line))

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

  • content/docs/releases/v16.mdx (via /api/v1/notifications (route, a path literal on a changed line))
  • content/docs/releases/v17/17-0.mdx (via NOT_IMPLEMENTED (literal, a string literal in CAPABILITY_ABSENCE_CODES; a string literal on a changed line), SERVICE_UNAVAILABLE (literal, a string literal in CAPABILITY_ABSENCE_CODES), /api/v1/ai/* (route, a path literal on a changed line), /api/v1/notifications (route, a path literal on a changed line))
  • content/docs/releases/v17/17-3.mdx (via SERVICE_UNAVAILABLE (literal, a string literal in CAPABILITY_ABSENCE_CODES))
  • content/docs/releases/v17/17-4.mdx (via SERVICE_UNAVAILABLE (literal, a string literal in CAPABILITY_ABSENCE_CODES))

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 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 2 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 2d34f320d1179239d35dfd5c2429761263adad06packageMentionDocs.

Which tree this was computed on

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

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

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

@claude

claude Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

Contract review — PR #17854 (card #14656)

Head reviewed: 44e14e367200bfd7d5d7da30c1489f6481e8f798. Merge base with main: 3c86008e240feaee3cfb7b31034b28b06b15a9ba — five paths. All readings below taken at 2026-09-12T13:30Z on that head, in the shared checkout against origin/* refs.

⚠️ Independence, stated before the verdict, because it bounds what this record is worth. The implementer is a mode:subagent dev of this same PM session, and this review is written by the seat that dispatched it. ⛔ This is not an arm's-length second opinion. The checkable independent artefact is the pre-review baseline: the seat published its own reading of the clause-② question at 5646136503 before the dev's report arrived, and that comment already committed to the yes → no correction and to ⛔ not dropping the changeset and ⛔ not raising @objectstack/types to minor to quiet the gate. Everything below is re-measured on the final head; nothing is carried over from the dev's report or from that baseline.

① Derived judgments — every acceptance-set and public-surface change this diff introduces, named and judged

1. @objectstack/types publishes nothing new. — correct.

git diff 3c86008e2..44e14e367 -- packages/types/src/server-fault-log.ts | grep -E '^[+-][^+-].*\bexport\b'

no output. That matcher alone is not a reading, so it is backed by controls that fire: the same [+-] matcher for \bfunction\b on the same diff returns 2, and export appears 8 times in the file at head. The zero is the matcher working, not the matcher missing.

⚠️ And that matcher could not have seen a return-type change, because a signature spanning lines puts ): boolean { on a line that carries no export. So the exported declarations were compared by content, before and after:

  • the exported list is identical, order-insensitively (ServerFaultRequest, ServerFaultLogInput, SERVER_FAULT_LOG_PREFIX, isServerFault, serverFaultLogMessage, serverFaultLogMeta, logServerFault, describeFaultRequest — 8 before, the same 8 after);
  • logServerFault's full signature is byte-identical across the change, ): boolean included — it already returned a boolean at the merge base;
  • both exported interfaces are byte-identical, member for member.
  • Every removed line in the whole source diff (10 of them) is a JSDoc line or a body line inside logServerFault. Nothing leaves the surface.

The seven new declarations are all module-private, read off the file at head — CAPABILITY_ABSENCE_CODES (:168), CAPABILITY_ABSENCE_NOTE (:175), isDeclaredCapabilityAbsence (:196), capabilityAbsenceRouteKey (:217), REPORTED_CAPABILITY_ABSENCE_ROUTES (:228), CAPABILITY_ABSENCE_ROUTE_CEILING (:238), claimCapabilityAbsenceReport (:244). None carries export.

⚠️ The seat's own earlier error, recorded so it is not repeated: the dispatch order reasoned that export * from './server-fault-log.js' (packages/types/src/index.ts:34) would publish the predicate. It does not — a star re-export re-exports what that module exports, and a module-private function is not among them. The barrel was measured correctly; what the barrel does with an unexported symbol was assumed. packages/types/src/index.ts and packages/types/package.json are both 0 paths in this diff, so the entry and the exports map are byte-identical anyway.

2. @objectstack/runtime publishes nothing new. — correct. Its two paths in the diff are packages/runtime/src/declared-capability-absence-warn-once.test.ts (new) and packages/runtime/src/dispatcher-5xx-always-logged.test.ts. Both are *.test.ts; no runtime source file moves.

3. packages/spec is untouched and the new spec references are type-only. — correct. No packages/spec/** path in the diff (5 paths, listed above). In server-fault-log.ts the spec references are import type { Logger } from '@objectstack/spec/contracts' (:103) and import type { StandardErrorCode } from '@objectstack/spec/api' (:104) — both import type, so no runtime edge is added. The predicate's own vocabulary, declaresServerFault, comes from ./error-leak.js, inside @objectstack/types.

4. The wire does not move. — correct, and structurally so. logServerFault (:324) takes an input and an optional logger, returns a boolean, and in its whole body touches only logger.warn / logger.error or the console fallback; it holds no response object and can therefore not change a byte. The dev's before/after block is the behavioural half of the same answer, and it was written to run on the pre-change tree (2/2 pass there, while the new suite as a whole goes 4 failed / 3 passed) — which is what makes it a measurement rather than a claim.

5. The ruling's execution line says «packages/rest + the shared funnel in @objectstack/types», and this diff edits no packages/rest file. That is the right delivery, not a gap — and here is the reading, which the seat owed from 5646125252.

git grep -c 'logServerFault' 44e14e367 -- packages/restzero hits in the whole package, with a control that fires on the same path set (sendError is spelled in at least five packages/rest files, error-response.ts among them). The REST door does not log 5xx itself: it reaches the funnel through sendError (packages/types/src/response-envelope.ts:204), which calls logServerFault at its single exit (:227). The dispatcher reaches it directly (packages/runtime/src/dispatcher-plugin.ts :318, :394, :656). ⇒ applying the predicate inside logServerFault makes both doors read one answer with nothing to edit on the REST side, and no door can opt out by forgetting a call. The ruling's constraint is 「⛔ Not spelled once per door: the REST door and the dispatcher read the same predicate」 — the delivered shape satisfies it more strongly than an exported predicate two doors agree to call, and the named package in the execution line was a route to that constraint, not the constraint.

6. The three pins the ruling named are present, and the one that matters can fail. The ruling asked for: 「a declared 501 on a polled route logs exactly once across N requests in one process, at warn; an undeclared 500 on the same route logs N times at error; the two doors agree」. On this head: the first two exist at both levels (funnel pins in packages/types/src/server-fault-log.test.ts, door-level pins at declared-capability-absence-warn-once.test.ts :152 and :198), and the two-door pin (:226) is not a double call to the funnel — it drives the real dispatcher route for door 1 and the real sendError for door 2, carrying door 1's answered envelope over as the fixture. The dev's ablation (gate the demotion on input.request !== undefined, which only the dispatcher supplies) reddened exactly that pin and nothing else, with the mutation proved live in the rebuilt dist and the restore proved byte-identical by blob hash. A pin that is shown to fail is worth the line it occupies.

7. All three boundaries of the demotion point the loud way. — correct. A door supplying no route coordinates is demoted but never deduped (claimCapabilityAbsenceReport returns true on an undefined key); at the 512 ceiling the registry stops adding rather than evicting, so an unrecorded route reports every time; and the predicate reads the envelope's declared code, never input.error, so a throw that resolved to 501 keeps its per-request error line and its stack. The quiet branch is the only branch that can be silent above 500, and it is reachable only for an envelope a door composed.

② semver level — patch on @objectstack/types and @objectstack/runtime

Consistent with ①: no published symbol is added, no exported type is widened, and no response byte moves. The changeset declares exactly those two packages at patch, which is also what the ruling asked for verbatim: 「Changeset: patch on the packages whose logging changes」.

The clause-② declaration is no. The ruling said so when it was written — 「Clause-②: no (re-read on the final diff; if any response byte moves, stop and report)」 — and the re-read on the final diff confirms it. The dispatch order's Clause-②: yes was the seat's error, owned at 5646136503; it is corrected at this review, which is the producer Check Changeset names for it: 「the level is right and the DECLARATION is wrong → correct it at the producer … the Clause-②: line is the claim's」. ⛔ The remedy is not raising @objectstack/types to minor to quiet the gate, and ⛔ not dropping the changeset.

⚠️ check-widening-tells.mjs backs none of this, and says so itself. node scripts/pm/check-widening-tells.mjs --declaration no --diff … exits 0, but its own output reads «5 changed file(s) — 0 judged against a declared surface … 5 NOT MEASURED. ⛔ NOTHING on this diff was examined for widening tells, so this exit 0 is evidence about no surface at all», naming all five paths under «no declared surface covers it». ⇒ that exit 0 is quoted here as NOT MEASURED, never as a clearance. The evidence for no is the export-surface reading in ①.1, not this gate.

③ Boundary flags and open_questions

open_questions: []. Two out_of_scope_findings, both noted, not filed, both answered here:

  1. «The REST door names no route to the funnel, so its declared absences are demoted to warn but not deduped.»Accepted as delivered. sendError passes only requestId; the one producer of parked route coordinates (instrumentRouteHandler) is applied by the dispatcher's server proxy, while rest-api-plugin mounts its direct-mount registrars on the raw server, so reading coordinates in sendError would be dead code today. The consequence is more lines, never fewer, and the reachable population is one non-polled route (packages/rest/src/external-datasource-routes.ts:383, a 503). It is not a reproducible defect, not a declared-contract violation, and not a metadata-authoring trap ⇒ ⛔ no card. Carrier, made concrete rather than left as «the next PR to touch the file»: the next queued card in this lane that opens the REST door's own fault logging is packages/rest logs 1,922 stack-frame lines per suite run from its OWN fault logging — logError hands Error objects to console.error, and 55.7% originate in error-response.ts #15484 (packages/rest/src/log.ts / error-response.ts), which is hard-serial behind this one; the seat carries this note into that dispatch order. The PR's own ## Acceptance notes remains the durable carrier.
  2. «vi.setConfig({ testTimeout: 30_000 }) is the price of per-test isolation.»Accepted. Each test re-executes the dispatcher's module graph so the per-process registry starts empty; the reason is written in the file itself, where its next editor meets it. ⛔ Not a hidden slowness allowance.

No flag is escalated; nothing here needs a maintainer.

Independence pair

Implemented-by: claude/issue-14656-declared-capability-absence-warn-once (mode:subagent)
Reviewed-by: os-sales — domain:cli execution seat, issue #6024, session_01TSf4DV7ziu4V5j73e46b7c
Independence: SELF-REVIEW — the implementer is a subagent of the reviewing seat's own session

Tier: default judgment, which is what this lane's clause-② review is — 「余席条款②复核 = 默认判断档自审加门禁」. ⛔ Not a spec-seat tiered adjudication, and it does not claim to be one.

Verdict

PASS — with the clause-② declaration corrected yesno at the producer, which is the seat's act, not the dev's.

Next, in one stroke and in this order: needs:contract-review comes off both carriers (card #14656 and this PR) with a provenance comment citing this record and this head, and the PR body's Clause-②: line is corrected in the same write as the PR-side label. Then the landing pre-check — this record on file, node scripts/pm/check-clause2-carriers.mjs --pair 17854 reading 0, and every check green (⛔ not the required subset) — before ready and the queue.


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

1 participant