Two-legged identifier-liveness gate over the published skills catalog (#13678) - #13892
Merged
huangyiirene merged 2 commits intoAug 31, 2026
Merged
Conversation
…ills catalog Leg 1 (phantom row): a skills/** table row citing an identifier that greps to zero in packages/** is a red. Leg 2 (missing row): a section REGISTERED as exhaustive over a schema symbol that does not document every member is a red. One corpus walk, two legs. Leg 2 is table-driven rather than inferred, and that is a measurement result: auto-binding a doc table to any enum it is a subset of scores 1 true positive in 3 on this corpus. The heuristic ships as --suggest, which cannot fail. Zero skills/** edits: the five surviving Leg 1 sites and the one measured Leg 2 gap are ledgered, not fixed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EnE7G31tqbxN1rqpQmzurT
- check-ratchet-remedy-authority CONTROL row (the sweep classified it `marked`
before the row was written; the row is recorded from that verdict).
- bare-root-worklist: REFUSE-WIDE for IMPL_ROOTS `packages` — the word index
reads 5405 of 5640 tracked files (96%), so the population IS the root and a
true declaration would name this gate on every card touching any package.
- The watch-hint self-test asserts the spelling without writing a second
declaration site; spelling it as a regex literal made the real one
unjudgeable ("2 declaration sites").
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EnE7G31tqbxN1rqpQmzurT
huangyiirene
marked this pull request as ready for review
August 31, 2026 16:41
huangyiirene
deleted the
claude/issue-13678-skill-identifier-liveness-gate
branch
August 31, 2026 17:05
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 #13678
A two-legged identifier-liveness gate over the published
skills/**catalog, plus its self-test and its registrations with the farm's convention gates. Zeroskills/**edits — the gate reads that corpus, it does not fix it.The two predicates, stated precisely
Leg 1 — phantom row. A CITATION is a markdown table row, outside a fenced block, whose FIRST cell is exactly one backticked token of identifier shape (dotted segments, optional
[]as inbands[].key), optionally followed by a short parenthetical annotation. It is LIVE when every dotted segment appears as a word token in the IMPLEMENTATION INDEX. Zero hits on any segment is a red naming file, line, identifier and the failing segment.The index is built from
packages/**source only — never fromskills/**and never fromcontent/docs/**. That exclusion is the load-bearing line in the leg: an index containing the teaching corpus would find every citation in the corpus that cites it, so every row would be live, the finding set would be empty on any tree, and the gate would be a green light wired to nothing.dist/is skipped for the mirror-image reason — a stale build output keeps a retired identifier alive for as long as nobody cleans it, making the verdict a function of the developer's working directory. Both are--self-testassertions, not prose.Three deliberate refusals in the extractor, each measured rather than assumed:
`none` (default)is a citation ofnone. A first cut required the cell to be exactly a backticked token; that omission silently dropped every annotated row, and on the Leg 2 side it manufactured two false reds by making complete tables read as 4-of-5 and 3-of-4.| Legacy | CEL |,| Old | New |). Reddening those runs the gate backwards. Columns whose HEADER names the pre-migration or negative side are structurally out of the population — structurally, not by ledger entry, so those files never enter a ledger whose budget a later edit could spend.check-corpus-claim-drift, deliberately: that gate exists because its defect lived in a comment inside a fence. This one reads table rows, and a pipe inside a fenced example is not one.Leg 2 — missing row. A BINDING is a human assertion, in a table in the gate, that one named section of one file is exhaustive over one named schema symbol. For each binding: resolve the symbol from its declared source (
z.enumor a named string-literal union, comments stripped first), locate the heading, take the section down to the next heading of the same-or-higher level, and require every member to appear inside some backticked span in that section. A missing member is a red.Two structural safety rules: the anchor must be unique in its file (an ambiguous heading fails rather than silently first-matching a different table than the row's author read), and the symbol must resolve (a binding naming a deleted enum is STALE and fails — without that, deleting the schema would turn the row green, the exact inversion of what the row is for).
Scope is the SECTION, not the file: an exhaustiveness claim covers what a reader sees under that heading, and a file-wide pool credits a member mentioned 800 lines away. Measured, the difference is real —
NavItemVariantreads 7-of-9 against its section and 8-of-9 against its whole file.Why Leg 2 is registered rather than inferred — the false-red measurements
The scope note asks for a conservative "presented as exhaustive" predicate. Three candidates were built and measured on the real corpus before this shape was chosen.
skills/**edits permitted(a) in detail. 17 bindings, 9 candidate reds on the first cut. Six were artefacts: two from the trailing-annotation bug above (
MetadataLockSchemaread 3-of-4 andSeedMode4-of-5 while both tables were complete), one from an enumeration split across several tables in one file (FlowNodeAction, whose own lead says "20 built-in node types"), and three from prose naming its own partiality. Of the three that survived every fix, two are one enum —AggregationMetricTypecarries six aggregation functions plus threenumber/string/booleanmembers whose comments read "Custom SQL expression returning X". A table titled "Basic Aggregation Functions" listing exactly the six is CORRECT, and this predicate calls it false. In two files.(b) in detail. The top-ranked hit is
skills/objectstack-ai/SKILL.md"Three action types dispatch headlessly" — it matches on both "every" and a numeral and is a deliberate 3-of-6 subset. Most other matches belong to a neighbouring sentence, not the table ("every author must remember to re-attach", "all optional").So the table-driven shape is a measurement result, not a preference, and it follows the idiom PR #13742 established next door: the engine takes the table as a parameter and
--self-testdrives synthetic rows through it, so genericity is proven without filling the table. The heuristic is not discarded — it ships as--suggest, a non-failing discovery mode that lists unregistered candidates, so the registry has a feeder and the numbers above stay reproducible instead of living only in a comment.False-red analysis on the current corpus — every hit dispositioned
Leg 1: 500 citations over 48 published files, checked against 91,177 word tokens from 5,405 files. Five rows survive the predicate. None is a phantom, and they are two structural classes:
objectstack-pm-dispatch/SKILL.md:98,102,103backlogRepo,conventionsFile,routingLabelPrefixskill-owned-configobjectstack-ui/SKILL.md:557,562dayStart,showMidnightsibling-repo../objectuiatpackages/plugin-gantt/src/GanttView.tsx. This repo ships backend only,packages/console/distis gitignored, andsdui.manifest.jsondoes not carry gantt plugin props — checked, not assumed.A sixth,
bare_fieldinobjectstack-formula, sits under a| Legacy | CEL |header and is removed structurally by the negative-column rule rather than ledgered.Leg 2: 9 registered bindings. Eight are complete today and are now pinned against future drift:
ApiMethod,ObjectAccessScopeSchema,HookEvent(x2 files),LifecycleClassSchema,SeedMode,ReportType,ActionType. The ninth is a real, known gap, ledgered rather than fixed:### Navigation Item Typesdocuments 7 of the 9NavItemVariantmembers —actionandcomponenthave no row. Filed as #13890, a sub-issue of the sweep program.Registering
ActionTypeagainst the UI skill's complete table while the AI skill documents a deliberate 3-of-6 subset of the same enum is the registry earning its keep: a binding names a SECTION, so the correct table is pinned and the subset is left alone.One finding worth a reviewer's eye, which the gate deliberately does not act on.
ApproverTypehas ten members includingrole, and the doc documents nine. Documenting the tenth would tripcheck:role-word, which makes "role" a reserved-forbidden word across this same corpus. Two gates over one corpus can be put in direct conflict by a schema member's name. No binding is registered for it, and that is a decision rather than an oversight.Robustness to the pending sweep merges
The brief asks that in-flight sweeps not redden this. Checked, not assumed: #13833 touches
objectstack-i18n, #13861 touchesobjectstack-upgradeand addsskills/spec-property-retirement. Neither touches any file this gate ledgers. Predicates are identifier-level throughout — there is no count pin anywhere in either leg, so a sweep that adds or rewrites prose moves nothing unless it adds a citation that greps to zero, which is the gate working.The two ledgers move in OPPOSITE directions, deliberately
kindandnoteare human judgements about why an identifier is unreachable from this repo; nothing in the tree encodes them, so an--updatethat rewrote this list would invent classifications it cannot know.--updateonly DELETES entries the scan no longer reaches. Shrink-only by construction, not by policy: there is no code path that grows it.--updatemoves it whichever way the tree moved. Only policy tells those apart, so the new-gap and grown-gap messages mark that path⛔ MAINTAINER-ONLYwhile the author's own remedy is offered first and unmarked.A gap that SHRANK fails and asks for
--update— an improvement that does not move the ledger leaves budget behind for a later edit to spend silently.Ablation — the gate CAN fail, on both legs
Commit-before-mutate: the implementation was committed first, so the restore leg points at
HEADandHEADreally holds it. Each leg proves its mutation reached DISK before any verdict is read (an editor's exit code proves nothing —sed/perl -i/replaceall exit 0 on zero hits), and each restore is proven by blob-hash equality against the HEAD blob, never by an exit code. The script carries atrap ... EXIT INT TERM, restores withgit checkout HEAD -- PATH(never the bare form, which restores from the index and would hand the mutation straight back at exit 0), and treats an empty hash as FAILURE rather than "nothing to compare".git status --porcelainis empty on the tree this PR was pushed from.Registrations
check-ratchet-remedy-authority— aCONTROLrow. The sweep classified this gatemarkedbefore the row was written, and the row is recorded from that verdict rather than from author intent; the corpus is set-equality audited both ways, so the row was demanded, not volunteered. Getting there needed the remedy text to carry real shrink testimony — the first wording said "weakens the gate", which the detector cannot anchor. Farm now: 12 marked, 6 refusing, 27 control rows.bare-root-worklist—REFUSE-WIDEforIMPL_ROOTS packages. The index reads 5,405 of 5,640 tracked files under that root (96%), so the population is not a subset of the root, it IS the root; a true declaration would name this gate on every card touching any package. Its other root,skills, IS declared beside the constant viaROOT_DIR_WATCH_HINTS— small, and the surface the gate exists for.check-watch-hint-literal— the watch-hint self-test asserts the spelling without writing a second declaration site. Spelling the wholeconst NAME = [...]as a regex literal creates one, and the gate then reported "2 declaration sites — this gate cannot judge a declaration it cannot locate". Measured, then fixed.lint.yml— one step in the job that carries the wholecheck:*family, with no paths filter. That matters more here than for its neighbours: the gate's two inputs areskills/**andpackages/**, so a filter on either would let the OTHER side drift a member in or an identifier out without ever running the check that notices.Verification
All readings taken on
1d84cf3b4, a clean tree, exit codes captured before any pipe.Full derived family union from
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(33 families — all run, both invocation spellings), every one exit 0:entry-guard·parse-guard·watch-hint-literal·ratchet-remedy-authority·skill-identifier-liveness·agent-test-spelling·cli-command-ids·bash32-floor·pnpm-filter-targets·node-version·pnpm-acquisition·nul-bytes·cross-package-test-inputs·required-contexts·shard-attestation·stall-guard-budget·turbo-task-graph·workflow-status-functions·type-check-coverage·type-check-debt·dual-build-cjs-loads·pm-dispatch-gates·bare-root-worklist --self-test·aggregator-roster·ci-filter-parity·position-name-fold-loaders·self-test-wired·self-test-workflow-commands·step-collectors·whole-set-label-write·ci-failure --self-test·docs-audit/check-drift-comment.Both convention-triggered obligations for adding a gate script were run and are green.
check:type-check-debtwas run after building the closure (turbo run build, 70/70 successful), because its--re-measurehalf silently measures a different world without it:OK — 29 ledger entries re-measured, 1531 raw tsc errors, none above its recorded number.Repo-wide
pnpm lint(eslint . --no-inline-config) run in full: exit 0 in 59s. No narrowing claimed.node scripts/check-test-completeness.mjsreturns exit 3 = PREREQUISITE NOT MET (it grades a savedturbo run testlog that only CI produces). Recorded as NOT MEASURED — it is not a finding and not a failure.The two edited gate scripts (
check-ratchet-remedy-authority.mjs,bare-root-worklist.mjs) carry no vitest suite — repo-wide, no test file names either — so their--self-testIS their suite, and both were run green.Changeset
None, and
skip-changesetapplied. Verified rather than assumed: the diff isscripts/**,.github/workflows/lint.yml, and one alias line in the private root manifest (@objectstack/spec-monorepo,private: true). Nothing underpackages/,apps/orexamples/changed, so this releases nothing — the workflow's own words for this case are "this PR edits a CI-internal script is the textbookskip-changesetcase".Scope
scripts/and.github/workflows/lint.ymlare not governed surfaces — regular code lane, opened as draft for the PM. Adjacent cards #13859 and #13835 are deliberately untouched. #13859 is adjacent but not subsumed: it is about a retirement-pin scanning one markdown file, a different instrument over a different question — this gate would not have caught it, because a withdrawn claim's sentence is semantics, and Leg 1 catches existence only.Generated by Claude Code