Skip to content

fix(scripts,spec): doc-authoring Rule 3 reaches refusal prose built inside functions, and the 55 tracker ids it was blind to - #13151

Merged
os-elon merged 10 commits into
mainfrom
claude/issue-13002-error-callback-refusals
Aug 29, 2026
Merged

fix(scripts,spec): doc-authoring Rule 3 reaches refusal prose built inside functions, and the 55 tracker ids it was blind to#13151
os-elon merged 10 commits into
mainfrom
claude/issue-13002-error-callback-refusals

Conversation

@os-trump

@os-trumpos-trump commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

Fixes#13002

The ruling this executes

Maintainer, 2026-08-29 (issue comment 5459669058), verbatim:

⚖️ RULED — 2026-08-29, maintainer, live director session batch #7 (session session_016SG9S6V15MqeAgkehDcTwk), verbatim: 「同意」 — the #11052 inheritance REACHES refusal prose built inside error: () => callbacks: same audience, same moment — the ban follows the audience, not the spelling. Adopted work, in the card's own stated shape:

  1. Narrow widening only: customerTextPosition learns to climb through a function that is itself the value of a zod error option (or error: inside a schema options object) — ⛔ never an unconditional climb through arbitrary function bodies (that would sweep values). The widening ships WITH self-test cases proving an unrecognised spelling goes loud, not silent — the card's own condition.
  2. Then strip the 32 tracker ids across the 8 files under the same keep-the-ADR-id rule the [finding] Three ADJACENT customer-facing text populations in packages/spec still carry tracker ids — 539 literals the refusal-message strip deliberately did not touch #12522 charter used.

Both halves land here, in that order, with the gate red between them.

1. Census re-run — the card's numbers reproduced exactly

The triage named this as confidence gap #1 ("普查(28 / 32 / 8)我没有复现"). An
independent AST scan over packages/spec/src (non-test .ts, string literals only),
run with a positive control, reproduces the card's measurement to the literal:

cardthis re-run
id-bearing literals2828
tracker ids in them3232
files88

Per-file, matching the card's own table: data/driver/common.zod.ts 15 ·
ui/component.zod.ts 5 · ui/action.zod.ts 2 · ui/dashboard.zod.ts 2 · one each in
data/driver/postgres.zod.ts, data/driver.zod.ts, data/object.zod.ts,
ui/app.zod.ts.

Positive control (synthetic tree): an id in an error: () => callback is found; an
id at an already-recognised message: is NOT reported (it is reachable today); an id in
an ordinary non-error helper IS reported by the census as a distinct shape, which is
what keeps the census from silently agreeing with the recognizer it exists to check.

Every literal read, and where it reaches the author

The A/B boundary the ruling presumed — "these 28 are customer-facing refusal prose, not
internal values" — holds, and the reading is stronger than expected: none of the 28
is a new bucket. Every one lands in a bucket Rule 3 already rules on; it is only built
by a function
on the way there.

the const / position the literal reachesterminal sinkfileliterals
INLINE_CREDENTIAL_REFUSEDz.never({ error: () => … })data/driver/common.zod.ts1
URL_EMBEDDED_CREDENTIAL_REFUSEDctx.addIssue({ message })data/driver/common.zod.ts3
URL_CREDENTIAL_QUERY_PARAM_REFUSEDctx.addIssue({ message })data/driver/common.zod.ts4
PASSTHROUGH_INLINE_CREDENTIAL_REFUSEDmessage:data/driver/common.zod.ts5
UNRESOLVED_PLACEHOLDER_REFUSEDctx.addIssue({ message })data/driver/common.zod.ts2
capRemovedretiredKey(…) tombstonedata/driver.zod.ts1
poolBelongsOnDatasourcestrictObjectguidance:data/driver/postgres.zod.ts1
declaredIndexUniqueScopeError (a $ZodErrorMap)error: …data/object.zod.ts1
inline error: (iss) => … on paramserror:ui/action.zod.ts2
navItemSurface (: StrictObjectOptions factory)strictObjecthistory:ui/app.zod.ts1
elementFilterRetired / elementFormRetiredretiredKey(…) tombstoneui/component.zod.ts3
objectBlockHistorystrictObjecthistory:ui/component.zod.ts2
COMPARE_TO_STRING_RETIREDstrictObjectretiredForms:ui/dashboard.zod.ts1
WIDGET_ACTION_RETIREDretiredKey(…) tombstoneui/dashboard.zod.ts1

No literal was ambiguous; nothing was stripped on a guess.

⚠️ The widening as ruled surfaces 19 more literals the card did not count

The ruling's clause 1 names two shapes: a function that is the value of a zod error
option, anderror: inside a schema options object. Teaching the sink pass about
error: is not optional — it is load-bearing for the ruled 28
(INLINE_CREDENTIAL_REFUSED reaches its sink only through
z.never({ error: () => INLINE_CREDENTIAL_REFUSED(key) }), so without it that const is
not a sink and its whole body stays invisible).

Once error: is a recognised sink, 19 further literals appear — refusal prose hoisted
into a const that an error: (issue) => … callback merely DISPATCHES to, so the literal
is not lexically inside any function at all. That is the same hoisted-const blind spot
Rule 3 already documents for message:, one option name over:

literalsidsfiles
built INSIDE a function (the ruled census)28328
hoisted beside an error: callback (surfaced by clause 1)19238
total stripped475513

Each of the 19 was read: CRYPTO_HASH_RETIRED, MANAGED_BY_SYSTEM_RETIRED,
FIELD_NODE_OBJECT_FORM_REMOVED, AGG_RETIRED_MIDDLE, RUNTIME_MODE_PREVIEW_RETIRED,
HOT_RELOAD_STATE_STRATEGY_RETIRED, GLOBAL_NAV_RETIRED, CHATTER_POSITION_RETIRED,
MASTER_DETAIL_FORM_TYPE_RETIRED, LIST_VIEW_EXPORT_PDF_RETIRED — every one a
retirement prescription printed verbatim at a rejected author, all through an error:
error map. Same audience, same moment; the ruling's own test. There is no per-string
exemption in this rule by design, so the gate cannot be green at head with these left in.
Flagged here for review as an interpretation of clause 1 rather than a silent expansion.

2. The recognizer, and what it refuses to be

customerTextPosition now crosses a function boundary — ArrowFunction,
FunctionExpression, and the ReturnStatement leg — under exactly one rule:

A function is transparent to the climb only when the FUNCTION ITSELF sits in a
recognised customer-facing position.

decided by asking customerTextPosition the same question about the function node that
it was asked about the literal. Three ways that can be true:

  • the function IS the value of an error property (the ruling's named shape);
  • the function IS the initializer of a const the existing collectTextSinkConsts fixed
    point already proved feeds a recognised sink — the hoisted spelling of the same thing;
  • the function DECLARES itself an options factory by its own return-type annotation
    ((v): StrictObjectOptions => ({ history: … })), once a STRICT_OPTION_KEYS key has
    been latched on the way up. Annotation-driven, never name-driven.

⛔ What it is not: an unconditional climb through arbitrary function bodies. That
version sweeps every string a helper happens to build, values included, and a rule that
reports values as prose is one authors get disabled. Pinned as hard as the reds:

  • an ordinary helper arrow returning a template value with an id in it, which nothing
    customer-facing consumes. Its body stays unreachable.
  • a local (const telemetryTag = …) and a non-STRICT_OPTION_KEYS key inside a
    RECOGNISED options factory. Crossing the boundary does not turn a body into one big
    text position.

Deliberately NOT pinned: a conditional operand sitting under a recognised key. The
climb has passed through ConditionalExpression since the rule was written and errs
toward INCLUSION at a recognised position on purpose. That is the pre-existing rule,
unchanged here; pinning the opposite would pin a claim the rule does not make.

The new population gets its own functionBuilt bucket, not a fold into message /
tombstone. The blindness floor is PER BUCKET, and folding would let this clause rot
back to undefined while those buckets' DIRECT members held the floor up — which is
exactly the silence the card was found by.

3. Evidence

Exit codes captured before any pipe; every verdict below is the gate's own printed line.

Baseline — the gate on origin/main is green over the same tree (control, so the red
below is the widening and not a pre-existing failure):

✓ doc authoring guard: 13338 customer-facing string(s) across 672 spec sources clean —
no internal issue-id references (message 778 · strictObject 3311 · tombstone 751 · describe 8498).

RED — widened gate, BEFORE the strip (the reproduce-first proof; GATE EXIT=1):

47 string(s): functionBuilt 28 · message 19.

GREEN — widened gate at head, AFTER the strip (GATE EXIT=0):

✓ doc authoring guard: 392 files clean — no bare metadata literals.
✓ doc authoring guard: 48 published skill files clean — no internal issue-id references.
✓ doc authoring guard: 13759 customer-facing string(s) across 673 spec sources clean —
no internal issue-id references
(message 940 · strictObject 3353 · tombstone 751 · describe 8498 · functionBuilt 217).

Self-tests — new cases, in the same edit

Eight cases added to selfTestRule3, six RED pairs and two precision negatives:

caseasserts
RED #13an id in an inline error: (iss) => … callback is RED, where = error: (built in a function), bucket functionBuilt
RED #14an id in a const an error: callback DISPATCHES to is RED (via PREVIEW_RETIRED)
RED #15an id inside a message: BUILDER function is RED (via INLINE_CREDENTIAL_REFUSED (built in a function))
RED #16an id RETURNED (the return leg, not a concise body) from a tombstone builder is RED
RED #17an id in a : StrictObjectOptions options factory is RED (strictObject history (built in a function))
RED #18an id in a plain error: STRING is RED, bucket message — nothing was built in a function
precisionthe body of an ordinary helper is NOT swept
precisiona local inside a RECOGNISED factory is not prose
⭐ floorfunctionBuilt can go blind ON ITS OWN while the other four stay populated

That last one is the ruling's own condition made mechanical: an unrecognised spelling
produces no flag, silently, so the only thing that can ever speak for this population
is its own seen floor.

Ablation on the recognizer — run from the COMMITTED implementation

Mutation: remove the error-option recognition and the function-boundary transparency,
restoring the pre-change return undefined at ArrowFunction / ReturnStatement.

  • Mutation confirmed on disk before measuring: the removed text is gone
    (throughFunction 2 → 0, the error: position 1 → 0), the replacement text is present
    (old stop 0 → 1), and the blob hash differs from the HEAD blob. A first attempt whose
    perl anchors matched ZERO times was caught by exactly this check and discarded —
    reported here because a silent zero-hit mutation reads as a successful ablation.
  • Predicted: flagged population drops to 0; the self-test reds loud.
  • Observed: 0 violation lines, 16 self-test failures naming every new case — and
    stronger than predicted, the gate itself goes RED rather than green, on the
    functionBuilt blindness floor:
    ✗ doc authoring guard: 672 spec source(s) were parsed and NOT ONE customer-facing string was recognised in this position: functionBuilt. An unrecognised spelling is not
    silent.
  • Restore leg, proved not assumed: restored blob 83ee8279… equals the HEAD blob,
    git diff HEAD empty, gate exit 0, self-test exit 0. The script carries a
    trap … EXIT INT TERM with absolute paths and reads an empty hash as FAILURE.

Gates, tests, typecheck

  • Derived family: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
    over the real change set → 61 gates, 54 PASS. The other 7 are all
    PREREQUISITE NOT MET / NOT MEASURED by their own printed verdicts, none a finding
    about this diff: check-dev-prereqs, check:dual-build-cjs-loads (need a whole-repo
    pnpm build), check-half-states (needs a real GitHub credential — the container token
    is a 14-byte placeholder), check-test-completeness (needs a CI-supplied turbo log
    argument). Three more that first reported NOT MEASURED were converted into real
    measurements by building the packages they read — check:doc-formula-expressions,
    check:doc-security-posture, check:skill-examples all exit 0.
  • Gate-script debt (a diff touching a gate script owes that script's own suite, not
    just the derived family): check:doc-authoring --self-test green;
    check:parse-guard, check:ratchet-remedy-authority,
    check-published-list-mirrors.mjs, check:pm-dispatch-gates (872 cases) all exit 0.
  • pnpm --filter @objectstack/spec test443 passed (443) files, 11752 passed tests.
  • pnpm --filter @objectstack/spec check:generated✓ All 14 generated artifacts are
    up to date.
    The strip moved five reference pages (id-only deletions in [REMOVED]
    tombstone prose); regenerated via check:generated --fix, never by hand.
  • pnpm --filter @objectstack/spec typecheck — exit 0. --listFiles proof: all 13
    edited sources are in the main program (1 hit each); the 10 edited test files are 0
    hits there — the main tsc program excludes test files — and 1 hit each in the
    test-layer program (tsconfig.test.json, 2042 files), which check:test-typecheck
    runs and reported OK. So the typecheck claim really does cover every edited file.
  • pnpm check:nul-bytes — exit 0, plus a self-scan for raw control bytes.
  • pnpm lint (repo-wide eslint with inline config disabled, not a narrowing) — exit 0.

Test twins moved with the strings

13 assertions across 9 spec test files pinned the tracker ids inside the message text.
Each now pins what the refusal SAYS (the ADR, the version, the error code, the
prescription's own clause) and adds a negative pin asserting the message matches no
issue-id shape, so a re-introduced id reds at the pin as well as at the gate. One further
twin outside packages/spec is covered in section 7.

4. What was kept, deliberately

ADR ids, protocol and package versions, error codes (400 INVALID_FIELD traces the
runtime twin far better than the id beside it), and the os migrate meta --from N
commands. Where an id was the whole parenthetical, the parenthetical went with it. Where
the reference is load-bearing for an INTERNAL reader it moved to an adjacent comment —
poolBelongsOnDatasource in data/driver/postgres.zod.ts is the one case.

⛔ Nothing was booked into any exempted or accepted ledger. The triage was explicit
that un-checked and checked-and-accepted must stay distinguishable; these 47 were never
checked, and they are now simply clean.

5. Coordination

Open PR #12950's branch surface was diffed against main before the first push: it
touches packages/spec/src/ui/page.zod.ts, ui/index.ts, ui/component-type-vocabulary.ts
and content/docs/references/ui/page.mdxno overlap with any file here, so no
whole-file deferral was needed. origin/main moved under this branch three times
mid-flight (starting with ca1965f2b, the cross-module guidance-const widening to the
same gate script); each was merged in, both sides' self-test cases are kept (that PR's
RED #11/#12 first, these renumbered #13#18), and every measurement above was retaken
after the last merge.

6. Open question for review

The gate family is drawn by SPELLING while the ruling is drawn by AUDIENCE, so the
boundary moves rather than disappears — the triage said this in as many words. A broader
census (any literal reaching the author from inside any function, including plain
function declarations that build warn() and ctx.addIssue() text) counts 47 more
literals / 51 more ids across 14 files that this PR deliberately does NOT touch:
bareDateRangePresetComparandMessage, assertSystemDataIsWritable, unknownKeyError,
nonListComparandError, stripLegacyApiMethods, validateCrossReferences and friends.
They are not a functionBuilt member because a function DECLARATION is never a
text-sink const under the existing name-based pass — which is what keeps this widening
inside the ruled scope. Recording it rather than acting on it; it is a fifth population,
and a fifth ruling.

7. Consumer-twin sweep (added after CI went red on the first head)

CI caught one twin the spec suite structurally could not: Test Core 4/6 and
Temporal Conformance (live PG + MySQL) were both red at the first head, and the job
logs show one root cause, not two — the live-DB job's own failure line is the same
vitest file, packages/services/service-analytics/src/__tests__/dataset-compiler.test.ts:115
(Test Files 1 failed | 82 passed), with the DB-container lines around it being the
suites' own poison probes. No live-DB timing or infrastructure failure was involved.

Reproduced first at that head before changing anything: the file fails on exactly that
assertion, expecting the thrown message to match a regex spelling #6188.

The sweep, widened from test files to every tracked source file. The 32 distinct
tracker ids this PR removed were derived mechanically from the diff itself (removed
minus re-added over packages/spec/src), never from memory, and every assertion call in
the repo — *.ts, *.tsx, *.mts, *.mjs, *.js, *.cjs, not just *.test.*, since
a shared testkit or fixture module can assert too — was parsed to its balanced closing
paren and checked for one of them:

count
tracked source files mentioning any stripped id393
assertions pinning a stripped id, before the fix5
of those, twins of this strip (fixed)1
of those, pinning a producer this diff never touched (left alone, each verified by running it)4
assertions pinning a stripped id after the fix, repo-wide4 — the same four

The four left alone are not twins, and that is proved rather than argued: their producing
files are not in this diff (git diff --name-only MERGEBASE HEAD does not list
them) and each still contains the id the pin looks for, so the pin still has its target.

  • packages/core/src/hot-reload.test.ts:259,302,310 pin packages/core's OWN
    registration-time refusal, which is deliberately not shared with the spec string (the
    test says so in its own comment) and lives outside packages/spec/src, so outside Rule
    3's population entirely. Verified: that file is 18 passed (18).
  • packages/spec/scripts/check-react-blocks-declaration-parity.test.ts:338 pins a report
    HEADER a gate script prints (check-react-blocks-declaration-parity.ts:268) — internal
    tooling output, not prose printed at a refused author. Verified: 18 passed (18).

The fix. Re-pinned on what the prescription keeps and a customer can resolve — the
package version and ADR-0049 enforce-or-remove — plus its Delete the aggregation
clause so the assertion still proves it is a PRESCRIPTION and not a bare refusal, plus a
negative pin that the message carries no tracker id in either spelling. Identical
treatment to the three in-spec twins in section 3; no assertion was weakened to something
vacuous, and no tracker id was re-added anywhere.

Re-run at the merged head (origin/main merged twice more during this round; the
final head is 3165b1c1a, read from the tree these were measured on):
@objectstack/service-analytics test83 passed (83) files / 1805 passed (1805) tests
— the same 83 files CI reported as 1 failed | 82 passed; @objectstack/spec test
443 passed (443) / 11752 passed (11752); packages/core hot-reload 18 passed (18);
check:doc-authoring green (13759 customer-facing string(s) across 673 spec sources clean … functionBuilt 217) and its --self-test green; check:generatedAll 14
generated artifacts are up to date
; check:nul-bytes exit 0; the re-derived gate family
54 of 61 PASS with the same 7 PREREQUISITE NOT MET by their own printed verdicts
(three of which were then rebuilt and re-run to exit 0). Exit codes captured before any
pipe throughout.

This PR was authored in Claude Code session session_01LpRNHxWZgSUgVnFT9mQQo4.

…side functions
Narrow widening only: the climb crosses a function boundary when the
FUNCTION ITSELF sits in a recognised customer-facing position -- a zod
`error` option, a text-sink const, or a `: StrictObjectOptions` factory.
Never an unconditional climb through function bodies.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LpRNHxWZgSUgVnFT9mQQo4
# Conflicts:
#	scripts/check-doc-authoring.mjs
@github-actions

github-actionsBot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

25 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 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 4801296e7aadc5fd07e26036d11bd028029cf2a9packageMentionDocs.

Which tree this was computed on

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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

…ADR, not the tracker id
The consumer-side twin of the packages/spec strip: this regex spelled #6188,
which the refusal prose no longer carries. Re-pinned on the version + ADR the
prescription keeps, plus the 'Delete the aggregation' clause, plus a negative
pin that no tracker id is present.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LpRNHxWZgSUgVnFT9mQQo4
@os-trumpClaude

Copy link
Copy Markdown
CollaboratorAuthor

Standing down on this failure — it is not this PR's (domain:spec seat, session_01LpRNHxWZgSUgVnFT9mQQo4).

Note this PR is DRAFT and parked for contract review regardless of CI (see the parking note on #13002): a green head here does not authorize landing, it just hands the reviewer a clean PR.


Generated by Claude Code

@os-trumpClaude

Copy link
Copy Markdown
CollaboratorAuthor

Closing the loop on the stand-down above: the single re-run came back green — Test Core is success at head 3165b1c1a, the same commit that failed, so the serve-publishes-bound-port ENOENT did not reproduce. No further re-runs are owed or will be taken, and this PR does not hold behind #13158.

The head is now clean for the reviewer. Landing posture is unchanged: DRAFT, needs:contract-review on both carriers, ⛔ not armed by this seat.


Generated by Claude Code

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

Labels

documentationImprovements or additions to documentationprotocol:dataprotocol:uisize/lteststooling

Projects

None yet

3 participants

@os-trump@os-elon@claude