Pin the teaching corpus against lexical claim drift: a table-driven $exists word ratchet - #13742
Merged
os-project-manager merged 1 commit intoAug 31, 2026
Merged
Conversation
A table-driven, shrink-only ratchet over `content/docs` and `skills`: it fails when a spelling the platform implements stands within a few lines of prose naming a semantic it does not have. #13539 taught `$exists` as a key-presence test when it asks whether the field HAS A VALUE. The false line was a `//` comment INSIDE an `os:check` block, so `check-skill-examples.ts` type-checked that exact block green — "has a value" and "the key is present" type identically — and both `skills/**` sites shipped to customers. Table-driven per the #13582 triage ruling, carrying the `$exists` family only. The engine takes the table as a parameter and `--self-test` drives a synthetic second row through it, so genericity is proven without filling the table. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
os-project-manager
marked this pull request as ready for review
August 31, 2026 09:20
This was referenced Aug 31, 2026
os-project-manager
deleted the
claude/issue-13582-lexical-antidrift-pin
branch
August 31, 2026 09:56
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #13582
A table-driven, shrink-only lexical anti-drift ratchet over the teaching corpus
(
content/docs+skills). It fails when a spelling the platform implements standswithin a few lines of prose naming a semantic it does not have.
#13539 taught
$existsas a key-presence test when it asks whether the field has avalue. The false line was a
//comment inside anos:checkblock, sopackages/spec/scripts/check-skill-examples.tstype-checked that exact block green —"has a value" and "the key is present" type identically — and both
skills/**sitesshipped to customers. The card's claim that no gate could catch it is half true: the
type half is unreachable, the lexical half is mechanical. This is the lexical half.
Ownership note for the
domain:skillsseatskills/**. Ownership staysdomain:devxperthe #13582 triage ruling — the gate's SUBJECT is the factual correctness of teaching
text, not the governance of an agent instruction face — but the skills seat should know a
gate now reads its tree. It costs
skills/**zero baseline entries today.The table
VOCABULARYis a list of rows; every engine function takes the table as a parameter.A row is:
idsubjectspellingwindowlinesclaimswindowtruthrefsTwo design points that are not decoration:
overlap (
existencesits inside "Field existence check"). Iterating counts one claimthree times and writes an inflated number into a ledger that is only allowed to shrink —
a debt the tree can never pay off, because paying it means deleting text that is not
there.
per-file total would make one row's repair read as the other's regression.
⛔ The table ships exactly one row (
exists-key-presence), per the ruling, and aself-test assertion pins that — so filling it is a deliberate, reviewable edit. Genericity
is proven without shipping a second word:
--self-testdrives a synthetic secondrow through the real engine and asserts it is keyed separately and honours its own
window. A table only one row ever reaches is a single regex wearing a table's clothes.The window is 4 lines, and the number is measured
The real defect had the false sentence and the operator on adjacent lines:
so a line-scoped rule would have missed the very site this gate exists for. Swept over the
corpus at 1, 2, 4, 6, 8, 12, 20 and 40 lines: every width from 1 to 20 yields the
identical ledger (2 files, 4 sites); the first extra site appears only at 40. So 4 sits
with ~5x margin below the nearest measured noise.
Every legitimate usage found — the point the dispatch insisted on
The dispatch named
has(record.foo)and said ⛔ do not stop there. Sweeping the claimphrases across both roots found eight, and the co-occurrence design keeps all eight
green structurally — none of these files mentions
$existsat all, so none of themneeds a baseline row:
skills/objectstack-formula/SKILL.mdhas(record.x)is true whenever the key exists — the one the dispatch namedcontent/docs/data-modeling/formulas.mdxhas()gotcha in prosecontent/docs/kernel/contracts/cache-service.mdxhas()probe: "Checks if a key exists"content/docs/automation/hooks.mdxskills/objectstack-data/rules/validation.mdcontent/docs/api/error-catalog.mdxsortable: true"content/docs/deployment/troubleshooting.mdxcontent/docs/protocol/objectui/actions.mdxThat is a better outcome than baselining them: a baselined file carries a budget
forever, and a file that never enters the ledger cannot have its budget quietly spent by a
later edit. All eight are pinned as self-test fixtures, plus a discrimination leg —
the same
has()sentence goes red once$existsis written beside it — so the greens areproven to come from the window and not from an inert rule.
The baseline, in full
Four sites in two files, and all four are true sentences: the repaired ObjectQL and
HTTP-protocol prose names "key presence" precisely in order to say
$existsis not one.A word ratchet judges spelling, not sense, which is exactly why they are baselined:
{ "content/docs/protocol/kernel/http-protocol.mdx": { "exists-key-presence": 1 }, "content/docs/protocol/objectql/query-syntax.mdx": { "exists-key-presence": 3 } }Beyond the four pieces the ruling named
注册 + --self-test + workflow 条目 + baselineare all present. Three additions:scripts/check-ratchet-remedy-authority.mjsaudits its
CONTROLcorpus for set equality both ways, so the new gate landed asUNCLASSIFIED: ... is reached by the sweep as "marked", and the control corpus does not cover it— measured, not guessed. ACONTROLrow records it, taken from the sweep'sown verdict rather than from author intent.
If
$existswere renamed or retired, the row would otherwise report green forever whilechecking nothing — the check:react-declaration-parity 是唯一没接进任何 workflow 的源码审计门禁,且无 MANIFEST 时静默 skip 退出 0 —— 它现在永远不可能红 #4690 shape. Retiring a spelling now has to be an edit to the
table, where review can see it. This is deliberately stricter than
check-role-word.mjs,whose empty corpus is green.
row would report the whole corpus and write that into a shrink-only ledger.
Verification
Local gate union run at
1e8d003b6, exit codes captured before any pipe.26 gates green, including the whole derived family from
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(31 derived):
check:corpus-claim-drift,check:ratchet-remedy-authority,check:entry-guard,check:parse-guard,check:watch-hint-literal,check-self-test-wired,check-self-test-workflow-commands,check-step-collectors,check-required-contexts,check-aggregator-roster,check-ci-filter-parity,check:role-word,check:nul-bytes,check:agent-test-spelling,check:bash32-floor,check:cli-command-ids,check:cross-package-test-inputs,check:node-version,check:pnpm-acquisition,check:pnpm-filter-targets,check:shard-attestation,check:stall-guard-budget,check:turbo-task-graph,check:workflow-status-functions,check-whole-set-label-write,check:type-check-coverage.2 NOT MEASURED, declared, neither a red:
check:type-check-debt— its--re-measureleg refuses: "56 workspace dependencies ofthe ledgered packages have no built type entry point on disk". A prerequisite, not a
finding; the live
check-type-check-coverageleg printedOK. This diff adds noTypeScript.
check-test-completeness— exit 3, and the script's own text says so: "Nothing wasmeasured ... ⛔ It is NOT a finding".
Ablation — four legs, each mutation confirmed on disk
No build step is involved (plain
.mjs, nodist/). Every leg mutated a committedfile, proved the anchor count moved
1 -> 0and the injection landed, ran, restored withgit checkout HEAD -- ABSOLUTE_PATH, and re-proved byte identity against the HEAD blob.Two earlier legs failed to land and the harness voided them rather than reading them.
t === i) — the rule shape that would have missed #13539os:checkcomment legexistenceclaim memberExistenceaxis-label legVOCABULARYrowFinal:
git diff HEADempty, blob50d42816e4f64e6614a60a3f3f5cf3c70f5dd1a0identical to HEAD.The gate was also validated against the pre-repair tree (
75b3bdc86^,e51c78f0c^):it catches all four false sites, including one the card's literal word-face misses —
skills/objectstack-query/rules/filters.md'sExistenceaxis label, which the bareexistenceclaim member catches at zero baseline cost on today's corpus.Notes
shape for a new gate — the last four new-gate commits (
4feeadbf,dc729f35,8b6bf51b,2be4a672) each shippedlint.yml+package.json+ script and zerochangesets.
skip-changesetapplied.lint.ymlhunk sits at line ~1790, besidecheck:role-word, ~400 lines away fromPR Re-derive the tenant-audit write-call-site census as an in-tree artifact (page + generator + drift gate) #13584's hunk at 1381 — no overlap with its tenant-audit census step.
Generated by Claude Code