Skip to content

refactor(spec): strip internal tracker ids from customer-facing zod refusal messages, and gate their return - #12527

Merged
huangyiirene merged 4 commits into
mainfrom
claude/issue-12124-message-issue-ids
Aug 26, 2026
Merged

refactor(spec): strip internal tracker ids from customer-facing zod refusal messages, and gate their return#12527
huangyiirene merged 4 commits into
mainfrom
claude/issue-12124-message-issue-ids

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Fixes#12124

Customer-facing zod refusal messages in packages/spec/src ended sentences with internal tracker ids. Those strings are printed at the author, verbatim, the moment their metadata is refused — by os validate, by a publish gate, by a parse — and that reader has no tracker to open. Per the triage of 2026-08-25 this inherits the #11052 skills-corpus ruling rather than re-deciding it, resting on the standing maintainer ruling of 2026-08-12, quoted verbatim and untranslated: 「处理 issue 时犯的错应该总结成经验,保留 issue id没有意义」

Census — the card's own command undercounts by 15 of 16

The dispatched census command is single-line. Refusal prose in this tree is written as multi-line 'a ' + 'b ' + 'c' chains, so message: and the id it carries land on different lines:

git grep -nE "message:.*#[0-9]{3,5}" packages/spec/src -> 1 hit
AST census, message-bearing constructs -> 16 literals / 15 messages / 9 files

Control (required before believing the total): both exemplars the card names are founddata/filter.zod.ts:1454, and the ui/action.zod.ts message blocks. Note the card estimated ~10 ids in action.zod.ts messages; the measured number in message strings is 4 (across 3 messages). The other action.zod.ts ids are in TSDoc comments, which are not this population.

The census buckets every id-bearing string literal under packages/spec/src by syntactic position, so the population is named against a total rather than asserted:

bucketliteralsin this PR
message (message: + positional validator messages)16yes -> 0
strictObject (guidance / history / aliases / retiredForms)181no
tombstone (retiredKey(...), *_RETIRED_KEY_GUIDANCE)176no
describe (docs prose)182no
other + test bodies2364no

The three adjacent buckets are the same audience and were deliberately not taken — each is larger than this whole population, each has its own class-wide pins, and AGENTS.md positively asks a tombstone prescription to carry a durable reference. Widening is a separate convention decision, filed as #12522 with the measurements. Rule 3's header carries the same note, so the scope choice is visible where the next author will stand.

Per-member disposition — 16 literals, 15 messages, 9 files

comment = the reference was load-bearing for an internal reader and moved to an adjacent // comment. strip = removed outright; git history keeps the anchor.

file:lineiddispositionafter
ai/skill.zod.ts:162#7113strip...the contract never declared that spelling.
ai/skill.zod.ts:177#7113strip...to test membership of that list.
api/endpoint-publish-gate.ts:366#5040 §7-3comment...before it can be served; call the third-party system from a flow instead
api/endpoint-publish-gate.ts:566#5040 §3.3comment`cacheTtl` is GET-only: it becomes a `Cache-Control` header...
data/filter.zod.ts:1454#7711strip...field names, never $-prefixed operators.
integration/connector.zod.ts:938#7990strip...the credential would land in cleartext. (ADR-0097 §3 already carried in the same message)
kernel/functional-completeness.ts:132cloud#687commentThis is the shape ADR-0078 was written for.
kernel/functional-completeness.ts:281#3196strip...`webhook.zod.ts` records that the `api` trigger was removed... (the message names the file itself)
system/auth-config.zod.ts:420#11739strip...(declaring member_default explicitly is allowed).
ui/action.zod.ts:1513#7278strip...one condition, one wording, one dialog.
ui/action.zod.ts:1567objectui#5933comment...under the interim precedence the declared onSuccess wins...
ui/action.zod.ts:1569#11519strip...is refused at authoring time. (adjacent comment already carried it)
ui/action.zod.ts:1605#11842strip...not this pair. (adjacent comment already carried it)
ui/view.zod.ts:530#5869strip, anchor kept...refuses it too (400 INVALID_FILTER).
ui/view.zod.ts:544#5869strip, anchor kept...refuses it too (400 INVALID_FILTER).
ui/i18n.zod.ts:198#5055strip...the retired key-reference form: nothing looks the key up...

11 strip · 4 comment-kept · 1 strip-with-customer-resolvable-anchor-kept. The two view.zod.ts sites keep 400 INVALID_FILTER: that error code is what an author can actually match their query-path failure against, which is the traceability the id was standing in for.

Escape hatch: no member is machine-load-bearing

The triage reserves re-entry to the decision box for an id that is load-bearing for a machine consumer rather than prose. Checked and none is:

Test twins

Five pins asserted the old wording. Each now pins the sentence carrying the claim, plus a negative pin that the message contains no issue id at all:

  • ui/view-filter-rule-value-shape.test.ts'400 INVALID_FILTER, #5869' -> '400 INVALID_FILTER' + negative pin
  • ai/skill-trigger-condition-value-shape.test.tstoContain('#7113') -> 'the contract never declared that spelling' + negative pin
  • ui/action-doubled-redirect.test.tstoContain('objectui#5933') -> 'interim precedence' + 'silently ignored' + negative pin
  • ui/i18n.test.tstoContain('#5055') -> 'the retired key-reference form' + negative pin

Three of those four were invisible to a fragment grep (they assert the bare id) and were found by running the suite, not by searching. Worth recording: the search-then-edit route would have shipped three red pins.

The gate — Rule 3 in check:doc-authoring

Extended the existing corpus gate rather than adding a new one: it already owns this convention (Rule 2 holds it over skills/**), already owns the precision-tuned id matcher — the trailing lookahead that keeps CSS hex colours out is load-bearing and is reused rather than re-typed — and is already a step in the required Lint & Repo Gates job, so no new CI wiring can go dormant.

It parses rather than scanning lines, for the reason the census shows: a line rule would have been a gate that runs, passes, and is structurally unable to reach 15/16 of its subject. parseSourceFile from scripts/ts-parse.mjs is the sanctioned entry (check:parse-guard reds on a raw ts.createSourceFile elsewhere under scripts/). It climbs out of + concatenation, template spans, parens and conditionals before asking what position a string occupies, and recognises two spellings — the message: property and a positional validator message (.regex(RE, 'msg')), the second of which one founding member is written in.

It asserts it is not blind. This population's steady state is empty, so "0 violations" and "the detector no longer matches how messages are spelled" are the same line. The gate reports how many message strings it recognised at all and reds on zero:

✓ doc authoring guard: 531 refusal-message string(s) across 648 spec sources clean — no internal issue-id references.

No exemption list, by design — same as Rule 2. check:ratchet-remedy-authority sweeps for exactly that shape and is green.

Self-test (node scripts/check-doc-authoring.mjs --self-test)

A red/green pair per claim over a real temporary tree: an id planted on a later line of a concatenated message is red (and the same fixture is asserted invisible to the single-line grep, so this PR's central claim is a measurement in the suite rather than a recollection); a template chain is red; a positional validator message is red; the repo#NNNN cross-repo spelling is red; a .default()value and a .describe() do not fire; test bodies are out of scope; a tree with no recognised message reports seen=0; an empty root is a hard error. Removing each planted id returns green from the same scan.

Reverse verification on the real tree

Direction fixed before running (red, naming file and id — not "more diagnostics", not a reversal):

baseline exit 0 · 531 message strings clean
mutate filter.zod.ts (restore "(#7711)") anchor 1->0, injected text present 1x
blob 7941430d != HEAD blob b659723f -> on disk
gate on mutated tree exit 1 · filter.zod.ts:1454 #7711 [message:]
restore (git checkout HEAD -- the file) blob b659723f == HEAD blob; git diff HEAD empty
gate on restored tree exit 0 · 531 message strings clean

The first attempt at that mutation was a silent no-opperl -0pi -e interpolated $- as a special variable and exited 0 with the file unchanged. The occurrence-count check caught it; the reading was discarded rather than quietly re-run. Recorded because the healthy-looking output of that first run is exactly what a fabricated ablation looks like.

One declaration pin correctly went stale

scripts/pm/dispatch-gates.mjs pinned that check:doc-authoring "claims nothing under packages/". Rule 3 makes that claim legitimately true of packages/spec/src, so the pin was updated to assert the new, real population — the gate reaches packages/spec/src, and still claims nothing elsewhere under packages/ nor spec outside its source tree. check:pm-dispatch-gates: 719 cases pass. Verified from the other side too: dispatch-gates now names check:doc-authoring for a packages/spec/src change, matched via that root.

Sweep discipline

16 files: the 9 census-population sources, the 4 test twins (one file overlaps), the gate, its declaration pin, and the changeset. Zero changes outside the census population plus the gate. Re-running the census after the edits shows message = 0 with the three adjacent buckets at 181 / 176 / 182, unchanged.

H17 hold respected: ui/view.zod.ts was edited at lines 530 and 544 only, far outside the pdf-enum region (~1326-1383); the #8346 hold is named and unaffected — a prose-only id strip moves none of its options. runtime/http-dispatcher.ts untouched.

Verification — all at e8ded0d, the final commit

checkresult
pnpm --filter @objectstack/spec testTest Files 431 passed | 1 skipped (432) · Tests 11462 passed | 1 skipped (11463)
pnpm --filter @objectstack/spec typecheckexit 0 — includes check:test-typecheck under tsconfig.test.json, so the edited test files really are compiled
pnpm --filter @objectstack/spec check:generated✓ All 14 generated artifacts are up to date
check:doc-authoring✓ 531 refusal-message string(s) across 648 spec sources clean
gate union (23 families)23 passed, 0 failed
spec-liveness family (4)all exit 0

Gate list derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (script-derived change set, three-dot semantics) rather than from the dispatch brief, then run. check:generated was re-run after a rebuild because it first refused to answerdist predated a later test edit — which is the gate declining a false green, not staleness in the artifacts.

Verdicts are read from each gate's own printed line, with exit codes captured before any pipe.

Two gate names in the brief's neighbourhood do not exist (check:changeset-fixed, check:changeset-no-major — pnpm answers Command not found, exit 254). Recorded as not measured, not as failures; the wired changeset gates (check:empty-changeset, check:changeset-gate-self-tests, check:objectui-changeset) are green. check:doc-formula-expressions first reported PREREQUISITE NOT MET twice (unbuilt @objectstack/formula, then @objectstack/lint); both were built and it then measured green.

Changeset

@objectstack/spec patch, following the #12439 precedent for message-prose in a published package: before/after, and an explicit statement that no accept/reject behaviour changes — the same inputs are refused on the same schemas with the same issue code, path and error shape. Only the sentence changes. check:adr-0087-registration: no declared-breaking changeset.

No governed surface is touched — today's register is docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.md, and this diff hits none of them.


Generated by Claude Code

…usal messages
15 refusal messages across 9 files under packages/spec/src rendered a tracker
id verbatim to an author running `os validate` / hitting a parse refusal. The
reader of those strings cannot open the tracker, so the id is a citation-shaped
token resolving to nothing.
Where the reference is genuinely load-bearing for an internal reader it moves
to an adjacent // comment (4 sites); elsewhere it is removed outright — git
history keeps the anchor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E5LFCYBJ3q2s6yW6oMLxwy
…sages
Rule 3 in check-doc-authoring: an AST scan of packages/spec/src for internal
issue ids in customer-facing zod refusal messages, inheriting the same ruling
Rule 2 applies to the published skill catalog.
It parses rather than scanning lines because refusal prose here is written as
multi-line string concatenation: the commissioning card's own single-line
census command found 1 of the 16 literals in the population. Recognises both
the `message:` property and the positional validator-message argument, and
asserts it saw message strings at all so a blinded detector reds instead of
reporting a clean population it never located.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E5LFCYBJ3q2s6yW6oMLxwy
Three twins asserted the id as a bare toContain('#7113' / 'objectui#5933' /
'#5055'). Each now pins the sentence that carries the claim, plus a negative
pin that the message contains no issue id at all.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E5LFCYBJ3q2s6yW6oMLxwy
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 11 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/spec/src/ui/i18n.zod.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/deployment/troubleshooting.mdx(via ActionSchema (symbol))
  • content/docs/protocol/objectui/actions.mdx(via ActionSchema (symbol))

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

  • content/docs/releases/v17.mdx(via ActionSchema (symbol), DeclarativeConnectorEntrySchema (symbol), checkFieldCompleteness (symbol))

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

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/src/ui/i18n.zod.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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 — 126 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 14b114553c3e7c61672e3d25a1d15036f7561e27packageMentionDocs.

Which tree this was computed on

This run read content/docs from 73a5d2835c18a4c2ed6d5252111f288e8d214016 — the merge of head e8ded0d2e261f7a841d8d3058afa83003341b37c into base 14b114553c3e7c61672e3d25a1d15036f7561e27, 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 73a5d2835c18a4c2ed6d5252111f288e8d214016 && git checkout 73a5d2835c18a4c2ed6d5252111f288e8d214016
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 14b114553c3e7c61672e3d25a1d15036f7561e27 e8ded0d2e261f7a841d8d3058afa83003341b37c && git checkout -B drift-repro 14b114553c3e7c61672e3d25a1d15036f7561e27 && git merge --no-ff e8ded0d2e261f7a841d8d3058afa83003341b37c
node scripts/docs-audit/affected-docs.mjs --json 14b114553c3e7c61672e3d25a1d15036f7561e27

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

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

Projects

None yet

2 participants

@huangyiirene@claude