safeword 0.57.0 → 0.59.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 (141) hide show
  1. package/dist/{architecture-3QIZFW3W.js → architecture-32CAOL4P.js} +386 -165
  2. package/dist/architecture-32CAOL4P.js.map +1 -0
  3. package/dist/{check-IV6KC65F.js → check-QZPVKHZF.js} +38 -30
  4. package/dist/check-QZPVKHZF.js.map +1 -0
  5. package/dist/{chunk-KIZYVSME.js → chunk-2KELQTLE.js} +32 -4
  6. package/dist/chunk-2KELQTLE.js.map +1 -0
  7. package/dist/{chunk-HTDMZQKA.js → chunk-D4UGNHLW.js} +2 -2
  8. package/dist/{chunk-P2IC575P.js → chunk-HI7ANNQI.js} +2 -2
  9. package/dist/chunk-HI7ANNQI.js.map +1 -0
  10. package/dist/{chunk-KC7L2P6V.js → chunk-HKX3Z32Y.js} +151 -14
  11. package/dist/chunk-HKX3Z32Y.js.map +1 -0
  12. package/dist/{chunk-YXNI7W5D.js → chunk-HWOJR33A.js} +124 -3
  13. package/dist/chunk-HWOJR33A.js.map +1 -0
  14. package/dist/{chunk-HGOG5ZLC.js → chunk-JSTWB4AX.js} +416 -97
  15. package/dist/chunk-JSTWB4AX.js.map +1 -0
  16. package/dist/chunk-LRYWFRPD.js +46 -0
  17. package/dist/chunk-LRYWFRPD.js.map +1 -0
  18. package/dist/{chunk-O3QF6QHX.js → chunk-N7PFYE7Z.js} +48 -24
  19. package/dist/chunk-N7PFYE7Z.js.map +1 -0
  20. package/dist/{chunk-D5H7VBXQ.js → chunk-WUJKWUMR.js} +167 -13
  21. package/dist/chunk-WUJKWUMR.js.map +1 -0
  22. package/dist/chunk-XDT3W5MH.js +213 -0
  23. package/dist/chunk-XDT3W5MH.js.map +1 -0
  24. package/dist/cli.js +48 -17
  25. package/dist/cli.js.map +1 -1
  26. package/dist/{codify-I2FLE35J.js → codify-S4DJVZ4R.js} +4 -4
  27. package/dist/{diff-55Y2SH4U.js → diff-TFNDFYCX.js} +38 -7
  28. package/dist/diff-TFNDFYCX.js.map +1 -0
  29. package/dist/{reset-FZRYTUFF.js → reset-W5GEXNGF.js} +4 -4
  30. package/dist/self-report-CQLAESVT.js +39 -0
  31. package/dist/self-report-CQLAESVT.js.map +1 -0
  32. package/dist/{setup-WKFBBSLJ.js → setup-4KLD7PGB.js} +13 -13
  33. package/dist/setup-4KLD7PGB.js.map +1 -0
  34. package/dist/{sync-config-3RYJAKKP.js → sync-config-IUOYVDVQ.js} +3 -4
  35. package/dist/{sync-learnings-QWFNKMK3.js → sync-learnings-XTHHJDPR.js} +3 -3
  36. package/dist/{sync-tickets-35ZSEKIE.js → sync-tickets-IQ4AACIH.js} +4 -4
  37. package/dist/{sync-tracker-YUXD7QKS.js → sync-tracker-2TGUIMAB.js} +4 -4
  38. package/dist/{test-plan-HWRDWG2X.js → test-plan-6X56C5JZ.js} +48 -10
  39. package/dist/test-plan-6X56C5JZ.js.map +1 -0
  40. package/dist/{ticket-new-GXYCW5ML.js → ticket-new-UIOKBXK2.js} +8 -4
  41. package/dist/{ticket-new-GXYCW5ML.js.map → ticket-new-UIOKBXK2.js.map} +1 -1
  42. package/dist/{upgrade-MCBH6GTD.js → upgrade-ZWRVFHXV.js} +22 -21
  43. package/dist/upgrade-ZWRVFHXV.js.map +1 -0
  44. package/package.json +13 -11
  45. package/templates/SAFEWORD.md +4 -2
  46. package/templates/codex/config.toml +12 -3
  47. package/templates/commands/audit.md +1 -1
  48. package/templates/commands/verify.md +57 -7
  49. package/templates/doc-templates/feature-spec-template.md +20 -0
  50. package/templates/doc-templates/impl-plan-template.md +10 -8
  51. package/templates/guides/llm-evals-guide.md +485 -0
  52. package/templates/guides/self-report-filing.md +69 -0
  53. package/templates/guides/testing-guide.md +145 -20
  54. package/templates/guides/verification-lanes-guide.md +574 -0
  55. package/templates/hooks/codex/post-tool-skill-nudge.ts +62 -0
  56. package/templates/hooks/codex/pre-tool-quality.ts +37 -0
  57. package/templates/hooks/cursor/after-file-edit.ts +3 -0
  58. package/templates/hooks/cursor/before-shell-execution.ts +43 -2
  59. package/templates/hooks/cursor/gate-adapter.ts +274 -24
  60. package/templates/hooks/cursor/post-tool-quality.ts +2 -16
  61. package/templates/hooks/cursor/post-tool-skill-nudge.ts +65 -0
  62. package/templates/hooks/cursor/pre-tool-quality.ts +9 -3
  63. package/templates/hooks/cursor/stop.ts +24 -1
  64. package/templates/hooks/lib/active-ticket.ts +25 -0
  65. package/templates/hooks/lib/architecture-document-nudge.ts +129 -0
  66. package/templates/hooks/lib/architecture-staged-scope.ts +130 -0
  67. package/templates/hooks/lib/auto-upgrade-lock.ts +89 -0
  68. package/templates/hooks/lib/auto-upgrade.ts +417 -0
  69. package/templates/hooks/lib/branch-staleness.ts +49 -0
  70. package/templates/hooks/lib/checkbox-transitions.ts +2 -2
  71. package/templates/hooks/lib/cursor-run-identity.ts +281 -0
  72. package/templates/hooks/lib/dependency-readiness.ts +83 -0
  73. package/templates/hooks/lib/done-gate.ts +9 -6
  74. package/templates/hooks/lib/ledger-git.ts +92 -0
  75. package/templates/hooks/lib/ledger-validation.ts +31 -24
  76. package/templates/hooks/lib/namespace-root.ts +1 -1
  77. package/templates/hooks/lib/quality-state.ts +17 -7
  78. package/templates/hooks/lib/review-trigger.ts +6 -31
  79. package/templates/hooks/lib/run-identity.ts +2 -7
  80. package/templates/hooks/lib/safeword-context.ts +83 -0
  81. package/templates/hooks/lib/self-report.ts +502 -0
  82. package/templates/hooks/lib/skill-nudge.ts +264 -0
  83. package/templates/hooks/post-tool-dependency-readiness.ts +84 -0
  84. package/templates/hooks/post-tool-lint.ts +3 -0
  85. package/templates/hooks/post-tool-quality.ts +9 -18
  86. package/templates/hooks/post-tool-skill-nudge.ts +172 -0
  87. package/templates/hooks/pre-tool-architecture-stage.ts +9 -0
  88. package/templates/hooks/pre-tool-quality.ts +25 -4
  89. package/templates/hooks/pre-tool-stale-main.ts +83 -0
  90. package/templates/hooks/prompt-questions.ts +2 -1
  91. package/templates/hooks/record-skill-invocation.ts +41 -8
  92. package/templates/hooks/session-auto-upgrade.ts +13 -300
  93. package/templates/hooks/session-codex-start.ts +39 -0
  94. package/templates/hooks/session-cursor-auto-upgrade.ts +35 -0
  95. package/templates/hooks/session-dependency-readiness.ts +36 -0
  96. package/templates/hooks/session-safeword-context.ts +19 -81
  97. package/templates/hooks/stop-quality.ts +29 -33
  98. package/templates/hooks/stop-self-report.ts +54 -0
  99. package/templates/skills/audit/SKILL.md +15 -4
  100. package/templates/skills/bdd/DISCOVERY.md +24 -8
  101. package/templates/skills/bdd/DONE.md +2 -0
  102. package/templates/skills/bdd/SCENARIOS.md +8 -2
  103. package/templates/skills/bdd/SKILL.md +1 -1
  104. package/templates/skills/bdd/SPLITTING.md +2 -0
  105. package/templates/skills/bdd/TDD.md +10 -3
  106. package/templates/skills/bdd/VERIFY.md +6 -1
  107. package/templates/skills/figure-it-out/SKILL.md +4 -0
  108. package/templates/skills/quality-review/SKILL.md +11 -2
  109. package/templates/skills/refactor/SKILL.md +21 -9
  110. package/templates/skills/review-spec/SKILL.md +4 -2
  111. package/templates/skills/self-review/SKILL.md +4 -0
  112. package/templates/skills/tdd-review/SKILL.md +9 -3
  113. package/templates/skills/testing/SKILL.md +33 -0
  114. package/templates/skills/ticket-system/SKILL.md +14 -2
  115. package/templates/skills/verify/SKILL.md +57 -7
  116. package/templates/spec-template.md +16 -0
  117. package/templates/surfaces-template.md +45 -0
  118. package/dist/architecture-3QIZFW3W.js.map +0 -1
  119. package/dist/check-IV6KC65F.js.map +0 -1
  120. package/dist/chunk-D5H7VBXQ.js.map +0 -1
  121. package/dist/chunk-FJYRWU2V.js +0 -21
  122. package/dist/chunk-FJYRWU2V.js.map +0 -1
  123. package/dist/chunk-HGOG5ZLC.js.map +0 -1
  124. package/dist/chunk-KC7L2P6V.js.map +0 -1
  125. package/dist/chunk-KIZYVSME.js.map +0 -1
  126. package/dist/chunk-O3QF6QHX.js.map +0 -1
  127. package/dist/chunk-P2IC575P.js.map +0 -1
  128. package/dist/chunk-TTFKJM7Q.js +0 -151
  129. package/dist/chunk-TTFKJM7Q.js.map +0 -1
  130. package/dist/chunk-YXNI7W5D.js.map +0 -1
  131. package/dist/diff-55Y2SH4U.js.map +0 -1
  132. package/dist/setup-WKFBBSLJ.js.map +0 -1
  133. package/dist/test-plan-HWRDWG2X.js.map +0 -1
  134. package/dist/upgrade-MCBH6GTD.js.map +0 -1
  135. /package/dist/{chunk-HTDMZQKA.js.map → chunk-D4UGNHLW.js.map} +0 -0
  136. /package/dist/{codify-I2FLE35J.js.map → codify-S4DJVZ4R.js.map} +0 -0
  137. /package/dist/{reset-FZRYTUFF.js.map → reset-W5GEXNGF.js.map} +0 -0
  138. /package/dist/{sync-config-3RYJAKKP.js.map → sync-config-IUOYVDVQ.js.map} +0 -0
  139. /package/dist/{sync-learnings-QWFNKMK3.js.map → sync-learnings-XTHHJDPR.js.map} +0 -0
  140. /package/dist/{sync-tickets-35ZSEKIE.js.map → sync-tickets-IQ4AACIH.js.map} +0 -0
  141. /package/dist/{sync-tracker-YUXD7QKS.js.map → sync-tracker-2TGUIMAB.js.map} +0 -0
@@ -2,34 +2,32 @@ import {
2
2
  GENERATED_ARCHITECTURE_FILENAME,
3
3
  isArchitectureDocumentEnforcementEnabled,
4
4
  resolveGeneratedArchitecturePath
5
- } from "./chunk-P2IC575P.js";
6
- import {
7
- detectWorkspaces
8
- } from "./chunk-TTFKJM7Q.js";
5
+ } from "./chunk-HI7ANNQI.js";
9
6
  import {
10
7
  error,
11
- success
8
+ success,
9
+ warn
12
10
  } from "./chunk-NWCBLTIE.js";
13
11
  import {
14
12
  exists,
15
13
  isDirectory,
16
14
  readFileSafe,
17
15
  readJson
18
- } from "./chunk-KIZYVSME.js";
16
+ } from "./chunk-2KELQTLE.js";
19
17
 
20
18
  // src/commands/architecture.ts
21
19
  import { execFileSync } from "child_process";
22
- import nodePath5 from "path";
20
+ import nodePath6 from "path";
23
21
  import process from "process";
24
22
 
25
23
  // src/utils/architecture-document.ts
26
24
  import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
27
- import nodePath4 from "path";
25
+ import nodePath5 from "path";
28
26
 
29
27
  // src/utils/architecture-fingerprint.ts
30
28
  import { createHash } from "crypto";
31
29
  import { readdirSync as readdirSync2, readFileSync } from "fs";
32
- import nodePath2 from "path";
30
+ import nodePath3 from "path";
33
31
 
34
32
  // src/utils/architecture-skeleton.ts
35
33
  import { readdirSync } from "fs";
@@ -37,7 +35,19 @@ import nodePath from "path";
37
35
  var PURPOSE_PLACEHOLDER = "No description yet \u2014 awaiting prose.";
38
36
  var GO_LAYOUT_DIRECTORIES = ["cmd", "internal", "pkg"];
39
37
  var RUST_ROOT_FILES = /* @__PURE__ */ new Set(["lib.rs", "main.rs"]);
38
+ var PYTHON_EXCLUDED_FILES = /* @__PURE__ */ new Set([
39
+ "setup.py",
40
+ "conftest.py",
41
+ "noxfile.py",
42
+ "__init__.py",
43
+ "__main__.py"
44
+ ]);
45
+ var byNodeName = (a, b) => a.name.localeCompare(b.name);
40
46
  function extractSkeleton(projectDirectory) {
47
+ if (exists(nodePath.join(projectDirectory, "pyproject.toml"))) {
48
+ const pythonNodes = pythonModuleNodes(projectDirectory);
49
+ if (pythonNodes.length > 0) return { nodes: pythonNodes };
50
+ }
41
51
  if (exists(nodePath.join(projectDirectory, "Cargo.toml"))) {
42
52
  return { nodes: rustModuleNodes(projectDirectory) };
43
53
  }
@@ -58,10 +68,10 @@ function enumerateModuleDirectories(directory, pathFor) {
58
68
  } catch {
59
69
  return [];
60
70
  }
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));
71
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => ({ name: entry.name, path: pathFor(entry.name), purpose: PURPOSE_PLACEHOLDER })).toSorted(byNodeName);
62
72
  }
63
73
  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));
74
+ return GO_LAYOUT_DIRECTORIES.filter((name) => isDirectory(nodePath.join(projectDirectory, name))).map((name) => ({ name, path: name, purpose: PURPOSE_PLACEHOLDER })).toSorted(byNodeName);
65
75
  }
66
76
  function rustModuleNodes(projectDirectory) {
67
77
  let entries;
@@ -89,72 +99,194 @@ function rustModuleNodes(projectDirectory) {
89
99
  byName.set(name, { name, path: `src/${entry.name}`, purpose: PURPOSE_PLACEHOLDER });
90
100
  }
91
101
  }
92
- return byName.values().toArray().toSorted((a, b) => a.name.localeCompare(b.name));
102
+ return byName.values().toArray().toSorted(byNodeName);
103
+ }
104
+ function pythonModuleNodes(projectDirectory) {
105
+ const sourceDirectory = nodePath.join(projectDirectory, "src");
106
+ if (isDirectory(sourceDirectory)) {
107
+ return pythonModulesFrom(
108
+ sourceDirectory,
109
+ (name) => `src/${name}`,
110
+ () => true
111
+ );
112
+ }
113
+ return pythonModulesFrom(
114
+ projectDirectory,
115
+ (name) => name,
116
+ (directory) => exists(nodePath.join(directory, "__init__.py"))
117
+ );
118
+ }
119
+ function pythonModulesFrom(directory, pathFor, keepPackageDirectory) {
120
+ let entries;
121
+ try {
122
+ entries = readdirSync(directory, { withFileTypes: true });
123
+ } catch {
124
+ return [];
125
+ }
126
+ const byName = /* @__PURE__ */ new Map();
127
+ for (const entry of entries) {
128
+ if (entry.isDirectory() && keepPackageDirectory(nodePath.join(directory, entry.name))) {
129
+ byName.set(entry.name, {
130
+ name: entry.name,
131
+ path: pathFor(entry.name),
132
+ purpose: PURPOSE_PLACEHOLDER
133
+ });
134
+ }
135
+ }
136
+ for (const entry of entries) {
137
+ if (entry.isDirectory() || !entry.name.endsWith(".py") || PYTHON_EXCLUDED_FILES.has(entry.name)) {
138
+ continue;
139
+ }
140
+ const name = entry.name.slice(0, -".py".length);
141
+ if (!byName.has(name)) {
142
+ byName.set(name, { name, path: pathFor(entry.name), purpose: PURPOSE_PLACEHOLDER });
143
+ }
144
+ }
145
+ return byName.values().toArray().toSorted(byNodeName);
146
+ }
147
+
148
+ // src/utils/boundary-config.ts
149
+ import nodePath2 from "path";
150
+ var DEPENDENCY_CRUISER_CONFIG_NAMES = [
151
+ ".dependency-cruiser.cjs",
152
+ ".dependency-cruiser.js",
153
+ ".dependency-cruiser.mjs",
154
+ ".dependency-cruiser.json"
155
+ ];
156
+ function readBoundaryConfig(projectDirectory) {
157
+ for (const name of DEPENDENCY_CRUISER_CONFIG_NAMES) {
158
+ const content = readFileSafe(nodePath2.join(projectDirectory, name));
159
+ if (content !== void 0) return content;
160
+ }
161
+ return "";
162
+ }
163
+
164
+ // src/utils/toml.ts
165
+ function readTomlTableArray(content, table, key) {
166
+ const found = tableArrayBody(content.split(/\r?\n/), table, key);
167
+ if (found === void 0) return void 0;
168
+ const values = found.body.matchAll(/"([^"]*)"|'([^']*)'/g).map((match) => match[1] ?? match[2] ?? "").filter((value) => value.length > 0).toArray();
169
+ return values.length > 0 ? values : void 0;
170
+ }
171
+ function isTomlTableEmptyArray(content, table, key) {
172
+ const found = tableArrayBody(content.split(/\r?\n/), table, key);
173
+ return found !== void 0 && found.closed && found.body.replaceAll(/[\s,]/g, "") === "";
174
+ }
175
+ function hasTomlTable(content, table) {
176
+ return content.split(/\r?\n/).some((line) => tableHeader(stripTomlComment(line).trim()) === table);
177
+ }
178
+ function* linesInTable(content, table) {
179
+ let inTable = false;
180
+ for (const rawLine of content.split(/\r?\n/)) {
181
+ const line = stripTomlComment(rawLine).trim();
182
+ if (line === "") continue;
183
+ const header = tableHeader(line);
184
+ if (header !== void 0) {
185
+ inTable = header === table;
186
+ continue;
187
+ }
188
+ if (inTable) yield line;
189
+ }
190
+ }
191
+ function hasTomlTableKey(content, table, key) {
192
+ for (const line of linesInTable(content, table)) {
193
+ if (tableValue(line, key) !== void 0) return true;
194
+ }
195
+ return false;
196
+ }
197
+ function readTomlTableString(content, table, key) {
198
+ for (const line of linesInTable(content, table)) {
199
+ const value = tableValue(line, key);
200
+ if (value?.startsWith('"')) {
201
+ const end = value.indexOf('"', 1);
202
+ if (end !== -1) return value.slice(1, end);
203
+ }
204
+ }
205
+ return void 0;
206
+ }
207
+ function tableArrayBody(lines, table, key) {
208
+ const opened = findTableArrayOpen(lines, table, key);
209
+ if (opened === void 0) return void 0;
210
+ let body = opened.initial;
211
+ for (let index = opened.line; index < lines.length; index += 1) {
212
+ if (index > opened.line) body += `
213
+ ${stripTomlComment(lines[index] ?? "")}`;
214
+ const close = indexOfOutsideQuotes(body, "]");
215
+ if (close !== -1) return { body: body.slice(0, close), closed: true };
216
+ }
217
+ return { body, closed: false };
218
+ }
219
+ function findTableArrayOpen(lines, table, key) {
220
+ let inTable = false;
221
+ for (const [index, rawLine] of lines.entries()) {
222
+ const trimmed = stripTomlComment(rawLine).trim();
223
+ const header = tableHeader(trimmed);
224
+ if (header !== void 0) {
225
+ inTable = header === table;
226
+ continue;
227
+ }
228
+ if (!inTable) continue;
229
+ const value = tableValue(trimmed, key);
230
+ if (value?.startsWith("[") === true) return { line: index, initial: value.slice(1) };
231
+ }
232
+ return void 0;
233
+ }
234
+ function indexOfOutsideQuotes(text, target) {
235
+ let quote = "";
236
+ let index = 0;
237
+ while (index < text.length) {
238
+ const character = text[index];
239
+ if (quote === "") {
240
+ if (character === '"' || character === "'") quote = character;
241
+ else if (character === target) return index;
242
+ } else if (character === quote) {
243
+ quote = "";
244
+ }
245
+ index += 1;
246
+ }
247
+ return -1;
248
+ }
249
+ function tableValue(line, key) {
250
+ const equals = line.indexOf("=");
251
+ if (equals === -1 || line.slice(0, equals).trim() !== key) return void 0;
252
+ return line.slice(equals + 1).trim();
253
+ }
254
+ function tableHeader(line) {
255
+ const match = /^\[\[?([^[\]]+)\]\]?\s*$/.exec(line);
256
+ return match?.[1] === void 0 ? void 0 : match[1].trim();
257
+ }
258
+ function stripTomlComment(line) {
259
+ const hash = indexOfOutsideQuotes(line, "#");
260
+ return hash === -1 ? line : line.slice(0, hash);
93
261
  }
94
262
 
95
263
  // src/utils/cargo-manifest.ts
96
264
  var DEPENDENCY_TABLES = ["dependencies", "dev-dependencies", "build-dependencies"];
97
265
  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;
266
+ return readTomlTableArray(content, "workspace", "members");
126
267
  }
127
268
  function readCargoPackageName(content) {
128
- return scanTables(content).packageName;
269
+ return readTomlTableString(content, "package", "name");
129
270
  }
130
271
  function readCargoDependencyNames(content) {
131
- return [...scanTables(content).dependencyNames];
132
- }
133
- function scanTables(content) {
134
- const scan = { packageName: void 0, dependencyNames: /* @__PURE__ */ new Set() };
272
+ const names = /* @__PURE__ */ new Set();
135
273
  let currentTable = "";
136
274
  for (const rawLine of content.split(/\r?\n/)) {
137
- const line = stripComment(rawLine).trim();
275
+ const line = stripTomlComment(rawLine).trim();
138
276
  if (line === "") continue;
139
277
  const header = /^\[\[?([^\]]+)\]\]?$/.exec(line);
140
278
  if (header?.[1] !== void 0) {
141
279
  currentTable = header[1].trim();
142
280
  const subTableDependency = dependencySubTableName(currentTable);
143
- if (subTableDependency !== void 0) scan.dependencyNames.add(subTableDependency);
281
+ if (subTableDependency !== void 0) names.add(subTableDependency);
144
282
  continue;
145
283
  }
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);
284
+ if (DEPENDENCY_TABLES.includes(currentTable)) {
285
+ const key = leadingKey(line);
286
+ if (key !== void 0) names.add(key);
287
+ }
157
288
  }
289
+ return [...names];
158
290
  }
159
291
  function dependencySubTableName(table) {
160
292
  for (const dependencyTable of DEPENDENCY_TABLES) {
@@ -168,13 +300,6 @@ function leadingKey(line) {
168
300
  const match = /^("[^"]+"|'[^']+'|[\w-]+)\s*=/.exec(line);
169
301
  return match?.[1] === void 0 ? void 0 : unquote(match[1]);
170
302
  }
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
303
  function unquote(value) {
179
304
  return value.replaceAll(/^["']|["']$/g, "");
180
305
  }
@@ -213,13 +338,22 @@ function dependencySectionNames(manifest) {
213
338
  return [...names];
214
339
  }
215
340
 
341
+ // src/utils/pyproject-manifest.ts
342
+ function readPyprojectName(content) {
343
+ return readTomlTableString(content, "project", "name");
344
+ }
345
+ function readUvWorkspaceMembers(content) {
346
+ return readTomlTableArray(content, "tool.uv.workspace", "members");
347
+ }
348
+ function readPyprojectDependencies(content) {
349
+ const specifiers = readTomlTableArray(content, "project", "dependencies") ?? [];
350
+ return specifiers.map((specifier) => distributionName(specifier)).filter((name) => name.length > 0);
351
+ }
352
+ function distributionName(specifier) {
353
+ return /^[\w.-]+/.exec(specifier.trim())?.[0] ?? "";
354
+ }
355
+
216
356
  // 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
357
  var SCHEMA_EXTENSIONS = /* @__PURE__ */ new Set([".sql", ".prisma"]);
224
358
  var SHAPE_SCAN_EXCLUDED_DIRECTORIES = /* @__PURE__ */ new Set([
225
359
  ".git",
@@ -246,16 +380,27 @@ function readDependencyNames(projectDirectory) {
246
380
  const names = new Set(readPackageJsonDependencyNames(projectDirectory));
247
381
  for (const goModule of readGoModuleRequires(projectDirectory)) names.add(goModule);
248
382
  for (const crate of readCargoDependencies(projectDirectory)) names.add(crate);
383
+ for (const distribution of readPyprojectDependencyNames(projectDirectory))
384
+ names.add(distribution);
249
385
  return [...names].toSorted(byString);
250
386
  }
251
387
  function readPackageJsonDependencyNames(projectDirectory) {
252
- const manifest = readJson2(nodePath2.join(projectDirectory, "package.json"));
388
+ const manifest = readJson(nodePath3.join(projectDirectory, "package.json"));
253
389
  return manifest === void 0 ? [] : dependencySectionNames(manifest);
254
390
  }
255
391
  function readCargoDependencies(projectDirectory) {
256
392
  try {
257
393
  return readCargoDependencyNames(
258
- readFileSync(nodePath2.join(projectDirectory, "Cargo.toml"), "utf8")
394
+ readFileSync(nodePath3.join(projectDirectory, "Cargo.toml"), "utf8")
395
+ );
396
+ } catch {
397
+ return [];
398
+ }
399
+ }
400
+ function readPyprojectDependencyNames(projectDirectory) {
401
+ try {
402
+ return readPyprojectDependencies(
403
+ readFileSync(nodePath3.join(projectDirectory, "pyproject.toml"), "utf8")
259
404
  );
260
405
  } catch {
261
406
  return [];
@@ -264,7 +409,7 @@ function readCargoDependencies(projectDirectory) {
264
409
  function readGoModuleRequires(projectDirectory) {
265
410
  let content;
266
411
  try {
267
- content = readFileSync(nodePath2.join(projectDirectory, "go.mod"), "utf8");
412
+ content = readFileSync(nodePath3.join(projectDirectory, "go.mod"), "utf8");
268
413
  } catch {
269
414
  return [];
270
415
  }
@@ -286,15 +431,6 @@ function collectGoRequireLines(lines, modules) {
286
431
  if (match?.[1] !== void 0) modules.add(match[1]);
287
432
  }
288
433
  }
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
434
  function collectSchemaFiles(projectDirectory) {
299
435
  const schemaFiles = [];
300
436
  const pending = [projectDirectory];
@@ -316,42 +452,56 @@ function scanDirectoryForSchema(projectDirectory, directory, schemaFiles, pendin
316
452
  for (const entry of entries) {
317
453
  if (entry.isDirectory()) {
318
454
  if (!SHAPE_SCAN_EXCLUDED_DIRECTORIES.has(entry.name)) {
319
- pending.push(nodePath2.join(directory, entry.name));
455
+ pending.push(nodePath3.join(directory, entry.name));
320
456
  }
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("\\", "/"));
457
+ } else if (SCHEMA_EXTENSIONS.has(nodePath3.extname(entry.name))) {
458
+ const absolutePath = nodePath3.join(directory, entry.name);
459
+ schemaFiles.push(nodePath3.relative(projectDirectory, absolutePath).replaceAll("\\", "/"));
324
460
  }
325
461
  }
326
462
  }
327
- function readJson2(filePath) {
328
- try {
329
- return JSON.parse(readFileSync(filePath, "utf8"));
330
- } catch {
331
- return void 0;
332
- }
333
- }
334
463
 
335
464
  // src/utils/architecture-monorepo.ts
336
465
  import { createHash as createHash2 } from "crypto";
337
466
  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
- ];
467
+ import nodePath4 from "path";
346
468
  var byString2 = (a, b) => a.localeCompare(b);
469
+ var ABSENT = { status: "absent" };
470
+ var parsed = (patterns) => ({ status: "parsed", patterns });
471
+ var unreadable = (manager, config) => ({
472
+ status: "unreadable",
473
+ manager,
474
+ config
475
+ });
476
+ function discoverWorkspaces(projectDirectory) {
477
+ const detections = [
478
+ detectJsWorkspaces(projectDirectory),
479
+ detectGoWork(projectDirectory),
480
+ detectCargoWorkspace(projectDirectory),
481
+ detectUvWorkspace(projectDirectory)
482
+ ];
483
+ return {
484
+ patterns: detections.flatMap(
485
+ (detection) => detection.status === "parsed" ? detection.patterns : []
486
+ ),
487
+ unreadable: detections.flatMap(
488
+ (detection) => detection.status === "unreadable" ? [{ manager: detection.manager, config: detection.config }] : []
489
+ )
490
+ };
491
+ }
347
492
  function discoverLeafDirectories(projectDirectory) {
348
- const patterns = detectWorkspaces(projectDirectory) ?? detectPnpmWorkspaces(projectDirectory) ?? detectGoWork(projectDirectory) ?? detectCargoWorkspace(projectDirectory);
349
- if (patterns === void 0) return [];
493
+ return resolveLeafDirectories(projectDirectory, discoverWorkspaces(projectDirectory).patterns);
494
+ }
495
+ function discoverUnreadableWorkspaces(projectDirectory) {
496
+ return discoverWorkspaces(projectDirectory).unreadable;
497
+ }
498
+ function resolveLeafDirectories(projectDirectory, patterns) {
499
+ if (patterns.length === 0) return [];
350
500
  const matches = /* @__PURE__ */ new Set();
351
501
  for (const pattern of patterns) {
352
502
  const globMatches = globSync(pattern, { cwd: projectDirectory });
353
503
  for (const match of globMatches) {
354
- const absolute = nodePath3.join(projectDirectory, match);
504
+ const absolute = nodePath4.join(projectDirectory, match);
355
505
  if (isDirectory(absolute) && hasRecognizedManifest(absolute)) {
356
506
  matches.add(absolute);
357
507
  }
@@ -359,14 +509,35 @@ function discoverLeafDirectories(projectDirectory) {
359
509
  }
360
510
  return [...matches].toSorted(byString2);
361
511
  }
512
+ function detectJsWorkspaces(projectDirectory) {
513
+ const packageJson = detectPackageJsonWorkspaces(projectDirectory);
514
+ return packageJson.status === "absent" ? detectPnpmWorkspaces(projectDirectory) : packageJson;
515
+ }
516
+ function detectPackageJsonWorkspaces(projectDirectory) {
517
+ const workspaces = readManifest(projectDirectory)?.workspaces;
518
+ if (workspaces === void 0) return ABSENT;
519
+ const list = workspaceList(workspaces);
520
+ if (list === void 0) return unreadable("package.json workspaces", "package.json");
521
+ const patterns = list.filter((entry) => typeof entry === "string");
522
+ return patterns.length > 0 ? parsed(patterns) : ABSENT;
523
+ }
524
+ function workspaceList(workspaces) {
525
+ if (Array.isArray(workspaces)) return workspaces;
526
+ if (isObjectRecord(workspaces) && Array.isArray(workspaces.packages)) return workspaces.packages;
527
+ return void 0;
528
+ }
529
+ function isObjectRecord(value) {
530
+ return typeof value === "object" && value !== null;
531
+ }
362
532
  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;
533
+ return readJson(nodePath4.join(directory, "package.json")) !== void 0 || readFileSafe(nodePath4.join(directory, "go.mod")) !== void 0 || readFileSafe(nodePath4.join(directory, "Cargo.toml")) !== void 0 || readFileSafe(nodePath4.join(directory, "pyproject.toml")) !== void 0;
364
534
  }
365
535
  function extractMonorepoModel(projectDirectory) {
366
- const packages = discoverLeafDirectories(projectDirectory).map((dir) => ({
536
+ const { patterns, unreadable: unreadableWorkspaces } = discoverWorkspaces(projectDirectory);
537
+ const packages = resolveLeafDirectories(projectDirectory, patterns).map((dir) => ({
367
538
  name: packageName(dir),
368
539
  dir,
369
- purpose: PURPOSE_PLACEHOLDER2,
540
+ purpose: PURPOSE_PLACEHOLDER,
370
541
  introspected: extractSkeleton(dir).nodes.length > 0
371
542
  })).toSorted((a, b) => byString2(a.name, b.name));
372
543
  const names = new Set(packages.map((node) => node.name));
@@ -379,7 +550,7 @@ function extractMonorepoModel(projectDirectory) {
379
550
  }
380
551
  }
381
552
  edges.sort((a, b) => byString2(a.from, b.from) || byString2(a.to, b.to));
382
- return { packages, edges };
553
+ return { packages, edges, unreadableWorkspaces };
383
554
  }
384
555
  function monorepoFingerprint(projectDirectory) {
385
556
  const model = extractMonorepoModel(projectDirectory);
@@ -389,47 +560,68 @@ function monorepoFingerprint(projectDirectory) {
389
560
  // index must re-render — otherwise the "not introspected" marker goes stale.
390
561
  packages: model.packages.map((node) => `${node.name}:${node.introspected}`),
391
562
  edges: model.edges.map((edge) => `${edge.from}->${edge.to}`),
392
- boundaryConfig: readBoundaryConfig2(projectDirectory)
563
+ boundaryConfig: readBoundaryConfig(projectDirectory),
564
+ // The unreadable-workspace advisory is root shape too: it must re-render when an
565
+ // unparseable config appears or is fixed. Contributed ONLY when non-empty, so a
566
+ // repo with no unreadable config keeps its existing fingerprint — no churn (UWP4XK).
567
+ ...model.unreadableWorkspaces.length > 0 && {
568
+ unreadable: model.unreadableWorkspaces.map((entry) => `${entry.manager}:${entry.config}`)
569
+ }
393
570
  };
394
571
  return createHash2("sha256").update(JSON.stringify(inputs)).digest("hex");
395
572
  }
396
573
  function readManifest(packageDirectory) {
397
- const manifest = readJson(nodePath3.join(packageDirectory, "package.json"));
574
+ const manifest = readJson(nodePath4.join(packageDirectory, "package.json"));
398
575
  return manifest !== null && typeof manifest === "object" ? manifest : void 0;
399
576
  }
400
577
  function packageName(packageDirectory) {
401
578
  const name = readManifest(packageDirectory)?.name;
402
579
  if (typeof name === "string" && name.length > 0) return name;
403
- return readGoModuleName(packageDirectory) ?? readCargoCrateName(packageDirectory) ?? nodePath3.basename(packageDirectory);
580
+ return readGoModuleName(packageDirectory) ?? readCargoCrateName(packageDirectory) ?? readPyprojectCrateName(packageDirectory) ?? nodePath4.basename(packageDirectory);
404
581
  }
405
582
  function readGoModuleName(packageDirectory) {
406
- const content = readFileSafe(nodePath3.join(packageDirectory, "go.mod"));
583
+ const content = readFileSafe(nodePath4.join(packageDirectory, "go.mod"));
407
584
  if (content === void 0) return void 0;
408
585
  return /^module\s+(\S+)/m.exec(content)?.[1];
409
586
  }
410
587
  function readCargoCrateName(packageDirectory) {
411
- const content = readFileSafe(nodePath3.join(packageDirectory, "Cargo.toml"));
588
+ const content = readFileSafe(nodePath4.join(packageDirectory, "Cargo.toml"));
412
589
  return content === void 0 ? void 0 : readCargoPackageName(content);
413
590
  }
591
+ function readPyprojectCrateName(packageDirectory) {
592
+ const content = readFileSafe(nodePath4.join(packageDirectory, "pyproject.toml"));
593
+ return content === void 0 ? void 0 : readPyprojectName(content);
594
+ }
595
+ function resolveTomlWorkspaceMembers(content, readMembers, table, label, configFile) {
596
+ const members = readMembers(content);
597
+ if (members !== void 0) return parsed(members);
598
+ if (isTomlTableEmptyArray(content, table, "members")) return ABSENT;
599
+ return unreadable(label, configFile);
600
+ }
601
+ function detectUvWorkspace(projectDirectory) {
602
+ const content = readFileSafe(nodePath4.join(projectDirectory, "pyproject.toml"));
603
+ if (content === void 0) return ABSENT;
604
+ if (!hasTomlTable(content, "tool.uv.workspace")) return ABSENT;
605
+ return resolveTomlWorkspaceMembers(
606
+ content,
607
+ readUvWorkspaceMembers,
608
+ "tool.uv.workspace",
609
+ "uv workspace",
610
+ "pyproject.toml"
611
+ );
612
+ }
414
613
  function manifestDependencyNames(packageDirectory) {
415
614
  const manifest = readManifest(packageDirectory);
416
615
  return manifest === void 0 ? [] : dependencySectionNames(manifest);
417
616
  }
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
617
  function detectPnpmWorkspaces(projectDirectory) {
426
- const content = readFileSafe(nodePath3.join(projectDirectory, "pnpm-workspace.yaml"));
427
- if (content === void 0) return void 0;
618
+ const content = readFileSafe(nodePath4.join(projectDirectory, "pnpm-workspace.yaml"));
619
+ if (content === void 0) return ABSENT;
428
620
  const lines = content.split(/\r?\n/);
621
+ if (lines.every((line) => !line.startsWith("packages:"))) return ABSENT;
429
622
  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;
623
+ const globs = start === -1 ? [] : collectPnpmGlobs(lines.slice(start + 1));
624
+ return globs.length > 0 ? parsed(globs) : unreadable("pnpm workspaces", "pnpm-workspace.yaml");
433
625
  }
434
626
  function collectPnpmGlobs(blockLines) {
435
627
  const globs = [];
@@ -444,13 +636,14 @@ function collectPnpmGlobs(blockLines) {
444
636
  return globs;
445
637
  }
446
638
  function detectGoWork(projectDirectory) {
447
- const content = readFileSafe(nodePath3.join(projectDirectory, "go.work"));
448
- if (content === void 0) return void 0;
639
+ const content = readFileSafe(nodePath4.join(projectDirectory, "go.work"));
640
+ if (content === void 0) return ABSENT;
449
641
  const lines = content.split(/\r?\n/);
450
642
  const directories = [];
451
643
  collectGoWorkBlock(lines, directories);
452
644
  collectGoWorkSingleLines(lines, directories);
453
- return directories.length > 0 ? directories : void 0;
645
+ if (directories.length > 0) return parsed(directories);
646
+ return lines.some((line) => /^\s*use\b/.test(line)) ? unreadable("go.work", "go.work") : ABSENT;
454
647
  }
455
648
  function collectGoWorkBlock(lines, directories) {
456
649
  for (const entry of readDelimitedBlock(lines, /^use\s*\(\s*$/)) {
@@ -474,8 +667,17 @@ function normalizeUseTarget(raw) {
474
667
  return unquoted.replace(/^\.\//, "");
475
668
  }
476
669
  function detectCargoWorkspace(projectDirectory) {
477
- const content = readFileSafe(nodePath3.join(projectDirectory, "Cargo.toml"));
478
- return content === void 0 ? void 0 : readCargoWorkspaceMembers(content);
670
+ const content = readFileSafe(nodePath4.join(projectDirectory, "Cargo.toml"));
671
+ if (content === void 0) return ABSENT;
672
+ if (!hasTomlTable(content, "workspace")) return ABSENT;
673
+ if (!hasTomlTableKey(content, "workspace", "members")) return ABSENT;
674
+ return resolveTomlWorkspaceMembers(
675
+ content,
676
+ readCargoWorkspaceMembers,
677
+ "workspace",
678
+ "Cargo [workspace]",
679
+ "Cargo.toml"
680
+ );
479
681
  }
480
682
 
481
683
  // src/utils/architecture-reconcile.ts
@@ -521,7 +723,7 @@ function healTarget(target) {
521
723
  const existing = readExisting(target.path);
522
724
  const action = decideAction(existing, target.fingerprint, target.hasContent);
523
725
  if (isWouldChangeAction(action)) {
524
- mkdirSync(nodePath4.dirname(target.path), { recursive: true });
726
+ mkdirSync(nodePath5.dirname(target.path), { recursive: true });
525
727
  const priorStamps = existing === void 0 ? /* @__PURE__ */ new Map() : parseSectionStamps(existing);
526
728
  const priorProse = existing === void 0 ? /* @__PURE__ */ new Map() : parseSectionProse(existing);
527
729
  writeFileSync(target.path, target.render(priorStamps, priorProse));
@@ -531,25 +733,24 @@ function healTarget(target) {
531
733
  function planTarget(target) {
532
734
  return decideAction(readExisting(target.path), target.fingerprint, target.hasContent);
533
735
  }
534
- function singleRepoTarget(projectDirectory) {
535
- const fingerprint = shapeFingerprint(projectDirectory);
536
- const nodes = extractSkeleton(projectDirectory).nodes;
736
+ function skeletonTarget(directory, path) {
737
+ const fingerprint = shapeFingerprint(directory);
738
+ const nodes = extractSkeleton(directory).nodes;
537
739
  return {
538
- path: resolveGeneratedArchitecturePath(projectDirectory),
740
+ path,
539
741
  fingerprint,
540
742
  hasContent: nodes.length > 0,
541
743
  render: (priorStamps, priorProse) => renderDocument(nodes, fingerprint, priorStamps, priorProse)
542
744
  };
543
745
  }
746
+ function singleRepoTarget(projectDirectory) {
747
+ return skeletonTarget(projectDirectory, resolveGeneratedArchitecturePath(projectDirectory));
748
+ }
544
749
  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
- };
750
+ return skeletonTarget(
751
+ packageDirectory,
752
+ nodePath5.join(packageDirectory, GENERATED_ARCHITECTURE_FILENAME)
753
+ );
553
754
  }
554
755
  function rootIndexTarget(projectDirectory) {
555
756
  const fingerprint = monorepoFingerprint(projectDirectory);
@@ -557,13 +758,18 @@ function rootIndexTarget(projectDirectory) {
557
758
  return {
558
759
  path: resolveGeneratedArchitecturePath(projectDirectory),
559
760
  fingerprint,
560
- hasContent: model.packages.length > 0,
761
+ // An unreadable workspace is content too: a root index that exists only to carry the
762
+ // "config unreadable" advisory is still worth writing — silence would read as "no
763
+ // monorepo here" when in fact one is present but unreadable (UWP4XK).
764
+ hasContent: model.packages.length > 0 || model.unreadableWorkspaces.length > 0,
561
765
  render: () => renderRootIndex(model, fingerprint)
562
766
  };
563
767
  }
564
768
  function projectTargets(projectDirectory) {
565
769
  const leaves = discoverLeafDirectories(projectDirectory);
566
- if (leaves.length === 0) return [singleRepoTarget(projectDirectory)];
770
+ if (leaves.length === 0 && discoverUnreadableWorkspaces(projectDirectory).length === 0) {
771
+ return [singleRepoTarget(projectDirectory)];
772
+ }
567
773
  return [rootIndexTarget(projectDirectory), ...leaves.map((leaf) => leafTarget(leaf))];
568
774
  }
569
775
  function selfHealProject(projectDirectory) {
@@ -630,6 +836,16 @@ function accumulateProseLine(line, inProse, buffer) {
630
836
  buffer.push(line);
631
837
  return true;
632
838
  }
839
+ function architectureFrontmatter(fingerprint) {
840
+ return `---
841
+ ${GENERATOR_KEY}: ${GENERATOR_VALUE}
842
+ ${FINGERPRINT_KEY}: ${fingerprint}
843
+ ---
844
+
845
+ # Architecture
846
+
847
+ `;
848
+ }
633
849
  function renderDocument(nodes, fingerprint, priorStamps, priorProse) {
634
850
  const verdicts = reconcileSections({
635
851
  priorStamps: Object.fromEntries(priorStamps),
@@ -643,14 +859,7 @@ function renderDocument(nodes, fingerprint, priorStamps, priorProse) {
643
859
  const prose = priorProse.get(verdict.node) ?? node?.purpose ?? "";
644
860
  return node === void 0 ? renderOrphanSection(verdict.node) : renderSection(node, stamp, verdict.status, prose);
645
861
  }).join("\n");
646
- return `---
647
- ${GENERATOR_KEY}: ${GENERATOR_VALUE}
648
- ${FINGERPRINT_KEY}: ${fingerprint}
649
- ---
650
-
651
- # Architecture
652
-
653
- ## Modules
862
+ return `${architectureFrontmatter(fingerprint)}## Modules
654
863
 
655
864
  ${sections}`;
656
865
  }
@@ -676,19 +885,21 @@ function renderRootIndex(model, fingerprint) {
676
885
  const edgeLines = model.edges.map((edge) => `- \`${edge.from}\` \u2192 \`${edge.to}\``).join("\n");
677
886
  const dependencies = model.edges.length === 0 ? "_No inter-package dependencies._\n" : `${edgeLines}
678
887
  `;
679
- return `---
680
- ${GENERATOR_KEY}: ${GENERATOR_VALUE}
681
- ${FINGERPRINT_KEY}: ${fingerprint}
682
- ---
683
-
684
- # Architecture
685
-
686
- ## Packages
888
+ return `${architectureFrontmatter(fingerprint)}## Packages
687
889
 
688
890
  ${sections}
689
891
  ## Dependencies
690
892
 
691
- ${dependencies}`;
893
+ ${dependencies}${renderCoverageGaps(model.unreadableWorkspaces)}`;
894
+ }
895
+ function renderCoverageGaps(unreadable2) {
896
+ if (unreadable2.length === 0) return "";
897
+ const items = unreadable2.map((entry) => `> - \`${entry.config}\` (${entry.manager})`).join("\n");
898
+ return `## Coverage gaps
899
+
900
+ > \u26A0 not introspected \u2014 workspace config unreadable. A present workspace manager's member list could not be parsed, so its packages may be missing above. Fix the config and re-run \`safeword architecture\`:
901
+ ${items}
902
+ `;
692
903
  }
693
904
  function renderPackageSection(node, stamp) {
694
905
  const body = node.introspected ? node.purpose : "> \u26A0 not introspected \u2014 no recognized source layout";
@@ -712,9 +923,18 @@ function architecture(cwd = process.cwd(), options = {}) {
712
923
  for (const result of results) {
713
924
  success(`Architecture state document ${result.action}: ${result.path}`);
714
925
  }
926
+ warnUnreadableWorkspaces(cwd);
715
927
  return Promise.resolve();
716
928
  }
929
+ function warnUnreadableWorkspaces(cwd) {
930
+ for (const workspace of discoverUnreadableWorkspaces(cwd)) {
931
+ warn(
932
+ `Workspace config present but unreadable: ${workspace.config} (${workspace.manager}). Its packages may be missing from the architecture doc \u2014 fix the config and re-run \`safeword architecture\`. (Advisory; nothing is blocked.)`
933
+ );
934
+ }
935
+ }
717
936
  function architectureStage(cwd) {
937
+ warnUnreadableWorkspaces(cwd);
718
938
  if (!isArchitectureDocumentEnforcementEnabled(cwd)) {
719
939
  success("Architecture doc enforcement is opted out (architectureDocEnforcement: false).");
720
940
  return Promise.resolve();
@@ -732,12 +952,13 @@ function architectureStage(cwd) {
732
952
  }
733
953
  function stageDocument(cwd, result) {
734
954
  try {
735
- const relativePath = nodePath5.relative(cwd, result.path);
955
+ const relativePath = nodePath6.relative(cwd, result.path);
736
956
  execFileSync("git", ["add", "--", relativePath], { cwd, stdio: "ignore" });
737
957
  } catch {
738
958
  }
739
959
  }
740
960
  function architectureCheck(cwd) {
961
+ warnUnreadableWorkspaces(cwd);
741
962
  if (!isArchitectureDocumentEnforcementEnabled(cwd)) {
742
963
  success("Architecture doc enforcement is opted out (architectureDocEnforcement: false).");
743
964
  return Promise.resolve();
@@ -755,4 +976,4 @@ function architectureCheck(cwd) {
755
976
  export {
756
977
  architecture
757
978
  };
758
- //# sourceMappingURL=architecture-3QIZFW3W.js.map
979
+ //# sourceMappingURL=architecture-32CAOL4P.js.map