safeword 0.46.2 → 0.47.0
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/dist/check-WKNC4NZT.js +112 -0
- package/dist/check-WKNC4NZT.js.map +1 -0
- package/dist/{chunk-G2UYJDPN.js → chunk-AWSLG6FU.js} +5 -3
- package/dist/chunk-AWSLG6FU.js.map +1 -0
- package/dist/{chunk-PZ6ZWEF6.js → chunk-IGULTNHR.js} +19 -6
- package/dist/chunk-IGULTNHR.js.map +1 -0
- package/dist/{chunk-HN3ITK4W.js → chunk-ILPPVUYD.js} +86 -3
- package/dist/chunk-ILPPVUYD.js.map +1 -0
- package/dist/chunk-QLXFPFIC.js +464 -0
- package/dist/chunk-QLXFPFIC.js.map +1 -0
- package/dist/{chunk-2EH6Z7P3.js → chunk-TB3BU2FA.js} +159 -102
- package/dist/chunk-TB3BU2FA.js.map +1 -0
- package/dist/{check-2RU6E3DW.js → chunk-UCQTQ37R.js} +106 -130
- package/dist/chunk-UCQTQ37R.js.map +1 -0
- package/dist/cli.js +15 -8
- package/dist/cli.js.map +1 -1
- package/dist/{codify-D6WZ5AS4.js → codify-B5RSNBMS.js} +63 -17
- package/dist/codify-B5RSNBMS.js.map +1 -0
- package/dist/{diff-ODVLDTIF.js → diff-XUUXNKJ6.js} +2 -2
- package/dist/lint-gherkin-KJH3GNJQ.js +49 -0
- package/dist/lint-gherkin-KJH3GNJQ.js.map +1 -0
- package/dist/{reset-I6G7C33F.js → reset-V4YNQDC6.js} +2 -2
- package/dist/{setup-FXQTOYBA.js → setup-ORWJN5I7.js} +23 -4
- package/dist/setup-ORWJN5I7.js.map +1 -0
- package/dist/{sync-tickets-MVFO7377.js → sync-tickets-3L4T6VUK.js} +2 -2
- package/dist/{upgrade-GEVFSMCL.js → upgrade-RHFRVM5G.js} +84 -4
- package/dist/upgrade-RHFRVM5G.js.map +1 -0
- package/package.json +10 -9
- package/templates/SAFEWORD.md +3 -1
- package/templates/codex/config.toml +16 -0
- package/templates/commands/verify.md +10 -1
- package/templates/cucumber/cucumber.mjs +20 -2
- package/templates/cucumber/safeword-lane.feature +3 -3
- package/templates/cucumber/shared.steps.ts +3 -3
- package/templates/doc-templates/test-definitions-feature.md +8 -18
- package/templates/guides/planning-guide.md +33 -18
- package/templates/hooks/codex/pre-tool-quality.ts +181 -0
- package/templates/hooks/lib/lint.ts +21 -0
- package/templates/hooks/lib/quality-state.ts +9 -0
- package/templates/hooks/lib/replan-relevance.ts +13 -2
- package/templates/hooks/lib/test-runner.ts +81 -28
- package/templates/hooks/pre-tool-quality.ts +2 -1
- package/templates/hooks/stop-quality.ts +5 -3
- package/templates/skills/bdd/SCENARIOS.md +53 -45
- package/templates/skills/bdd/SKILL.md +1 -1
- package/templates/skills/bdd/TDD.md +9 -0
- package/templates/skills/review-spec/SKILL.md +2 -2
- package/templates/skills/verify/SKILL.md +10 -1
- package/dist/check-2RU6E3DW.js.map +0 -1
- package/dist/chunk-2EH6Z7P3.js.map +0 -1
- package/dist/chunk-G2UYJDPN.js.map +0 -1
- package/dist/chunk-HN3ITK4W.js.map +0 -1
- package/dist/chunk-PZ6ZWEF6.js.map +0 -1
- package/dist/codify-D6WZ5AS4.js.map +0 -1
- package/dist/setup-FXQTOYBA.js.map +0 -1
- package/dist/upgrade-GEVFSMCL.js.map +0 -1
- /package/dist/{diff-ODVLDTIF.js.map → diff-XUUXNKJ6.js.map} +0 -0
- /package/dist/{reset-I6G7C33F.js.map → reset-V4YNQDC6.js.map} +0 -0
- /package/dist/{sync-tickets-MVFO7377.js.map → sync-tickets-3L4T6VUK.js.map} +0 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isNewerVersion
|
|
3
|
+
} from "./chunk-FJYRWU2V.js";
|
|
4
|
+
import {
|
|
5
|
+
checkHealth,
|
|
6
|
+
reportHealthSummary
|
|
7
|
+
} from "./chunk-UCQTQ37R.js";
|
|
8
|
+
import {
|
|
9
|
+
syncTickets
|
|
10
|
+
} from "./chunk-AWSLG6FU.js";
|
|
11
|
+
import "./chunk-NHXVS5FL.js";
|
|
12
|
+
import "./chunk-IGULTNHR.js";
|
|
13
|
+
import "./chunk-QLXFPFIC.js";
|
|
14
|
+
import "./chunk-TB3BU2FA.js";
|
|
15
|
+
import "./chunk-3BMVTFFM.js";
|
|
16
|
+
import "./chunk-LODQOJEK.js";
|
|
17
|
+
import "./chunk-HSC7TELY.js";
|
|
18
|
+
import {
|
|
19
|
+
header,
|
|
20
|
+
info,
|
|
21
|
+
keyValue,
|
|
22
|
+
success,
|
|
23
|
+
warn
|
|
24
|
+
} from "./chunk-445LAX4Y.js";
|
|
25
|
+
|
|
26
|
+
// src/commands/check.ts
|
|
27
|
+
import process from "process";
|
|
28
|
+
async function checkLatestVersion(timeout = 3e3) {
|
|
29
|
+
try {
|
|
30
|
+
const controller = new AbortController();
|
|
31
|
+
const timeoutId = setTimeout(() => {
|
|
32
|
+
controller.abort();
|
|
33
|
+
}, timeout);
|
|
34
|
+
const response = await fetch("https://registry.npmjs.org/safeword/latest", {
|
|
35
|
+
signal: controller.signal
|
|
36
|
+
});
|
|
37
|
+
clearTimeout(timeoutId);
|
|
38
|
+
if (!response.ok) return void 0;
|
|
39
|
+
const data = await response.json();
|
|
40
|
+
return data.version ?? void 0;
|
|
41
|
+
} catch {
|
|
42
|
+
return void 0;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function reportUpdateStatus(health) {
|
|
46
|
+
info("\nChecking for updates...");
|
|
47
|
+
const latestVersion = await checkLatestVersion();
|
|
48
|
+
if (!latestVersion) {
|
|
49
|
+
warn("Couldn't check for updates (offline?)");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
health.latestVersion = latestVersion;
|
|
53
|
+
health.updateAvailable = isNewerVersion(health.cliVersion, latestVersion);
|
|
54
|
+
if (health.updateAvailable) {
|
|
55
|
+
warn(`Update available: v${latestVersion}`);
|
|
56
|
+
info("Run `bunx safeword@latest upgrade` to upgrade");
|
|
57
|
+
} else {
|
|
58
|
+
success("CLI is up to date");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function reportVersionMismatch(health) {
|
|
62
|
+
if (!health.projectVersion) return;
|
|
63
|
+
if (isNewerVersion(health.cliVersion, health.projectVersion)) {
|
|
64
|
+
warn(`Project config (v${health.projectVersion}) is newer than CLI (v${health.cliVersion})`);
|
|
65
|
+
info("Consider upgrading the CLI");
|
|
66
|
+
} else if (isNewerVersion(health.projectVersion, health.cliVersion)) {
|
|
67
|
+
info(`
|
|
68
|
+
Upgrade available for project config`);
|
|
69
|
+
info(
|
|
70
|
+
`Run \`safeword upgrade\` to update from v${health.projectVersion} to v${health.cliVersion}`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function regenerateTicketIndex(cwd) {
|
|
75
|
+
try {
|
|
76
|
+
const result = syncTickets(cwd);
|
|
77
|
+
if (result.wrote) {
|
|
78
|
+
info("Regenerated ticket index (INDEX.md / INDEX-completed.md)");
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (process.env.DEBUG) {
|
|
82
|
+
console.error("[check] ticket index regen failed:", error);
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function check(options) {
|
|
88
|
+
const cwd = process.cwd();
|
|
89
|
+
header("Safeword Health Check");
|
|
90
|
+
const health = await checkHealth(cwd);
|
|
91
|
+
if (!health.configured) {
|
|
92
|
+
info("Not configured. Run `safeword setup` to initialize.");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
regenerateTicketIndex(cwd);
|
|
96
|
+
keyValue("Safeword CLI", `v${health.cliVersion}`);
|
|
97
|
+
keyValue("Project config", health.projectVersion ? `v${health.projectVersion}` : "unknown");
|
|
98
|
+
if (options.offline) {
|
|
99
|
+
info("\nSkipped update check (offline mode)");
|
|
100
|
+
} else {
|
|
101
|
+
await reportUpdateStatus(health);
|
|
102
|
+
}
|
|
103
|
+
reportVersionMismatch(health);
|
|
104
|
+
const hasIssues = reportHealthSummary(health);
|
|
105
|
+
if (hasIssues) {
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export {
|
|
110
|
+
check
|
|
111
|
+
};
|
|
112
|
+
//# sourceMappingURL=check-WKNC4NZT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/check.ts"],"sourcesContent":["/**\n * Check command - Verify project health and configuration\n *\n * The config-health core lives in ../health.ts (shared with the setup/upgrade\n * self-verify, ticket 3293WH). This command adds the standalone-only\n * surfaces: npm update-check, version display, and ticket-index refresh.\n */\n\nimport process from 'node:process';\n\nimport { checkHealth, type HealthStatus, reportHealthSummary } from '../health.js';\nimport { syncTickets } from '../ticket-sync/index.js';\nimport { header, info, keyValue, success, warn } from '../utils/output.js';\nimport { isNewerVersion } from '../utils/version.js';\n\ninterface CheckOptions {\n offline?: boolean;\n}\n\n/**\n * Check for latest version from npm (with timeout)\n * @param timeout\n */\nasync function checkLatestVersion(timeout = 3000): Promise<string | undefined> {\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => {\n controller.abort();\n }, timeout);\n\n const response = await fetch('https://registry.npmjs.org/safeword/latest', {\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) return undefined;\n\n const data = (await response.json()) as { version?: string };\n return data.version ?? undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Check for CLI updates and report status\n * @param health\n */\nasync function reportUpdateStatus(health: HealthStatus): Promise<void> {\n info('\\nChecking for updates...');\n const latestVersion = await checkLatestVersion();\n\n if (!latestVersion) {\n warn(\"Couldn't check for updates (offline?)\");\n return;\n }\n\n health.latestVersion = latestVersion;\n health.updateAvailable = isNewerVersion(health.cliVersion, latestVersion);\n\n if (health.updateAvailable) {\n warn(`Update available: v${latestVersion}`);\n info('Run `bunx safeword@latest upgrade` to upgrade');\n } else {\n success('CLI is up to date');\n }\n}\n\n/**\n * Compare project version vs CLI version and report\n * @param health\n */\nfunction reportVersionMismatch(health: HealthStatus): void {\n if (!health.projectVersion) return;\n\n if (isNewerVersion(health.cliVersion, health.projectVersion)) {\n warn(`Project config (v${health.projectVersion}) is newer than CLI (v${health.cliVersion})`);\n info('Consider upgrading the CLI');\n } else if (isNewerVersion(health.projectVersion, health.cliVersion)) {\n info(`\\nUpgrade available for project config`);\n info(\n `Run \\`safeword upgrade\\` to update from v${health.projectVersion} to v${health.cliVersion}`,\n );\n }\n}\n\n/**\n * Regenerate the ticket discovery index, swallowing any error — index\n * freshness must never block or fail a health check. Reports only when it\n * actually rewrote a file.\n * @param cwd\n */\nfunction regenerateTicketIndex(cwd: string): void {\n try {\n const result = syncTickets(cwd);\n if (result.wrote) {\n info('Regenerated ticket index (INDEX.md / INDEX-completed.md)');\n }\n } catch (error: unknown) {\n // Best-effort: index freshness must never fail the health check. Surface\n // under DEBUG, then return — the deliberate swallow point.\n if (process.env.DEBUG) {\n console.error('[check] ticket index regen failed:', error);\n }\n return;\n }\n}\n\n/**\n *\n * @param options\n */\nexport async function check(options: CheckOptions): Promise<void> {\n const cwd = process.cwd();\n\n header('Safeword Health Check');\n\n const health = await checkHealth(cwd);\n\n // Not configured\n if (!health.configured) {\n info('Not configured. Run `safeword setup` to initialize.');\n return;\n }\n\n // Keep the ticket discovery index fresh at this checkpoint (best-effort —\n // never fail the health check on index regen). Ticket 1GGD28.\n regenerateTicketIndex(cwd);\n\n // Show versions\n keyValue('Safeword CLI', `v${health.cliVersion}`);\n keyValue('Project config', health.projectVersion ? `v${health.projectVersion}` : 'unknown');\n\n // Check for updates (unless offline)\n if (options.offline) {\n info('\\nSkipped update check (offline mode)');\n } else {\n await reportUpdateStatus(health);\n }\n\n reportVersionMismatch(health);\n const hasIssues = reportHealthSummary(health);\n\n if (hasIssues) {\n process.exit(1);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAQA,OAAO,aAAa;AAepB,eAAe,mBAAmB,UAAU,KAAmC;AAC7E,MAAI;AACF,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,YAAY,WAAW,MAAM;AACjC,iBAAW,MAAM;AAAA,IACnB,GAAG,OAAO;AAEV,UAAM,WAAW,MAAM,MAAM,8CAA8C;AAAA,MACzE,QAAQ,WAAW;AAAA,IACrB,CAAC;AAED,iBAAa,SAAS;AAEtB,QAAI,CAAC,SAAS,GAAI,QAAO;AAEzB,UAAM,OAAQ,MAAM,SAAS,KAAK;AAClC,WAAO,KAAK,WAAW;AAAA,EACzB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMA,eAAe,mBAAmB,QAAqC;AACrE,OAAK,2BAA2B;AAChC,QAAM,gBAAgB,MAAM,mBAAmB;AAE/C,MAAI,CAAC,eAAe;AAClB,SAAK,uCAAuC;AAC5C;AAAA,EACF;AAEA,SAAO,gBAAgB;AACvB,SAAO,kBAAkB,eAAe,OAAO,YAAY,aAAa;AAExE,MAAI,OAAO,iBAAiB;AAC1B,SAAK,sBAAsB,aAAa,EAAE;AAC1C,SAAK,+CAA+C;AAAA,EACtD,OAAO;AACL,YAAQ,mBAAmB;AAAA,EAC7B;AACF;AAMA,SAAS,sBAAsB,QAA4B;AACzD,MAAI,CAAC,OAAO,eAAgB;AAE5B,MAAI,eAAe,OAAO,YAAY,OAAO,cAAc,GAAG;AAC5D,SAAK,oBAAoB,OAAO,cAAc,yBAAyB,OAAO,UAAU,GAAG;AAC3F,SAAK,4BAA4B;AAAA,EACnC,WAAW,eAAe,OAAO,gBAAgB,OAAO,UAAU,GAAG;AACnE,SAAK;AAAA,qCAAwC;AAC7C;AAAA,MACE,4CAA4C,OAAO,cAAc,QAAQ,OAAO,UAAU;AAAA,IAC5F;AAAA,EACF;AACF;AAQA,SAAS,sBAAsB,KAAmB;AAChD,MAAI;AACF,UAAM,SAAS,YAAY,GAAG;AAC9B,QAAI,OAAO,OAAO;AAChB,WAAK,0DAA0D;AAAA,IACjE;AAAA,EACF,SAAS,OAAgB;AAGvB,QAAI,QAAQ,IAAI,OAAO;AACrB,cAAQ,MAAM,sCAAsC,KAAK;AAAA,IAC3D;AACA;AAAA,EACF;AACF;AAMA,eAAsB,MAAM,SAAsC;AAChE,QAAM,MAAM,QAAQ,IAAI;AAExB,SAAO,uBAAuB;AAE9B,QAAM,SAAS,MAAM,YAAY,GAAG;AAGpC,MAAI,CAAC,OAAO,YAAY;AACtB,SAAK,qDAAqD;AAC1D;AAAA,EACF;AAIA,wBAAsB,GAAG;AAGzB,WAAS,gBAAgB,IAAI,OAAO,UAAU,EAAE;AAChD,WAAS,kBAAkB,OAAO,iBAAiB,IAAI,OAAO,cAAc,KAAK,SAAS;AAG1F,MAAI,QAAQ,SAAS;AACnB,SAAK,uCAAuC;AAAA,EAC9C,OAAO;AACL,UAAM,mBAAmB,MAAM;AAAA,EACjC;AAEA,wBAAsB,MAAM;AAC5B,QAAM,YAAY,oBAAoB,MAAM;AAE5C,MAAI,WAAW;AACb,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;","names":[]}
|
|
@@ -5,6 +5,10 @@ import {
|
|
|
5
5
|
resolveTicketsDirectory
|
|
6
6
|
} from "./chunk-3BMVTFFM.js";
|
|
7
7
|
|
|
8
|
+
// src/ticket-sync/index.ts
|
|
9
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
10
|
+
import nodePath from "path";
|
|
11
|
+
|
|
8
12
|
// src/utils/ticket-relations.ts
|
|
9
13
|
function parseTicketIdList(raw) {
|
|
10
14
|
if (raw === void 0) return [];
|
|
@@ -56,8 +60,6 @@ function findTicketsInCycles(nodes) {
|
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
// src/ticket-sync/index.ts
|
|
59
|
-
import { existsSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
60
|
-
import nodePath from "path";
|
|
61
63
|
var TICKETS_RELATIVE_PATH = "<namespace-root>/tickets";
|
|
62
64
|
var INDEX_FILENAME = "INDEX.md";
|
|
63
65
|
var COMPLETED_INDEX_FILENAME = "INDEX-completed.md";
|
|
@@ -242,4 +244,4 @@ export {
|
|
|
242
244
|
readTickets,
|
|
243
245
|
syncTickets
|
|
244
246
|
};
|
|
245
|
-
//# sourceMappingURL=chunk-
|
|
247
|
+
//# sourceMappingURL=chunk-AWSLG6FU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ticket-sync/index.ts","../src/utils/ticket-relations.ts"],"sourcesContent":["/**\n * Ticket sync — generates capability-discovery indexes over the ticket corpus:\n * `<namespace-root>/tickets/INDEX.md` (active tickets, grouped by epic) and\n * `INDEX-completed.md` (the `completed/` archive). Mirrors `learning-sync`\n * (plain markdown + grep, no skill-description char cap) so \"is there already\n * a ticket for X?\" is one grep instead of a hundreds-of-folders hunt.\n *\n * Fired manually via `safeword sync-tickets`, as a `safeword check` step, and\n * after `ticket new`.\n *\n * Ticket 1GGD28.\n */\n\nimport { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport nodePath from 'node:path';\n\nimport { resolveTicketsDirectory } from '../utils/configured-paths.js';\nimport { formatTicketReference } from '../utils/ticket-reference.js';\nimport { deriveBlocks, parseTicketIdList } from '../utils/ticket-relations.js';\n\n/** Placeholder label for callers that read a directory without a project cwd. */\nexport const TICKETS_RELATIVE_PATH = '<namespace-root>/tickets';\nexport const INDEX_FILENAME = 'INDEX.md';\nexport const COMPLETED_INDEX_FILENAME = 'INDEX-completed.md';\nexport const COMPLETED_DIRNAME = 'completed';\n\nconst NO_EPIC_GROUP = '(no epic)';\nconst SKIP_DIRECTORIES = new Set([COMPLETED_DIRNAME, 'tmp']);\n\nexport interface TicketEntry {\n id: string;\n folder: string; // folder name, e.g. 1GGD28-ticket-discovery-index\n relativePath: string; // e.g. <namespace-root>/tickets/1GGD28-ticket-discovery-index\n title: string;\n status: string;\n epic: string | undefined; // undefined → grouped under \"(no epic)\"\n goal: string | undefined; // the **Goal:** one-liner, when present\n dependsOn: string[]; // ticket ids this one depends on (directed edge); [] when none\n}\n\nexport interface TicketSyncResult {\n wrote: boolean;\n active: TicketEntry[];\n completed: TicketEntry[];\n skipped: { folder: string; reason: string }[];\n indexPath: string;\n completedIndexPath: string;\n}\n\n/** Strip a single layer of matching surrounding quotes. */\nfunction stripQuotes(value: string): string {\n if (\n value.length >= 2 &&\n ((value.startsWith(\"'\") && value.endsWith(\"'\")) ||\n (value.startsWith('\"') && value.endsWith('\"')))\n ) {\n return value.slice(1, -1);\n }\n return value;\n}\n\n/** Parse the leading `--- … ---` frontmatter block into a key→value map. */\nfunction parseFrontmatter(content: string): { fields: Map<string, string>; bodyStart: number } {\n const lines = content.split('\\n');\n const fields = new Map<string, string>();\n if (lines[0]?.trim() !== '---') return { fields, bodyStart: 0 };\n\n for (let index = 1; index < lines.length; index += 1) {\n const line = lines[index] ?? '';\n if (line.trim() === '---') return { fields, bodyStart: index + 1 };\n const match = /^([a-z_][\\w-]*):(.*)$/i.exec(line);\n if (match?.[1] !== undefined) fields.set(match[1], stripQuotes((match[2] ?? '').trim()));\n }\n return { fields, bodyStart: 0 };\n}\n\n/** First `# H1` heading text in the body, if any. */\nfunction firstHeading(bodyLines: string[]): string | undefined {\n for (const line of bodyLines) {\n if (line.startsWith('# ')) return line.slice(2).trim();\n }\n return undefined;\n}\n\n/** The `**Goal:**` one-liner from the body, label stripped, if present. */\nfunction goalLine(bodyLines: string[]): string | undefined {\n for (const line of bodyLines) {\n const match = /^\\*\\*Goal:\\*\\*(.*)$/.exec(line.trim());\n if (match?.[1] !== undefined) {\n const goal = match[1].trim();\n if (goal.length > 0) return goal;\n }\n }\n return undefined;\n}\n\n/**\n * Parse a single ticket.md. Returns the entry (minus relativePath) when it has\n * an `id:`, or a skip reason. Title resolves frontmatter `title` → first H1 →\n * frontmatter `slug` → folder name.\n */\nfunction parseTicket(\n filePath: string,\n folder: string,\n): { ok: true; entry: Omit<TicketEntry, 'relativePath'> } | { ok: false; reason: string } {\n const content = readFileSync(filePath, 'utf8');\n const { fields, bodyStart } = parseFrontmatter(content);\n\n const id = fields.get('id');\n if (id === undefined || id.length === 0) {\n return { ok: false, reason: 'missing id: in frontmatter' };\n }\n\n const bodyLines = content.split('\\n').slice(bodyStart);\n const title = fields.get('title') ?? firstHeading(bodyLines) ?? fields.get('slug') ?? folder;\n const status = fields.get('status') ?? '—';\n const epic = fields.get('epic');\n\n return {\n ok: true,\n entry: {\n id,\n folder,\n title,\n status,\n epic,\n goal: goalLine(bodyLines),\n dependsOn: parseTicketIdList(fields.get('depends_on')),\n },\n };\n}\n\n/** Parse every ticket folder directly under `directory`, returning entries +\n * skip reasons. Folders without a ticket.md are silently ignored (not skipped).\n * `pathPrefix` is prepended to the folder for the entry's relativePath. */\nfunction readTicketFolders(\n directory: string,\n pathPrefix: string,\n): { entries: TicketEntry[]; skipped: { folder: string; reason: string }[] } {\n if (!existsSync(directory)) return { entries: [], skipped: [] };\n\n const entries: TicketEntry[] = [];\n const skipped: { folder: string; reason: string }[] = [];\n\n const folders = readdirSync(directory, { withFileTypes: true })\n .filter(dirent => dirent.isDirectory() && !SKIP_DIRECTORIES.has(dirent.name))\n .map(dirent => dirent.name)\n .toSorted((a, b) => a.localeCompare(b));\n\n for (const folder of folders) {\n const ticketPath = nodePath.join(directory, folder, 'ticket.md');\n if (!existsSync(ticketPath)) continue; // not a ticket folder — ignore\n const parsed = parseTicket(ticketPath, folder);\n if (parsed.ok) {\n entries.push({ ...parsed.entry, relativePath: `${pathPrefix}/${folder}` });\n } else {\n skipped.push({ folder, reason: parsed.reason });\n }\n }\n\n return { entries, skipped };\n}\n\n/**\n * Read the corpus into active (top-level) and completed (`completed/`) entries,\n * each sorted by id, plus any skipped folders. INDEX*.md are files, so the\n * directory filter excludes them from being parsed as tickets.\n */\nexport function readTickets(\n ticketsDirectory: string,\n relativeLabel: string = TICKETS_RELATIVE_PATH,\n): {\n active: TicketEntry[];\n completed: TicketEntry[];\n skipped: { folder: string; reason: string }[];\n} {\n const active = readTicketFolders(ticketsDirectory, relativeLabel);\n const completed = readTicketFolders(\n nodePath.join(ticketsDirectory, COMPLETED_DIRNAME),\n `${relativeLabel}/${COMPLETED_DIRNAME}`,\n );\n\n const byId = (a: TicketEntry, b: TicketEntry) => a.id.localeCompare(b.id);\n return {\n active: active.entries.toSorted(byId),\n completed: completed.entries.toSorted(byId),\n skipped: [...active.skipped, ...completed.skipped],\n };\n}\n\n/** Render a list of related ticket ids slug-first, falling back to the bare id\n * for targets outside this index (cross-variant or not-yet-created). */\nfunction renderRelation(ids: string[], labelById: Map<string, string>): string {\n return ids\n .map(id => {\n const title = labelById.get(id);\n return title === undefined ? id : formatTicketReference(id, title);\n })\n .join(', ');\n}\n\n/** Render one entry as a block: header, optional goal, relation edges, path. */\nfunction renderEntry(\n entry: TicketEntry,\n blocks: Map<string, string[]>,\n labelById: Map<string, string>,\n): string[] {\n const epic = entry.epic ?? '—';\n const lines = [\n `- **${formatTicketReference(entry.id, entry.title)}** (${entry.status}, epic: ${epic})`,\n ];\n if (entry.goal !== undefined) lines.push(` ${entry.goal}`);\n if (entry.dependsOn.length > 0) {\n lines.push(` blocked by: ${renderRelation(entry.dependsOn, labelById)}`);\n }\n const blocking = blocks.get(entry.id) ?? [];\n if (blocking.length > 0) lines.push(` blocks: ${renderRelation(blocking, labelById)}`);\n lines.push(` → \\`${entry.relativePath}\\``);\n return lines;\n}\n\n/** Group entries by epic; \"(no epic)\" sorts last, every other group alphabetical. */\nfunction groupByEpic(entries: TicketEntry[]): [string, TicketEntry[]][] {\n const groups = new Map<string, TicketEntry[]>();\n for (const entry of entries) {\n const key = entry.epic ?? NO_EPIC_GROUP;\n const bucket = groups.get(key);\n if (bucket) bucket.push(entry);\n else groups.set(key, [entry]);\n }\n return [...groups.entries()].toSorted(([a], [b]) => {\n if (a === NO_EPIC_GROUP) return 1;\n if (b === NO_EPIC_GROUP) return -1;\n return a.localeCompare(b);\n });\n}\n\n/**\n * Render the full index for one variant. Deterministic: same entries → same\n * bytes. No size cap — agents Read or grep the file.\n */\nexport function buildIndexContent(\n entries: TicketEntry[],\n options: { variant: 'active' | 'completed' },\n): string {\n const isActive = options.variant === 'active';\n const header = [\n isActive ? '# Project Tickets — Index' : '# Project Tickets — Completed Archive',\n '',\n '<!-- Auto-generated by `safeword sync-tickets`. Do not edit by hand. -->',\n isActive\n ? '<!-- Active tickets, grouped by epic. Completed tickets live in INDEX-completed.md. -->'\n : '<!-- Completed tickets (the completed/ archive), grouped by epic. -->',\n '',\n ];\n\n if (entries.length === 0) {\n return [...header, isActive ? 'No active tickets.' : 'No completed tickets.', ''].join('\\n');\n }\n\n const blocks = deriveBlocks(entries);\n const labelById = new Map(entries.map(entry => [entry.id, entry.title]));\n\n const lines = [...header, `## Tickets (${entries.length})`, ''];\n for (const [epic, group] of groupByEpic(entries)) {\n lines.push(`### ${epic}`, '');\n for (const entry of group) lines.push(...renderEntry(entry, blocks, labelById));\n lines.push('');\n }\n return lines.join('\\n');\n}\n\n/** Write `content` to `path` only when it differs; report whether it wrote. */\nfunction writeIfChanged(path: string, content: string): boolean {\n const previous = existsSync(path) ? readFileSync(path, 'utf8') : undefined;\n if (previous === content) return false;\n writeFileSync(path, content);\n return true;\n}\n\n/**\n * Generate/update both ticket indexes from the corpus. No-op (creates nothing)\n * when the tickets directory is absent. The completed archive is written when\n * a `completed/` directory exists or completed entries are present.\n */\nexport function syncTickets(cwd: string): TicketSyncResult {\n const ticketsDirectory = resolveTicketsDirectory(cwd);\n const relativeLabel = nodePath.relative(cwd, ticketsDirectory) || TICKETS_RELATIVE_PATH;\n const indexPath = nodePath.join(ticketsDirectory, INDEX_FILENAME);\n const completedIndexPath = nodePath.join(ticketsDirectory, COMPLETED_INDEX_FILENAME);\n\n if (!existsSync(ticketsDirectory)) {\n return { wrote: false, active: [], completed: [], skipped: [], indexPath, completedIndexPath };\n }\n\n const { active, completed, skipped } = readTickets(ticketsDirectory, relativeLabel);\n\n const wroteActive = writeIfChanged(indexPath, buildIndexContent(active, { variant: 'active' }));\n\n const completedDirectory = nodePath.join(ticketsDirectory, COMPLETED_DIRNAME);\n const wroteCompleted =\n completed.length > 0 || existsSync(completedDirectory)\n ? writeIfChanged(completedIndexPath, buildIndexContent(completed, { variant: 'completed' }))\n : false;\n\n return {\n wrote: wroteActive || wroteCompleted,\n active,\n completed,\n skipped,\n indexPath,\n completedIndexPath,\n };\n}\n","/**\n * Structured ticket relations (ticket AKZJXC).\n *\n * One canonical directed edge — `depends_on` — stored as an inline-array scalar\n * the hand-rolled frontmatter parser can hold. The inverse (`blocks`) is always\n * derived across the corpus; cycles and dangling refs surface as warnings, never\n * errors (mirrors safeword's tolerant ID resolution).\n */\n\n/** A ticket reduced to its id and its outgoing `depends_on` edges. */\nexport interface TicketNode {\n id: string;\n dependsOn: string[];\n}\n\n/**\n * Parse a `depends_on` frontmatter scalar into ticket ids. Accepts the inline\n * array form (`[A, B]`) or a bare comma list (`A, B`); trims each id and drops\n * empties. Missing/empty input → `[]`.\n * @param raw the raw frontmatter value, or undefined when the key is absent\n */\nexport function parseTicketIdList(raw?: string): string[] {\n if (raw === undefined) return [];\n const inner = raw.trim().replace(/^\\[/, '').replace(/\\]$/, '');\n return inner\n .split(',')\n .map(id => id.trim())\n .filter(id => id.length > 0);\n}\n\n/**\n * Invert the `depends_on` graph into `id → ids that depend on it` (the derived\n * `blocks` edges). Only ids that block something appear as keys; each value\n * preserves corpus order.\n * @param nodes every ticket's id + depends_on edges\n */\nexport function deriveBlocks(nodes: TicketNode[]): Map<string, string[]> {\n const blocks = new Map<string, string[]>();\n for (const node of nodes) {\n for (const target of node.dependsOn) {\n const blockers = blocks.get(target) ?? [];\n blockers.push(node.id);\n blocks.set(target, blockers);\n }\n }\n return blocks;\n}\n\n/**\n * `depends_on` targets absent from the corpus, as `{from, missing}` pairs sorted\n * by from then missing. Warn-only — a target may live on another branch or in\n * completed/.\n * @param nodes every ticket's id + depends_on edges\n */\nexport function findDanglingDependencies(nodes: TicketNode[]): { from: string; missing: string }[] {\n const known = new Set(nodes.map(node => node.id));\n const dangling: { from: string; missing: string }[] = [];\n for (const node of nodes) {\n for (const target of node.dependsOn) {\n if (!known.has(target)) dangling.push({ from: node.id, missing: target });\n }\n }\n return dangling.toSorted(\n (a, b) => a.from.localeCompare(b.from) || a.missing.localeCompare(b.missing),\n );\n}\n\n/**\n * Sorted ids of tickets that participate in any `depends_on` cycle (a node\n * reachable from itself, including a self-edge). Warn-only. Dangling targets are\n * inert — they have no outgoing edges, so they can't form a cycle.\n * @param nodes every ticket's id + depends_on edges\n */\nexport function findTicketsInCycles(nodes: TicketNode[]): string[] {\n const edges = new Map(nodes.map(node => [node.id, node.dependsOn]));\n const inCycle = new Set<string>();\n\n for (const start of edges.keys()) {\n // DFS along depends_on edges; reaching `start` again means it's on a cycle.\n const stack = [...(edges.get(start) ?? [])];\n const seen = new Set<string>();\n while (stack.length > 0) {\n const next = stack.pop();\n if (next === undefined) continue;\n if (next === start) {\n inCycle.add(start);\n break;\n }\n if (seen.has(next)) continue;\n seen.add(next);\n stack.push(...(edges.get(next) ?? []));\n }\n }\n\n return [...inCycle].toSorted((a, b) => a.localeCompare(b));\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseFeatureAcReferences
|
|
3
|
+
} from "./chunk-QLXFPFIC.js";
|
|
4
|
+
|
|
1
5
|
// src/utils/markdown-sections.ts
|
|
2
6
|
function computeSkipMask(lines) {
|
|
3
7
|
const skip = [];
|
|
@@ -91,18 +95,26 @@ function advance(state, heading, byJtbd) {
|
|
|
91
95
|
}
|
|
92
96
|
var EMPTY_REPORT = { uncovered: [], stale: [], orphan: [] };
|
|
93
97
|
function buildCoverageReport(specContent, testDefinitionsContent) {
|
|
98
|
+
const scenarioReferences = testDefinitionsContent === void 0 ? void 0 : parseScenarioTitles(testDefinitionsContent).map((title) => parseAcReferenceFromTitle(title)).filter((reference) => reference !== void 0);
|
|
99
|
+
return buildCoverageReportFromReferences(specContent, scenarioReferences);
|
|
100
|
+
}
|
|
101
|
+
function buildCoverageReportFromFeature(specContent, featureContent) {
|
|
102
|
+
return buildCoverageReportFromReferences(
|
|
103
|
+
specContent,
|
|
104
|
+
featureContent === void 0 ? void 0 : parseFeatureAcReferences(featureContent)
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
function buildCoverageReportFromReferences(specContent, scenarioReferences) {
|
|
94
108
|
const byJtbd = parseAcIdsByJtbd(specContent);
|
|
95
109
|
const knownAcIds = /* @__PURE__ */ new Set();
|
|
96
110
|
for (const acIds of byJtbd.values()) for (const id of acIds) knownAcIds.add(id);
|
|
97
111
|
if (knownAcIds.size === 0) return { ...EMPTY_REPORT };
|
|
98
|
-
if (
|
|
112
|
+
if (scenarioReferences === void 0) return { ...EMPTY_REPORT };
|
|
99
113
|
const knownJtbds = new Set(byJtbd.keys());
|
|
100
114
|
const covered = /* @__PURE__ */ new Set();
|
|
101
115
|
const stale = /* @__PURE__ */ new Set();
|
|
102
116
|
const orphan = /* @__PURE__ */ new Set();
|
|
103
|
-
for (const
|
|
104
|
-
const reference = parseAcReferenceFromTitle(title);
|
|
105
|
-
if (reference === void 0) continue;
|
|
117
|
+
for (const reference of scenarioReferences) {
|
|
106
118
|
if (knownAcIds.has(reference)) {
|
|
107
119
|
covered.add(reference);
|
|
108
120
|
} else if (knownJtbds.has(jtbdPart(reference))) {
|
|
@@ -147,6 +159,7 @@ export {
|
|
|
147
159
|
stripInlineComments,
|
|
148
160
|
parseHeading,
|
|
149
161
|
parseAcReferenceFromTitle,
|
|
150
|
-
buildCoverageReport
|
|
162
|
+
buildCoverageReport,
|
|
163
|
+
buildCoverageReportFromFeature
|
|
151
164
|
};
|
|
152
|
-
//# sourceMappingURL=chunk-
|
|
165
|
+
//# sourceMappingURL=chunk-IGULTNHR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/markdown-sections.ts","../src/utils/scenario-coverage.ts"],"sourcesContent":["/**\n * Markdown section-walk primitives shared by the `## `-block parsers in\n * src/utils (personas, glossary, scenario-coverage). Extracted per ticket\n * WQ4RH3 — Rule of Three on a single CommonMark comment/fence-skip behavior.\n *\n * NOTE: the hook-side parser (`.safeword/hooks/lib/jtbd.ts`) deliberately does\n * NOT share this module — deployed hooks run standalone from `.safeword/hooks/`\n * and cannot import the CLI's dist. That copy is an intentional cross-runtime\n * boundary, not accidental duplication.\n */\n\n/**\n * Per-line boolean[] where `true` means \"skip this line during parsing\"\n * because it lives inside a triple-backtick code fence or a block-level HTML\n * comment (`<!-- ... -->`). The array has the same length as the input so a\n * caller can use the line index directly as a 1-indexed line number.\n *\n * Per CommonMark, only a line that BEGINS with `<!--` (after optional indent)\n * opens a block-level HTML comment; inline `<!--` mid-line is inline HTML,\n * handled by {@link stripInlineComments}, not by this mask.\n */\nexport function computeSkipMask(lines: readonly string[]): boolean[] {\n const skip: boolean[] = [];\n let insideCodeFence = false;\n let insideComment = false;\n for (const line of lines) {\n if (line.trimStart().startsWith('```')) {\n skip.push(true);\n insideCodeFence = !insideCodeFence;\n continue;\n }\n if (insideCodeFence) {\n skip.push(true);\n continue;\n }\n if (!insideComment && line.trimStart().startsWith('<!--')) insideComment = true;\n if (insideComment) {\n skip.push(true);\n if (line.includes('-->')) insideComment = false;\n continue;\n }\n skip.push(false);\n }\n return skip;\n}\n\n/**\n * Strip inline `<!-- ... -->` comments from a single line of text. Per\n * CommonMark, an HTML comment that appears mid-line (after other content) is\n * inline HTML and doesn't appear in rendered output. Regex-free and bounded:\n * each `<!--` advances the scan past its matching `-->`, so the function is\n * O(n) with no backtracking. An unclosed inline comment leaves the rest of the\n * line intact — block-level handling lives in {@link computeSkipMask}.\n */\nexport function stripInlineComments(text: string): string {\n let result = '';\n let pos = 0;\n while (pos < text.length) {\n const open = text.indexOf('<!--', pos);\n if (open === -1) {\n result += text.slice(pos);\n break;\n }\n result += text.slice(pos, open);\n const close = text.indexOf('-->', open + 4);\n if (close === -1) {\n // Unclosed inline comment — emit the rest as-is. The line-state machine\n // in computeSkipMask handles multi-line block comments separately.\n result += text.slice(open);\n break;\n }\n pos = close + 3;\n }\n return result;\n}\n\nconst HEADING_WHITESPACE = /^\\s/;\n\n/**\n * An ATX heading → `{ level, text }`, or undefined for a non-heading line.\n * Counts leading `#` manually (no quantifier-over-quantifier regex) and requires\n * a whitespace separator (space or tab, per CommonMark) before the heading text.\n * Shared by the `## `-block parsers (scenario-coverage, test-skeleton).\n */\nexport function parseHeading(line: string): { level: number; text: string } | undefined {\n const trimmed = line.trim();\n let level = 0;\n while (level < trimmed.length && trimmed.charAt(level) === '#') level += 1;\n if (level === 0 || level > 6) return undefined;\n const rest = trimmed.slice(level);\n if (rest.length === 0 || !HEADING_WHITESPACE.test(rest)) return undefined;\n return { level, text: rest.trim() };\n}\n","/**\n * Scenario-lineage coverage (ticket XT1FFM).\n *\n * Pure helpers behind `safeword check`'s advisory coverage report:\n * - parseAcIdsByJtbd — a spec.md's Acceptance Criteria ids, grouped by JTBD;\n * - parseAcReferenceFromTitle — a scenario title's `<jtbd-id>.AC<#>` reference;\n * - buildCoverageReport — cross-references the two into three buckets:\n * uncovered (a spec AC no scenario references),\n * stale (a scenario ref whose JTBD exists but whose AC# does not),\n * orphan (a scenario ref whose JTBD is absent from the spec).\n *\n * The `## `-section walk reuses `computeSkipMask` from `./markdown-sections.js`\n * (the shared CommonMark comment/fence-skip primitive, ticket WQ4RH3). The\n * hook-side `jtbd.ts` keeps its own copy across the deployed-hook runtime\n * boundary — it cannot import the CLI dist.\n *\n * No I/O — callers pass file content; check.ts owns ticket discovery.\n */\n\nimport { parseFeatureAcReferences } from './gherkin-feature.js';\nimport { computeSkipMask, parseHeading } from './markdown-sections.js';\n\nconst JTBD_HEADING = 'jobs to be done';\nconst SCENARIO_PREFIX = '### Scenario:';\n\nexport interface CoverageReport {\n /** AC ids declared in spec.md that no scenario references. */\n uncovered: string[];\n /** Scenario refs whose JTBD exists but whose AC number does not. */\n stale: string[];\n /** Scenario refs whose JTBD is absent from spec.md entirely. */\n orphan: string[];\n}\n\n/**\n * Conformant scenario title: a single whitespace-free token shaped\n * `<jtbd-id>.AC<#>` with an optional `.<scenario_name>` tail. The lazy\n * `\\S+?` plus the mandatory `.AC<digits>` anchor keep this linear — no two\n * adjacent greedy `\\S+` groups to backtrack between. Free-text titles (which\n * contain spaces) can never match from `^`.\n */\nconst CONFORMANT_TITLE = /^(\\S+?)\\.AC(\\d+)(?:\\.|$)/;\n\nexport function parseAcReferenceFromTitle(title: string): string | undefined {\n const match = CONFORMANT_TITLE.exec(title.trim());\n if (!match) return undefined;\n return `${match[1] ?? ''}.AC${match[2] ?? ''}`;\n}\n\n/**\n * Group Acceptance-Criteria ids by their JTBD id within a spec.md's\n * `## Jobs To Be Done` section. Each `### ` heading opens a JTBD (id = its\n * first token); each `#### ` heading under it is an AC (id = its first token).\n * HTML-commented and fenced content is skipped, so the template's commented\n * example never counts. A JTBD with no ACs maps to an empty array — it is\n * still a known JTBD id for orphan-vs-stale classification.\n */\ninterface WalkState {\n inSection: boolean;\n currentJtbd: string | undefined;\n}\n\nexport function parseAcIdsByJtbd(specContent: string): Map<string, string[]> {\n const lines = specContent.split('\\n');\n const skip = computeSkipMask(lines);\n const byJtbd = new Map<string, string[]>();\n let state: WalkState = { inSection: false, currentJtbd: undefined };\n\n for (const [index, line] of lines.entries()) {\n if (skip[index]) continue;\n const heading = parseHeading(line);\n if (heading !== undefined) state = advance(state, heading, byJtbd);\n }\n\n return byJtbd;\n}\n\n/** Apply one heading to the JTBD/AC walk, recording ACs into `byJtbd`. */\nfunction advance(\n state: WalkState,\n heading: { level: number; text: string },\n byJtbd: Map<string, string[]>,\n): WalkState {\n if (heading.level <= 2) {\n return { inSection: heading.text.toLowerCase() === JTBD_HEADING, currentJtbd: undefined };\n }\n if (!state.inSection) return state;\n if (heading.level === 3) {\n const currentJtbd = firstToken(heading.text);\n if (!byJtbd.has(currentJtbd)) byJtbd.set(currentJtbd, []);\n return { inSection: true, currentJtbd };\n }\n if (state.currentJtbd !== undefined) {\n appendAc(byJtbd, state.currentJtbd, firstToken(heading.text));\n }\n return state;\n}\n\nconst EMPTY_REPORT: CoverageReport = { uncovered: [], stale: [], orphan: [] };\n\n/**\n * Build the advisory coverage report for one ticket's (spec, test-definitions)\n * pair. Degrades quietly: an empty report when the spec declares no ACs, or\n * when `testDefinitionsContent` is omitted (no test-definitions.md yet — a\n * ticket that hasn't reached define-behavior must not drown in uncovered-AC\n * noise). Free-text scenario titles contribute no coverage and raise no flag.\n */\nexport function buildCoverageReport(\n specContent: string,\n testDefinitionsContent?: string,\n): CoverageReport {\n const scenarioReferences =\n testDefinitionsContent === undefined\n ? undefined\n : parseScenarioTitles(testDefinitionsContent)\n .map(title => parseAcReferenceFromTitle(title))\n .filter((reference): reference is string => reference !== undefined);\n return buildCoverageReportFromReferences(specContent, scenarioReferences);\n}\n\nexport function buildCoverageReportFromFeature(\n specContent: string,\n featureContent?: string,\n): CoverageReport {\n return buildCoverageReportFromReferences(\n specContent,\n featureContent === undefined ? undefined : parseFeatureAcReferences(featureContent),\n );\n}\n\nfunction buildCoverageReportFromReferences(\n specContent: string,\n scenarioReferences?: readonly string[],\n): CoverageReport {\n const byJtbd = parseAcIdsByJtbd(specContent);\n const knownAcIds = new Set<string>();\n for (const acIds of byJtbd.values()) for (const id of acIds) knownAcIds.add(id);\n\n if (knownAcIds.size === 0) return { ...EMPTY_REPORT };\n if (scenarioReferences === undefined) return { ...EMPTY_REPORT };\n\n const knownJtbds = new Set(byJtbd.keys());\n const covered = new Set<string>();\n const stale = new Set<string>();\n const orphan = new Set<string>();\n\n for (const reference of scenarioReferences) {\n if (knownAcIds.has(reference)) {\n covered.add(reference);\n } else if (knownJtbds.has(jtbdPart(reference))) {\n stale.add(reference);\n } else {\n orphan.add(reference);\n }\n }\n\n return {\n uncovered: [...knownAcIds].filter(id => !covered.has(id)),\n stale: [...stale],\n orphan: [...orphan],\n };\n}\n\n/** Append an AC id to a JTBD's list, creating the list on first use. */\nfunction appendAc(byJtbd: Map<string, string[]>, jtbd: string, acId: string): void {\n const acIds = byJtbd.get(jtbd) ?? [];\n acIds.push(acId);\n byJtbd.set(jtbd, acIds);\n}\n\n/** Strip the trailing `.AC<#>` segment to recover the JTBD id of a reference. */\nfunction jtbdPart(reference: string): string {\n return reference.replace(/\\.AC\\d+$/, '');\n}\n\n/**\n * Extract every `### Scenario:` title from a test-definitions.md, skipping\n * commented/fenced regions.\n */\nfunction parseScenarioTitles(content: string): string[] {\n const lines = content.split('\\n');\n const skip = computeSkipMask(lines);\n const titles: string[] = [];\n for (const [index, line] of lines.entries()) {\n if (skip[index]) continue;\n const trimmed = line.trim();\n if (trimmed.startsWith(SCENARIO_PREFIX)) {\n titles.push(trimmed.slice(SCENARIO_PREFIX.length).trim());\n }\n }\n return titles;\n}\n\n/** First whitespace-delimited token of a heading's text (its id). */\nfunction firstToken(text: string): string {\n return text.split(/\\s+/)[0] ?? '';\n}\n"],"mappings":"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","names":[]}
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
addInstalledPack,
|
|
4
4
|
isGitRepo,
|
|
5
5
|
isPackInstalled
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-TB3BU2FA.js";
|
|
7
7
|
import {
|
|
8
8
|
SAFEWORD_PEER_DEPENDENCIES
|
|
9
9
|
} from "./chunk-HSC7TELY.js";
|
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
exists,
|
|
12
12
|
info,
|
|
13
13
|
listItem,
|
|
14
|
-
readJson
|
|
14
|
+
readJson,
|
|
15
|
+
warn
|
|
15
16
|
} from "./chunk-445LAX4Y.js";
|
|
16
17
|
|
|
17
18
|
// src/packs/install.ts
|
|
@@ -27,6 +28,85 @@ function installPack(packId, cwd) {
|
|
|
27
28
|
addInstalledPack(cwd, packId);
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
// src/utils/codex.ts
|
|
32
|
+
import { execFileSync } from "child_process";
|
|
33
|
+
var MIN_CODEX_HOOK_VERSION = "0.133.0";
|
|
34
|
+
var CODEX_CONFIG_PATH = ".codex/config.toml";
|
|
35
|
+
var CODEX_TRUST_NEXT_STEP = "Open Codex and run `/hooks` to review and trust safeword project hooks before relying on Codex gates.";
|
|
36
|
+
function parseVersionPart(part) {
|
|
37
|
+
if (part.length === 0) return void 0;
|
|
38
|
+
const parsed = Number(part);
|
|
39
|
+
if (!Number.isInteger(parsed) || parsed < 0) return void 0;
|
|
40
|
+
return parsed;
|
|
41
|
+
}
|
|
42
|
+
function parseVersionCore(core) {
|
|
43
|
+
const parts = core.split(".");
|
|
44
|
+
if (parts.length !== 3) return void 0;
|
|
45
|
+
const major = parseVersionPart(parts[0] ?? "");
|
|
46
|
+
const minor = parseVersionPart(parts[1] ?? "");
|
|
47
|
+
const patch = parseVersionPart(parts[2] ?? "");
|
|
48
|
+
if (major === void 0) return void 0;
|
|
49
|
+
if (minor === void 0) return void 0;
|
|
50
|
+
if (patch === void 0) return void 0;
|
|
51
|
+
return [major, minor, patch];
|
|
52
|
+
}
|
|
53
|
+
function parseSemver(version) {
|
|
54
|
+
const trimmed = version.trim();
|
|
55
|
+
const normalized = trimmed.startsWith("v") ? trimmed.slice(1) : trimmed;
|
|
56
|
+
const withoutBuildMetadata = normalized.split("+")[0] ?? "";
|
|
57
|
+
const [core = "", prerelease] = withoutBuildMetadata.split("-", 2);
|
|
58
|
+
const parsedCore = parseVersionCore(core);
|
|
59
|
+
if (!parsedCore) return void 0;
|
|
60
|
+
const [major, minor, patch] = parsedCore;
|
|
61
|
+
return {
|
|
62
|
+
major,
|
|
63
|
+
minor,
|
|
64
|
+
patch,
|
|
65
|
+
prerelease
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function compareSemver(a, b) {
|
|
69
|
+
const parsedA = parseSemver(a);
|
|
70
|
+
const parsedB = parseSemver(b);
|
|
71
|
+
if (!parsedA || !parsedB) return 0;
|
|
72
|
+
for (const key of ["major", "minor", "patch"]) {
|
|
73
|
+
if (parsedA[key] < parsedB[key]) return -1;
|
|
74
|
+
if (parsedA[key] > parsedB[key]) return 1;
|
|
75
|
+
}
|
|
76
|
+
if (parsedA.prerelease && !parsedB.prerelease) return -1;
|
|
77
|
+
if (!parsedA.prerelease && parsedB.prerelease) return 1;
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
function parseCodexVersion(output) {
|
|
81
|
+
const tokens = output.replaceAll("\n", " ").replaceAll(" ", " ").split(" ");
|
|
82
|
+
for (const token of tokens) {
|
|
83
|
+
const trimmed = token.trim();
|
|
84
|
+
const normalized = trimmed.startsWith("v") ? trimmed.slice(1) : trimmed;
|
|
85
|
+
if (parseSemver(normalized)) return normalized;
|
|
86
|
+
}
|
|
87
|
+
return void 0;
|
|
88
|
+
}
|
|
89
|
+
function getCodexVersionWarning(output) {
|
|
90
|
+
const version = parseCodexVersion(output);
|
|
91
|
+
if (!version || compareSemver(version, MIN_CODEX_HOOK_VERSION) >= 0) return void 0;
|
|
92
|
+
return `Codex ${version} is below safeword's supported hook baseline (${MIN_CODEX_HOOK_VERSION}). Upgrade Codex before trusting safeword's Codex gates.`;
|
|
93
|
+
}
|
|
94
|
+
function reconciledCodexConfig(result) {
|
|
95
|
+
return result.created.includes(CODEX_CONFIG_PATH) || result.updated.includes(CODEX_CONFIG_PATH);
|
|
96
|
+
}
|
|
97
|
+
function warnIfCodexBelowHookFloor() {
|
|
98
|
+
try {
|
|
99
|
+
const output = execFileSync("codex", ["--version"], {
|
|
100
|
+
encoding: "utf8",
|
|
101
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
102
|
+
timeout: 5e3
|
|
103
|
+
});
|
|
104
|
+
const warning = getCodexVersionWarning(output);
|
|
105
|
+
if (warning) warn(warning);
|
|
106
|
+
} catch {
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
30
110
|
// src/utils/eslint-peer-check.ts
|
|
31
111
|
import nodePath from "path";
|
|
32
112
|
function getSupportedEslintMajors() {
|
|
@@ -304,7 +384,10 @@ function printAutoPatchBailLine() {
|
|
|
304
384
|
|
|
305
385
|
export {
|
|
306
386
|
installPack,
|
|
387
|
+
CODEX_TRUST_NEXT_STEP,
|
|
388
|
+
reconciledCodexConfig,
|
|
389
|
+
warnIfCodexBelowHookFloor,
|
|
307
390
|
getEslintPeerMismatchWarning,
|
|
308
391
|
maybeAutoPatchOrNudge
|
|
309
392
|
};
|
|
310
|
-
//# sourceMappingURL=chunk-
|
|
393
|
+
//# sourceMappingURL=chunk-ILPPVUYD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/packs/install.ts","../src/utils/codex.ts","../src/utils/eslint-peer-check.ts","../src/utils/vendored-ignores-nudge.ts","../src/utils/eslint-auto-patch.ts"],"sourcesContent":["/**\n * Pack Installation\n *\n * Install language packs and update config.\n */\n\nimport { isGitRepo } from '../utils/git.js';\nimport { addInstalledPack, isPackInstalled } from './config.js';\nimport { LANGUAGE_PACKS } from './registry.js';\n\n/**\n * Install a language pack.\n *\n * Runs the pack's setup function and updates config.json.\n * Idempotent - does nothing if pack is already installed.\n *\n * @param packId - Pack ID to install (e.g., 'python')\n * @param cwd - Project root directory\n * @throws Error if pack ID is unknown\n */\nexport function installPack(packId: string, cwd: string): void {\n // Idempotent - skip if already installed\n if (isPackInstalled(cwd, packId)) {\n return;\n }\n\n const pack = LANGUAGE_PACKS[packId];\n if (!pack) {\n throw new Error(`Unknown pack: ${packId}`);\n }\n\n pack.setup(cwd, { isGitRepo: isGitRepo(cwd) });\n addInstalledPack(cwd, packId);\n}\n","import { execFileSync } from 'node:child_process';\n\nimport { warn } from './output.js';\n\nexport const MIN_CODEX_HOOK_VERSION = '0.133.0';\nexport const CODEX_CONFIG_PATH = '.codex/config.toml';\nexport const CODEX_TRUST_NEXT_STEP =\n 'Open Codex and run `/hooks` to review and trust safeword project hooks before relying on Codex gates.';\n\ninterface ParsedSemver {\n major: number;\n minor: number;\n patch: number;\n prerelease?: string;\n}\n\nfunction parseVersionPart(part: string): number | undefined {\n if (part.length === 0) return undefined;\n const parsed = Number(part);\n if (!Number.isInteger(parsed) || parsed < 0) return undefined;\n return parsed;\n}\n\nfunction parseVersionCore(core: string): [number, number, number] | undefined {\n const parts = core.split('.');\n if (parts.length !== 3) return undefined;\n\n const major = parseVersionPart(parts[0] ?? '');\n const minor = parseVersionPart(parts[1] ?? '');\n const patch = parseVersionPart(parts[2] ?? '');\n\n if (major === undefined) return undefined;\n if (minor === undefined) return undefined;\n if (patch === undefined) return undefined;\n\n return [major, minor, patch];\n}\n\nfunction parseSemver(version: string): ParsedSemver | undefined {\n const trimmed = version.trim();\n const normalized = trimmed.startsWith('v') ? trimmed.slice(1) : trimmed;\n const withoutBuildMetadata = normalized.split('+')[0] ?? '';\n const [core = '', prerelease] = withoutBuildMetadata.split('-', 2);\n const parsedCore = parseVersionCore(core);\n if (!parsedCore) return undefined;\n\n const [major, minor, patch] = parsedCore;\n\n return {\n major,\n minor,\n patch,\n prerelease,\n };\n}\n\nfunction compareSemver(a: string, b: string): -1 | 0 | 1 {\n const parsedA = parseSemver(a);\n const parsedB = parseSemver(b);\n if (!parsedA || !parsedB) return 0;\n\n for (const key of ['major', 'minor', 'patch'] as const) {\n if (parsedA[key] < parsedB[key]) return -1;\n if (parsedA[key] > parsedB[key]) return 1;\n }\n\n if (parsedA.prerelease && !parsedB.prerelease) return -1;\n if (!parsedA.prerelease && parsedB.prerelease) return 1;\n return 0;\n}\n\nfunction parseCodexVersion(output: string): string | undefined {\n const tokens = output.replaceAll('\\n', ' ').replaceAll('\\t', ' ').split(' ');\n for (const token of tokens) {\n const trimmed = token.trim();\n const normalized = trimmed.startsWith('v') ? trimmed.slice(1) : trimmed;\n if (parseSemver(normalized)) return normalized;\n }\n return undefined;\n}\n\nfunction getCodexVersionWarning(output: string): string | undefined {\n const version = parseCodexVersion(output);\n if (!version || compareSemver(version, MIN_CODEX_HOOK_VERSION) >= 0) return undefined;\n\n return `Codex ${version} is below safeword's supported hook baseline (${MIN_CODEX_HOOK_VERSION}). Upgrade Codex before trusting safeword's Codex gates.`;\n}\n\nexport function reconciledCodexConfig(result: { created: string[]; updated: string[] }): boolean {\n return result.created.includes(CODEX_CONFIG_PATH) || result.updated.includes(CODEX_CONFIG_PATH);\n}\n\n/** Warn when an installed Codex CLI predates the hook baseline safeword relies on. */\nexport function warnIfCodexBelowHookFloor(): void {\n try {\n const output = execFileSync('codex', ['--version'], {\n encoding: 'utf8',\n stdio: ['ignore', 'pipe', 'pipe'],\n timeout: 5000,\n });\n const warning = getCodexVersionWarning(output);\n if (warning) warn(warning);\n } catch {\n // Codex is optional; only warn when an installed CLI is known to be too old.\n }\n}\n","/**\n * ESLint peer-dep mismatch detection for safeword install/upgrade flows.\n *\n * Reads the project's declared `eslint` version (dependencies or devDependencies)\n * and compares its major against safeword's own peerDependencies.eslint range.\n * Returns a human-readable warning when the customer's major is outside the\n * supported set; returns undefined otherwise (including when nothing is\n * declared or the range can't be parsed — only positively-mismatched majors\n * warn).\n *\n * Background: safeword's ESLint preset bundles plugins whose internals can\n * break on ESLint majors safeword hasn't tested against. The vitest plugin's\n * transitive @typescript-eslint/utils@7.x crashed on ESLint 10 LegacyESLint\n * removal; that motivated this guard so the next collision surfaces at\n * install time rather than the first lint run.\n */\n\nimport nodePath from 'node:path';\n\nimport { SAFEWORD_PEER_DEPENDENCIES } from '../version.js';\nimport { exists, readJson } from './fs.js';\n\ninterface ProjectPackageJson {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n}\n\n/**\n * Pull the supported eslint major versions from safeword's own peerDependencies.\n * `^9.22.0` → [9]; `^9.22.0 || ^10.0.0` → [9, 10].\n *\n * Reads from version.ts rather than calling require() locally — version.ts sits\n * at src/ depth (same as the bundled dist/) so its `require('../package.json')`\n * resolves correctly in both contexts. A direct require() here would resolve\n * `../../package.json` to a nonexistent path post-bundling.\n */\nfunction getSupportedEslintMajors(): number[] {\n const range = SAFEWORD_PEER_DEPENDENCIES.eslint;\n if (!range) return [];\n return extractMajors(range);\n}\n\n/**\n * Extract the leading numeric major from a single version comparator\n * (e.g. `^9.22.0`, `>=9.0.0`, `9.x`, `9.0.0`). Returns undefined for ranges\n * that can't be coerced (workspace:*, file:, git+, *, latest).\n */\nfunction extractMajorFromComparator(comparator: string): number | undefined {\n const cleaned = comparator.trim().replace(/^[\\^~>=<]+/, '');\n const match = /^(\\d+)\\./.exec(cleaned) ?? /^(\\d+)(?:\\.x|\\.\\*|$)/.exec(cleaned);\n const majorString = match?.[1];\n if (majorString === undefined) return undefined;\n const major = Number.parseInt(majorString, 10);\n return Number.isNaN(major) ? undefined : major;\n}\n\n/**\n * Parse a possibly-disjunctive range (`^9.22.0 || ^10.0.0`) into the set of\n * majors it covers. Comparators that don't yield a major are dropped.\n */\nfunction extractMajors(range: string): number[] {\n const majors = new Set<number>();\n for (const part of range.split('||')) {\n const major = extractMajorFromComparator(part);\n if (major !== undefined) majors.add(major);\n }\n return [...majors].toSorted((a, b) => a - b);\n}\n\n/**\n * Returns a warning string when the project's declared eslint major is\n * outside safeword's supported peer range; undefined otherwise.\n */\nexport function getEslintPeerMismatchWarning(cwd: string): string | undefined {\n const packageJsonPath = nodePath.join(cwd, 'package.json');\n if (!exists(packageJsonPath)) return undefined;\n\n const pkg = readJson(packageJsonPath) as ProjectPackageJson | undefined;\n if (!pkg) return undefined;\n\n const declared = pkg.dependencies?.eslint ?? pkg.devDependencies?.eslint;\n if (!declared) return undefined;\n\n const installedMajor = extractMajorFromComparator(declared);\n if (installedMajor === undefined) return undefined;\n\n const supportedMajors = getSupportedEslintMajors();\n if (supportedMajors.length === 0) return undefined;\n if (supportedMajors.includes(installedMajor)) return undefined;\n\n const supportedDisplay = supportedMajors.map(major => `${major}.x`).join(' or ');\n return [\n `Project declares eslint@${declared} (major ${installedMajor}), but safeword`,\n `supports eslint ${supportedDisplay}. Safeword's bundled plugins are tested`,\n `against the supported range; lint may crash on other majors (e.g. plugin`,\n `transitives that reference removed ESLint APIs).`,\n ].join('\\n');\n}\n","import { readFileSync } from 'node:fs';\nimport nodePath from 'node:path';\n\nimport { autoPatchEslintConfig } from './eslint-auto-patch.js';\nimport { info, listItem } from './output.js';\n\nexport interface VendoredIgnoresNudgeOptions {\n cwd: string;\n /** Path (relative to cwd) of the consumer's existing ESLint config, or undefined if none. */\n existingEslintConfig: string | undefined;\n /** True if the project has JavaScript/TypeScript sources safeword can lint. */\n hasJavaScript: boolean;\n}\n\n/**\n * Decide whether to print the install-time nudge telling the user to add\n * `...safeword.configs.vendoredIgnores` to their existing eslint config.\n *\n * Emits iff:\n * 1. an existing eslint config is detected, AND\n * 2. the project has JavaScript, AND\n * 3. the config file's text does NOT already mention `.safeword/` or\n * `vendoredIgnores`\n *\n * The substring check makes the nudge self-quiescing: once the user applies\n * the snippet (manually following the 153 nudge or via 154's auto-patch),\n * repeat `setup`/`upgrade` runs go quiet.\n */\nexport function shouldEmitVendoredIgnoresNudge(options: VendoredIgnoresNudgeOptions): boolean {\n const { cwd, existingEslintConfig, hasJavaScript } = options;\n if (!hasJavaScript) return false;\n if (!existingEslintConfig) return false;\n const fullPath = nodePath.join(cwd, existingEslintConfig);\n let text: string;\n try {\n text = readFileSync(fullPath, 'utf8');\n } catch {\n return true;\n }\n return !text.includes('.safeword/') && !text.includes('vendoredIgnores');\n}\n\n/**\n * Print the nudge using safeword's standard output helpers.\n *\n * Module-internal — callers should use {@link maybePrintVendoredIgnoresNudge}\n * which wraps the decide-then-print sequence.\n */\nfunction printVendoredIgnoresNudge(): void {\n info(\n \"\\nSafeword vendors hook scripts under .safeword/. Add this line to your existing ESLint config so your lint doesn't flag them:\",\n );\n listItem(\"import safeword from 'safeword/eslint';\");\n listItem('// ... your existing configs');\n listItem('...safeword.configs.vendoredIgnores,');\n}\n\nexport interface AutoPatchOrNudgeOptions extends VendoredIgnoresNudgeOptions {\n /** True if the user passed `--no-modify` to setup/upgrade. Defaults to false. */\n noModify?: boolean;\n}\n\n/**\n * Orchestrator for ticket 154: try to auto-patch the consumer's eslint\n * config; on opt-out or bail, fall through to the 153 print-only nudge.\n *\n * Decision tree:\n * - If `shouldEmitVendoredIgnoresNudge` returns false (no existing\n * config / non-JS / already handled) → silent.\n * - Else if `--no-modify` flag OR `SAFEWORD_NO_MODIFY` env var → print\n * the 153 nudge only (no edit attempted).\n * - Else attempt auto-patch:\n * - `patched` → print confirmation with paths\n * - `idempotent-skip` → silent (defensive — the predicate above\n * already caught this case)\n * - `bailed` → print bail line + 153 nudge (caller sees both, the\n * user knows safeword tried and gets the manual snippet)\n */\nexport function maybeAutoPatchOrNudge(options: AutoPatchOrNudgeOptions): void {\n if (!shouldEmitVendoredIgnoresNudge(options)) return;\n if (isOptOut(options.noModify ?? false)) {\n printVendoredIgnoresNudge();\n return;\n }\n if (!options.existingEslintConfig) return;\n const configPath = nodePath.join(options.cwd, options.existingEslintConfig);\n const result = autoPatchEslintConfig({ configPath });\n if (result.kind === 'patched') {\n printAutoPatchConfirmation(result.configPath, result.backupPath);\n return;\n }\n if (result.kind === 'idempotent-skip') return;\n printAutoPatchBailLine();\n printVendoredIgnoresNudge();\n}\n\nfunction isOptOut(noModifyFlag: boolean): boolean {\n if (noModifyFlag) return true;\n const envValue = process.env.SAFEWORD_NO_MODIFY;\n return envValue !== undefined && envValue !== '';\n}\n\nfunction printAutoPatchConfirmation(configPath: string, backupPath: string): void {\n info(`\\nAdded vendoredIgnores to ${configPath}; backup at ${backupPath}`);\n}\n\nfunction printAutoPatchBailLine(): void {\n info(\n \"\\nCouldn't auto-patch your eslint config (unrecognized export shape or syntax check failed). Add it manually:\",\n );\n}\n","/**\n * Auto-patch a downstream project's flat ESLint config to spread\n * `safeword.configs.vendoredIgnores`. Used by `safeword setup` and\n * `safeword upgrade` (ticket 154).\n *\n * Textual insertion, not AST — the heuristic handles the common shapes\n * (bare array literal + `defineConfig(...)` wrapper). Anything else\n * (function-returning-config, single-imported-config, unrecognized\n * wrapper) bails out so the caller can fall back to ticket 153's\n * print-only nudge.\n *\n * Safety:\n * - `.safeword-bak` written before any edit\n * - `node --check` validates JS variants (.mjs/.js/.cjs); failure\n * restores the backup and bails\n * - TS variants (.ts/.mts/.cts) skip syntax check — node can't parse\n * TS, and pulling in a TS-aware checker would balloon dep weight for\n * a one-line insert\n *\n * Idempotency by substring: any config text containing\n * `vendoredIgnores` is treated as already-patched (covers both prior\n * auto-patch and manual application of the 153 print-nudge).\n */\n\nimport { execSync } from 'node:child_process';\nimport { copyFileSync, readFileSync, writeFileSync } from 'node:fs';\nimport nodePath from 'node:path';\n\nconst VENDORED_MARKER = 'vendoredIgnores';\nconst ESM_IMPORT = \"import safeword from 'safeword/eslint';\";\nconst CJS_IMPORT = \"const safeword = require('safeword/eslint');\";\nconst SPREAD = '...safeword.configs.vendoredIgnores,';\nconst BACKUP_SUFFIX = '.safeword-bak';\nconst TS_EXTENSIONS = new Set(['.ts', '.mts', '.cts']);\nconst CJS_EXTENSIONS = new Set(['.cjs']);\n\nexport interface AutoPatchOptions {\n /** Path (absolute or relative to cwd) of the flat ESLint config to patch. */\n configPath: string;\n}\n\nexport type AutoPatchResult =\n | { kind: 'patched'; configPath: string; backupPath: string }\n | { kind: 'idempotent-skip' }\n | { kind: 'bailed'; reason: BailReason };\n\ntype BailReason = 'read-failed' | 'unrecognized-shape' | 'syntax-check-failed' | 'write-failed';\n\n/**\n * Walk a string starting at an opening `[` and return the index of the\n * matching `]`. Returns -1 if no match (unbalanced) or if any string /\n * comment is unterminated. Tracks single/double/backtick strings and\n * `//` + `/* * /` comments so brackets inside literals don't fool the\n * walker.\n */\nfunction findMatchingBracket(source: string, openIndex: number): number {\n if (source[openIndex] !== '[') return -1;\n let depth = 1;\n let i = openIndex + 1;\n while (i < source.length) {\n const after = skipNonContent(source, i);\n if (after === -1) return -1;\n if (after > i) {\n i = after;\n continue;\n }\n const ch = source[i];\n if (ch === '[') depth += 1;\n else if (ch === ']') {\n depth -= 1;\n if (depth === 0) return i;\n }\n i += 1;\n }\n return -1;\n}\n\n/**\n * If `source[i]` starts a comment or string literal, return the index just\n * past its end. Otherwise return `i` unchanged. Returns -1 if a comment or\n * string is unterminated.\n */\nfunction skipNonContent(source: string, i: number): number {\n const ch = source[i];\n const next = source[i + 1];\n if (ch === '/' && next === '/') {\n const nl = source.indexOf('\\n', i);\n return nl === -1 ? -1 : nl + 1;\n }\n if (ch === '/' && next === '*') {\n const close = source.indexOf('*/', i + 2);\n return close === -1 ? -1 : close + 2;\n }\n if (ch === \"'\" || ch === '\"' || ch === '`') {\n return skipStringLiteral(source, i);\n }\n return i;\n}\n\nfunction skipStringLiteral(source: string, startIndex: number): number {\n const quote = source[startIndex];\n let i = startIndex + 1;\n while (i < source.length) {\n const ch = source[i];\n if (ch === '\\\\') {\n i += 2;\n continue;\n }\n if (ch === quote) return i + 1;\n i += 1;\n }\n return -1;\n}\n\n/**\n * Find the position of the closing `]` of the default-export config array.\n *\n * Recognized shapes:\n * - `export default [...]` (ESM, bare array)\n * - `export default defineConfig([...])` (ESM, wrapper)\n * - `module.exports = [...]` (CJS, bare array)\n * - `module.exports = defineConfig([...])` (CJS, wrapper)\n *\n * Returns -1 on any unrecognized shape (function-returning-config,\n * single-imported-config, unknown wrapper).\n */\nfunction findDefaultExportArrayClose(source: string): number {\n const esmResult = findArrayCloseAfter(source, 'export default', false);\n if (esmResult !== -1) return esmResult;\n return findArrayCloseAfter(source, 'module.exports', true);\n}\n\nfunction skipWhitespace(source: string, start: number): number {\n let i = start;\n while (i < source.length && /\\s/.test(source[i] ?? '')) i += 1;\n return i;\n}\n\n/**\n * Walk backward from `start` over whitespace, returning the index of the\n * last non-whitespace char. -1 if the entire prefix is whitespace.\n */\nfunction lastNonWhitespaceIndex(source: string, start: number): number {\n let i = start;\n while (i >= 0 && /\\s/.test(source[i] ?? '')) i -= 1;\n return i;\n}\n\nfunction findArrayCloseAfter(source: string, marker: string, requireEquals: boolean): number {\n const markerIndex = source.indexOf(marker);\n if (markerIndex === -1) return -1;\n let cursor = skipWhitespace(source, markerIndex + marker.length);\n if (requireEquals) {\n if (source[cursor] !== '=') return -1;\n cursor = skipWhitespace(source, cursor + 1);\n }\n if (source[cursor] === '[') return findMatchingBracket(source, cursor);\n if (source.startsWith('defineConfig', cursor)) {\n return findDefineConfigArrayClose(source, cursor);\n }\n return -1;\n}\n\nfunction findDefineConfigArrayClose(source: string, defineStart: number): number {\n const paren = skipWhitespace(source, defineStart + 'defineConfig'.length);\n if (source[paren] !== '(') return -1;\n const inner = skipWhitespace(source, paren + 1);\n if (source[inner] !== '[') return -1;\n return findMatchingBracket(source, inner);\n}\n\n/**\n * Insert the safeword import after the last existing import/require line,\n * or at the very top if there are none. Uses ESM `import` syntax for\n * `.mjs/.js/.ts/.mts/.cts`, CJS `require` syntax for `.cjs`.\n */\nfunction ensureSafewordImport(source: string, eol: string, isCjs: boolean): string {\n const importLine = isCjs ? CJS_IMPORT : ESM_IMPORT;\n if (source.includes(importLine) || source.includes(\"from 'safeword/eslint'\")) return source;\n const importLineRegex = isCjs\n ? /^(?:const|let|var)\\s.+?=\\s*require\\s*\\(.+?\\);[ \\t]*\\r?$/gm\n : /^import\\s.+?;[ \\t]*\\r?$/gm;\n let lastImportEnd = -1;\n for (const match of source.matchAll(importLineRegex)) {\n lastImportEnd = (match.index ?? 0) + match[0].length;\n }\n if (lastImportEnd === -1) {\n return importLine + eol + source;\n }\n return source.slice(0, lastImportEnd) + eol + importLine + source.slice(lastImportEnd);\n}\n\nfunction detectEol(source: string): string {\n return source.includes('\\r\\n') ? '\\r\\n' : '\\n';\n}\n\nfunction isTypeScriptConfig(configPath: string): boolean {\n return TS_EXTENSIONS.has(nodePath.extname(configPath));\n}\n\nfunction isCjsConfig(configPath: string): boolean {\n return CJS_EXTENSIONS.has(nodePath.extname(configPath));\n}\n\n/**\n * Main entry point. See module docstring for behavior contract.\n */\nexport function autoPatchEslintConfig(options: AutoPatchOptions): AutoPatchResult {\n const { configPath } = options;\n\n let source: string;\n try {\n source = readFileSync(configPath, 'utf8');\n } catch {\n return { kind: 'bailed', reason: 'read-failed' };\n }\n\n if (source.includes(VENDORED_MARKER)) {\n return { kind: 'idempotent-skip' };\n }\n\n const closeIndex = findDefaultExportArrayClose(source);\n if (closeIndex === -1) {\n return { kind: 'bailed', reason: 'unrecognized-shape' };\n }\n\n const eol = detectEol(source);\n const sourceWithImport = ensureSafewordImport(source, eol, isCjsConfig(configPath));\n // closeIndex was computed on `source`; if `ensureSafewordImport` prepended\n // content, recompute on the new text. Either way it's idempotent and cheap.\n const finalClose = findDefaultExportArrayClose(sourceWithImport);\n if (finalClose === -1) {\n // Shouldn't happen — we just verified the shape — but bail defensively.\n return { kind: 'bailed', reason: 'unrecognized-shape' };\n }\n\n // Decide whether we need a leading comma. Walk backward from the\n // closing `]`, skipping whitespace; if the last non-whitespace char is\n // `[` (empty array) or `,` (trailing-comma style) no comma is needed.\n // Otherwise the last element lacks a trailing comma and we must add\n // one before our spread.\n const probe = lastNonWhitespaceIndex(sourceWithImport, finalClose - 1);\n const charBefore = sourceWithImport[probe];\n const needsLeadingComma = charBefore !== '[' && charBefore !== ',';\n\n const before = sourceWithImport.slice(0, finalClose);\n const after = sourceWithImport.slice(finalClose);\n const prefix = `${needsLeadingComma ? ',' : ''}${eol} `;\n const patched = `${before}${prefix}${SPREAD}${eol}${after}`;\n\n const writeResult = writeAndValidate(configPath, patched, !isTypeScriptConfig(configPath));\n if (!writeResult.ok) return { kind: 'bailed', reason: writeResult.reason };\n return { kind: 'patched', configPath, backupPath: writeResult.backupPath };\n}\n\nfunction writeAndValidate(\n configPath: string,\n patched: string,\n runSyntaxCheck: boolean,\n):\n | { ok: true; backupPath: string }\n | { ok: false; reason: 'write-failed' | 'syntax-check-failed' } {\n const backupPath = `${configPath}${BACKUP_SUFFIX}`;\n try {\n copyFileSync(configPath, backupPath);\n writeFileSync(configPath, patched, 'utf8');\n } catch {\n return { ok: false, reason: 'write-failed' };\n }\n if (!runSyntaxCheck) return { ok: true, backupPath };\n try {\n execSync(`node --check \"${configPath}\"`, { stdio: 'pipe' });\n return { ok: true, backupPath };\n } catch {\n // Revert so the user's config is byte-identical to its pre-command state.\n try {\n copyFileSync(backupPath, configPath);\n } catch {\n // Best-effort revert; if this fails the backup is still on disk.\n }\n return { ok: false, reason: 'syntax-check-failed' };\n }\n}\n"],"mappings":"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","names":["readFileSync","nodePath","nodePath","nodePath","readFileSync"]}
|