From 74667b86de7ad9bf2b4f884b45047da1a9dde724 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 23:49:45 +0000 Subject: [PATCH 1/2] fix(spec): keep machine constants off the skill-reference `Exports:` fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `Exports: …` line a pointer row falls back to when its module has no doc block ranked by SOURCE ORDER and had no notion of authorable surface, so `slice(0, 5)` kept whichever five exports were declared first. Three of the eleven modules reaching that fallback declare machine constants near the top, so three published rows headlined them — `DEPRECATED_APPROVER_TYPES` and `NON_AUTHORABLE_APPROVER_TYPES` among them, on a surface loaded whole into a customer agent's context window to teach it what it may author. SCREAMING_SNAKE exports are now dropped and source order kept for the rest, with the cap of five applied AFTER filtering so the authorable names behind the constants are promoted rather than the row merely shortened. A module whose whole export surface is machine constants falls through to no description rather than a bare `Exports:`. Schema-first sorting was considered and NOT taken: on the row that motivated this it demotes `ApproverType`, the enum an author actually writes. The rule moves to `scripts/lib/export-list.ts` so it can be pinned without running the generator (`main()` runs on import), and `export-list.test.ts` enforces it as unit cases plus a corpus gate over the checked-in artifacts. Closes #12201 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01RMTpSRF5CjMmQBFfPtPCwJ --- .../skill-refs-exports-fallback-ranking.md | 42 +++++ .github/workflows/ci.yml | 5 + .../spec/scripts/build-skill-references.ts | 16 +- packages/spec/scripts/export-list.test.ts | 171 ++++++++++++++++++ packages/spec/scripts/lib/export-list.ts | 115 ++++++++++++ scripts/cross-package-test-inputs.mjs | 14 ++ .../references/_index.md | 2 +- skills/objectstack-i18n/references/_index.md | 2 +- .../objectstack-platform/references/_index.md | 2 +- turbo.json | 3 +- 10 files changed, 362 insertions(+), 10 deletions(-) create mode 100644 .changeset/skill-refs-exports-fallback-ranking.md create mode 100644 packages/spec/scripts/export-list.test.ts create mode 100644 packages/spec/scripts/lib/export-list.ts diff --git a/.changeset/skill-refs-exports-fallback-ranking.md b/.changeset/skill-refs-exports-fallback-ranking.md new file mode 100644 index 0000000000..e7790c2366 --- /dev/null +++ b/.changeset/skill-refs-exports-fallback-ranking.md @@ -0,0 +1,42 @@ +--- +"@objectstack/spec": patch +--- + +fix(spec): keep machine constants off the skill-reference `Exports:` fallback + +When a `.zod.ts` has no module doc block, `build-skill-references.ts` falls back +to listing its exports. That line is TRUE — an accurate list of what the module +exports, which is why #12094 kept the fallback rather than refusing. What was +wrong is the RANKING: the list was whichever five exports happened to be +DECLARED FIRST, and the extraction had no notion of authorable surface, so any +`export const` qualified — including constants whose own names say they are not +for authoring. + +Three of the eleven modules that reach this fallback declare their machine +constants near the top, so three published rows headlined them: + +- `automation/approval.zod.ts` named `DEPRECATED_APPROVER_TYPES`, + `NON_AUTHORABLE_APPROVER_TYPES`, `ORG_MEMBERSHIP_LEVELS` and + `APPROVER_EXPRESSION_ROOTS` — four of its five slots +- `kernel/plugin.zod.ts` named `CORE_PLUGIN_TYPES`, `CONSUMER_INSTALLABLE_TYPES` +- `system/translation.zod.ts` named `LEGACY_OBJECT_FIRST_KEYS` + +`skills/**` is loaded whole into a customer agent's context window and its job +is to teach that agent what it may author, so a row headlining +`DEPRECATED_APPROVER_TYPES` and `NON_AUTHORABLE_APPROVER_TYPES` pointed an +authoring agent at exactly the vocabulary it must not use, with nothing on the +line marking them as such. No gate could see it: `check:skill-refs` compares the +artifact against the generator, and the generator ranked faithfully. + +`SCREAMING_SNAKE` exports are now dropped and source order is kept for what +remains, with the cap of five applied AFTER filtering so the authorable names +waiting behind the constants are promoted rather than the row merely shortened. +A module whose entire export surface is machine constants falls through to no +description at all rather than printing a bare `Exports:`. + +Sorting `*Schema` exports first was considered and NOT taken: on the very row +that motivated this it demotes `ApproverType` — the approver-type enum an author +actually writes — below four schema objects, which is worse by this surface's +own standard. The rule moves to `scripts/lib/export-list.ts` so it can be pinned +without running the generator, and `scripts/export-list.test.ts` enforces it +both as unit cases and as a corpus gate over the checked-in artifacts. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 03f401b309..c4d019d348 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -173,6 +173,11 @@ jobs: - 'docs/audits/2026-07-unknown-key-strictness-ledger.md' - 'skills/objectstack-formula/**' - 'skills/objectstack-automation/SKILL.md' + # @objectstack/spec's scripts/export-list.test.ts corpus gate reads + # the whole published catalog (#12201). Verbatim per the declaration; + # it subsumes the two narrower skills entries above, which are left + # as the packages that declared them spelled them. + - 'skills/**' - '.github/workflows/scaffold-e2e.yml' - '.claude/skills/spec-property-retirement/SKILL.md' diff --git a/packages/spec/scripts/build-skill-references.ts b/packages/spec/scripts/build-skill-references.ts index 29cd355b6e..b50f2f496f 100644 --- a/packages/spec/scripts/build-skill-references.ts +++ b/packages/spec/scripts/build-skill-references.ts @@ -25,6 +25,7 @@ import fs from 'fs'; import path from 'path'; +import { exportListDescription } from './lib/export-list'; import { findModuleDocBlock } from './lib/file-description'; import { createSink, type Owns } from './lib/generated-output'; @@ -212,6 +213,14 @@ function resolveAll(entryFiles: string[]): { files: string[]; missing: string[] * A module with no doc block of its own falls through to the export list * rather than refusing: that line states a true fact about the file, where the * wrong block asserted a false one about its subject. + * + * WHICH exports that line may name is `exportListDescription()`'s rule, and it + * lives beside this one in `lib/` for the same reason (#12201): the list used + * to rank by source order with no notion of authorable surface, so three rows + * headlined `DEPRECATED_APPROVER_TYPES`, `CORE_PLUGIN_TYPES` and friends — + * machine vocabulary, published to a surface whose job is to teach an agent + * what it may author. `check:skill-refs` could not see that either; it compares + * the artifact against this generator, which ranked faithfully. */ function extractDescription(filePath: string): string { const content = fs.readFileSync(filePath, 'utf-8'); @@ -228,12 +237,7 @@ function extractDescription(filePath: string): string { return sentence.length > 120 ? sentence.slice(0, 117) + '...' : sentence; } } - const exports: string[] = []; - const re = /export\s+const\s+(\w+Schema|\w+)\s*(?:[:=])/g; - let m: RegExpExecArray | null; - while ((m = re.exec(content)) !== null) exports.push(m[1]); - if (exports.length > 0) return `Exports: ${exports.slice(0, 5).join(', ')}`; - return ''; + return exportListDescription(content) ?? ''; } // ── Index generator ────────────────────────────────────────────────────────── diff --git a/packages/spec/scripts/export-list.test.ts b/packages/spec/scripts/export-list.test.ts new file mode 100644 index 0000000000..36320865b8 --- /dev/null +++ b/packages/spec/scripts/export-list.test.ts @@ -0,0 +1,171 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Pin for WHICH exports the skill-reference `Exports: …` fallback publishes — + * #12201. + * + * The fallback ranked by SOURCE ORDER and had no notion of authorable surface, + * so `slice(0, 5)` kept whichever five exports happened to be declared first. + * Three rows in the published catalog therefore headlined machine constants + * whose own names say they are not for authoring — + * `DEPRECATED_APPROVER_TYPES`, `NON_AUTHORABLE_APPROVER_TYPES`, + * `CORE_PLUGIN_TYPES`, `CONSUMER_INSTALLABLE_TYPES`, + * `LEGACY_OBJECT_FIRST_KEYS` — on a surface loaded whole into a customer + * agent's context window to teach it what it may author. + * + * No gate could see it: `check:skill-refs` compares the artifact against the + * generator, and the generator reproduced the ranking faithfully. That is the + * same blind spot #5059 found one layer up, and the answer is the same one — + * the rule is extracted to a pure module and this file IS its enforcement. + * + * MEASURED (reverse verification) — and the two halves of this file fail + * DIFFERENTLY, which is why both exist. Dropping the `MACHINE_CONSTANT` test + * from `exportListDescription` (keeping everything else) turns four of the six + * unit cases below red immediately — the three about which names survive, plus + * the fall-through case, whose `null` exists only because filtering can empty a + * list. The corpus gate meanwhile stays GREEN: it reads the checked-in + * artifacts, and those only move when someone regenerates. + * What turns the corpus gate red is regenerating with the rule dropped — i.e. + * the state this card found, measured before the fix as 7 offenders across the + * 3 rows (`DEPRECATED_APPROVER_TYPES`, `NON_AUTHORABLE_APPROVER_TYPES`, + * `ORG_MEMBERSHIP_LEVELS`, `APPROVER_EXPRESSION_ROOTS`, + * `LEGACY_OBJECT_FIRST_KEYS`, `CORE_PLUGIN_TYPES`, + * `CONSUMER_INSTALLABLE_TYPES`). + * + * So the unit cases catch a rule that was weakened, and the corpus gate catches + * an artifact that was regenerated from one — including from a `.zod.ts` that + * grew a new constant. Neither subsumes the other. The "keeps a lone all-caps + * token", "no exports at all" and cap-of-five cases stay green under that + * ablation either way, because for those inputs the two rules agree; that + * asymmetry is the point, since the defect was invisible on exactly the inputs + * anyone would have thought to check. + * + * The corpus gate at the end is the part that cannot rot. It re-derives the + * verdict from the checked-in `skills/**` artifacts — the bytes a customer + * agent actually loads — so a future `.zod.ts` that declares a new + * `SCREAMING_SNAKE` const above its schemas cannot quietly re-acquire a + * hazardous row. + */ + +import fs from 'fs'; +import path from 'path'; +import url from 'url'; + +import { describe, expect, it } from 'vitest'; + +import { exportListDescription } from './lib/export-list'; + +const HERE = path.dirname(url.fileURLToPath(import.meta.url)); +const REPO_ROOT = path.resolve(HERE, '../../..'); +const SKILLS_DIR = path.resolve(REPO_ROOT, 'skills'); + +/** Same convention the filter encodes, restated so the gate is self-contained. */ +const SCREAMING_SNAKE = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/; + +describe('exportListDescription — machine constants never headline a pointer row', () => { + it('drops SCREAMING_SNAKE constants and keeps source order for the rest', () => { + // `automation/approval.zod.ts`, reduced. The published row opened + // "Exports: ApproverType, DEPRECATED_APPROVER_TYPES, + // NON_AUTHORABLE_APPROVER_TYPES, ORG_MEMBERSHIP_LEVELS, + // APPROVER_EXPRESSION_ROOTS" — four of five names unusable by an author. + const source = [ + "export const ApproverType = z.enum(['user', 'role']);", + 'export const DEPRECATED_APPROVER_TYPES = [] as const;', + 'export const NON_AUTHORABLE_APPROVER_TYPES = [] as const;', + 'export const ORG_MEMBERSHIP_LEVELS = [] as const;', + 'export const APPROVER_EXPRESSION_ROOTS = [] as const;', + "export const ApprovalDecision = z.enum(['approve']);", + 'export const ApprovalNodeApproverSchema = z.object({});', + ].join('\n'); + + expect(exportListDescription(source)).toBe( + 'Exports: ApproverType, ApprovalDecision, ApprovalNodeApproverSchema', + ); + }); + + it('keeps source order — it does NOT sort *Schema exports first', () => { + // Adjudicated on #12201 and pinned here so it is not "improved" later: + // Schema-first ranking demotes `ApproverType`, the enum an author actually + // writes, below the schema objects — worse by this surface's own standard. + const source = [ + 'export const PluginContextSchema = z.object({});', + 'export const CORE_PLUGIN_TYPES = [] as const;', + 'export const ApproverType = z.enum([]);', + 'export const PluginSchema = z.object({});', + ].join('\n'); + + expect(exportListDescription(source)).toBe( + 'Exports: PluginContextSchema, ApproverType, PluginSchema', + ); + }); + + it('applies the cap of five AFTER filtering, so authorable names are promoted', () => { + // Slicing first would let the constants consume the row's five slots and + // then be deleted from it, shortening the row instead of repairing it. + const source = [ + 'export const A_CONST = 1;', + 'export const B_CONST = 1;', + 'export const One = 1;', + 'export const Two = 1;', + 'export const Three = 1;', + 'export const Four = 1;', + 'export const Five = 1;', + 'export const Six = 1;', + ].join('\n'); + + expect(exportListDescription(source)).toBe('Exports: One, Two, Three, Four, Five'); + }); + + it('keeps a lone all-caps token — the boundary is deliberate', () => { + // No export in the eleven-module fallback corpus is a lone all-caps token, + // so the corpus cannot distinguish "all caps" from "all caps with an + // underscore". The narrower rule is chosen; widening it is a decision, and + // this case is where that decision gets made. + expect(exportListDescription('export const URL = 1;')).toBe('Exports: URL'); + }); + + it('falls through (null) when every export is a machine constant', () => { + // Not `Exports:` with nothing after it — the caller prints no description. + const source = ['export const CORE_PLUGIN_TYPES = [];', 'export const OTHER_KEYS = [];'].join('\n'); + expect(exportListDescription(source)).toBeNull(); + }); + + it('falls through (null) when the module exports no const at all', () => { + expect(exportListDescription('export function f() {}\n')).toBeNull(); + }); +}); + +describe('published catalog — no Exports: row names a machine constant', () => { + /** Every `Exports: …` pointer row in the checked-in skill references. */ + const publishedRows = (): { file: string; source: string; names: string[] }[] => { + const rows: { file: string; source: string; names: string[] }[] = []; + for (const skill of fs.readdirSync(SKILLS_DIR)) { + const index = path.resolve(SKILLS_DIR, skill, 'references/_index.md'); + if (!fs.existsSync(index)) continue; + for (const line of fs.readFileSync(index, 'utf-8').split('\n')) { + const match = /^- `([^`]+)` — Exports: (.+)$/.exec(line); + if (match) { + rows.push({ + file: path.relative(REPO_ROOT, index), + source: match[1], + names: match[2].split(',').map(n => n.trim()), + }); + } + } + } + return rows; + }; + + it('finds the fallback rows at all', () => { + // Nothing parsed means nothing compared, and "no hazardous row" would read + // as green — the same failure mode the generator's own emptiness guard has. + expect(publishedRows().length).toBeGreaterThan(0); + }); + + it('names no SCREAMING_SNAKE constant on any published row', () => { + const offenders = publishedRows().flatMap(row => + row.names.filter(name => SCREAMING_SNAKE.test(name)).map(name => `${row.file}: ${row.source} → ${name}`), + ); + expect(offenders).toEqual([]); + }); +}); diff --git a/packages/spec/scripts/lib/export-list.ts b/packages/spec/scripts/lib/export-list.ts new file mode 100644 index 0000000000..43ea062cbe --- /dev/null +++ b/packages/spec/scripts/lib/export-list.ts @@ -0,0 +1,115 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * The `Exports: …` line a skill-reference row falls back to when its module has + * no doc block of its own — and which exports may appear on it. + * + * Extracted from `build-skill-references.ts` (#12201) for the same reason + * `file-description.ts` (#5059), `format-type.ts` (#4912) and `escape-mdx.ts` + * (#5452) were: the generator is a top-level script that runs `main()` on + * import, so the only way to assert on this list used to be to run the whole + * thing and read the emitted `_index.md`. + * + * ## Why the list is filtered + * + * The line is TRUE either way — an accurate list of what the module exports, + * which is why #12094 kept the fallback instead of refusing (an honest export + * list beats a confidently wrong prose sentence). What is wrong is the + * RANKING. Two properties combined badly: + * + * 1. Rank was SOURCE ORDER — `slice(0, 5)` kept whichever five happened to be + * declared first, which is a fact about file layout, not about importance. + * 2. The extraction has no notion of authorable surface — any `export const` + * qualified, including constants whose own names say they are not for + * authoring. + * + * So a `.zod.ts` that declares its machine constants near the top headlined + * them. Measured on the post-#12094 catalog, three of the eleven modules that + * reach this fallback did exactly that: + * + * - `automation/approval.zod.ts` — `DEPRECATED_APPROVER_TYPES`, + * `NON_AUTHORABLE_APPROVER_TYPES` + * - `kernel/plugin.zod.ts` — `CORE_PLUGIN_TYPES`, `CONSUMER_INSTALLABLE_TYPES` + * - `system/translation.zod.ts` — `LEGACY_OBJECT_FIRST_KEYS` + * + * `skills/**` is loaded WHOLE into a customer agent's context window, and its + * job is to teach that agent what it may author. A row headlining + * `DEPRECATED_APPROVER_TYPES` and `NON_AUTHORABLE_APPROVER_TYPES` points an + * authoring agent at precisely the vocabulary it must not use, with nothing on + * the line marking them as such. Nothing is broken and no gate is wrong — this + * is the "make AI-written metadata hard to get wrong" axis, and it is why the + * repair belongs on this surface rather than in a lint rule about naming. + * + * ## Why filtering, and NOT `*Schema`-first sorting + * + * Machine constants are dropped and source order is kept for everything that + * survives. Sorting `*Schema` exports ahead of the rest was considered and + * deliberately NOT taken: on the very row that motivated this card it demotes + * `ApproverType` — the approver-type enum an author actually writes — below + * four schema objects, which is worse by this surface's own standard. The + * hazard that was measured is machine vocabulary appearing AT ALL, not schemas + * appearing late. + * + * The loud-refusal alternative (require a module doc block on every `.zod.ts` + * reachable from `SKILL_MAP`, and drop this fallback) is also not taken — + * #12094 declined it for this same population and that reasoning stands. + * Authoring the missing module doc blocks remains a separate editorial + * question; it would remove the symptom without any generator change, and this + * filter does not stand in its way. + */ + +/** + * A machine constant by naming convention: all caps, with at least one + * underscore. + * + * The underscore is REQUIRED rather than incidental. `SCREAMING_SNAKE` is a + * convention about multi-word constants, and the separator is what makes the + * reading unambiguous — a lone all-caps token (`URL`, `ID`, `MCP`) is as + * plausibly an acronym inside a name as it is a constant. Measured across the + * eleven modules that reach this fallback, the two readings are + * indistinguishable: every all-caps export in the corpus has an underscore, and + * no export is a lone all-caps token. Where the corpus cannot choose, the + * narrower rule wins, and `export-list.test.ts` pins that boundary — so + * widening it later is a decision someone makes on evidence, not a regex + * someone quietly loosens. + */ +const MACHINE_CONSTANT = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/; + +/** + * Every `export const` name, in source order. + * + * Carried over from the generator verbatim, alternation included. The + * `\w+Schema|\w+` branch is redundant — the second alternative subsumes the + * first for every input, since both are anchored by the same trailing + * `\s*[:=]` — but this change is about RANKING, and rewriting the extraction + * at the same time would widen what the diff has to be trusted about. + */ +const EXPORT_CONST = /export\s+const\s+(\w+Schema|\w+)\s*(?:[:=])/g; + +/** How many names a pointer row lists before it becomes noise. */ +const MAX_NAMES = 5; + +/** + * The `Exports: …` description for a module with no doc block of its own, or + * `null` when there is nothing authorable to name. + * + * `null` is the "fall through" answer, and it is distinct from an empty list on + * purpose: the caller prints no description at all rather than a bare + * `Exports:` with nothing after it. A module whose entire public surface is + * machine constants has nothing to say to an authoring agent, and 宁可缺, + * 不要错 — a row with no description is a gap the reader can see. + * + * The cap is applied AFTER filtering, not before. Slicing first would let a + * module's constants consume the row's five slots and then be deleted from it, + * so the fix would merely SHORTEN the hazardous rows instead of promoting the + * authorable names waiting behind them — `system/translation.zod.ts` would + * publish four names where five were available. + */ +export function exportListDescription(source: string): string | null { + const names: string[] = []; + for (const match of source.matchAll(EXPORT_CONST)) { + if (!MACHINE_CONSTANT.test(match[1])) names.push(match[1]); + } + if (names.length === 0) return null; + return `Exports: ${names.slice(0, MAX_NAMES).join(', ')}`; +} diff --git a/scripts/cross-package-test-inputs.mjs b/scripts/cross-package-test-inputs.mjs index 83e44886e3..6e4c0d3539 100644 --- a/scripts/cross-package-test-inputs.mjs +++ b/scripts/cross-package-test-inputs.mjs @@ -149,6 +149,20 @@ export const CROSS_PACKAGE_TEST_INPUTS = { // maintainer-ruled): the retirement playbook that teaches authors the // prescription sentence the pin holds. One file, not `.claude/**`. '.claude/skills/spec-property-retirement/SKILL.md', + // scripts/export-list.test.ts ends in a corpus gate over the PUBLISHED + // skill references — it enumerates `skills/` and reads every + // `/references/_index.md`, asserting none of their `Exports:` rows + // names a machine constant (#12201). The artifacts are what a customer + // agent actually loads, so they are the population that gate must judge; + // checking the generator's output in memory instead would re-assert the + // rule and see nothing about what is checked in. + // + // The whole subtree rather than `skills/*/references/_index.md`: the test + // reads the DIRECTORY too (a new skill dir changes its verdict), and a + // glob that names only files does not cover a directory listing — + // `coversDirectory` is the check, and it is why the narrower spelling was + // tried first and rejected by this gate. + 'skills/**', ], heldBy: { // The two repo-wide `*.object.ts` walkers. Each seeds a recognised diff --git a/skills/objectstack-automation/references/_index.md b/skills/objectstack-automation/references/_index.md index 978ba17d6c..c3d730ab41 100644 --- a/skills/objectstack-automation/references/_index.md +++ b/skills/objectstack-automation/references/_index.md @@ -9,7 +9,7 @@ from `node_modules` — there is no local copy in the skill bundle. ## Core schemas -- `node_modules/@objectstack/spec/src/automation/approval.zod.ts` — Exports: ApproverType, DEPRECATED_APPROVER_TYPES, NON_AUTHORABLE_APPROVER_TYPES, ORG_MEMBERSHIP_LEVELS, APPROVER_EXPRESSION_ROOTS +- `node_modules/@objectstack/spec/src/automation/approval.zod.ts` — Exports: ApproverType, ApprovalDecision, ApprovalNodeApproverSchema, DecisionOutputDefSchema, ApprovalEscalationSchema - `node_modules/@objectstack/spec/src/automation/execution.zod.ts` — Automation Execution Protocol - `node_modules/@objectstack/spec/src/automation/flow.zod.ts` — Flow Node Types — **built-in seed set** (ADR-0018). - `node_modules/@objectstack/spec/src/automation/node-executor.zod.ts` — Node Executor Plugin Protocol — Wait Node Pause/Resume diff --git a/skills/objectstack-i18n/references/_index.md b/skills/objectstack-i18n/references/_index.md index f7f5f72c5c..e91ee1fc01 100644 --- a/skills/objectstack-i18n/references/_index.md +++ b/skills/objectstack-i18n/references/_index.md @@ -9,7 +9,7 @@ from `node_modules` — there is no local copy in the skill bundle. ## Core schemas -- `node_modules/@objectstack/spec/src/system/translation.zod.ts` — Exports: LocaleSchema, FieldTranslationSchema, ActionResultDialogTranslationSchema, ObjectTranslationDataSchema, LEGACY_OBJECT_FIRST_KEYS +- `node_modules/@objectstack/spec/src/system/translation.zod.ts` — Exports: LocaleSchema, FieldTranslationSchema, ActionResultDialogTranslationSchema, ObjectTranslationDataSchema, TranslationDataSchema - `node_modules/@objectstack/spec/src/ui/i18n.zod.ts` — Display-label and ARIA-label primitives shared by every `ui/` shape. ## Transitive dependencies diff --git a/skills/objectstack-platform/references/_index.md b/skills/objectstack-platform/references/_index.md index d150a469fa..b0e81065fe 100644 --- a/skills/objectstack-platform/references/_index.md +++ b/skills/objectstack-platform/references/_index.md @@ -16,7 +16,7 @@ from `node_modules` — there is no local copy in the skill bundle. - `node_modules/@objectstack/spec/src/kernel/metadata-plugin.zod.ts` — Metadata Plugin Protocol - `node_modules/@objectstack/spec/src/kernel/plugin-capability.zod.ts` — Plugin Capability Protocol - `node_modules/@objectstack/spec/src/kernel/plugin-loading.zod.ts` — Plugin Loading Protocol -- `node_modules/@objectstack/spec/src/kernel/plugin.zod.ts` — Exports: PluginContextSchema, CORE_PLUGIN_TYPES, CONSUMER_INSTALLABLE_TYPES, PluginSchema +- `node_modules/@objectstack/spec/src/kernel/plugin.zod.ts` — Exports: PluginContextSchema, PluginSchema - `node_modules/@objectstack/spec/src/kernel/service-registry.zod.ts` — Service Registry Protocol ## Transitive dependencies diff --git a/turbo.json b/turbo.json index f79ad70857..dbb3d093fe 100644 --- a/turbo.json +++ b/turbo.json @@ -47,7 +47,8 @@ "$TURBO_ROOT$/packages/plugins/plugin-audit/src/**", "$TURBO_ROOT$/content/docs/api/error-catalog.mdx", "$TURBO_ROOT$/docs/audits/2026-07-unknown-key-strictness-ledger.md", - "$TURBO_ROOT$/.claude/skills/spec-property-retirement/SKILL.md" + "$TURBO_ROOT$/.claude/skills/spec-property-retirement/SKILL.md", + "$TURBO_ROOT$/skills/**" ] }, "@objectstack/core#test": { From 8378a1dc46691a71ef35beb97de2bc6089a86127 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 26 Aug 2026 00:19:24 +0000 Subject: [PATCH 2/2] record #12201's skills/** root in the parity gate's rollback pin (13 -> 14, by name) The pin is judged over the live declaration table by design; the skills/** declaration this PR adds moves the rollback count, so it is recorded by name per the pin's own convention. Both legs green: self-test 40 assertions, main check 105/84 globs covered. --- scripts/check-ci-filter-parity.mjs | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/scripts/check-ci-filter-parity.mjs b/scripts/check-ci-filter-parity.mjs index 89a9fa0700..f4dafa275e 100644 --- a/scripts/check-ci-filter-parity.mjs +++ b/scripts/check-ci-filter-parity.mjs @@ -600,15 +600,21 @@ export async function selfTest() { // plus, since #10178, the two @objectstack/rest declarations it added: the // state-machine doc page, covered only through the `content/**` root #10015 // added, and the automation skill file, covered only through its own - // single-file `crosspkg` entry the way #10848's SKILL.md is. Ten plus one - // plus two: the rollback now uncovers thirteen. This pin is judged over the - // LIVE declaration table on purpose: a declaration added under a root the - // rollback keeps leaves the count alone, one under a new root moves it and - // is recorded here by name. + // single-file `crosspkg` entry the way #10848's SKILL.md is. Plus, since + // #12201, the one declaration under the `skills/**` root that card added + // (the export-list corpus gate reads the published catalog from inside + // @objectstack/spec). Ten plus one plus two plus one: the rollback now + // uncovers fourteen. This pin is judged over the LIVE declaration table on + // purpose: a declaration added under a root the rollback keeps leaves the + // count alone, one under a new root moves it and is recorded here by name. const preFix = judge(fixtureWorkflow({ core: real.filters?.core, crosspkg: ['scripts/**'] }), CROSS_PACKAGE_TEST_INPUTS); assert( - new Set(uncoveredGlobs(preFix)).size === 13, - `rolling \`crosspkg\` back to its pre-#10015 list uncovers the ten it fixed plus #10848's one plus #10178's two -- got ${new Set(uncoveredGlobs(preFix)).size}`, + new Set(uncoveredGlobs(preFix)).size === 14, + `rolling \`crosspkg\` back to its pre-#10015 list uncovers the ten it fixed plus #10848's one plus #10178's two plus #12201's one -- got ${new Set(uncoveredGlobs(preFix)).size}`, + ); + assert( + uncoveredGlobs(preFix).includes('skills/**'), + `-- and #12201 added the published-catalog root the export-list corpus gate reads, by name`, ); assert( uncoveredGlobs(preFix).includes('.claude/skills/spec-property-retirement/SKILL.md'), @@ -646,8 +652,8 @@ export async function selfTest() { `same-root-different-file case observed failing and then covered by naming the file, a glob covered by ` + `\`core\`, one covered only by \`crosspkg\` and one covered by neither judged separately in one table, the ` + `stale-entry direction, seven refusals over subjects that could not be read, the checked-in ci.yml, the ` + - `pre-#10015 rollback uncovering the ten it fixed plus #10848's one plus #10178's two, and the CI wiring ` + - `read out of lint.yml.`, + `pre-#10015 rollback uncovering the ten it fixed plus #10848's one plus #10178's two plus #12201's one, ` + + `and the CI wiring read out of lint.yml.`, ); return 0; }