Skip to content

Commit eed6bb0

Browse files
committed
fix(spec): keep internal tracker ids out of the published skill catalog
`check:doc-authoring` went red the moment `automation/io-node-config.zod.ts` joined a package list: the generator publishes the first sentence of a module's doc block, and that file's opens with a bare tracker citation. The gate has no per-passage exemption, by design, and its argument is the audience -- `skills` ships to customer projects and is loaded WHOLE into customer context windows, where a tracker id resolves to nothing for the people paying for the tokens. The gate prescribes stripping the id AT THE SOURCE and regenerating. That remedy is not taken here, and the reasons are specific rather than convenient: the source is a `.zod.ts` this card is forbidden to edit; it is a file the package PUBLISHES, so editing it would change what `@objectstack/spec` ships and reopen the changeset decision this PR settled from precedent; and the same sentence is also projected to `content/docs/references/automation`, which would drag a second generated tree into a diff whose surface is the skill catalog. Three surfaces to remove one token. The decisive measurement is that the gate does NOT flag that identical sentence on the docs page: the rule is about the skill catalog specifically. So the strip is applied at the boundary INTO that catalog, where the rule lives. Every future pointer row is covered, rather than this one being corrected once. The criterion is the gate's own, restated with a pin over the shapes that must and must not match -- an ordinal, a hex colour, an over-long number and a doubled hash all survive untouched. This is a deviation from the gate's stated remedy and is flagged as such in the PR body for a reviewer to overrule. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H2oQebDDxYKfWZusyd8GXk
1 parent c412a47 commit eed6bb0

4 files changed

Lines changed: 91 additions & 3 deletions

File tree

packages/spec/scripts/build-skill-references.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
checkCoreEntryShape,
3535
checkSingleOwner,
3636
checkTransitiveAllowlist,
37+
stripInternalIssueIds,
3738
} from './lib/skill-map-guards';
3839

3940
// ── Paths ────────────────────────────────────────────────────────────────────
@@ -301,11 +302,11 @@ function extractDescription(filePath: string): string {
301302
const firstLine = lines[0];
302303
if (firstLine && firstLine.length > 5) {
303304
const clean = firstLine.replace(/^#+\s*/, '');
304-
const sentence = clean.split(/\.\s/)[0];
305+
const sentence = stripInternalIssueIds(clean.split(/\.\s/)[0]);
305306
return sentence.length > 120 ? sentence.slice(0, 117) + '...' : sentence;
306307
}
307308
}
308-
return exportListDescription(content) ?? '';
309+
return stripInternalIssueIds(exportListDescription(content) ?? '');
309310
}
310311

311312
// ── Index generator ──────────────────────────────────────────────────────────

packages/spec/scripts/lib/skill-map-guards.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,56 @@
3737
* in the map.
3838
*/
3939

40+
// ── What the catalog may publish ─────────────────────────────────────────────
41+
42+
/**
43+
* The gate's criterion for an internal tracker id, restated.
44+
*
45+
* Same shape as `scripts/check-doc-authoring.mjs`'s `INTERNAL_ID_SOURCE`: three
46+
* to five digits, so the ordinal "the #1 mistake" and a six-digit hex colour
47+
* are both below/above it, and neither `##` nor an HTML entity's `&#` counts.
48+
* Restated rather than imported because that gate is a `.mjs` in the repo root
49+
* `scripts/` tree and exports nothing; the pin in
50+
* `skill-map-guards.test.ts` holds the two spellings together by asserting the
51+
* shapes that must and must not match.
52+
*/
53+
const INTERNAL_ISSUE_ID = /(?<![#&])#[0-9]{3,5}(?![0-9A-Za-z])/g;
54+
55+
/**
56+
* Drop internal issue ids from a line about to be published to `skills/**`.
57+
*
58+
* `skills/**` ships to customer projects and is loaded WHOLE into customer
59+
* agent context windows, so `check:doc-authoring` refuses a tracker id there
60+
* with no per-passage exemption -- a reader in a customer session has no
61+
* tracker, no `git log` and no ADRs, and the token resolves to nothing for the
62+
* audience paying for it (maintainer ruling 2026-08-12).
63+
*
64+
* The index rows are PROJECTED from module doc blocks in `packages/spec/src`,
65+
* so a citation written for a repo reader becomes catalog prose the moment its
66+
* schema joins a package's list -- which is exactly how
67+
* `automation/io-node-config.zod.ts` arrived with `(#4045)` in its opening
68+
* sentence. The same sentence is also published to
69+
* `content/docs/references/**`, and the gate does NOT flag it there: the rule
70+
* is about the skill catalog specifically. So the strip belongs at the BOUNDARY
71+
* into that catalog rather than in the source, where it would rewrite a
72+
* legitimate repo-facing citation, change bytes the package publishes, and drag
73+
* the generated docs tree along.
74+
*
75+
* Sanitising here rather than refusing is deliberate and is the narrower of the
76+
* two: a refusal would be satisfiable only by editing the schema source, which
77+
* is what the paragraph above argues against. The class becomes impossible
78+
* rather than corrected once -- no future pointer row can carry an id.
79+
*/
80+
export function stripInternalIssueIds(text: string): string {
81+
return text
82+
.replace(/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?=#[0-9]{3,5}(?![0-9A-Za-z]))/g, '')
83+
.replace(INTERNAL_ISSUE_ID, '')
84+
.replace(/\(\s*[,;·]?\s*\)/g, '')
85+
.replace(/[ \t]{2,}/g, ' ')
86+
.replace(/\s+([.,;:])/g, '$1')
87+
.trim();
88+
}
89+
4090
/** A `SKILL_MAP`-shaped value: skill name → its core schema paths. */
4191
export type SkillCoreMap = Record<string, readonly string[]>;
4292

packages/spec/scripts/skill-map-guards.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
checkCoreEntryShape,
3939
checkSingleOwner,
4040
checkTransitiveAllowlist,
41+
stripInternalIssueIds,
4142
type SkillCoreMap,
4243
} from './lib/skill-map-guards';
4344

@@ -181,6 +182,37 @@ describe('checkTransitiveAllowlist — a constraint that constrains nothing is r
181182
});
182183
});
183184

185+
describe('stripInternalIssueIds — the catalog carries no tracker ids', () => {
186+
it('drops a trailing citation and the parenthesis it sat in', () => {
187+
expect(
188+
stripInternalIssueIds('Config contracts for the flat IO builtins — `notify` and `http` (#4045).'),
189+
).toBe('Config contracts for the flat IO builtins — `notify` and `http`.');
190+
});
191+
192+
it('drops the owner/repo spelling whole', () => {
193+
expect(stripInternalIssueIds('removed in objectstack-ai/objectstack#4286 — use the rule'))
194+
.toBe('removed in — use the rule');
195+
});
196+
197+
it('drops a mid-sentence id and leaves one space behind', () => {
198+
expect(stripInternalIssueIds('Metadata Protection Model — Phase 1 (#1234) and later'))
199+
.toBe('Metadata Protection Model — Phase 1 and later');
200+
});
201+
202+
// The other half of the criterion: what must survive. Each of these is a
203+
// shape `check:doc-authoring` explicitly allows, so stripping it here would
204+
// silently rewrite prose the gate never objected to.
205+
it.each([
206+
['the #1 authoring mistake', 'an ordinal is one digit, below the floor'],
207+
['colour #ff00aa is the accent', 'a hex colour starts with no digit'],
208+
['id #123456789 is not a tracker id', 'nine digits is above the ceiling'],
209+
['HTTP 404 is not a citation', 'no # at all'],
210+
['array##4045 is not a citation', 'a doubled # is excluded by the lookbehind'],
211+
])('leaves %j alone (%s)', (text) => {
212+
expect(stripInternalIssueIds(text)).toBe(text);
213+
});
214+
});
215+
184216
describe('the generator wires the guards in', () => {
185217
const source = (): string => fs.readFileSync(GENERATOR, 'utf-8');
186218

@@ -204,4 +236,9 @@ describe('the generator wires the guards in', () => {
204236
expect(source()).toContain('checkTransitiveAllowlist(SKILL_MAP, TRANSITIVE_ALLOWLIST, closures)');
205237
expect(source()).toContain('allowed.includes(f)');
206238
});
239+
240+
it('strips internal ids on the description path, not somewhere unreachable', () => {
241+
expect(source()).toContain('stripInternalIssueIds(clean.split');
242+
expect(source()).toContain('stripInternalIssueIds(exportListDescription(content)');
243+
});
207244
});

skills/objectstack-automation/references/_index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ from `node_modules` — there is no local copy in the skill bundle.
1313
- `node_modules/@objectstack/spec/src/automation/builtin-node-config.zod.ts` — Config contracts for the remaining flat builtins — the CRUD quartet
1414
- `node_modules/@objectstack/spec/src/automation/execution.zod.ts` — Automation Execution Protocol
1515
- `node_modules/@objectstack/spec/src/automation/flow.zod.ts` — Exports: FlowNodeAction, FlowVariableSchema, FlowNodeSchema, FlowEdgeSchema, FlowSchema
16-
- `node_modules/@objectstack/spec/src/automation/io-node-config.zod.ts` — Config contracts for the flat IO builtins — `notify` and `http` (#4045).
16+
- `node_modules/@objectstack/spec/src/automation/io-node-config.zod.ts` — Config contracts for the flat IO builtins — `notify` and `http`.
1717
- `node_modules/@objectstack/spec/src/automation/node-executor.zod.ts` — Node Executor Plugin Protocol — Wait Node Pause/Resume
1818
- `node_modules/@objectstack/spec/src/automation/time-relative-trigger.zod.ts` — Time-Relative Trigger Protocol
1919
- `node_modules/@objectstack/spec/src/automation/webhook.zod.ts` — Exports: WebhookTriggerType, WebhookSchema

0 commit comments

Comments
 (0)