safeword 0.55.0 → 0.57.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.
Files changed (131) hide show
  1. package/dist/architecture-3QIZFW3W.js +758 -0
  2. package/dist/architecture-3QIZFW3W.js.map +1 -0
  3. package/dist/{check-TKA2IIC7.js → check-IV6KC65F.js} +16 -9
  4. package/dist/check-IV6KC65F.js.map +1 -0
  5. package/dist/chunk-3JDN4VVS.js +149 -0
  6. package/dist/chunk-3JDN4VVS.js.map +1 -0
  7. package/dist/chunk-AKPXZ75V.js +23 -0
  8. package/dist/chunk-AKPXZ75V.js.map +1 -0
  9. package/dist/{chunk-SLPSZC2D.js → chunk-D5H7VBXQ.js} +7 -5
  10. package/dist/{chunk-SLPSZC2D.js.map → chunk-D5H7VBXQ.js.map} +1 -1
  11. package/dist/{chunk-GT7KMCFG.js → chunk-GS3TBFXU.js} +2 -2
  12. package/dist/{chunk-JQVVJCLH.js → chunk-HGOG5ZLC.js} +101 -14
  13. package/dist/chunk-HGOG5ZLC.js.map +1 -0
  14. package/dist/{chunk-5ES7OYBI.js → chunk-HTDMZQKA.js} +58 -8
  15. package/dist/chunk-HTDMZQKA.js.map +1 -0
  16. package/dist/{chunk-G3BDQLVU.js → chunk-KC7L2P6V.js} +16 -151
  17. package/dist/chunk-KC7L2P6V.js.map +1 -0
  18. package/dist/{chunk-I2GV5QKO.js → chunk-KIZYVSME.js} +2 -45
  19. package/dist/chunk-KIZYVSME.js.map +1 -0
  20. package/dist/{chunk-VLK2DXJ7.js → chunk-MT4WBU2P.js} +76 -43
  21. package/dist/chunk-MT4WBU2P.js.map +1 -0
  22. package/dist/chunk-NWCBLTIE.js +46 -0
  23. package/dist/chunk-NWCBLTIE.js.map +1 -0
  24. package/dist/{chunk-IE32BCVN.js → chunk-O3QF6QHX.js} +43 -21
  25. package/dist/chunk-O3QF6QHX.js.map +1 -0
  26. package/dist/{chunk-WJOSBJ37.js → chunk-P2IC575P.js} +8 -2
  27. package/dist/chunk-P2IC575P.js.map +1 -0
  28. package/dist/chunk-TTFKJM7Q.js +151 -0
  29. package/dist/chunk-TTFKJM7Q.js.map +1 -0
  30. package/dist/chunk-ZAJS56IE.js +74 -0
  31. package/dist/chunk-ZAJS56IE.js.map +1 -0
  32. package/dist/chunk-ZESHX2BU.js +14 -0
  33. package/dist/chunk-ZESHX2BU.js.map +1 -0
  34. package/dist/cli.js +30 -14
  35. package/dist/cli.js.map +1 -1
  36. package/dist/{codify-64DSX5YK.js → codify-I2FLE35J.js} +4 -3
  37. package/dist/{codify-64DSX5YK.js.map → codify-I2FLE35J.js.map} +1 -1
  38. package/dist/connect-X7S7BNWV.js +24 -0
  39. package/dist/connect-X7S7BNWV.js.map +1 -0
  40. package/dist/{diff-WFF3TQH2.js → diff-55Y2SH4U.js} +9 -7
  41. package/dist/{diff-WFF3TQH2.js.map → diff-55Y2SH4U.js.map} +1 -1
  42. package/dist/index.js +1 -1
  43. package/dist/offer-KQINNE43.js +15 -0
  44. package/dist/offer-KQINNE43.js.map +1 -0
  45. package/dist/presets/typescript/index.d.ts +0 -42
  46. package/dist/presets/typescript/index.js +1 -1
  47. package/dist/prompt-OD3EGKOL.js +7 -0
  48. package/dist/{reset-GY7TFOIB.js → reset-FZRYTUFF.js} +8 -6
  49. package/dist/{reset-GY7TFOIB.js.map → reset-FZRYTUFF.js.map} +1 -1
  50. package/dist/run-FLIIOY4G.js +9 -0
  51. package/dist/run-FLIIOY4G.js.map +1 -0
  52. package/dist/{setup-VVQ3EKND.js → setup-WKFBBSLJ.js} +57 -21
  53. package/dist/setup-WKFBBSLJ.js.map +1 -0
  54. package/dist/{sync-config-IDVS7DTC.js → sync-config-3RYJAKKP.js} +5 -3
  55. package/dist/sync-config-3RYJAKKP.js.map +1 -0
  56. package/dist/{sync-learnings-RY6SVKZ2.js → sync-learnings-QWFNKMK3.js} +4 -3
  57. package/dist/{sync-learnings-RY6SVKZ2.js.map → sync-learnings-QWFNKMK3.js.map} +1 -1
  58. package/dist/{sync-tickets-LMQKAABL.js → sync-tickets-35ZSEKIE.js} +11 -4
  59. package/dist/sync-tickets-35ZSEKIE.js.map +1 -0
  60. package/dist/sync-tracker-YUXD7QKS.js +512 -0
  61. package/dist/sync-tracker-YUXD7QKS.js.map +1 -0
  62. package/dist/{test-plan-PV6C5IUO.js → test-plan-HWRDWG2X.js} +4 -3
  63. package/dist/{test-plan-PV6C5IUO.js.map → test-plan-HWRDWG2X.js.map} +1 -1
  64. package/dist/{ticket-new-BK5VDS4X.js → ticket-new-GXYCW5ML.js} +6 -4
  65. package/dist/{ticket-new-BK5VDS4X.js.map → ticket-new-GXYCW5ML.js.map} +1 -1
  66. package/dist/{upgrade-IQNLPR5G.js → upgrade-MCBH6GTD.js} +39 -33
  67. package/dist/upgrade-MCBH6GTD.js.map +1 -0
  68. package/package.json +5 -6
  69. package/templates/SAFEWORD.md +10 -3
  70. package/templates/commands/audit.md +97 -26
  71. package/templates/commands/explain.md +93 -0
  72. package/templates/commands/refactor.md +1 -1
  73. package/templates/commands/self-review.md +1 -1
  74. package/templates/commands/verify.md +75 -14
  75. package/templates/cursor/rules/safeword-refactoring.mdc +1 -1
  76. package/templates/doc-templates/architecture-template.md +1 -1
  77. package/templates/doc-templates/impl-plan-template.md +10 -5
  78. package/templates/doc-templates/ticket-template.md +11 -0
  79. package/templates/guides/architecture-guide.md +64 -52
  80. package/templates/guides/cold-start-check.md +91 -0
  81. package/templates/hooks/codex/pre-tool-quality-helpers.ts +112 -0
  82. package/templates/hooks/codex/pre-tool-quality.ts +19 -115
  83. package/templates/hooks/cursor/after-file-edit.ts +5 -2
  84. package/templates/hooks/cursor/before-shell-execution.ts +55 -0
  85. package/templates/hooks/cursor/gate-adapter.ts +208 -0
  86. package/templates/hooks/cursor/post-tool-quality.ts +70 -0
  87. package/templates/hooks/cursor/pre-tool-quality.ts +101 -0
  88. package/templates/hooks/cursor/stop.ts +5 -2
  89. package/templates/hooks/lib/active-ticket.ts +166 -0
  90. package/templates/hooks/lib/blocked-on-gate.ts +106 -0
  91. package/templates/hooks/lib/dependency-readiness.ts +37 -5
  92. package/templates/hooks/lib/done-gate.ts +172 -0
  93. package/templates/hooks/lib/quality-state.ts +63 -11
  94. package/templates/hooks/lib/quality.ts +2 -2
  95. package/templates/hooks/lib/run-identity.ts +150 -0
  96. package/templates/hooks/post-tool-bypass-warn.ts +2 -4
  97. package/templates/hooks/post-tool-quality.ts +3 -0
  98. package/templates/hooks/pre-tool-architecture-stage.ts +63 -0
  99. package/templates/hooks/pre-tool-config-guard.ts +1 -1
  100. package/templates/hooks/pre-tool-quality.ts +109 -2
  101. package/templates/hooks/prompt-questions.ts +13 -1
  102. package/templates/hooks/record-skill-invocation.ts +22 -9
  103. package/templates/hooks/session-bun-check.sh +4 -4
  104. package/templates/hooks/session-dependency-readiness.ts +5 -1
  105. package/templates/hooks/session-lint-check.ts +3 -1
  106. package/templates/hooks/stop-quality.ts +21 -3
  107. package/templates/hooks/write-review-stamp.ts +4 -2
  108. package/templates/skills/audit/SKILL.md +97 -24
  109. package/templates/skills/bdd/DISCOVERY.md +42 -13
  110. package/templates/skills/bdd/SCENARIOS.md +1 -1
  111. package/templates/skills/quality-review/SKILL.md +3 -3
  112. package/templates/skills/refactor/SKILL.md +34 -11
  113. package/templates/skills/self-review/SKILL.md +1 -1
  114. package/templates/skills/ticket-system/SKILL.md +7 -1
  115. package/templates/skills/verify/SKILL.md +77 -14
  116. package/templates/spec-template.md +29 -0
  117. package/dist/architecture-CYFAXY2U.js +0 -256
  118. package/dist/architecture-CYFAXY2U.js.map +0 -1
  119. package/dist/check-TKA2IIC7.js.map +0 -1
  120. package/dist/chunk-5ES7OYBI.js.map +0 -1
  121. package/dist/chunk-G3BDQLVU.js.map +0 -1
  122. package/dist/chunk-I2GV5QKO.js.map +0 -1
  123. package/dist/chunk-IE32BCVN.js.map +0 -1
  124. package/dist/chunk-JQVVJCLH.js.map +0 -1
  125. package/dist/chunk-VLK2DXJ7.js.map +0 -1
  126. package/dist/chunk-WJOSBJ37.js.map +0 -1
  127. package/dist/setup-VVQ3EKND.js.map +0 -1
  128. package/dist/sync-tickets-LMQKAABL.js.map +0 -1
  129. package/dist/upgrade-IQNLPR5G.js.map +0 -1
  130. /package/dist/{chunk-GT7KMCFG.js.map → chunk-GS3TBFXU.js.map} +0 -0
  131. /package/dist/{sync-config-IDVS7DTC.js.map → prompt-OD3EGKOL.js.map} +0 -0
@@ -0,0 +1,758 @@
1
+ import {
2
+ GENERATED_ARCHITECTURE_FILENAME,
3
+ isArchitectureDocumentEnforcementEnabled,
4
+ resolveGeneratedArchitecturePath
5
+ } from "./chunk-P2IC575P.js";
6
+ import {
7
+ detectWorkspaces
8
+ } from "./chunk-TTFKJM7Q.js";
9
+ import {
10
+ error,
11
+ success
12
+ } from "./chunk-NWCBLTIE.js";
13
+ import {
14
+ exists,
15
+ isDirectory,
16
+ readFileSafe,
17
+ readJson
18
+ } from "./chunk-KIZYVSME.js";
19
+
20
+ // src/commands/architecture.ts
21
+ import { execFileSync } from "child_process";
22
+ import nodePath5 from "path";
23
+ import process from "process";
24
+
25
+ // src/utils/architecture-document.ts
26
+ import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
27
+ import nodePath4 from "path";
28
+
29
+ // src/utils/architecture-fingerprint.ts
30
+ import { createHash } from "crypto";
31
+ import { readdirSync as readdirSync2, readFileSync } from "fs";
32
+ import nodePath2 from "path";
33
+
34
+ // src/utils/architecture-skeleton.ts
35
+ import { readdirSync } from "fs";
36
+ import nodePath from "path";
37
+ var PURPOSE_PLACEHOLDER = "No description yet \u2014 awaiting prose.";
38
+ var GO_LAYOUT_DIRECTORIES = ["cmd", "internal", "pkg"];
39
+ var RUST_ROOT_FILES = /* @__PURE__ */ new Set(["lib.rs", "main.rs"]);
40
+ function extractSkeleton(projectDirectory) {
41
+ if (exists(nodePath.join(projectDirectory, "Cargo.toml"))) {
42
+ return { nodes: rustModuleNodes(projectDirectory) };
43
+ }
44
+ const sourceNodes = enumerateModuleDirectories(
45
+ nodePath.join(projectDirectory, "src"),
46
+ (name) => `src/${name}`
47
+ );
48
+ if (sourceNodes.length > 0) return { nodes: sourceNodes };
49
+ if (exists(nodePath.join(projectDirectory, "go.mod"))) {
50
+ return { nodes: goLayoutNodes(projectDirectory) };
51
+ }
52
+ return { nodes: sourceNodes };
53
+ }
54
+ function enumerateModuleDirectories(directory, pathFor) {
55
+ let entries;
56
+ try {
57
+ entries = readdirSync(directory, { withFileTypes: true });
58
+ } catch {
59
+ return [];
60
+ }
61
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => ({ name: entry.name, path: pathFor(entry.name), purpose: PURPOSE_PLACEHOLDER })).toSorted((a, b) => a.name.localeCompare(b.name));
62
+ }
63
+ function goLayoutNodes(projectDirectory) {
64
+ return GO_LAYOUT_DIRECTORIES.filter((name) => isDirectory(nodePath.join(projectDirectory, name))).map((name) => ({ name, path: name, purpose: PURPOSE_PLACEHOLDER })).toSorted((a, b) => a.name.localeCompare(b.name));
65
+ }
66
+ function rustModuleNodes(projectDirectory) {
67
+ let entries;
68
+ try {
69
+ entries = readdirSync(nodePath.join(projectDirectory, "src"), { withFileTypes: true });
70
+ } catch {
71
+ return [];
72
+ }
73
+ const byName = /* @__PURE__ */ new Map();
74
+ for (const entry of entries) {
75
+ if (entry.isDirectory()) {
76
+ byName.set(entry.name, {
77
+ name: entry.name,
78
+ path: `src/${entry.name}`,
79
+ purpose: PURPOSE_PLACEHOLDER
80
+ });
81
+ }
82
+ }
83
+ for (const entry of entries) {
84
+ if (entry.isDirectory() || !entry.name.endsWith(".rs") || RUST_ROOT_FILES.has(entry.name)) {
85
+ continue;
86
+ }
87
+ const name = entry.name.slice(0, -".rs".length);
88
+ if (!byName.has(name)) {
89
+ byName.set(name, { name, path: `src/${entry.name}`, purpose: PURPOSE_PLACEHOLDER });
90
+ }
91
+ }
92
+ return byName.values().toArray().toSorted((a, b) => a.name.localeCompare(b.name));
93
+ }
94
+
95
+ // src/utils/cargo-manifest.ts
96
+ var DEPENDENCY_TABLES = ["dependencies", "dev-dependencies", "build-dependencies"];
97
+ function readCargoWorkspaceMembers(content) {
98
+ const body = workspaceMembersArrayBody(content.split(/\r?\n/));
99
+ if (body === void 0) return void 0;
100
+ const globs = body.matchAll(/"([^"]*)"|'([^']*)'/g).map((match) => match[1] ?? match[2] ?? "").filter((glob) => glob.length > 0).toArray();
101
+ return globs.length > 0 ? globs : void 0;
102
+ }
103
+ function workspaceMembersArrayBody(lines) {
104
+ let inWorkspace = false;
105
+ let body;
106
+ for (const rawLine of lines) {
107
+ const line = stripComment(rawLine);
108
+ const trimmed = line.trim();
109
+ if (body === void 0) {
110
+ const header = /^\[\[?([^[\]]+)\]\]?\s*$/.exec(trimmed);
111
+ if (header?.[1] !== void 0) {
112
+ inWorkspace = header[1].trim() === "workspace";
113
+ continue;
114
+ }
115
+ const opening = inWorkspace ? /members\s*=\s*\[(.*)$/.exec(trimmed) : void 0;
116
+ if (opening?.[1] === void 0) continue;
117
+ body = opening[1];
118
+ } else {
119
+ body += `
120
+ ${line}`;
121
+ }
122
+ const close = body.indexOf("]");
123
+ if (close !== -1) return body.slice(0, close);
124
+ }
125
+ return body;
126
+ }
127
+ function readCargoPackageName(content) {
128
+ return scanTables(content).packageName;
129
+ }
130
+ function readCargoDependencyNames(content) {
131
+ return [...scanTables(content).dependencyNames];
132
+ }
133
+ function scanTables(content) {
134
+ const scan = { packageName: void 0, dependencyNames: /* @__PURE__ */ new Set() };
135
+ let currentTable = "";
136
+ for (const rawLine of content.split(/\r?\n/)) {
137
+ const line = stripComment(rawLine).trim();
138
+ if (line === "") continue;
139
+ const header = /^\[\[?([^\]]+)\]\]?$/.exec(line);
140
+ if (header?.[1] !== void 0) {
141
+ currentTable = header[1].trim();
142
+ const subTableDependency = dependencySubTableName(currentTable);
143
+ if (subTableDependency !== void 0) scan.dependencyNames.add(subTableDependency);
144
+ continue;
145
+ }
146
+ collectKey(line, currentTable, scan);
147
+ }
148
+ return scan;
149
+ }
150
+ function collectKey(line, currentTable, scan) {
151
+ const key = leadingKey(line);
152
+ if (key === void 0) return;
153
+ if (currentTable === "package" && key === "name") {
154
+ scan.packageName ??= stringValue(line);
155
+ } else if (DEPENDENCY_TABLES.includes(currentTable)) {
156
+ scan.dependencyNames.add(key);
157
+ }
158
+ }
159
+ function dependencySubTableName(table) {
160
+ for (const dependencyTable of DEPENDENCY_TABLES) {
161
+ if (!table.startsWith(`${dependencyTable}.`)) continue;
162
+ const name = unquote(table.slice(dependencyTable.length + 1).trim());
163
+ return name.length > 0 ? name : void 0;
164
+ }
165
+ return void 0;
166
+ }
167
+ function leadingKey(line) {
168
+ const match = /^("[^"]+"|'[^']+'|[\w-]+)\s*=/.exec(line);
169
+ return match?.[1] === void 0 ? void 0 : unquote(match[1]);
170
+ }
171
+ function stringValue(line) {
172
+ return /=\s*"([^"]*)"/.exec(line)?.[1];
173
+ }
174
+ function stripComment(line) {
175
+ const hash = line.indexOf("#");
176
+ return hash === -1 ? line : line.slice(0, hash);
177
+ }
178
+ function unquote(value) {
179
+ return value.replaceAll(/^["']|["']$/g, "");
180
+ }
181
+
182
+ // src/utils/manifest-block.ts
183
+ function readDelimitedBlock(lines, opener) {
184
+ const entries = [];
185
+ let inBlock = false;
186
+ for (const line of lines) {
187
+ const trimmed = line.trim();
188
+ if (!inBlock) {
189
+ if (opener.test(trimmed)) inBlock = true;
190
+ continue;
191
+ }
192
+ if (trimmed.startsWith(")")) inBlock = false;
193
+ else if (trimmed !== "" && !trimmed.startsWith("//")) entries.push(trimmed);
194
+ }
195
+ return entries;
196
+ }
197
+
198
+ // src/utils/manifest-dependencies.ts
199
+ var DEPENDENCY_SECTIONS = [
200
+ "dependencies",
201
+ "devDependencies",
202
+ "peerDependencies",
203
+ "optionalDependencies"
204
+ ];
205
+ function dependencySectionNames(manifest) {
206
+ const names = /* @__PURE__ */ new Set();
207
+ for (const section of DEPENDENCY_SECTIONS) {
208
+ const entry = manifest[section];
209
+ if (entry !== null && typeof entry === "object") {
210
+ for (const name of Object.keys(entry)) names.add(name);
211
+ }
212
+ }
213
+ return [...names];
214
+ }
215
+
216
+ // src/utils/architecture-fingerprint.ts
217
+ var DEPENDENCY_CRUISER_CONFIG_NAMES = [
218
+ ".dependency-cruiser.cjs",
219
+ ".dependency-cruiser.js",
220
+ ".dependency-cruiser.mjs",
221
+ ".dependency-cruiser.json"
222
+ ];
223
+ var SCHEMA_EXTENSIONS = /* @__PURE__ */ new Set([".sql", ".prisma"]);
224
+ var SHAPE_SCAN_EXCLUDED_DIRECTORIES = /* @__PURE__ */ new Set([
225
+ ".git",
226
+ ".project",
227
+ ".safeword",
228
+ "dist",
229
+ "node_modules"
230
+ ]);
231
+ var byString = (a, b) => a.localeCompare(b);
232
+ function collectShapeInputs(projectDirectory) {
233
+ const moduleNames = extractSkeleton(projectDirectory).nodes.map((node) => node.name).toSorted(byString);
234
+ return {
235
+ moduleNames,
236
+ dependencyNames: readDependencyNames(projectDirectory),
237
+ boundaryConfig: readBoundaryConfig(projectDirectory),
238
+ schemaFiles: collectSchemaFiles(projectDirectory)
239
+ };
240
+ }
241
+ function shapeFingerprint(projectDirectory) {
242
+ const inputs = collectShapeInputs(projectDirectory);
243
+ return createHash("sha256").update(JSON.stringify(inputs)).digest("hex");
244
+ }
245
+ function readDependencyNames(projectDirectory) {
246
+ const names = new Set(readPackageJsonDependencyNames(projectDirectory));
247
+ for (const goModule of readGoModuleRequires(projectDirectory)) names.add(goModule);
248
+ for (const crate of readCargoDependencies(projectDirectory)) names.add(crate);
249
+ return [...names].toSorted(byString);
250
+ }
251
+ function readPackageJsonDependencyNames(projectDirectory) {
252
+ const manifest = readJson2(nodePath2.join(projectDirectory, "package.json"));
253
+ return manifest === void 0 ? [] : dependencySectionNames(manifest);
254
+ }
255
+ function readCargoDependencies(projectDirectory) {
256
+ try {
257
+ return readCargoDependencyNames(
258
+ readFileSync(nodePath2.join(projectDirectory, "Cargo.toml"), "utf8")
259
+ );
260
+ } catch {
261
+ return [];
262
+ }
263
+ }
264
+ function readGoModuleRequires(projectDirectory) {
265
+ let content;
266
+ try {
267
+ content = readFileSync(nodePath2.join(projectDirectory, "go.mod"), "utf8");
268
+ } catch {
269
+ return [];
270
+ }
271
+ const lines = content.split(/\r?\n/);
272
+ const modules = /* @__PURE__ */ new Set();
273
+ collectGoRequireBlock(lines, modules);
274
+ collectGoRequireLines(lines, modules);
275
+ return [...modules].toSorted(byString);
276
+ }
277
+ function collectGoRequireBlock(lines, modules) {
278
+ for (const entry of readDelimitedBlock(lines, /^require\s*\(\s*$/)) {
279
+ const [modulePath] = entry.split(/\s+/);
280
+ if (modulePath !== void 0 && modulePath.length > 0) modules.add(modulePath);
281
+ }
282
+ }
283
+ function collectGoRequireLines(lines, modules) {
284
+ for (const line of lines) {
285
+ const match = /^require\s+(\S+)\s+\S/.exec(line.trim());
286
+ if (match?.[1] !== void 0) modules.add(match[1]);
287
+ }
288
+ }
289
+ function readBoundaryConfig(projectDirectory) {
290
+ for (const name of DEPENDENCY_CRUISER_CONFIG_NAMES) {
291
+ try {
292
+ return readFileSync(nodePath2.join(projectDirectory, name), "utf8");
293
+ } catch {
294
+ }
295
+ }
296
+ return "";
297
+ }
298
+ function collectSchemaFiles(projectDirectory) {
299
+ const schemaFiles = [];
300
+ const pending = [projectDirectory];
301
+ while (pending.length > 0) {
302
+ const directory = pending.pop();
303
+ if (directory !== void 0) {
304
+ scanDirectoryForSchema(projectDirectory, directory, schemaFiles, pending);
305
+ }
306
+ }
307
+ return schemaFiles.toSorted(byString);
308
+ }
309
+ function scanDirectoryForSchema(projectDirectory, directory, schemaFiles, pending) {
310
+ let entries;
311
+ try {
312
+ entries = readdirSync2(directory, { withFileTypes: true });
313
+ } catch {
314
+ return;
315
+ }
316
+ for (const entry of entries) {
317
+ if (entry.isDirectory()) {
318
+ if (!SHAPE_SCAN_EXCLUDED_DIRECTORIES.has(entry.name)) {
319
+ pending.push(nodePath2.join(directory, entry.name));
320
+ }
321
+ } else if (SCHEMA_EXTENSIONS.has(nodePath2.extname(entry.name))) {
322
+ const absolutePath = nodePath2.join(directory, entry.name);
323
+ schemaFiles.push(nodePath2.relative(projectDirectory, absolutePath).replaceAll("\\", "/"));
324
+ }
325
+ }
326
+ }
327
+ function readJson2(filePath) {
328
+ try {
329
+ return JSON.parse(readFileSync(filePath, "utf8"));
330
+ } catch {
331
+ return void 0;
332
+ }
333
+ }
334
+
335
+ // src/utils/architecture-monorepo.ts
336
+ import { createHash as createHash2 } from "crypto";
337
+ import { globSync } from "fs";
338
+ import nodePath3 from "path";
339
+ var PURPOSE_PLACEHOLDER2 = "No description yet \u2014 awaiting prose.";
340
+ var DEPENDENCY_CRUISER_CONFIG_NAMES2 = [
341
+ ".dependency-cruiser.cjs",
342
+ ".dependency-cruiser.js",
343
+ ".dependency-cruiser.mjs",
344
+ ".dependency-cruiser.json"
345
+ ];
346
+ var byString2 = (a, b) => a.localeCompare(b);
347
+ function discoverLeafDirectories(projectDirectory) {
348
+ const patterns = detectWorkspaces(projectDirectory) ?? detectPnpmWorkspaces(projectDirectory) ?? detectGoWork(projectDirectory) ?? detectCargoWorkspace(projectDirectory);
349
+ if (patterns === void 0) return [];
350
+ const matches = /* @__PURE__ */ new Set();
351
+ for (const pattern of patterns) {
352
+ const globMatches = globSync(pattern, { cwd: projectDirectory });
353
+ for (const match of globMatches) {
354
+ const absolute = nodePath3.join(projectDirectory, match);
355
+ if (isDirectory(absolute) && hasRecognizedManifest(absolute)) {
356
+ matches.add(absolute);
357
+ }
358
+ }
359
+ }
360
+ return [...matches].toSorted(byString2);
361
+ }
362
+ function hasRecognizedManifest(directory) {
363
+ return readJson(nodePath3.join(directory, "package.json")) !== void 0 || readFileSafe(nodePath3.join(directory, "go.mod")) !== void 0 || readFileSafe(nodePath3.join(directory, "Cargo.toml")) !== void 0;
364
+ }
365
+ function extractMonorepoModel(projectDirectory) {
366
+ const packages = discoverLeafDirectories(projectDirectory).map((dir) => ({
367
+ name: packageName(dir),
368
+ dir,
369
+ purpose: PURPOSE_PLACEHOLDER2,
370
+ introspected: extractSkeleton(dir).nodes.length > 0
371
+ })).toSorted((a, b) => byString2(a.name, b.name));
372
+ const names = new Set(packages.map((node) => node.name));
373
+ const edges = [];
374
+ for (const node of packages) {
375
+ for (const dependencyName of manifestDependencyNames(node.dir)) {
376
+ if (names.has(dependencyName) && dependencyName !== node.name) {
377
+ edges.push({ from: node.name, to: dependencyName });
378
+ }
379
+ }
380
+ }
381
+ edges.sort((a, b) => byString2(a.from, b.from) || byString2(a.to, b.to));
382
+ return { packages, edges };
383
+ }
384
+ function monorepoFingerprint(projectDirectory) {
385
+ const model = extractMonorepoModel(projectDirectory);
386
+ const inputs = {
387
+ // Introspection status is part of the root shape: a package gaining or losing
388
+ // a `src/` tree flips its root-index line (marker ↔ described), so the root
389
+ // index must re-render — otherwise the "not introspected" marker goes stale.
390
+ packages: model.packages.map((node) => `${node.name}:${node.introspected}`),
391
+ edges: model.edges.map((edge) => `${edge.from}->${edge.to}`),
392
+ boundaryConfig: readBoundaryConfig2(projectDirectory)
393
+ };
394
+ return createHash2("sha256").update(JSON.stringify(inputs)).digest("hex");
395
+ }
396
+ function readManifest(packageDirectory) {
397
+ const manifest = readJson(nodePath3.join(packageDirectory, "package.json"));
398
+ return manifest !== null && typeof manifest === "object" ? manifest : void 0;
399
+ }
400
+ function packageName(packageDirectory) {
401
+ const name = readManifest(packageDirectory)?.name;
402
+ if (typeof name === "string" && name.length > 0) return name;
403
+ return readGoModuleName(packageDirectory) ?? readCargoCrateName(packageDirectory) ?? nodePath3.basename(packageDirectory);
404
+ }
405
+ function readGoModuleName(packageDirectory) {
406
+ const content = readFileSafe(nodePath3.join(packageDirectory, "go.mod"));
407
+ if (content === void 0) return void 0;
408
+ return /^module\s+(\S+)/m.exec(content)?.[1];
409
+ }
410
+ function readCargoCrateName(packageDirectory) {
411
+ const content = readFileSafe(nodePath3.join(packageDirectory, "Cargo.toml"));
412
+ return content === void 0 ? void 0 : readCargoPackageName(content);
413
+ }
414
+ function manifestDependencyNames(packageDirectory) {
415
+ const manifest = readManifest(packageDirectory);
416
+ return manifest === void 0 ? [] : dependencySectionNames(manifest);
417
+ }
418
+ function readBoundaryConfig2(projectDirectory) {
419
+ for (const name of DEPENDENCY_CRUISER_CONFIG_NAMES2) {
420
+ const content = readFileSafe(nodePath3.join(projectDirectory, name));
421
+ if (content !== void 0) return content;
422
+ }
423
+ return "";
424
+ }
425
+ function detectPnpmWorkspaces(projectDirectory) {
426
+ const content = readFileSafe(nodePath3.join(projectDirectory, "pnpm-workspace.yaml"));
427
+ if (content === void 0) return void 0;
428
+ const lines = content.split(/\r?\n/);
429
+ const start = lines.findIndex((line) => /^packages:\s*$/.test(line));
430
+ if (start === -1) return void 0;
431
+ const globs = collectPnpmGlobs(lines.slice(start + 1));
432
+ return globs.length > 0 ? globs : void 0;
433
+ }
434
+ function collectPnpmGlobs(blockLines) {
435
+ const globs = [];
436
+ for (const line of blockLines) {
437
+ const trimmed = line.trim();
438
+ if (trimmed === "" || trimmed.startsWith("#")) continue;
439
+ const item = /^\s+-\s+(\S.*)$/.exec(line);
440
+ if (item?.[1] === void 0) break;
441
+ const glob = item[1].trim().replaceAll(/^["']|["']$/g, "");
442
+ if (glob.length > 0 && !glob.startsWith("!")) globs.push(glob);
443
+ }
444
+ return globs;
445
+ }
446
+ function detectGoWork(projectDirectory) {
447
+ const content = readFileSafe(nodePath3.join(projectDirectory, "go.work"));
448
+ if (content === void 0) return void 0;
449
+ const lines = content.split(/\r?\n/);
450
+ const directories = [];
451
+ collectGoWorkBlock(lines, directories);
452
+ collectGoWorkSingleLines(lines, directories);
453
+ return directories.length > 0 ? directories : void 0;
454
+ }
455
+ function collectGoWorkBlock(lines, directories) {
456
+ for (const entry of readDelimitedBlock(lines, /^use\s*\(\s*$/)) {
457
+ const target = normalizeUseTarget(entry);
458
+ if (target !== void 0) directories.push(target);
459
+ }
460
+ }
461
+ function collectGoWorkSingleLines(lines, directories) {
462
+ for (const line of lines) {
463
+ const match = /^use\s+(\S.*)$/.exec(line.trim());
464
+ if (match?.[1] === void 0 || match[1].startsWith("(")) continue;
465
+ const target = normalizeUseTarget(match[1]);
466
+ if (target !== void 0) directories.push(target);
467
+ }
468
+ }
469
+ function normalizeUseTarget(raw) {
470
+ const commentAt = raw.indexOf("//");
471
+ const noComment = (commentAt === -1 ? raw : raw.slice(0, commentAt)).trim();
472
+ const unquoted = noComment.replaceAll(/^["']|["']$/g, "");
473
+ if (unquoted === "" || /\s/.test(unquoted)) return void 0;
474
+ return unquoted.replace(/^\.\//, "");
475
+ }
476
+ function detectCargoWorkspace(projectDirectory) {
477
+ const content = readFileSafe(nodePath3.join(projectDirectory, "Cargo.toml"));
478
+ return content === void 0 ? void 0 : readCargoWorkspaceMembers(content);
479
+ }
480
+
481
+ // src/utils/architecture-reconcile.ts
482
+ function reconcileSections(input) {
483
+ const verdicts = input.nodeNames.map((node) => ({
484
+ node,
485
+ status: liveNodeStatus(input.priorStamps[node], input.fingerprint)
486
+ }));
487
+ const present = new Set(input.nodeNames);
488
+ for (const node of Object.keys(input.priorStamps)) {
489
+ if (!present.has(node)) verdicts.push({ node, status: "orphaned" });
490
+ }
491
+ return verdicts;
492
+ }
493
+ function liveNodeStatus(stamp, fingerprint) {
494
+ if (stamp === void 0) return "placeholder";
495
+ if (stamp !== fingerprint) return "stale";
496
+ return "current";
497
+ }
498
+
499
+ // src/utils/architecture-document.ts
500
+ var WOULD_CHANGE_ACTIONS = /* @__PURE__ */ new Set(["created", "healed", "regenerated"]);
501
+ function isWouldChangeAction(action) {
502
+ return WOULD_CHANGE_ACTIONS.has(action);
503
+ }
504
+ var FINGERPRINT_KEY = "fingerprint";
505
+ var GENERATOR_KEY = "generator";
506
+ var GENERATOR_VALUE = "safeword-architecture";
507
+ function frontmatterBody(content) {
508
+ return /^---\r?\n([\s\S]*?)\r?\n---/.exec(content)?.[1];
509
+ }
510
+ function isSafewordOwned(content) {
511
+ return frontmatterBody(content)?.split(/\r?\n/).includes(`${GENERATOR_KEY}: ${GENERATOR_VALUE}`) ?? false;
512
+ }
513
+ function readDocumentFingerprint(content) {
514
+ const line = frontmatterBody(content)?.split(/\r?\n/).find((candidate) => candidate.startsWith(`${FINGERPRINT_KEY}:`));
515
+ if (line === void 0) return void 0;
516
+ const value = line.slice(FINGERPRINT_KEY.length + 1).trim();
517
+ return value.length > 0 ? value : void 0;
518
+ }
519
+ var RECONCILED_PREFIX = "<!-- reconciled:";
520
+ function healTarget(target) {
521
+ const existing = readExisting(target.path);
522
+ const action = decideAction(existing, target.fingerprint, target.hasContent);
523
+ if (isWouldChangeAction(action)) {
524
+ mkdirSync(nodePath4.dirname(target.path), { recursive: true });
525
+ const priorStamps = existing === void 0 ? /* @__PURE__ */ new Map() : parseSectionStamps(existing);
526
+ const priorProse = existing === void 0 ? /* @__PURE__ */ new Map() : parseSectionProse(existing);
527
+ writeFileSync(target.path, target.render(priorStamps, priorProse));
528
+ }
529
+ return { action, path: target.path };
530
+ }
531
+ function planTarget(target) {
532
+ return decideAction(readExisting(target.path), target.fingerprint, target.hasContent);
533
+ }
534
+ function singleRepoTarget(projectDirectory) {
535
+ const fingerprint = shapeFingerprint(projectDirectory);
536
+ const nodes = extractSkeleton(projectDirectory).nodes;
537
+ return {
538
+ path: resolveGeneratedArchitecturePath(projectDirectory),
539
+ fingerprint,
540
+ hasContent: nodes.length > 0,
541
+ render: (priorStamps, priorProse) => renderDocument(nodes, fingerprint, priorStamps, priorProse)
542
+ };
543
+ }
544
+ function leafTarget(packageDirectory) {
545
+ const fingerprint = shapeFingerprint(packageDirectory);
546
+ const nodes = extractSkeleton(packageDirectory).nodes;
547
+ return {
548
+ path: nodePath4.join(packageDirectory, GENERATED_ARCHITECTURE_FILENAME),
549
+ fingerprint,
550
+ hasContent: nodes.length > 0,
551
+ render: (priorStamps, priorProse) => renderDocument(nodes, fingerprint, priorStamps, priorProse)
552
+ };
553
+ }
554
+ function rootIndexTarget(projectDirectory) {
555
+ const fingerprint = monorepoFingerprint(projectDirectory);
556
+ const model = extractMonorepoModel(projectDirectory);
557
+ return {
558
+ path: resolveGeneratedArchitecturePath(projectDirectory),
559
+ fingerprint,
560
+ hasContent: model.packages.length > 0,
561
+ render: () => renderRootIndex(model, fingerprint)
562
+ };
563
+ }
564
+ function projectTargets(projectDirectory) {
565
+ const leaves = discoverLeafDirectories(projectDirectory);
566
+ if (leaves.length === 0) return [singleRepoTarget(projectDirectory)];
567
+ return [rootIndexTarget(projectDirectory), ...leaves.map((leaf) => leafTarget(leaf))];
568
+ }
569
+ function selfHealProject(projectDirectory) {
570
+ return projectTargets(projectDirectory).map((target) => healTarget(target));
571
+ }
572
+ function planSelfHealProject(projectDirectory) {
573
+ return projectTargets(projectDirectory).map((target) => planTarget(target));
574
+ }
575
+ function readExisting(path) {
576
+ try {
577
+ return readFileSync2(path, "utf8");
578
+ } catch {
579
+ return void 0;
580
+ }
581
+ }
582
+ function decideAction(existing, fingerprint, hasModules) {
583
+ if (existing === void 0) return hasModules ? "created" : "noop";
584
+ if (!isSafewordOwned(existing)) return "skipped";
585
+ const recorded = readDocumentFingerprint(existing);
586
+ if (recorded === void 0) return "regenerated";
587
+ if (recorded !== fingerprint) return "healed";
588
+ return "unchanged";
589
+ }
590
+ function parseSectionStamps(content) {
591
+ const stamps = /* @__PURE__ */ new Map();
592
+ const pattern = /^### (.+)\n+<!-- reconciled: (\S+) -->/gm;
593
+ for (const match of content.matchAll(pattern)) {
594
+ const name = match[1];
595
+ const stamp = match[2];
596
+ if (name !== void 0 && stamp !== void 0) stamps.set(name.trim(), stamp);
597
+ }
598
+ return stamps;
599
+ }
600
+ function parseSectionProse(content) {
601
+ const prose = /* @__PURE__ */ new Map();
602
+ let name;
603
+ let inProse = false;
604
+ let buffer = [];
605
+ const flush = (next) => {
606
+ if (name !== void 0) {
607
+ const text = buffer.join("\n").trim();
608
+ if (text.length > 0) prose.set(name, text);
609
+ }
610
+ name = next;
611
+ buffer = [];
612
+ inProse = false;
613
+ };
614
+ for (const line of content.split(/\r?\n/)) {
615
+ const heading = /^### (.+)$/.exec(line)?.[1];
616
+ if (heading !== void 0) {
617
+ flush(heading.trim());
618
+ } else if (line.startsWith("## ")) {
619
+ flush(void 0);
620
+ } else if (name !== void 0) {
621
+ inProse = accumulateProseLine(line, inProse, buffer);
622
+ }
623
+ }
624
+ flush(void 0);
625
+ return prose;
626
+ }
627
+ function accumulateProseLine(line, inProse, buffer) {
628
+ if (line.startsWith(RECONCILED_PREFIX) || line.startsWith("> \u26A0")) return inProse;
629
+ if (!inProse) return /^`[^`]*`\s*$/.test(line);
630
+ buffer.push(line);
631
+ return true;
632
+ }
633
+ function renderDocument(nodes, fingerprint, priorStamps, priorProse) {
634
+ const verdicts = reconcileSections({
635
+ priorStamps: Object.fromEntries(priorStamps),
636
+ nodeNames: nodes.map((node) => node.name),
637
+ fingerprint
638
+ });
639
+ const nodeByName = new Map(nodes.map((node) => [node.name, node]));
640
+ const sections = verdicts.map((verdict) => {
641
+ const node = nodeByName.get(verdict.node);
642
+ const stamp = priorStamps.get(verdict.node) ?? fingerprint;
643
+ const prose = priorProse.get(verdict.node) ?? node?.purpose ?? "";
644
+ return node === void 0 ? renderOrphanSection(verdict.node) : renderSection(node, stamp, verdict.status, prose);
645
+ }).join("\n");
646
+ return `---
647
+ ${GENERATOR_KEY}: ${GENERATOR_VALUE}
648
+ ${FINGERPRINT_KEY}: ${fingerprint}
649
+ ---
650
+
651
+ # Architecture
652
+
653
+ ## Modules
654
+
655
+ ${sections}`;
656
+ }
657
+ function renderSection(node, stamp, status, prose) {
658
+ const marker = status === "stale" ? "\n> \u26A0 stale: structure changed since this section was reconciled.\n" : "";
659
+ return `### ${node.name}
660
+
661
+ ${RECONCILED_PREFIX} ${stamp} -->
662
+
663
+ \`${node.path}\`
664
+
665
+ ${prose}
666
+ ${marker}`;
667
+ }
668
+ function renderOrphanSection(name) {
669
+ return `### ${name}
670
+
671
+ > \u26A0 orphaned: this section describes a module that no longer exists.
672
+ `;
673
+ }
674
+ function renderRootIndex(model, fingerprint) {
675
+ const sections = model.packages.map((node) => renderPackageSection(node, fingerprint)).join("\n");
676
+ const edgeLines = model.edges.map((edge) => `- \`${edge.from}\` \u2192 \`${edge.to}\``).join("\n");
677
+ const dependencies = model.edges.length === 0 ? "_No inter-package dependencies._\n" : `${edgeLines}
678
+ `;
679
+ return `---
680
+ ${GENERATOR_KEY}: ${GENERATOR_VALUE}
681
+ ${FINGERPRINT_KEY}: ${fingerprint}
682
+ ---
683
+
684
+ # Architecture
685
+
686
+ ## Packages
687
+
688
+ ${sections}
689
+ ## Dependencies
690
+
691
+ ${dependencies}`;
692
+ }
693
+ function renderPackageSection(node, stamp) {
694
+ const body = node.introspected ? node.purpose : "> \u26A0 not introspected \u2014 no recognized source layout";
695
+ return `### ${node.name}
696
+
697
+ ${RECONCILED_PREFIX} ${stamp} -->
698
+
699
+ ${body}
700
+ `;
701
+ }
702
+
703
+ // src/commands/architecture.ts
704
+ function architecture(cwd = process.cwd(), options = {}) {
705
+ if (options.check) {
706
+ return architectureCheck(cwd);
707
+ }
708
+ if (options.stage) {
709
+ return architectureStage(cwd);
710
+ }
711
+ const results = selfHealProject(cwd);
712
+ for (const result of results) {
713
+ success(`Architecture state document ${result.action}: ${result.path}`);
714
+ }
715
+ return Promise.resolve();
716
+ }
717
+ function architectureStage(cwd) {
718
+ if (!isArchitectureDocumentEnforcementEnabled(cwd)) {
719
+ success("Architecture doc enforcement is opted out (architectureDocEnforcement: false).");
720
+ return Promise.resolve();
721
+ }
722
+ const changed = selfHealProject(cwd).filter((result) => isWouldChangeAction(result.action));
723
+ if (changed.length === 0) {
724
+ success("Architecture docs need no change.");
725
+ return Promise.resolve();
726
+ }
727
+ for (const result of changed) {
728
+ stageDocument(cwd, result);
729
+ success(`Architecture doc ${result.action} and staged: ${result.path}`);
730
+ }
731
+ return Promise.resolve();
732
+ }
733
+ function stageDocument(cwd, result) {
734
+ try {
735
+ const relativePath = nodePath5.relative(cwd, result.path);
736
+ execFileSync("git", ["add", "--", relativePath], { cwd, stdio: "ignore" });
737
+ } catch {
738
+ }
739
+ }
740
+ function architectureCheck(cwd) {
741
+ if (!isArchitectureDocumentEnforcementEnabled(cwd)) {
742
+ success("Architecture doc enforcement is opted out (architectureDocEnforcement: false).");
743
+ return Promise.resolve();
744
+ }
745
+ const stale = planSelfHealProject(cwd).filter((action) => isWouldChangeAction(action));
746
+ if (stale.length > 0) {
747
+ error(
748
+ `Architecture docs are stale (${stale.join(", ")}). Run \`safeword architecture\` to regenerate, then commit the result.`
749
+ );
750
+ process.exit(1);
751
+ }
752
+ success("Architecture docs are current.");
753
+ return Promise.resolve();
754
+ }
755
+ export {
756
+ architecture
757
+ };
758
+ //# sourceMappingURL=architecture-3QIZFW3W.js.map