Repository navigation
feat(gates): check the marked fenced examples in the skill guides - #7427
Merged
Merged
Conversation
Nothing in this repository read inside a code fence under `skills/`: 112 TypeScript fences and 56 JSON fences, checked by no gate at all. Every gate that could reach them is scoped elsewhere by construction — the paths gate deliberately reads inline code spans in prose only, and the three doc gates walk `content/docs` plus the package READMEs. So a fence could import a symbol that does not exist and stay green forever, and two rounds of exactly that had already been cleaned by hand. Ports objectstack's opt-in marker convention: a fence is checked only when the line immediately above it is the marker HTML comment. TypeScript fences compile `--strict` against the packages' built `dist/*.d.ts` — the surface a reader who installs `@object-ui/*` actually resolves — and JSON fences must parse. The type-check harness is imported from `check-doc-snippet-types.mjs` rather than re-rolled, so the syntax/semantics split and its four self-controls are shared rather than duplicated. Adjacency is strict and an orphan marker fails the run: a marker that opts nothing in while its author believes otherwise is this repository's recurring "looks like enforcement, isn't" class. Unmarked fences are ignored, so the gate starts at zero red; a run in which nothing is marked exits 2 rather than 0, because a gate that checks nothing must not report success. Starting population, measured by the gate's own `--measure` at the branch point and landed as 56 marker lines: 17 of 112 TypeScript fences and 39 of 56 JSON fences already hold up. No ratchet and no count pin — whether the marked population becomes shrink-only is a separate decision. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
os-zhuang
approved these changes
Sep 3, 2026
os-zhuang
marked this pull request as ready for review
September 3, 2026 06:27
This was referenced Sep 3, 2026
This was referenced Sep 3, 2026
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 #7359
Nothing in this repository read inside a code fence under
skills/. At the branch point,skills/objectui/carried 112ts/tsx/typescriptfences and 56jsonfences, andos:checkoccurred 0 times. Every gate that could have reached them is scoped elsewhere by construction —check-skills-paths.mjsdeliberately reads inline code spans in prose only ("a fence is a worked example"),check-doc-links.mjshas noskillsrow inSCAN_ROOTS, and the threecontent/docsgates say so in their own headers. So a fence could import a symbol that does not exist and stay green forever, and two rounds of exactly that had already been cleaned by hand.This ports objectstack's opt-in marker convention from
packages/spec/scripts/check-skill-examples.ts, adds an objectui runner, wires it into CI, and lands the marker on exactly the fences that already hold up.Branch point:
75e7436. Head at the readings below:70fdb0e.Design
The marker, and adjacency. A fence is checked only when the line immediately above it is the marker: an HTML comment whose entire content is
os:check. (The exact spelling isMARKERinscripts/check-skill-examples.mjsand is not reproduced in this body — GitHub's body sanitizer eats angle-bracket fragments, backticked ones included.) Two properties make that spelling the right one, both inherited from objectstack's reasoning: it renders to nothing, and it leaves the fence info string a bare```typescript, so the three gates in this repo that key on the info string still see the block. A fence-meta tag like```ts checkwould have punched a hole in every one of them.Adjacency is strict — not "the nearest non-blank line above". A marker anywhere else is an orphan and fails the run. This is the one rule where lenience costs everything: a marker separated from its fence by a blank line, or left behind when its example was rewritten, opts in nothing while its author believes their example is gated. That is this repository's recurring "looks like enforcement, isn't" class (#3009, #3181, #3494, #4846, #7115).
Fence-awareness. A marker that sits inside some other fenced block — a
```markdownillustration of the convention — is at once not an opt-in and not an orphan: it is not at top level, so it claims nothing. One walk (fenceSpans) answers both "is this marker at top level?" and "does this line open a fence?", and it also supplies each fence's closing line, so the body's end is decided by exactly the predicate that decided it opened. objectstack measured a second, looser closer disagreeing with the walk's in both directions (an indented closer; a CRLF file), so there is deliberately only one here.How imports resolve: the BUILT
dist/*.d.ts. A markedtsfence compiles--strictagainst each package's built declarations, resolved through thepathsmapcheck-doc-snippet-types.mjsderives from every package's ownexports/types, plus that gate's declared-dependency rule for third-party specifiers. The alternative — mapping@object-ui/*tosrc/throughtsconfig.base.json— is rejected for the reason that gate's header already states: the root config maps the workspace to source, so a harness that inherited it would judge the guides against code no reader resolves. The reader of a skill guide is a consumer who installs@object-ui/reactfrom npm; that is the surface, so that is what is compiled against.The declared precondition. The cost of that decision is an exit code, never a silent green:
012Exit 2 prints
PRECONDITION NOT METand is never a pass — zero with nothing run reads as coverage, the failure shape this gate family exists to prevent (#4846). The 1-vs-2 split is this repository's convention, not a new one (check-eager-closure-budget.mjs,check-doc-snippet-types.mjs).The harness is imported, not re-rolled.
derivePackageTypePaths,deriveDeclaredDependencyPathsandcompileSnippetscome fromcheck-doc-snippet-types.mjs, so the syntax/semantics split (one parse error suppresses every semantic diagnostic in atscprogram) and its four self-controls — RESOLUTION lands on a built artifact, a planted SENTINEL must produce TS2305, a POSITIVE control must be clean, an UNDECLARED specifier must still fail — are shared rather than duplicated. That gate's header records what hand-rolling the harness three times cost, including one false green.scripts/__tests__/check-skill-examples.test.tspins the import and pins that this script builds nots.createProgramof its own.One honest edge, printed on every run. A bare specifier that neither map covers still resolves if the repository root declares it, because pnpm symlinks the root's own dependency set into
/node_modules. In the marked population that isvitestand@playwright/test. The guides do tell the reader to install vitest, so the examples are not wrong — but the gate is not what proves it. The inherited UNDECLARED control does not close this: it bounds resolution against transitive packages, a different leak. So every run prints anUnmapped specifiers:line naming them, rather than letting it be a silent property of a green. Tightening it is a follow-up below.Starting population
Measured by the gate's own
--measure(which judges every candidate, marked or not) at the branch point, and re-measured at head:ts/tsx/typescriptjson/jsoncThe 95 unmarked TypeScript fences and 17 unmarked JSON fences are fragments by construction — a
columns: [...]subtree, a block continuing the one above it, ajsonfence carrying//comments and...elisions. ⛔ No fence's content was edited to make it pass. The only edit insideskills/objectui/**is marker lines: 56 insertions, 0 deletions, every added line byte-identical.Files that gained markers:
skills/objectui/SKILL.mdskills/objectui/guides/auth-permissions.mdskills/objectui/guides/data-integration.mdskills/objectui/guides/i18n.mdskills/objectui/guides/mobile.mdskills/objectui/guides/page-builder.mdskills/objectui/guides/plugin-development.mdskills/objectui/guides/project-setup.mdskills/objectui/guides/schema-expressions.mdskills/objectui/guides/testing.mdskills/objectui/rules/composition.mdskills/objectui/rules/protocol.mdskills/objectui/rules/styling.mdWhole published bundle (
skills/objectui/**/*.md): 4356 → 4412 lines, +56 — one inert comment line per marked fence, no prose added or rewritten.⛔ No ratchet, no count pin. Whether the marked population becomes shrink-only, the way
check-doc-fence-languages.mjstreats its declared-file population, is recorded as a follow-up rather than decided here. What is enforced is a floor: a run in which nothing is marked exits 2, because a gate that checks nothing must not report success.CI wiring
Its own workflow,
.github/workflows/skill-examples.yml, with nopaths/paths-ignore— modelled onskills-paths.yml.Not the
lint.ymlroute:pnpm checkin this repo isnode packages/cli/dist/cli.js check, the product's own command, not a gate aggregation — there is nocheck:*aggregation to join. The house route for a scripts gate here is a dedicated workflow, and the reason the trigger carries no filter is stated inskills-paths.yml's own header, quoted rather than re-derived: "both of those workflows list'**/*.md',content/**anddocs/**under thepaths-ignoreof theirpushtrigger, with no per-job path filter available in GitHub Actions. A push that only edits a guide would therefore start neither — and editing only a guide is the single most likely way a stated path goes dead."check-skill-examples.test.tspins that, the same waycheck-skills-paths.test.tspins it for its own gate — nopaths, nopaths-ignore, and exactly one home.Not a second step inside
skills-paths.yml: that file's header asks that it stay a checkout plus onenodecall, and this gate cannot be that. Its criterion is the published type surface, so the packages the marked fences import must exist asdist/*.d.tsfirst. The workflow body therefore followsdoc-snippet-types.yml: install, derive the filter from the gate (--build-filter), refuse an empty filter rather than expanding to a whole-workspace build, build, self-test, run. Today the filter names 11 packages, and it grows only as the marked population grows — which is what keeps this inside the 2026-08-16 ruling on #4846 that rejected a per-PR full-repo build.Because it is unfiltered and blocking, the check is classified rather than left to default into silence:
Skill Example Checkis added toREQUIRED_CONTEXTSinscripts/dependabot-merge-gate.mjs(whose test asserts its three buckets partition the produced check set exactly), the workflow subscribesmerge_group(merge-queue-reporting.test.tsderives that floor fromREQUIRED_CONTEXTS), andcontent/docs/guide/ci-cd-pipeline.mdgains an inventory row and a section (ci-cd-pipeline-doc.test.tsrequires a heading naming every workflow file).Reverse verification
Every leg: mutate → prove the mutation landed on disk by counting the injected and the removed text (never a bare
git diff --stat) → run the gate → restore withgit checkout HEAD -- PATH→ prove the restore by blob hash and an emptygit diff HEAD. Absolute paths throughout, with atrap … EXIT INT TERMrestoring all three files. No build step sits between mutation and measurement — the artifact the gate reads is the markdown being mutated, so there is nodistfor a stale copy to hide in.Every marked skill example holds up against the built types.tsfenceuseObjectTranslation→useObjectTranslationNoSuchExportinguides/i18n.md; removed-text count 7 → 0, injected-text count → 7[semantic] skills/objectui/guides/i18n.md:40:10 TS2305: Module '"@object-ui/i18n"' has no exported member 'useObjectTranslationNoSuchExport'.·Semantic phase: 17 of 17 ts fence(s) judged, 1 failed.jsonfencerules/styling.md; removed-text 1 → 0, injected-text → 1[json] skills/objectui/rules/styling.md:183 Expected double-quoted property name in JSON at position 86·JSON phase: 39 fence(s) parsed, 1 failed.rules/composition.md; marker-followed-by-blank occurrences → 1skills/objectui/rules/composition.md:203 [orphan-marker]HEADThe opt-in half is measured too, not assumed. On the same tree, in the same runs:
--measurereports 12 parse failures + 83 semantic failures + 17 JSON failures across all 168 candidates, while the gate itself reports0 failedand exits 0. An unmarked broken fence is ignored, by measurement.Harness controls, from the head run — a probe that cannot fail is not a probe:
Gates
All run at head
70fdb0e, on the tree this PR pushes. Exit codes captured by redirect before any pipe.node scripts/check-skill-examples.mjsEvery marked skill example holds up against the built types.(Semantic phase: 17 of 17 … 0 failed·JSON phase: 39 … 0 failed)node scripts/check-skill-examples.mjs --self-testcheck-skill-examples self-test: 19 cases pass (marker adjacency both directions, orphans, nested illustration, json/jsonc, and the compiler legs through the real harness)node scripts/check-skill-examples.mjs --measureStarting population — ts: 17/112 pass; json: 39/56 pass.pnpm exec vitest run× 12 wiring/pin suitesTest Files 12 passed (12)·Tests 318 passed (318)— includescheck-skill-examples,check-skills-paths,check-doc-snippet-types,merge-queue-reporting,dependabot-merge-gate,ci-cd-pipeline-doc,workflow-cache-save-bound,lint-workflow,scripts-type-check,entry-guard-wiringnode scripts/check-skills-paths.mjscheck-skills-paths: OK (88/89 stated path(s) resolve across 20 guide file(s); 1 baselined)node scripts/check-doc-links.mjsnode scripts/check-control-bytes.mjsnode scripts/check-shell-escape-residue.mjsOK (5/5 root(s) resolved … skills: 16 file(s), 196 fence(s) … 1304 fenced block(s) examined)— the marker lines do not disturb its fence walknode scripts/check-doc-fence-languages.mjsevery TypeScript block in 225 document(s) is fenced ts/tsx/typescript, except 80 declared file(s) … (⛔ SHRINK-ONLY)node scripts/check-doc-snippet-types.mjsEvery covered documentation snippet compiles against the built types.(Semantic phase: 417 of 417 block(s) judged, 0 failed)node scripts/check-changeset-presence.mjsNo source or published contract of a released package changed in this range, so no changeset is owed.— its own verdict decides; no changeset addednode scripts/check-lint-coverage.mjsnode scripts/check-governed-queue-guard.mjs --self-test132 cases passnode scripts/check-governed-queue-guard.mjs --test CHANGE-SET-PATHSGOVERNED— the correct verdict for this diff, not a red:Do not flip it ready … Park it as a DRAFTESLint narrowing.
pnpm lintisturbo run lint, i.e. each workspace package running its owneslint .;scripts/has nopackage.json, so it is in no package's lint run at all — the same structural gapscripts-type-check.test.tsrecords for type-check and closes withtsconfig.scripts.json. Ran ESLint directly on the lintable files this PR touches:pnpm exec eslint --format json scripts/__tests__/check-skill-examples.test.ts scripts/__tests__/merge-queue-reporting.test.ts→ exit 0, 2 files linted, 0 errors, 0 warnings. Invariance for untouched files:eslint.config.jsconfigures no type-aware linting (noproject, noprojectServiceanywhere in it), so no edit in this PR can move the verdict for a file it does not touch. The new.mjsis also clean (0/0), though no config block ineslint.config.jsmatches**/*.mjs— its blocks match**/*.{ts,tsx}only.Follow-ups, recorded not taken
skills/objectui/— 123 TypeScript fences and 63 JSON fences are checked by nothing #7359 step 3, explicitly deferred. The number is re-derivable at any time with--measure, so the decision does not need this PR's memory.anyin a marked block. objectstack's runner treats it as a third assertion — a marker is the claim "this compiles", and every property access on ananyproves nothing. Worth porting; it is a new assertion rather than the convention, so it is not in this flight.vitestand@playwright/testresolve out of the repository root'snode_modules. Printed on every run today; a control that bounds it the way the UNDECLARED control bounds transitives would close it..claude/skills/**.check-skills-paths.mjsscans it (check-skills-paths lost 55 of its 93 assertions when the contributor guides moved out ofskills/— widen SCAN_ROOT to cover.claude/skills#7358 widened it there); this gate's root is the published bundle. Widening is a separate, visible edit with its own measurement.packages/**file):check-sdui-registration-pins.test.tsderivespackages/app-shell/dist/console/connect/ConnectAgentWidget.jswhere it expects thesrc/…tsxpath, andcheck-readme-exports.test.tsreportsexcerptsNotJudged1 where it expects 3. Both suites claim to hold "in BOTH build states"; CI'sTest (shard N/4)runs on an unbuilt tree, so neither is visible there. Reported here rather than fixed.Draft
Draft, and it stays draft.
skills/objectui/**is a governed surface (ui#6866 批 #7) andcheck-governed-queue-guard.mjs --testreturns exit 3 GOVERNED over this change set. No ready flip, no auto-merge, no reviewer requests — a human merge is the review record for a governed surface.🤖 Generated with Claude Code
https://claude.ai/code/session_01LraLgQVGq8egUwfYZpbYt1
Generated by Claude Code