Uh oh!
There was an error while loading. Please reload this page.
fix(scripts,spec): doc-authoring Rule 3 reaches refusal prose built inside functions, and the 55 tracker ids it was blind to - #13151
Conversation
…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
📓 Docs Drift Check25 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not 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
Coarse fallback — 126 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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 |
…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-trump
commented
Aug 29, 2026
Standing down on this failure — it is not this PR's (domain:spec seat,
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-trump
commented
Aug 29, 2026
Closing the loop on the stand-down above: the single re-run came back green — Test Core is success at head The head is now clean for the reviewer. Landing posture is unchanged: DRAFT, Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#13002
The ruling this executes
Maintainer, 2026-08-29 (issue comment 5459669058), verbatim:
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:
Per-file, matching the card's own table:
data/driver/common.zod.ts15 ·ui/component.zod.ts5 ·ui/action.zod.ts2 ·ui/dashboard.zod.ts2 · one each indata/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; anid at an already-recognised
message:is NOT reported (it is reachable today); an id inan ordinary non-
errorhelper IS reported by the census as a distinct shape, which iswhat 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.
INLINE_CREDENTIAL_REFUSEDz.never({ error: () => … })data/driver/common.zod.tsURL_EMBEDDED_CREDENTIAL_REFUSEDctx.addIssue({ message })data/driver/common.zod.tsURL_CREDENTIAL_QUERY_PARAM_REFUSEDctx.addIssue({ message })data/driver/common.zod.tsPASSTHROUGH_INLINE_CREDENTIAL_REFUSEDmessage:data/driver/common.zod.tsUNRESOLVED_PLACEHOLDER_REFUSEDctx.addIssue({ message })data/driver/common.zod.tscapRemovedretiredKey(…)tombstonedata/driver.zod.tspoolBelongsOnDatasourcestrictObjectguidance:data/driver/postgres.zod.tsdeclaredIndexUniqueScopeError(a$ZodErrorMap)error: …data/object.zod.tserror: (iss) => …onparamserror:ui/action.zod.tsnavItemSurface(: StrictObjectOptionsfactory)strictObjecthistory:ui/app.zod.tselementFilterRetired/elementFormRetiredretiredKey(…)tombstoneui/component.zod.tsobjectBlockHistorystrictObjecthistory:ui/component.zod.tsCOMPARE_TO_STRING_RETIREDstrictObjectretiredForms:ui/dashboard.zod.tsWIDGET_ACTION_RETIREDretiredKey(…)tombstoneui/dashboard.zod.tsNo literal was ambiguous; nothing was stripped on a guess.
The ruling's clause 1 names two shapes: a function that is the value of a zod
erroroption, and
error:inside a schema options object. Teaching the sink pass abouterror:is not optional — it is load-bearing for the ruled 28(
INLINE_CREDENTIAL_REFUSEDreaches its sink only throughz.never({ error: () => INLINE_CREDENTIAL_REFUSED(key) }), so without it that const isnot a sink and its whole body stays invisible).
Once
error:is a recognised sink, 19 further literals appear — refusal prose hoistedinto a const that an
error: (issue) => …callback merely DISPATCHES to, so the literalis 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:error:callback (surfaced by clause 1)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 aretirement 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
customerTextPositionnow crosses a function boundary —ArrowFunction,FunctionExpression, and theReturnStatementleg — under exactly one rule:decided by asking
customerTextPositionthe same question about the function node thatit was asked about the literal. Three ways that can be true:
errorproperty (the ruling's named shape);collectTextSinkConstsfixedpoint already proved feeds a recognised sink — the hoisted spelling of the same thing;
(
(v): StrictObjectOptions => ({ history: … })), once aSTRICT_OPTION_KEYSkey hasbeen 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:
customer-facing consumes. Its body stays unreachable.
const telemetryTag = …) and a non-STRICT_OPTION_KEYSkey inside aRECOGNISED 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
ConditionalExpressionsince the rule was written and errstoward 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
functionBuiltbucket, not a fold intomessage/tombstone. The blindness floor is PER BUCKET, and folding would let this clause rotback to
undefinedwhile those buckets' DIRECT members held the floor up — which isexactly 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/mainis green over the same tree (control, so the redbelow is the widening and not a pre-existing failure):
RED — widened gate, BEFORE the strip (the reproduce-first proof;
GATE EXIT=1):GREEN — widened gate at head, AFTER the strip (
GATE EXIT=0):Self-tests — new cases, in the same edit
Eight cases added to
selfTestRule3, six RED pairs and two precision negatives:error: (iss) => …callback is RED,where=error: (built in a function), bucketfunctionBuilterror:callback DISPATCHES to is RED (via PREVIEW_RETIRED)message:BUILDER function is RED (via INLINE_CREDENTIAL_REFUSED (built in a function))returnleg, not a concise body) from a tombstone builder is RED: StrictObjectOptionsoptions factory is RED (strictObject history (built in a function))error:STRING is RED, bucketmessage— nothing was built in a functionfunctionBuiltcan go blind ON ITS OWN while the other four stay populatedThat 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
seenfloor.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 undefinedatArrowFunction/ReturnStatement.(
throughFunction2 → 0, theerror: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
perlanchors matched ZERO times was caught by exactly this check and discarded —reported here because a silent zero-hit mutation reads as a successful ablation.
16self-test failures naming every new case — andstronger than predicted, the gate itself goes RED rather than green, on the
functionBuiltblindness 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 notsilent.
83ee8279…equals the HEAD blob,git diff HEADempty, gate exit 0, self-test exit 0. The script carries atrap … EXIT INT TERMwith absolute paths and reads an empty hash as FAILURE.Gates, tests, typecheck
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackover 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-repopnpm build),check-half-states(needs a real GitHub credential — the container tokenis a 14-byte placeholder),
check-test-completeness(needs a CI-supplied turbo logargument). 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-examplesall exit 0.just the derived family):
check:doc-authoring --self-testgreen;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 test— 443 passed (443) files, 11752 passed tests.pnpm --filter @objectstack/spec check:generated— ✓ All 14 generated artifacts areup 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.--listFilesproof: all 13edited sources are in the main program (1 hit each); the 10 edited test files are 0
hits there — the main
tscprogram excludes test files — and 1 hit each in thetest-layer program (
tsconfig.test.json, 2042 files), whichcheck:test-typecheckruns 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/specis covered in section 7.4. What was kept, deliberately
ADR ids, protocol and package versions, error codes (
400 INVALID_FIELDtraces theruntime twin far better than the id beside it), and the
os migrate meta --from Ncommands. 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 —
poolBelongsOnDatasourceindata/driver/postgres.zod.tsis 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
mainbefore the first push: ittouches
packages/spec/src/ui/page.zod.ts,ui/index.ts,ui/component-type-vocabulary.tsand
content/docs/references/ui/page.mdx— no overlap with any file here, so nowhole-file deferral was needed.
origin/mainmoved under this branch three timesmid-flight (starting with
ca1965f2b, the cross-module guidance-const widening to thesame 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()andctx.addIssue()text) counts 47 moreliterals / 51 more ids across 14 files that this PR deliberately does NOT touch:
bareDateRangePresetComparandMessage,assertSystemDataIsWritable,unknownKeyError,nonListComparandError,stripLegacyApiMethods,validateCrossReferencesand friends.They are not a
functionBuiltmember because a function DECLARATION is never atext-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/6andTemporal Conformance (live PG + MySQL)were both red at the first head, and the joblogs 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 thesuites' 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 inthe repo —
*.ts,*.tsx,*.mts,*.mjs,*.js,*.cjs, not just*.test.*, sincea shared testkit or fixture module can assert too — was parsed to its balanced closing
paren and checked for one of them:
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 HEADdoes not listthem) 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,310pinpackages/core's OWNregistration-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 Rule3's population entirely. Verified: that file is 18 passed (18).
packages/spec/scripts/check-react-blocks-declaration-parity.test.ts:338pins a reportHEADER a gate script prints (
check-react-blocks-declaration-parity.ts:268) — internaltooling 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 itsDelete the aggregationclause 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/mainmerged twice more during this round; thefinal 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 test443 passed (443) / 11752 passed (11752);
packages/corehot-reload 18 passed (18);check:doc-authoringgreen (13759 customer-facing string(s) across 673 spec sources clean … functionBuilt 217) and its--self-testgreen;check:generatedAll 14generated artifacts are up to date;
check:nul-bytesexit 0; the re-derived gate family54 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.