Skip to content

fix(scripts,runtime): the code-helper stamp shape reaches the object-literal position - #13477

Merged
os-project-manager merged 3 commits into
mainfrom
claude/issue-13233-codehelper-object-literal-position
Aug 30, 2026
Merged

fix(scripts,runtime): the code-helper stamp shape reaches the object-literal position#13477
os-project-manager merged 3 commits into
mainfrom
claude/issue-13233-codehelper-object-literal-position

Conversation

@claude

@claude claude Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Fixes #13233

Widens the code-carrying-helper stamp shape to the object-literal position, lands the verdict rows that widening owes, and answers the card's declared blocker — the undischargeable unresolved findings — with a mechanism rather than a suppression.

The gap

codehelper was anchored on an assignment, .code = ident. The reasoning it implements — the identifier is a PARAMETER, so the literals live at the CALL SITES — belongs to the helper, not to =, and it holds just as well for the helper that builds an object literal:

function postureError(code: string, message: string) {
  return { severity: 'error', code, message };   // <- nothing matched
}

Nothing fired. objlit needs a quote, objlitconst needs a SCREAMING_SNAKE identifier (the conventional parameter name is code), objlittemplate needs backticks. So there was no site and no unresolved entry — the one way the gate's own "reported, never dropped" bound can fail without anything saying so.

The re-census, measured on this head — not quoted

The card's table was taken before the class-method closure merged, so it was re-derived here through the gate's own deriveSites over packages/** non-test source, not through a separate instrument.

figure card (R31) measured here
helpers reached 14 13
call sites newly reached 116 117
new verdict rows 29 29
helpers reducing to nothing 5 5
reached SCREAMING_SNAKE 31 33
unregistered wire codes hiding 0 0

The grade-bearing row is the last one and it holds: of the 33 SCREAMING_SNAKE values reached, 33 are already registered, so zero real wire codes were hiding behind this blindness. Closing it prevents a future defect; it fixed no present one.

The helper count is a definition difference, not drift: the card counts 14 as 13 named + 1 anonymous, and an anonymous helper can produce neither a site nor an unresolved (every call scan is built from the declaration's name), so 13 is the actionable figure and the named counts agree exactly.

What the rows are

All 29 are one genre: ADR-0114 D2 FieldErrorCode members (required, invalid_type, min_value, …) across four validation helpers. The evidence is stronger than a reading of the call sites — each helper's code parameter is typed to that closed z.enum, so the vocabulary is compiler-enforced against a catalog that is not the ledger's, and it lands at ApiError.details.fields[].code, never at error.code. ADR-0112 D6 draws exactly this line. Declared foreign-vocabulary, door: 'none'.

The blocker, and how it is answered

An unresolved entry is pushed unconditionally and no row discharged it, so each of the five would have been a red gate with no verdict available. A reason: 'helper' entry is now dischargeable by a row in a second declared list, UNRESOLVED_CODE_HELPERS, carrying a door, a verdict and its evidence — reconciled in both directions, so a row whose helper the scan stops reporting goes stale and reds.

The restriction is the whole argument, and it is what keeps this a widening rather than an exemption: only reason: 'helper'. The other two reasons name a remedy the author can carry out (constant → resolve it; reassigned → spell it const), so a row there would buy an exemption from work that is possible. A helper has no such remedy — its callers may live in another package, pass a vocabulary declared out of the gate's population, or pass a genuine runtime value. Relative to origin/main the gate sees strictly more: before this change none of the five produced a site or an unresolved, because no shape reached them.

The five, classified: an SMS one-time password (deliverPhoneOtp), a YAML fence body (block), the kebab diagnostic vocabulary already declared out of both gates (Parser#error), a helper whose parameter is typed to the closed ErrorCode union so the compiler enforces it (sendError, 0 in-file callers), and the dispatcher door itself, which authors no code but re-stamps one off a caught value (http-dispatcher.error).

Precision this position does not have — recorded, not smoothed over

This falsifies the dispatch's framing that the unresolved findings are irreducible error codes. Two of the thirteen helpers reached carry no error code at all: deliverPhoneOtp(phone, code) stamps an SMS one-time password into a notification-template payload, and check-yaml-examples.block(code, decl) stamps a raw documentation fence body. They are not irreducible — they are out of population.

.code = ident needs a property named code on a value being mutated, which is nearly always an error; { code } is simply how any record carries any field called code. And no sibling-key test separates them, which was measured rather than assumed: the obvious candidate — require the stamping literal to carry a message sibling, the envelope shape — fails on the largest true positive. record-validator's fail stamps { field, code, def, constraint, messageKey, options, value } with no message at all and alone contributes 15 of the 29 rows; share-links.sendErr stamps { code } and nothing else. That narrowing would drop 25 of the 29 rows to remove 2 false positives. So both are classified, not filtered: a predicate drawn around the cases one finds inconvenient is an exemption wearing a predicate's clothes.

How the shape earns "object literal"

The regex is a candidate generator; two structural guards decide. The innermost enclosing bracket at the code token must be a { — computed once per file by a quote-aware pass (enclosingOpeners) — and the identifier must be a parameter of the enclosing declaration. The bracket guard is what separates an object literal from the argument list f(a, code, b) and the array [a, code], three positions written identically. This is the over-match that made the measuring round discard a regex instrument, and it is real: without the guard the naive anchor reports 19 helpers where the guarded shape reports 13.

Bounded in-place fix, named here because it is not the card's defect

--self-test iterates the samples map, not SHAPES, and nothing held the two equal — so a shape added without a sample is simply never pinned, while the run prints a confident "N shapes OK" with N one too small. It was live: objlithelper landed unpinned and the count read 8 of 9. One assertion now holds SHAPESsamples. Same defect class as the card (a published list whose price is that an unrecognised member reports nothing, silently), mechanical, same gate, no new verification surface.

Verification

Union re-run at final head 4a207492.

Both legs of the gate — a CI step is --self-test && the check, and running one is not running the other:

check-dispatcher-error-vocabulary --self-test: 9 shapes + 217 assertions OK (vocabulary + #9098 door typing)
check-dispatcher-error-vocabulary: OK — 51 unregistered code-stamping site(s), all classified; 1 awaiting a ledger entry (#8846).

Baseline on origin/main was OK — 22 … all classified, so the widening is +29 sites, all with rows, and the discharge list clears all five helpers.

Ablations — each mutation proven on disk by hash before the run, each restore proven byte-identical to its HEAD blob afterwards, with git diff HEAD empty:

  • delete the Parser#error discharge row → 1 finding(s) [unresolved-constant] packages/sdui-parser/src/parse.ts: the code helper 'error(code)' (objlithelper) …. The discharge is not vacuous.
  • delete one verdict row (max_scale) → 1 finding(s) [unclassified-site] … stamps unregistered code 'max_scale' (objlithelper) …. The rows are load-bearing.
  • remove the enclosing-bracket guard → both legs red. The self-test fails on code passed through an argument list was read as an object-literal stamp, and the real tree grows exactly the two argument-list false helpers (containsJsx, findBareAny in check-skill-examples.ts) that the census correction predicted. Restored, self-test green again at 9 shapes + 217 assertions.

Other gates, exit codes captured before any pipe: check:doc-authoring (red first — issue ids in why: string prose; ids stripped, ADR ids kept, now green with the sibling-package baseline held), check:nul-bytes, check:error-code-casing, check:self-test-wired, check:pm-dispatch-gates, bare-root-worklist --self-test, check:cross-package-test-inputs, check:entry-guard, check:parse-guard, check:published-files, check:test-source-alias, check:type-source-resolution, check:watch-hint-literal, check:pnpm-filter-targets, check:agent-test-spelling, check:bash32-floor, check:cli-command-ids, check:slot-lookup, check:page-declaration-shape, check:logger-receiver-detach, check-ci-filter-parity, check-comment-mask-adoption, check-keyed-text-bounds, check-plugin-teardown-shape, check-shard-attestation, check-undeclared-dep-imports, the changeset family, and release-rehearsal-clone --self-test — all green.

pnpm --filter @objectstack/runtime typecheck clean, and --listFiles confirms the edited file is in that program rather than assumed to be. vitest run src/error-envelope.conformance.test.ts src/package-door-error-parity.test.ts src/http-dispatcher.error-leak.test.ts64 passed. pnpm exec turbo run build --filter=@objectstack/runtime — 30/30. Full-repo pnpm lint ran and passed, so no narrowing is claimed.

NOT MEASURED, reported as such rather than as green: check-test-completeness, check-half-states and check:dual-build-cjs-loads each exit 3, their own declared "nothing was swept" code — they need a CI test run or a full pnpm build this branch does not perform.

Scope note for the reviewer

The dispatch declared the file surface as scripts/check-dispatcher-error-vocabulary.mjs alone, while the card body, the triage comment and the claim comment's own ruling all require the 29 verdict rows — which can only live in packages/runtime/src/dispatcher-error-vocabulary.ts, registered in advance as "a rider on the same change, not a re-routing". The rider was taken after checking that no in-flight PR touches either file (all 14 open non-release PRs enumerated, zero hits), so the serialization the surface line exists to protect is not at risk. Flagged rather than assumed.

dispatcher-error-vocabulary.ts is not re-exported from the package index, so no published surface is widened and no runtime behaviour moves — the gate's population does.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/api/error-catalog.mdx (via invalid_boolean (literal), invalid_date (literal), invalid_email (literal), invalid_initial_state (literal), invalid_number (literal), invalid_option (literal), invalid_phone (literal), invalid_time (literal), invalid_transition (literal), invalid_type (literal), invalid_url (literal), max_length (literal), max_scale (literal), max_value (literal), min_length (literal), min_value (literal), reference_ambiguous (literal), reference_not_found (literal))
  • content/docs/api/error-handling-client.mdx (via invalid_email (literal), max_length (literal))
  • content/docs/api/error-handling-server.mdx (via invalid_transition (literal), invalid_type (literal), max_length (literal), min_length (literal), min_value (literal))
  • content/docs/data-modeling/fields.mdx (via reference_not_found (literal))
  • content/docs/deployment/cli.mdx (via invalid_type (literal))
  • content/docs/protocol/objectql/state-machine.mdx (via invalid_initial_state (literal), invalid_option (literal), invalid_transition (literal))
  • content/docs/protocol/objectql/types.mdx (via invalid_email (literal), invalid_phone (literal), invalid_url (literal), max_length (literal))
  • content/docs/protocol/objectui/concept.mdx (via invalid_type (literal))

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

  • content/docs/releases/v16.mdx (via invalid_initial_state (literal))
  • content/docs/releases/v17.mdx (via invalid_type (literal), max_length (literal), max_scale (literal), min_length (literal), min_value (literal))

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
  • 6 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 23 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 b172e2e3b7072f28bb6e8a883feac057159f2cd2packageMentionDocs.

Which tree this was computed on

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

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

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

@github-actions github-actions Bot added documentation Improvements or additions to documentation tooling labels Aug 30, 2026
@os-project-manager
os-project-manager marked this pull request as ready for review August 30, 2026 14:30
@os-project-manager
os-project-manager added this pull request to the merge queue Aug 30, 2026
Merged via the queue into main with commit 3800e42 Aug 30, 2026
34 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-13233-codehelper-object-literal-position branch August 30, 2026 14:54
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/xl tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants