Skip to content

Commit d764eeb

Browse files
refactor(scripts): tighten contributor claim policy
1 parent 0f2ca2b commit d764eeb

3 files changed

Lines changed: 87 additions & 37 deletions

File tree

.github/workflows/ci-security-scan.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,9 @@ jobs:
191191
if grep -rEnI 'docker/agent-image-pin(\.local)?\.env' \
192192
--exclude-dir=node_modules --exclude-dir=target --exclude-dir=.git \
193193
. \
194-
| grep -vE '^\./(MIGRATION\.md|docs/admin/agent-image-digests\.md|scripts/check-agent-instructions\.ts):' ; then
195-
echo "::error::The legacy pin file was removed in v0.10.0; new references must live in MIGRATION.md only."
194+
| grep -vE '^\./(MIGRATION\.md|docs/admin/agent-image-digests\.md):' \
195+
| grep -vE '^\./scripts/check-agent-instructions\.ts:[0-9]+:[[:space:]]*value: "docker/agent-image-pin(\.local)?\.env",$' ; then
196+
echo "::error::Legacy pin-file names are allowed only in migration history, the digest guide, and exact instruction-checker exceptions."
196197
exit 1
197198
fi
198199

scripts/check-agent-instructions.test.ts

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ function only(failures: readonly string[]): string {
2626

2727
/** A symlink to `target`, as an override value. */
2828
const symlinkTo = (target: string): { readonly target: string } => ({ target });
29+
const markdownSpans = (markdown: string): readonly string[] => codeSpans(markdown, "markdown");
2930

3031
type Override = string | { readonly target: string } | { readonly kind: "opaque" } | null;
3132

@@ -181,27 +182,30 @@ await test("code is not prose: a reference inside a span, a fence or a comment i
181182
});
182183

183184
await test("the markdown scanner extracts code spans, not fences or comments", () => {
184-
assert.deepEqual(codeSpans("Use `scripts/check.ts` and ``a ` b``."), [
185+
assert.deepEqual(markdownSpans("Use `scripts/check.ts` and ``a ` b``."), [
185186
"scripts/check.ts",
186187
"a ` b",
187188
]);
188-
assert.deepEqual(codeSpans("A `line\nwrap` is one span."), ["line wrap"]);
189-
assert.deepEqual(codeSpans("A `CRLF\r\nwrap` is one span."), ["CRLF wrap"]);
190-
assert.deepEqual(codeSpans("A `CR\rwrap` is one span."), ["CR wrap"]);
191-
assert.deepEqual(codeSpans("` padded ` and ` `"), ["padded", " "]);
192-
assert.deepEqual(codeSpans("<!-- `hidden.md` -->\n```md\n`fenced.md`\n```\n`shown.md`"), [
189+
assert.deepEqual(markdownSpans("A `line\nwrap` is one span."), ["line wrap"]);
190+
assert.deepEqual(markdownSpans("A `CRLF\r\nwrap` is one span."), ["CRLF wrap"]);
191+
assert.deepEqual(markdownSpans("A `CR\rwrap` is one span."), ["CR wrap"]);
192+
assert.deepEqual(markdownSpans("` padded ` and ` `"), ["padded", " "]);
193+
assert.deepEqual(markdownSpans("<!-- `hidden.md` -->\n```md\n`fenced.md`\n```\n`shown.md`"), [
193194
"shown.md",
194195
]);
195-
assert.deepEqual(codeSpans("\\`escaped.md\\`\n\n `indented.md`"), []);
196-
assert.deepEqual(codeSpans("`a <!-- literal --> span`"), ["a <!-- literal --> span"]);
197-
assert.deepEqual(codeSpans("> ```md\n> `quoted.md`\n> ```\n`shown.md`"), ["shown.md"]);
198-
assert.deepEqual(codeSpans("<Tabs>\n<TabItem>\nUse `inside.md`\n</TabItem>\n</Tabs>", true), [
196+
assert.deepEqual(markdownSpans("\\`escaped.md\\`\n\n `indented.md`"), []);
197+
assert.deepEqual(markdownSpans("`a <!-- literal --> span`"), ["a <!-- literal --> span"]);
198+
assert.deepEqual(markdownSpans("> ```md\n> `quoted.md`\n> ```\n`shown.md`"), ["shown.md"]);
199+
assert.deepEqual(codeSpans("<Tabs>\n<TabItem>\nUse `inside.md`\n</TabItem>\n</Tabs>", "mdx"), [
199200
"inside.md",
200201
]);
201-
assert.deepEqual(codeSpans("`not closed``"), []);
202-
assert.deepEqual(codeSpans("```md\n`hidden.md`\n``` not a close\n`still-hidden.md`\n```"), []);
203-
assert.deepEqual(codeSpans("```md\n<!--\n```\n`shown.md`"), ["shown.md"]);
204-
assert.deepEqual(codeSpans("<!--\n```md\n-->\n`shown.md`"), ["shown.md"]);
202+
assert.deepEqual(markdownSpans("`not closed``"), []);
203+
assert.deepEqual(
204+
markdownSpans("```md\n`hidden.md`\n``` not a close\n`still-hidden.md`\n```"),
205+
[],
206+
);
207+
assert.deepEqual(markdownSpans("```md\n<!--\n```\n`shown.md`"), ["shown.md"]);
208+
assert.deepEqual(markdownSpans("<!--\n```md\n-->\n`shown.md`"), ["shown.md"]);
205209
});
206210

207211
await test("contributor docs reject missing repository paths and npm packages", () => {
@@ -229,16 +233,6 @@ await test("an intentional non-checkout path is allowed only in the document tha
229233
),
230234
[],
231235
);
232-
assert.deepEqual(
233-
analyse(
234-
snapshot({
235-
"webapp/src/features/a.ts": { kind: "opaque" },
236-
"webapp/src/features/b.ts": { kind: "opaque" },
237-
"docs/contributor/setup.md": "Use `src/features/`.\n",
238-
}),
239-
),
240-
[],
241-
);
242236
assert.match(
243237
only(
244238
analyse(
@@ -252,6 +246,26 @@ await test("an intentional non-checkout path is allowed only in the document tha
252246
);
253247
});
254248

249+
await test("a unique shorthand directory resolves independently of its descendant count", () => {
250+
assert.deepEqual(
251+
analyse(
252+
snapshot({
253+
"webapp/src/features/a.ts": { kind: "opaque" },
254+
"webapp/src/features/b.ts": { kind: "opaque" },
255+
"docs/contributor/setup.md": "Use `src/features/`.\n",
256+
}),
257+
),
258+
[],
259+
);
260+
});
261+
262+
await test("an invalid contributor MDX document is reported with its path", () => {
263+
assert.throws(
264+
() => analyse(snapshot({ "docs/contributor/broken.mdx": "<Component/ name>" })),
265+
/docs\/contributor\/broken\.mdx:/,
266+
);
267+
});
268+
255269
await test("path claims cannot escape through a typo, basename, or unrelated suffix match", () => {
256270
for (const claim of ["scrips/missing.ts", "missing.md", "./config.md"]) {
257271
const failure = only(

scripts/check-agent-instructions.ts

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@
1414
* uncommanded — a typed-only skill with no opencode command to type.
1515
* unmirrored — a Codex copy of a skill that has drifted from the Claude Code original.
1616
* duplicated — two agent files with one body.
17-
*
18-
* Contributor docs are checked separately for inline-code paths and npm packages that resolve to
19-
* nothing in the checkout.
17+
* stale — a contributor document names a repository path or npm package that does not resolve.
2018
*
2119
* `unread` runs before all of them and reports this gate's own blind spot: an agent file the
2220
* classifier never opened, which every check below would otherwise read as a file that says nothing.
@@ -178,12 +176,41 @@ const INTENTIONALLY_MISSING_PATHS = [
178176
value: "webapp/.env",
179177
reason: "a developer-local environment file that must stay untracked",
180178
},
179+
{
180+
document: "docs/contributor/agent/workspace-abi.mdx",
181+
value: "inputs/manifest.json",
182+
reason: "a path inside the staged agent workspace, not the repository checkout",
183+
},
184+
{
185+
document: "docs/contributor/agent/workspace-abi.mdx",
186+
value: "out/result.json",
187+
reason: "a path inside the staged agent workspace, not the repository checkout",
188+
},
189+
{
190+
document: "docs/contributor/agent/workspace-abi.mdx",
191+
value: "out/watchdog-killed.json",
192+
reason: "a path inside the staged agent workspace, not the repository checkout",
193+
},
194+
{
195+
document: "docs/contributor/artifact-source-contract.mdx",
196+
value: "inputs/manifest.json",
197+
reason: "a path inside the staged agent workspace, not the repository checkout",
198+
},
199+
{
200+
document: "docs/contributor/practice-review-glossary.mdx",
201+
value: "inputs/history/delta.json",
202+
reason: "a path inside the staged agent workspace, not the repository checkout",
203+
},
204+
{
205+
document: "docs/contributor/practice-review-glossary.mdx",
206+
value: "out/feedback.json",
207+
reason: "a path inside the staged agent workspace, not the repository checkout",
208+
},
181209
] satisfies readonly ClaimException[];
182210

183211
const PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*|[a-z0-9][a-z0-9._-]*)$/;
184212
const PACKAGE_SHAPED = /-(?:cli|config|core|js|node|package|plugin|react|sdk|test|ts)$/;
185213
const FILE_SHAPED = /\.(?:java|js|jsonc?|mdx?|mjs|sh|ts|tsx|xml|ya?ml)$/;
186-
const RUNTIME_ROOTS = new Set(["inputs", "out"]);
187214
const exists = (repo: Repo, path: string): boolean =>
188215
repo.present.has(path) || repo.paths.some((present) => present.startsWith(`${path}/`));
189216

@@ -225,9 +252,7 @@ function looksLikePath(value: string, roots: ReadonlySet<string>): boolean {
225252
roots.has(first) ||
226253
(first.startsWith(".") && value.includes("/")) ||
227254
(value.includes("/") && basename(value).startsWith(".")) ||
228-
(FILE_SHAPED.test(value) &&
229-
!RUNTIME_ROOTS.has(first) &&
230-
(value.includes("/") || /\.mdx?$/.test(value)))
255+
(FILE_SHAPED.test(value) && (value.includes("/") || /\.mdx?$/.test(value)))
231256
);
232257
}
233258

@@ -256,7 +281,15 @@ function staleContributorClaims(repo: Repo): readonly string[] {
256281
const failures: string[] = [];
257282
for (const file of repo.present.values()) {
258283
if (file.kind !== "text" || !isContributorDoc(file.path)) continue;
259-
for (const value of new Set(codeSpans(file.content, file.path.endsWith(".mdx")))) {
284+
let spans: readonly string[];
285+
try {
286+
spans = codeSpans(file.content, file.path.endsWith(".mdx") ? "mdx" : "markdown");
287+
} catch (error) {
288+
throw new Error(`${file.path}: ${error instanceof Error ? error.message : String(error)}`, {
289+
cause: error,
290+
});
291+
}
292+
for (const value of new Set(spans)) {
260293
const candidate = value.replace(/[.,:;]$/, "").replace(/#.*$/, "");
261294
if (candidate.includes("…") || candidate.includes("...") || /[*<>{}$\s]/.test(candidate))
262295
continue;
@@ -378,7 +411,7 @@ interface MarkdownNode {
378411
readonly children?: readonly MarkdownNode[];
379412
}
380413

381-
export function codeSpans(markdown: string, mdx = false): readonly string[] {
414+
export function codeSpans(markdown: string, syntax: "markdown" | "mdx"): readonly string[] {
382415
const spans: string[] = [];
383416
const visit = (node: MarkdownNode): void => {
384417
if (node.type === "inlineCode" && typeof node.value === "string") {
@@ -389,7 +422,9 @@ export function codeSpans(markdown: string, mdx = false): readonly string[] {
389422
visit(
390423
fromMarkdown(
391424
markdown,
392-
mdx ? { extensions: [mdxjs()], mdastExtensions: [mdxFromMarkdown()] } : undefined,
425+
syntax === "mdx"
426+
? { extensions: [mdxjs()], mdastExtensions: [mdxFromMarkdown()] }
427+
: undefined,
393428
),
394429
);
395430
return spans;
@@ -766,6 +801,6 @@ if (process.argv[1] === import.meta.filename) {
766801
console.log(
767802
`check-agent-instructions: ${repo.guides.length} AGENTS.md reach Claude Code and opencode; ` +
768803
`${skills.length} skills, ${mirrored} of them mirrored for Codex, ` +
769-
`${repo.commands.length} opencode commands; contributor-doc paths and packages resolve.`,
804+
`${repo.commands.length} opencode commands; contributor-doc references checked.`,
770805
);
771806
}

0 commit comments

Comments
 (0)