Skip to content

Pin the teaching corpus against lexical claim drift: a table-driven $exists word ratchet - #13742

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-13582-lexical-antidrift-pin
Aug 31, 2026
Merged

Pin the teaching corpus against lexical claim drift: a table-driven $exists word ratchet#13742
os-project-manager merged 1 commit into
mainfrom
claude/issue-13582-lexical-antidrift-pin

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

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 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
packages/spec/scripts/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. 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:skills seat

⚠️ One of this gate's two roots is skills/**. Ownership stays domain:devx per
the #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

VOCABULARY is a list of rows; every engine function takes the table as a parameter.
A row is:

field meaning
id stable ledger key (the baseline is keyed by file and row)
subject the spelling, for humans
spelling the operator/key regex — a site needs this within window lines
claims the contradicting phrasings, joined into one alternation
window max line distance, per row
truth what the platform really does — printed in every diagnostic
refs where that truth was established

Two design points that are not decoration:

  • Claims are joined into one alternation, not iterated. The members deliberately
    overlap (existence sits inside "Field existence check"). Iterating counts one claim
    three 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.
  • The ledger is keyed by row, so two rows in one file are two separable debts. A single
    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 a
self-test assertion pins that — so filling it is a deliberate, reviewable edit. Genericity
is proven without shipping a second word: --self-test drives a synthetic second
row 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:

// Field exists (NoSQL)
where: { metadata: { $exists: true } }

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 claim
phrases across both roots found eight, and the co-occurrence design keeps all eight
green structurally — none of these files mentions $exists at all, so none of them
needs a baseline row
:

site text
skills/objectstack-formula/SKILL.md has(record.x) is true whenever the key exists — the one the dispatch named
content/docs/data-modeling/formulas.mdx the same has() gotcha in prose
content/docs/kernel/contracts/cache-service.mdx a cache has() probe: "Checks if a key exists"
content/docs/automation/hooks.mdx "Field existence is checked on both surfaces"
skills/objectstack-data/rules/validation.md "never a script-based existence check"
content/docs/api/error-catalog.mdx "Ensure the field exists and has sortable: true"
content/docs/deployment/troubleshooting.mdx "Verify the field exists on the object"
content/docs/protocol/objectui/actions.mdx "when no matching field exists"

That 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 $exists is written beside it — so the greens are
proven 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 $exists is 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 条目 + baseline are all present. Three additions:

  • A fifth piece the dispatch did not name. scripts/check-ratchet-remedy-authority.mjs
    audits its CONTROL corpus for set equality both ways, so the new gate landed as
    UNCLASSIFIED: ... is reached by the sweep as "marked", and the control corpus does not cover it — measured, not guessed. A CONTROL row records it, taken from the sweep's
    own verdict
    rather than from author intent.
  • A row whose subject appears nowhere in the corpus is REFUSED, not silently green.
    If $exists were renamed or retired, the row would otherwise report green forever while
    checking 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.
  • A claim that can match the empty string is REFUSED before anything is scanned; such a
    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-measure leg refuses: "56 workspace dependencies of
    the ledgered packages have no built type entry point on disk"
    . A prerequisite, not a
    finding; the live check-type-check-coverage leg printed OK. This diff adds no
    TypeScript.
  • check-test-completeness — exit 3, and the script's own text says so: "Nothing was
    measured ... ⛔ It is NOT a finding"
    .

Ablation — four legs, each mutation confirmed on disk

No build step is involved (plain .mjs, no dist/). Every leg mutated a committed
file, proved the anchor count moved 1 -> 0 and the injection landed, ran, restored with
git 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.

leg mutation result
A' proximity to line-scoped (t === i) — the rule shape that would have missed #13539 RED, 4 named failures, incl. the os:check comment leg
B' the naive phrase ban — co-occurrence removed entirely RED, 10 named failures — every legitimate-usage leg fires
D drop the bare existence claim member RED, 3 named failures, incl. the Existence axis-label leg
E smuggle a second VOCABULARY row RED, 10 named failures, incl. the one-row cap

Final: git diff HEAD empty, blob 50d42816e4f64e6614a60a3f3f5cf3c70f5dd1a0 identical 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's Existence axis label, which the bare
existence claim member catches at zero baseline cost on today's corpus.

Notes


Generated by Claude Code

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 os-project-manager added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed size/xl labels Aug 31, 2026 — with Claude
@github-actions github-actions Bot added ci/cd dependencies Pull requests that update a dependency file labels Aug 31, 2026
@os-project-manager
os-project-manager marked this pull request as ready for review August 31, 2026 09:20
@os-project-manager
os-project-manager added this pull request to the merge queue Aug 31, 2026
Merged via the queue into main with commit adf4bf4 Aug 31, 2026
40 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-13582-lexical-antidrift-pin branch August 31, 2026 09:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants