@cosyte/script-utils 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/catalog.json ADDED
@@ -0,0 +1,210 @@
1
+ {
2
+ "$comment": "GENERATED by cosyte/config scripts/catalog.mjs from each package's package.json and catalog/tiers.json. Do not edit by hand: change the package.json or the tiers file, then run `node scripts/catalog.mjs write` from the umbrella.",
3
+ "libraries": [
4
+ {
5
+ "name": "@cosyte/hl7",
6
+ "slug": "hl7",
7
+ "description": "Developer-focused HL7 v2 toolkit for Node.js and TypeScript, message parsing and building, mutation and serialization, and one-line helpers.",
8
+ "line": "0.1",
9
+ "engines": {
10
+ "node": ">=22.0.0"
11
+ },
12
+ "repository": "https://github.com/cosyte/hl7",
13
+ "docs": "https://docs.cosyte.com/hl7",
14
+ "npm": "https://www.npmjs.com/package/@cosyte/hl7",
15
+ "tier": "standard",
16
+ "standard": "HL7 v2"
17
+ },
18
+ {
19
+ "name": "@cosyte/mllp",
20
+ "slug": "mllp",
21
+ "description": "MLLP transport for HL7 v2 in Node.js and TypeScript: client and server, framing, ACK correlation, auto-reconnect, TLS, and an in-memory transport for tests.",
22
+ "line": "0.1",
23
+ "engines": {
24
+ "node": ">=22.0.0"
25
+ },
26
+ "repository": "https://github.com/cosyte/mllp",
27
+ "docs": "https://docs.cosyte.com/mllp",
28
+ "npm": "https://www.npmjs.com/package/@cosyte/mllp",
29
+ "tier": "standard",
30
+ "standard": "MLLP"
31
+ },
32
+ {
33
+ "name": "@cosyte/fhir",
34
+ "slug": "fhir",
35
+ "description": "FHIR R4 for Node.js and TypeScript: a resource model that keeps every value exactly as written, JSON and XML codecs, layered validation, and a safety readout for status and negation.",
36
+ "line": "0.1",
37
+ "engines": {
38
+ "node": ">=22.0.0"
39
+ },
40
+ "repository": "https://github.com/cosyte/fhir",
41
+ "docs": "https://docs.cosyte.com/fhir",
42
+ "npm": "https://www.npmjs.com/package/@cosyte/fhir",
43
+ "tier": "standard",
44
+ "standard": "FHIR"
45
+ },
46
+ {
47
+ "name": "@cosyte/ccda",
48
+ "slug": "ccda",
49
+ "description": "C-CDA parser, serializer, and builder for Node.js and TypeScript: lenient on parse, spec-clean on emit.",
50
+ "line": "0.1",
51
+ "engines": {
52
+ "node": ">=22.0.0"
53
+ },
54
+ "repository": "https://github.com/cosyte/ccda",
55
+ "docs": "https://docs.cosyte.com/ccda",
56
+ "npm": "https://www.npmjs.com/package/@cosyte/ccda",
57
+ "tier": "standard",
58
+ "standard": "C-CDA"
59
+ },
60
+ {
61
+ "name": "@cosyte/x12",
62
+ "slug": "x12",
63
+ "description": "Developer-focused ASC X12 healthcare EDI parser, serializer, and builder for Node.js and TypeScript: lenient on parse, spec-clean on emit.",
64
+ "line": "0.1",
65
+ "engines": {
66
+ "node": ">=22.0.0"
67
+ },
68
+ "repository": "https://github.com/cosyte/x12",
69
+ "docs": "https://docs.cosyte.com/x12",
70
+ "npm": "https://www.npmjs.com/package/@cosyte/x12",
71
+ "tier": "standard",
72
+ "standard": "X12"
73
+ },
74
+ {
75
+ "name": "@cosyte/ncpdp",
76
+ "slug": "ncpdp",
77
+ "description": "NCPDP parser, serializer, and builder for Node.js and TypeScript: SCRIPT ePrescribing and Telecom pharmacy claims, lenient on parse, spec-clean on emit.",
78
+ "line": "0.1",
79
+ "engines": {
80
+ "node": ">=22.0.0"
81
+ },
82
+ "repository": "https://github.com/cosyte/ncpdp",
83
+ "docs": "https://docs.cosyte.com/ncpdp",
84
+ "npm": "https://www.npmjs.com/package/@cosyte/ncpdp",
85
+ "tier": "standard",
86
+ "standard": "NCPDP"
87
+ },
88
+ {
89
+ "name": "@cosyte/astm",
90
+ "slug": "astm",
91
+ "description": "ASTM parser, serializer, and builder for Node.js and TypeScript: lenient on parse, spec-clean on emit.",
92
+ "line": "0.1",
93
+ "engines": {
94
+ "node": ">=22.0.0"
95
+ },
96
+ "repository": "https://github.com/cosyte/astm",
97
+ "docs": "https://docs.cosyte.com/astm",
98
+ "npm": "https://www.npmjs.com/package/@cosyte/astm",
99
+ "tier": "standard",
100
+ "standard": "ASTM"
101
+ },
102
+ {
103
+ "name": "@cosyte/dicom",
104
+ "slug": "dicom",
105
+ "description": "DICOM Part 10 parser + utility library for Node.js and TypeScript: metadata-first, vendor-quirky-tolerant, dual ESM/CJS.",
106
+ "line": "0.1",
107
+ "engines": {
108
+ "node": ">=22.0.0"
109
+ },
110
+ "repository": "https://github.com/cosyte/dicom",
111
+ "docs": "https://docs.cosyte.com/dicom",
112
+ "npm": "https://www.npmjs.com/package/@cosyte/dicom",
113
+ "tier": "standard",
114
+ "standard": "DICOM"
115
+ },
116
+ {
117
+ "name": "@cosyte/terminology",
118
+ "slug": "terminology",
119
+ "description": "Terminology engine for US healthcare code systems in Node.js and TypeScript: FHIR $translate, $lookup, $validate-code and $expand over code systems, value sets and maps you supply.",
120
+ "line": "0.1",
121
+ "engines": {
122
+ "node": ">=22.0.0"
123
+ },
124
+ "repository": "https://github.com/cosyte/terminology",
125
+ "docs": "https://docs.cosyte.com/terminology",
126
+ "npm": "https://www.npmjs.com/package/@cosyte/terminology",
127
+ "tier": "built-on"
128
+ },
129
+ {
130
+ "name": "@cosyte/transform",
131
+ "slug": "transform",
132
+ "description": "HL7 v2 → FHIR R4 transformation library for Node.js and TypeScript: IG-grounded, fail-safe, value-free diagnostics; never a confident wrong FHIR value.",
133
+ "line": "0.1",
134
+ "engines": {
135
+ "node": ">=22.0.0"
136
+ },
137
+ "repository": "https://github.com/cosyte/transform",
138
+ "docs": "https://docs.cosyte.com/transform",
139
+ "npm": "https://www.npmjs.com/package/@cosyte/transform",
140
+ "tier": "built-on"
141
+ },
142
+ {
143
+ "name": "@cosyte/deid",
144
+ "slug": "deid",
145
+ "description": "Healthcare de-identification for Node.js and TypeScript: applies a HIPAA Safe Harbor policy to HL7 v2, C-CDA, FHIR R4, X12, NCPDP Telecom and DICOM, fails closed, and emits a value-free manifest.",
146
+ "line": "0.1",
147
+ "engines": {
148
+ "node": ">=22.0.0"
149
+ },
150
+ "repository": "https://github.com/cosyte/deid",
151
+ "docs": "https://docs.cosyte.com/deid",
152
+ "npm": "https://www.npmjs.com/package/@cosyte/deid",
153
+ "tier": "built-on"
154
+ },
155
+ {
156
+ "name": "@cosyte/synth",
157
+ "slug": "synth",
158
+ "description": "Deterministic, seedable synthetic healthcare test fixtures for Node.js and TypeScript: HL7 v2, FHIR R4 and US Core, C-CDA, X12, NCPDP and ASTM, spec-clean by construction and never real PHI.",
159
+ "line": "0.1",
160
+ "engines": {
161
+ "node": ">=22.0.0"
162
+ },
163
+ "repository": "https://github.com/cosyte/synth",
164
+ "docs": "https://docs.cosyte.com/synth",
165
+ "npm": "https://www.npmjs.com/package/@cosyte/synth",
166
+ "tier": "built-on"
167
+ },
168
+ {
169
+ "name": "@cosyte/dates",
170
+ "slug": "dates",
171
+ "description": "Date and time utilities for the @cosyte/* healthcare parsers: precision-preserving conversion and validation that never guesses a timezone.",
172
+ "line": "0.1",
173
+ "engines": {
174
+ "node": ">=22.0.0"
175
+ },
176
+ "repository": "https://github.com/cosyte/dates",
177
+ "docs": "https://docs.cosyte.com/dates",
178
+ "npm": "https://www.npmjs.com/package/@cosyte/dates",
179
+ "tier": "built-on"
180
+ },
181
+ {
182
+ "name": "@cosyte/cli",
183
+ "slug": "cli",
184
+ "description": "The cosyte CLI: a PHI-safe developer front door over the @cosyte/* healthcare parsers. Parse HL7 v2 / FHIR from a file or stdin to typed JSON, with a documented exit-code contract.",
185
+ "line": "0.1",
186
+ "engines": {
187
+ "node": ">=22.0.0 <26.0.0"
188
+ },
189
+ "repository": "https://github.com/cosyte/cli",
190
+ "docs": "https://docs.cosyte.com/cli",
191
+ "npm": "https://www.npmjs.com/package/@cosyte/cli",
192
+ "tier": "built-on"
193
+ }
194
+ ],
195
+ "engine": {
196
+ "name": "@cosyte/pathways",
197
+ "slug": "pathways",
198
+ "description": "A code-first healthcare integration engine for Node.js: channels in YAML, transforms in TypeScript. Early and not yet released.",
199
+ "line": "0.0",
200
+ "engines": {
201
+ "node": ">=24.0.0"
202
+ },
203
+ "repository": "https://github.com/cosyte/pathways",
204
+ "docs": "https://docs.cosyte.com/pathways",
205
+ "tier": "engine",
206
+ "displayName": "Pathways",
207
+ "status": "in development",
208
+ "onNpm": false
209
+ }
210
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Types for `@cosyte/script-utils/claude-card`: the checker for the CLAUDE.md card every Cosyte
3
+ * repository keeps. The spec is `documentation/claude-card.md` in cosyte/config.
4
+ */
5
+
6
+ /** The most lines a card may have, counted as `wc -l` counts them. */
7
+ export declare const CARD_MAX_LINES: 60;
8
+
9
+ /** The most characters (code points) a line may have, code blocks included. */
10
+ export declare const CARD_MAX_LINE_LENGTH: 100;
11
+
12
+ /** The most bytes of UTF-8 a card may have. */
13
+ export declare const CARD_MAX_BYTES: 5120;
14
+
15
+ /** The three sections, in order. */
16
+ export declare const CARD_SECTIONS: readonly ["Gate", "Hazard", "Rules"];
17
+
18
+ /** The line that imports AGENTS.md into the card. */
19
+ export declare const AGENTS_IMPORT: "@AGENTS.md";
20
+
21
+ /** A rule id a finding can carry. */
22
+ export type CardRule =
23
+ | "title"
24
+ | "intro"
25
+ | "section-missing"
26
+ | "section-order"
27
+ | "section-extra"
28
+ | "section-empty"
29
+ | "lines"
30
+ | "width"
31
+ | "bytes"
32
+ | "agents-import";
33
+
34
+ /** Every rule id, with what it requires. */
35
+ export declare const CARD_RULES: Readonly<Record<CardRule, string>>;
36
+
37
+ /** One place a card departs from the spec. */
38
+ export interface CardFinding {
39
+ rule: CardRule;
40
+ /** 1-based line, or 0 when the finding is about the whole file. */
41
+ line: number;
42
+ message: string;
43
+ }
44
+
45
+ export interface CardContext {
46
+ /** Whether an AGENTS.md sits beside the card. Left out, the import rule is not checked. */
47
+ agentsMd?: boolean;
48
+ }
49
+
50
+ /** Check a CLAUDE.md card against the spec. Returns no findings when it follows it. */
51
+ export declare function checkClaudeCard(text: string, context?: CardContext): CardFinding[];
52
+
53
+ export interface ClaudeCardGateOptions {
54
+ /** The repository root holding CLAUDE.md. Default: the working directory. */
55
+ dir?: string;
56
+ /** CLI arguments; only `--dir <path>` is accepted. */
57
+ argv?: string[];
58
+ }
59
+
60
+ /**
61
+ * Run the checker over one repository and return the exit code: 0 the card follows the spec, 1 at
62
+ * least one finding, 2 CLAUDE.md could not be read. It never exits the process.
63
+ */
64
+ export declare function runClaudeCardGate(options?: ClaudeCardGateOptions): 0 | 1 | 2;
package/claude-card.js ADDED
@@ -0,0 +1,296 @@
1
+ // @cosyte/script-utils/claude-card
2
+ //
3
+ // THE CLAUDE.md CARD CHECKER. Every Cosyte repository keeps a short CLAUDE.md that every Claude Code
4
+ // session in it reads first: what must pass before a push (`## Gate`), what a change can break that
5
+ // a green gate would not show (`## Hazard`), and the rules that bind there (`## Rules`). The spec is
6
+ // `documentation/claude-card.md` in cosyte/config; this file is what enforces it.
7
+ //
8
+ // THE RULES, each one id a finding names:
9
+ //
10
+ // title line 1 is `# <name>` (after a YAML front matter block, when there is one)
11
+ // intro a paragraph sits between the title and the first section
12
+ // section-missing `## Gate`, `## Hazard` and `## Rules` are all there (a heading may go on
13
+ // after its word: `## Hazard: ...`, `## Rules that bind here`)
14
+ // section-order in that order
15
+ // section-extra no other level-2 heading
16
+ // section-empty none of the three is empty
17
+ // lines at most `CARD_MAX_LINES` lines, counted as `wc -l` counts them
18
+ // width every line at most `CARD_MAX_LINE_LENGTH` characters, code included
19
+ // bytes at most `CARD_MAX_BYTES` bytes of UTF-8
20
+ // agents-import when an AGENTS.md sits beside the card, the card imports it on a line of its own
21
+ // (`@AGENTS.md`); an import of an AGENTS.md that is not there is a finding too
22
+ //
23
+ // EXIT CODES, the contract every gate here keeps: 0 the card follows the spec, 1 a finding, 2 the
24
+ // gate could not read its input.
25
+ //
26
+ // ZERO DEPENDENCIES AND NO BUILD STEP, like the rest of this package.
27
+
28
+ import { existsSync, readFileSync } from "node:fs";
29
+ import { join, resolve } from "node:path";
30
+
31
+ import { isCliEntrypoint } from "./index.js";
32
+
33
+ /** The most lines a card may have. */
34
+ export const CARD_MAX_LINES = 60;
35
+
36
+ /** The most characters (code points) a line may have, code blocks included. */
37
+ export const CARD_MAX_LINE_LENGTH = 100;
38
+
39
+ /**
40
+ * The most bytes of UTF-8 a card may have: 5 KiB, about 1,300 tokens, paid by every session in the
41
+ * repository before any work starts. 60 lines of 100 characters is up to 6,060 bytes, so this is
42
+ * the cap that binds; the spec gives the measurement behind the number.
43
+ */
44
+ export const CARD_MAX_BYTES = 5_120;
45
+
46
+ /** The three sections, in order. A heading matches when it is the word, or the word then more. */
47
+ export const CARD_SECTIONS = Object.freeze(["Gate", "Hazard", "Rules"]);
48
+
49
+ /** The line that imports AGENTS.md into the card. */
50
+ export const AGENTS_IMPORT = "@AGENTS.md";
51
+
52
+ /** Every rule id a finding can carry, with what it requires. */
53
+ export const CARD_RULES = Object.freeze({
54
+ title: "The card opens with `# <name>`, after a YAML front matter block if it has one.",
55
+ intro: "A paragraph sits between the title and the first section.",
56
+ "section-missing": "`## Gate`, `## Hazard` and `## Rules` are all there.",
57
+ "section-order": "The three sections appear in that order.",
58
+ "section-extra": "There is no other level-2 heading.",
59
+ "section-empty": "None of the three sections is empty.",
60
+ lines: `At most ${CARD_MAX_LINES} lines.`,
61
+ width: `Every line is at most ${CARD_MAX_LINE_LENGTH} characters, code blocks included.`,
62
+ bytes: `At most ${CARD_MAX_BYTES.toLocaleString("en-US")} bytes.`,
63
+ "agents-import": `The card imports a sibling AGENTS.md on a line of its own (${AGENTS_IMPORT}).`,
64
+ });
65
+
66
+ /** Which of the three sections a level-2 heading's text is, or `null` for none of them. */
67
+ function sectionOf(text) {
68
+ for (const word of CARD_SECTIONS) {
69
+ if (text.startsWith(word) && !/[A-Za-z0-9]/.test(text.charAt(word.length))) return word;
70
+ }
71
+ return null;
72
+ }
73
+
74
+ /**
75
+ * An ATX heading's level and text, or `null` when the line is not one.
76
+ *
77
+ * Read with index arithmetic rather than one regular expression: a pattern of the shape
78
+ * `[ \t]+(.*?)[ \t]*#*[ \t]*$` backtracks polynomially on a long run of tabs or spaces.
79
+ */
80
+ function parseHeading(line) {
81
+ let level = 0;
82
+ while (level < line.length && line[level] === "#") level += 1;
83
+ if (level === 0 || level > 6) return null;
84
+ if (line[level] !== " " && line[level] !== "\t") return null;
85
+ let text = line.slice(level).trim();
86
+ // An optional closing run of `#`, which counts only when a space or tab comes before it.
87
+ let end = text.length;
88
+ while (end > 0 && text[end - 1] === "#") end -= 1;
89
+ if (end === 0) text = "";
90
+ else if (end < text.length && (text[end - 1] === " " || text[end - 1] === "\t")) {
91
+ text = text.slice(0, end).trimEnd();
92
+ }
93
+ return { level, text };
94
+ }
95
+
96
+ /** The headings outside fenced code blocks, with 0-based line numbers. */
97
+ function headingsOf(lines) {
98
+ const headings = [];
99
+ let fence = null;
100
+ lines.forEach((line, index) => {
101
+ const marker = /^ {0,3}(`{3,}|~{3,})/.exec(line);
102
+ if (fence !== null) {
103
+ if (marker && marker[1][0] === fence[0] && marker[1].length >= fence.length) fence = null;
104
+ return;
105
+ }
106
+ if (marker) {
107
+ fence = marker[1];
108
+ return;
109
+ }
110
+ const heading = parseHeading(line);
111
+ if (heading !== null) headings.push({ ...heading, line: index });
112
+ });
113
+ return headings;
114
+ }
115
+
116
+ /**
117
+ * Check a CLAUDE.md card against the spec.
118
+ *
119
+ * @param {string} text The card's contents.
120
+ * @param {{ agentsMd?: boolean }} [context] `agentsMd`: whether an AGENTS.md sits beside the card.
121
+ * When it is left out, the import rule is not checked.
122
+ * @returns {{ rule: string, line: number, message: string }[]} The findings; empty when the card
123
+ * follows the spec. `line` is 1-based, 0 for the whole file.
124
+ */
125
+ export function checkClaudeCard(text, context = {}) {
126
+ if (typeof text !== "string") throw new TypeError("checkClaudeCard(text) expects a string.");
127
+ const findings = [];
128
+ const add = (rule, line, message) => findings.push({ rule, line, message });
129
+ const lines = text.split("\n");
130
+ if (lines[lines.length - 1] === "") lines.pop();
131
+
132
+ // ---- Size ---------------------------------------------------------------------------------
133
+ const lineCount = (text.match(/\n/g) ?? []).length;
134
+ if (lineCount > CARD_MAX_LINES) {
135
+ add("lines", 0, `the card is ${lineCount} lines; the limit is ${CARD_MAX_LINES}.`);
136
+ }
137
+ const bytes = Buffer.byteLength(text, "utf8");
138
+ if (bytes > CARD_MAX_BYTES) {
139
+ add(
140
+ "bytes",
141
+ 0,
142
+ `the card is ${bytes.toLocaleString("en-US")} bytes; the limit is ` +
143
+ `${CARD_MAX_BYTES.toLocaleString("en-US")}. Move detail to documentation/agent-notes.md.`,
144
+ );
145
+ }
146
+ lines.forEach((line, index) => {
147
+ const length = [...line.replace(/\r$/, "")].length;
148
+ if (length > CARD_MAX_LINE_LENGTH) {
149
+ add(
150
+ "width",
151
+ index + 1,
152
+ `line ${index + 1} is ${length} characters; the limit is ${CARD_MAX_LINE_LENGTH}.`,
153
+ );
154
+ }
155
+ });
156
+
157
+ // ---- Title and intro ------------------------------------------------------------------------
158
+ // A YAML front matter block may come first (a repository whose documents carry one); the title
159
+ // is then the first line after it.
160
+ let titleLine = 0;
161
+ if (lines[0] === "---") {
162
+ const close = lines.indexOf("---", 1);
163
+ if (close > 0) titleLine = close + 1;
164
+ while (titleLine < lines.length && lines[titleLine].trim() === "") titleLine += 1;
165
+ }
166
+ if (!/^# \S/.test(lines[titleLine] ?? "")) {
167
+ add("title", titleLine + 1, "the card must open with its title, `# <name>`.");
168
+ }
169
+ const headings = headingsOf(lines);
170
+ const firstSection = headings.find((h) => h.level === 2);
171
+ const introEnd = firstSection === undefined ? lines.length : firstSection.line;
172
+ const intro = lines
173
+ .slice(titleLine + 1, introEnd)
174
+ .filter((line) => line.trim() !== "" && line.trim() !== AGENTS_IMPORT && !/^#/.test(line));
175
+ if (intro.length === 0) {
176
+ add("intro", 2, "say what this repository is in a paragraph under the title.");
177
+ }
178
+
179
+ // ---- Sections -------------------------------------------------------------------------------
180
+ const found = new Map();
181
+ for (const heading of headings.filter((h) => h.level === 2)) {
182
+ const word = sectionOf(heading.text);
183
+ if (word === null) {
184
+ add(
185
+ "section-extra",
186
+ heading.line + 1,
187
+ `"## ${heading.text}" is not one of the card's sections (${CARD_SECTIONS.join(", ")}); ` +
188
+ `make it a ### inside one of them, or move it to the agent notes.`,
189
+ );
190
+ } else if (!found.has(word)) {
191
+ found.set(word, heading);
192
+ } else {
193
+ add("section-extra", heading.line + 1, `"## ${heading.text}" repeats the ${word} section.`);
194
+ }
195
+ }
196
+ for (const word of CARD_SECTIONS) {
197
+ if (!found.has(word)) add("section-missing", 0, `the card has no "## ${word}" section.`);
198
+ }
199
+ const present = CARD_SECTIONS.filter((word) => found.has(word));
200
+ for (let i = 1; i < present.length; i += 1) {
201
+ if (found.get(present[i]).line < found.get(present[i - 1]).line) {
202
+ add(
203
+ "section-order",
204
+ found.get(present[i]).line + 1,
205
+ `"## ${present[i]}" must come after "## ${present[i - 1]}" (the order is ` +
206
+ `${CARD_SECTIONS.join(", ")}).`,
207
+ );
208
+ break;
209
+ }
210
+ }
211
+ for (const [word, heading] of found) {
212
+ const next = headings.find((h) => h.line > heading.line && h.level <= 2);
213
+ const body = lines.slice(heading.line + 1, next === undefined ? lines.length : next.line);
214
+ if (body.every((line) => line.trim() === "")) {
215
+ add("section-empty", heading.line + 1, `the ${word} section is empty.`);
216
+ }
217
+ }
218
+
219
+ // ---- AGENTS.md ------------------------------------------------------------------------------
220
+ if (typeof context.agentsMd === "boolean") {
221
+ const imports = lines.some((line) => line.trim() === AGENTS_IMPORT);
222
+ if (context.agentsMd && !imports) {
223
+ add(
224
+ "agents-import",
225
+ 0,
226
+ `an AGENTS.md sits beside the card, so the card imports it on a line of its own: ` +
227
+ `${AGENTS_IMPORT}.`,
228
+ );
229
+ }
230
+ if (!context.agentsMd && imports) {
231
+ add(
232
+ "agents-import",
233
+ 0,
234
+ `the card imports ${AGENTS_IMPORT}, and there is no AGENTS.md beside it.`,
235
+ );
236
+ }
237
+ }
238
+
239
+ return findings;
240
+ }
241
+
242
+ /**
243
+ * Run the card checker over one repository: read its CLAUDE.md, note whether an AGENTS.md sits
244
+ * beside it, report, and return the exit code. It never exits the process.
245
+ *
246
+ * @param {{ dir?: string, argv?: string[] }} [options] `dir` is the repository root (default: the
247
+ * working directory). `argv` is the CLI arguments, read only for `--dir <path>`.
248
+ * @returns {0 | 1 | 2} 0 the card follows the spec, 1 a finding, 2 the card could not be read.
249
+ */
250
+ export function runClaudeCardGate(options = {}) {
251
+ let dir = options.dir ?? process.cwd();
252
+ const argv = options.argv ?? [];
253
+ for (let i = 0; i < argv.length; i += 1) {
254
+ if (argv[i] === "--dir" && typeof argv[i + 1] === "string") {
255
+ dir = argv[i + 1];
256
+ i += 1;
257
+ } else if (argv[i] !== "--") {
258
+ process.stderr.write(
259
+ `CLAUDE.md card: ${JSON.stringify(argv[i])} is not an argument this check accepts (only ` +
260
+ `--dir <repository root>). Nothing was checked.\n`,
261
+ );
262
+ return 2;
263
+ }
264
+ }
265
+ dir = resolve(dir);
266
+
267
+ let text;
268
+ try {
269
+ text = readFileSync(join(dir, "CLAUDE.md"), "utf8");
270
+ } catch (err) {
271
+ process.stderr.write(`CLAUDE.md card: could not read CLAUDE.md in ${dir}: ${err.message}\n`);
272
+ return 2;
273
+ }
274
+ const findings = checkClaudeCard(text, { agentsMd: existsSync(join(dir, "AGENTS.md")) });
275
+ if (findings.length === 0) {
276
+ const lineCount = (text.match(/\n/g) ?? []).length;
277
+ process.stdout.write(
278
+ `CLAUDE.md card: follows the spec (${lineCount} of ${CARD_MAX_LINES} lines, ` +
279
+ `${Buffer.byteLength(text, "utf8").toLocaleString("en-US")} of ` +
280
+ `${CARD_MAX_BYTES.toLocaleString("en-US")} bytes).\n`,
281
+ );
282
+ return 0;
283
+ }
284
+ const report = findings
285
+ .map((f) => ` CLAUDE.md${f.line > 0 ? `:${f.line}` : ""} [${f.rule}] ${f.message}`)
286
+ .join("\n");
287
+ process.stderr.write(
288
+ `CLAUDE.md card: CLAUDE.md does not follow the card spec.\n${report}\n` +
289
+ `${findings.length} finding(s). The spec is documentation/claude-card.md in cosyte/config.\n`,
290
+ );
291
+ return 1;
292
+ }
293
+
294
+ if (isCliEntrypoint(import.meta.url)) {
295
+ process.exitCode = runClaudeCardGate({ argv: process.argv.slice(2) });
296
+ }
@@ -46,9 +46,9 @@ export interface ExtraRule {
46
46
  /** What a hit report calls it. */
47
47
  name: string;
48
48
  /** One pattern, as a source string or a `RegExp`. */
49
- pattern?: string | RegExp;
49
+ pattern?: string | RegExp | { source: string; flags?: string };
50
50
  /** Several patterns, when one rule needs two case policies. */
51
- patterns?: (string | RegExp)[];
51
+ patterns?: (string | RegExp | { source: string; flags?: string })[];
52
52
  /** Text this rule MUST match. At least one is required. */
53
53
  positives: string[];
54
54
  /** Text this rule must NOT match. At least one is required: it is what stops a widening. */
@@ -221,3 +221,13 @@ export declare const DEFAULT_EXIT_CODES: InternalRefsExitCodes;
221
221
 
222
222
  /** Option names refused by name, with the reason each one is not something a caller may supply. */
223
223
  export declare const REFUSED_OPTIONS: ReadonlyMap<string, string>;
224
+
225
+ /** Where the command-line form reads the repository's axes: `scripts/internal-refs.json`. */
226
+ export declare const INTERNAL_REFS_CONFIG_PATH: "scripts/internal-refs.json";
227
+
228
+ /**
229
+ * The command-line form: run the gate over the repository in the working directory, with the
230
+ * axes read from `scripts/internal-refs.json` (or `--config <path>`). Exit 0 clean, 1 hits, 2 the
231
+ * gate could not run. `node node_modules/@cosyte/script-utils/internal-refs.js`.
232
+ */
233
+ export declare function runInternalRefsCli(argv: string[]): number;