safeword 0.63.0 → 0.67.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 (139) hide show
  1. package/dist/{architecture-T4RR7Q5J.js → architecture-K67T4PSI.js} +282 -120
  2. package/dist/architecture-K67T4PSI.js.map +1 -0
  3. package/dist/{check-MUL6YRIO.js → check-IARKNMMO.js} +11 -9
  4. package/dist/{check-MUL6YRIO.js.map → check-IARKNMMO.js.map} +1 -1
  5. package/dist/{chunk-FTMQT2V3.js → chunk-CYICTM6W.js} +21 -2
  6. package/dist/chunk-CYICTM6W.js.map +1 -0
  7. package/dist/{chunk-OE6HYAWU.js → chunk-I2C5SGFB.js} +4 -4
  8. package/dist/chunk-I2C5SGFB.js.map +1 -0
  9. package/dist/{chunk-DN67MPYH.js → chunk-ILDGIT74.js} +19 -14
  10. package/dist/chunk-ILDGIT74.js.map +1 -0
  11. package/dist/{chunk-6BEAVNR2.js → chunk-JQZCESMU.js} +7 -5
  12. package/dist/chunk-JQZCESMU.js.map +1 -0
  13. package/dist/chunk-M4FTAAXQ.js +24 -0
  14. package/dist/chunk-M4FTAAXQ.js.map +1 -0
  15. package/dist/{chunk-7F523R3I.js → chunk-MZZLPZDQ.js} +462 -312
  16. package/dist/chunk-MZZLPZDQ.js.map +1 -0
  17. package/dist/chunk-NDBLQB7Z.js +50 -0
  18. package/dist/chunk-NDBLQB7Z.js.map +1 -0
  19. package/dist/{chunk-ZESHX2BU.js → chunk-SEDNCRMM.js} +1 -1
  20. package/dist/chunk-SEDNCRMM.js.map +1 -0
  21. package/dist/{chunk-LBFUREUF.js → chunk-TJWIGDMT.js} +6 -4
  22. package/dist/chunk-TJWIGDMT.js.map +1 -0
  23. package/dist/{chunk-ZBFYIJNE.js → chunk-VDHXT7SV.js} +145 -23
  24. package/dist/chunk-VDHXT7SV.js.map +1 -0
  25. package/dist/{chunk-2KELQTLE.js → chunk-X26EJNPZ.js} +21 -1
  26. package/dist/chunk-X26EJNPZ.js.map +1 -0
  27. package/dist/{chunk-CYITXCZO.js → chunk-YJ6BSUPX.js} +5 -3
  28. package/dist/chunk-YJ6BSUPX.js.map +1 -0
  29. package/dist/{chunk-YYBLVWZZ.js → chunk-ZPH6ECOQ.js} +40 -1
  30. package/dist/chunk-ZPH6ECOQ.js.map +1 -0
  31. package/dist/cli.js +26 -21
  32. package/dist/cli.js.map +1 -1
  33. package/dist/{codify-SPIWSH2R.js → codify-QC6UXHI5.js} +20 -9
  34. package/dist/codify-QC6UXHI5.js.map +1 -0
  35. package/dist/{diff-BZOC4JXC.js → diff-452OHB47.js} +5 -4
  36. package/dist/{diff-BZOC4JXC.js.map → diff-452OHB47.js.map} +1 -1
  37. package/dist/{github-rest-3BZDDOX6.js → github-rest-XHCZPXMW.js} +5 -2
  38. package/dist/github-rest-XHCZPXMW.js.map +1 -0
  39. package/dist/index.js +1 -1
  40. package/dist/{lint-gherkin-RKLOOSNO.js → lint-gherkin-KFWEX4LL.js} +5 -4
  41. package/dist/{lint-gherkin-RKLOOSNO.js.map → lint-gherkin-KFWEX4LL.js.map} +1 -1
  42. package/dist/presets/typescript/index.js +1 -1
  43. package/dist/{reset-646VL6X4.js → reset-2N55QL4F.js} +6 -5
  44. package/dist/{reset-646VL6X4.js.map → reset-2N55QL4F.js.map} +1 -1
  45. package/dist/{retro-GGVK4BYL.js → retro-7MVICR6B.js} +2 -2
  46. package/dist/{setup-Q4Y7Y6PN.js → setup-7SYJNJKR.js} +19 -17
  47. package/dist/setup-7SYJNJKR.js.map +1 -0
  48. package/dist/{sync-config-VJFZJATR.js → sync-config-P3KXXKTR.js} +4 -3
  49. package/dist/{sync-learnings-THGNXZCT.js → sync-learnings-TA7F3OYP.js} +3 -3
  50. package/dist/{sync-tickets-P2FFD7DH.js → sync-tickets-7CW3FE3D.js} +5 -5
  51. package/dist/{sync-tracker-SV6AEV2O.js → sync-tracker-3S3HZMRX.js} +4 -4
  52. package/dist/sync-tracker-3S3HZMRX.js.map +1 -0
  53. package/dist/{test-plan-VMQRP2WF.js → test-plan-M7HEJIVM.js} +103 -20
  54. package/dist/test-plan-M7HEJIVM.js.map +1 -0
  55. package/dist/ticket-new-RNS5RTUK.js +357 -0
  56. package/dist/ticket-new-RNS5RTUK.js.map +1 -0
  57. package/dist/{upgrade-R52BFPBM.js → upgrade-KEEDWKDK.js} +15 -13
  58. package/dist/upgrade-KEEDWKDK.js.map +1 -0
  59. package/package.json +21 -20
  60. package/templates/SAFEWORD.md +21 -27
  61. package/templates/commands/self-review.md +3 -3
  62. package/templates/doc-templates/ticket-template.md +1 -1
  63. package/templates/guides/architecture-guide.md +15 -11
  64. package/templates/guides/context-files-guide.md +9 -14
  65. package/templates/guides/data-architecture-guide.md +9 -17
  66. package/templates/guides/design-doc-guide.md +7 -17
  67. package/templates/guides/learning-extraction.md +23 -112
  68. package/templates/guides/llm-evals-guide.md +14 -40
  69. package/templates/guides/llm-writing-guide.md +49 -27
  70. package/templates/guides/planning-guide.md +8 -7
  71. package/templates/guides/retro.md +15 -8
  72. package/templates/guides/self-report-filing.md +14 -6
  73. package/templates/guides/testing-guide.md +28 -37
  74. package/templates/guides/verification-lanes-guide.md +14 -31
  75. package/templates/guides/zombie-process-cleanup.md +1 -37
  76. package/templates/hooks/codex/stop.ts +1 -2
  77. package/templates/hooks/cursor/gate-adapter.ts +5 -2
  78. package/templates/hooks/lib/active-ticket.ts +6 -6
  79. package/templates/hooks/lib/architecture-document-nudge.ts +65 -17
  80. package/templates/hooks/lib/jtbd.ts +35 -21
  81. package/templates/hooks/lib/namespace-root.ts +15 -4
  82. package/templates/hooks/lib/parse-annotation.ts +1 -3
  83. package/templates/hooks/lib/phase-provenance.ts +175 -11
  84. package/templates/hooks/lib/process-kill-guard.ts +99 -0
  85. package/templates/hooks/lib/quality.ts +1 -6
  86. package/templates/hooks/lib/update-cache.ts +1 -3
  87. package/templates/hooks/lib/work-log-stamp.ts +84 -0
  88. package/templates/hooks/post-tool-quality.ts +4 -1
  89. package/templates/hooks/post-tool-work-log.ts +60 -0
  90. package/templates/hooks/pre-tool-quality.ts +33 -14
  91. package/templates/hooks/prompt-questions.ts +2 -2
  92. package/templates/hooks/stop-quality.ts +1 -1
  93. package/templates/prompts/architecture.md +3 -1
  94. package/templates/skills/audit/SKILL.md +66 -26
  95. package/templates/skills/bdd/DISCOVERY.md +24 -28
  96. package/templates/skills/bdd/DONE.md +1 -1
  97. package/templates/skills/bdd/SCENARIOS.md +38 -45
  98. package/templates/skills/bdd/SKILL.md +5 -31
  99. package/templates/skills/bdd/SPLITTING.md +3 -15
  100. package/templates/skills/bdd/TDD.md +9 -11
  101. package/templates/skills/bdd/VERIFY.md +2 -6
  102. package/templates/skills/brainstorm/SKILL.md +1 -1
  103. package/templates/skills/debug/SKILL.md +5 -10
  104. package/templates/skills/elicit/SKILL.md +2 -2
  105. package/templates/skills/figure-it-out/SKILL.md +3 -3
  106. package/templates/skills/quality-review/SKILL.md +5 -12
  107. package/templates/skills/refactor/SKILL.md +12 -26
  108. package/templates/skills/retro/SKILL.md +2 -1
  109. package/templates/skills/review-spec/SKILL.md +2 -2
  110. package/templates/skills/self-review/SKILL.md +4 -4
  111. package/templates/skills/tdd-review/SKILL.md +2 -12
  112. package/templates/skills/testing/SKILL.md +1 -1
  113. package/templates/skills/ticket-system/SKILL.md +23 -19
  114. package/templates/skills/verify/SKILL.md +33 -14
  115. package/templates/spec-template.md +11 -9
  116. package/dist/architecture-T4RR7Q5J.js.map +0 -1
  117. package/dist/chunk-2KELQTLE.js.map +0 -1
  118. package/dist/chunk-6BEAVNR2.js.map +0 -1
  119. package/dist/chunk-7F523R3I.js.map +0 -1
  120. package/dist/chunk-CYITXCZO.js.map +0 -1
  121. package/dist/chunk-DN67MPYH.js.map +0 -1
  122. package/dist/chunk-FTMQT2V3.js.map +0 -1
  123. package/dist/chunk-LBFUREUF.js.map +0 -1
  124. package/dist/chunk-OE6HYAWU.js.map +0 -1
  125. package/dist/chunk-YYBLVWZZ.js.map +0 -1
  126. package/dist/chunk-ZBFYIJNE.js.map +0 -1
  127. package/dist/chunk-ZESHX2BU.js.map +0 -1
  128. package/dist/codify-SPIWSH2R.js.map +0 -1
  129. package/dist/github-rest-3BZDDOX6.js.map +0 -1
  130. package/dist/setup-Q4Y7Y6PN.js.map +0 -1
  131. package/dist/sync-tracker-SV6AEV2O.js.map +0 -1
  132. package/dist/test-plan-VMQRP2WF.js.map +0 -1
  133. package/dist/ticket-new-K4X2ZO6V.js +0 -219
  134. package/dist/ticket-new-K4X2ZO6V.js.map +0 -1
  135. package/dist/upgrade-R52BFPBM.js.map +0 -1
  136. /package/dist/{retro-GGVK4BYL.js.map → retro-7MVICR6B.js.map} +0 -0
  137. /package/dist/{sync-config-VJFZJATR.js.map → sync-config-P3KXXKTR.js.map} +0 -0
  138. /package/dist/{sync-learnings-THGNXZCT.js.map → sync-learnings-TA7F3OYP.js.map} +0 -0
  139. /package/dist/{sync-tickets-P2FFD7DH.js.map → sync-tickets-7CW3FE3D.js.map} +0 -0
@@ -1,14 +1,19 @@
1
+ import {
2
+ listArchitectureRecords
3
+ } from "./chunk-M4FTAAXQ.js";
1
4
  import {
2
5
  GENERATED_ARCHITECTURE_FILENAME,
3
6
  isArchitectureDocumentEnforcementEnabled,
7
+ resolveArchitectureNarrative,
4
8
  resolveGeneratedArchitecturePath
5
- } from "./chunk-FTMQT2V3.js";
9
+ } from "./chunk-CYICTM6W.js";
6
10
  import {
7
11
  exists,
12
+ findAllInTree,
8
13
  isDirectory,
9
14
  readFileSafe,
10
15
  readJson
11
- } from "./chunk-2KELQTLE.js";
16
+ } from "./chunk-X26EJNPZ.js";
12
17
  import {
13
18
  error,
14
19
  success,
@@ -32,6 +37,107 @@ import nodePath3 from "path";
32
37
  // src/utils/architecture-skeleton.ts
33
38
  import { readdirSync } from "fs";
34
39
  import nodePath from "path";
40
+
41
+ // src/utils/toml.ts
42
+ function readTomlTableArray(content, table, key) {
43
+ const found = tableArrayBody(content.split(/\r?\n/), table, key);
44
+ if (found === void 0) return void 0;
45
+ const values = found.body.matchAll(/"([^"]*)"|'([^']*)'/g).map((match) => match[1] ?? match[2] ?? "").filter((value) => value.length > 0).toArray();
46
+ return values.length > 0 ? values : void 0;
47
+ }
48
+ function isTomlTableEmptyArray(content, table, key) {
49
+ const found = tableArrayBody(content.split(/\r?\n/), table, key);
50
+ return found !== void 0 && found.closed && found.body.replaceAll(/[\s,]/g, "") === "";
51
+ }
52
+ function hasTomlTable(content, table) {
53
+ return content.split(/\r?\n/).some((line) => tableHeader(stripTomlComment(line).trim()) === table);
54
+ }
55
+ function* linesInTable(content, table) {
56
+ let inTable = false;
57
+ for (const rawLine of content.split(/\r?\n/)) {
58
+ const line = stripTomlComment(rawLine).trim();
59
+ if (line === "") continue;
60
+ const header = tableHeader(line);
61
+ if (header !== void 0) {
62
+ inTable = header === table;
63
+ continue;
64
+ }
65
+ if (inTable) yield line;
66
+ }
67
+ }
68
+ function hasTomlTableKey(content, table, key) {
69
+ for (const line of linesInTable(content, table)) {
70
+ if (tableValue(line, key) !== void 0) return true;
71
+ }
72
+ return false;
73
+ }
74
+ function readTomlTableString(content, table, key) {
75
+ for (const line of linesInTable(content, table)) {
76
+ const value = tableValue(line, key);
77
+ if (value?.startsWith('"')) {
78
+ const end = value.indexOf('"', 1);
79
+ if (end !== -1) return value.slice(1, end);
80
+ }
81
+ }
82
+ return void 0;
83
+ }
84
+ function tableArrayBody(lines, table, key) {
85
+ const opened = findTableArrayOpen(lines, table, key);
86
+ if (opened === void 0) return void 0;
87
+ let body = opened.initial;
88
+ for (let index = opened.line; index < lines.length; index += 1) {
89
+ if (index > opened.line) body += `
90
+ ${stripTomlComment(lines[index] ?? "")}`;
91
+ const close = indexOfOutsideQuotes(body, "]");
92
+ if (close !== -1) return { body: body.slice(0, close), closed: true };
93
+ }
94
+ return { body, closed: false };
95
+ }
96
+ function findTableArrayOpen(lines, table, key) {
97
+ let inTable = false;
98
+ for (const [index, rawLine] of lines.entries()) {
99
+ const trimmed = stripTomlComment(rawLine).trim();
100
+ const header = tableHeader(trimmed);
101
+ if (header !== void 0) {
102
+ inTable = header === table;
103
+ continue;
104
+ }
105
+ if (!inTable) continue;
106
+ const value = tableValue(trimmed, key);
107
+ if (value?.startsWith("[") === true) return { line: index, initial: value.slice(1) };
108
+ }
109
+ return void 0;
110
+ }
111
+ function indexOfOutsideQuotes(text, target) {
112
+ let quote = "";
113
+ let index = 0;
114
+ while (index < text.length) {
115
+ const character = text[index];
116
+ if (quote === "") {
117
+ if (character === '"' || character === "'") quote = character;
118
+ else if (character === target) return index;
119
+ } else if (character === quote) {
120
+ quote = "";
121
+ }
122
+ index += 1;
123
+ }
124
+ return -1;
125
+ }
126
+ function tableValue(line, key) {
127
+ const equals = line.indexOf("=");
128
+ if (equals === -1 || line.slice(0, equals).trim() !== key) return void 0;
129
+ return line.slice(equals + 1).trim();
130
+ }
131
+ function tableHeader(line) {
132
+ const match = /^\[\[?([^[\]]+)\]\]?\s*$/.exec(line);
133
+ return match?.[1] === void 0 ? void 0 : match[1].trim();
134
+ }
135
+ function stripTomlComment(line) {
136
+ const hash = indexOfOutsideQuotes(line, "#");
137
+ return hash === -1 ? line : line.slice(0, hash);
138
+ }
139
+
140
+ // src/utils/architecture-skeleton.ts
35
141
  var PURPOSE_PLACEHOLDER = "No description yet \u2014 awaiting prose.";
36
142
  var GO_LAYOUT_DIRECTORIES = ["cmd", "internal", "pkg"];
37
143
  var RUST_ROOT_FILES = /* @__PURE__ */ new Set(["lib.rs", "main.rs"]);
@@ -42,6 +148,7 @@ var PYTHON_EXCLUDED_FILES = /* @__PURE__ */ new Set([
42
148
  "__init__.py",
43
149
  "__main__.py"
44
150
  ]);
151
+ var JS_SOURCE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
45
152
  var byNodeName = (a, b) => a.name.localeCompare(b.name);
46
153
  function extractSkeleton(projectDirectory) {
47
154
  if (exists(nodePath.join(projectDirectory, "pyproject.toml"))) {
@@ -51,7 +158,7 @@ function extractSkeleton(projectDirectory) {
51
158
  if (exists(nodePath.join(projectDirectory, "Cargo.toml"))) {
52
159
  return { nodes: rustModuleNodes(projectDirectory) };
53
160
  }
54
- const sourceNodes = enumerateModuleDirectories(
161
+ const sourceNodes = enumerateJsSourceRoot(
55
162
  nodePath.join(projectDirectory, "src"),
56
163
  (name) => `src/${name}`
57
164
  );
@@ -59,16 +166,70 @@ function extractSkeleton(projectDirectory) {
59
166
  if (exists(nodePath.join(projectDirectory, "go.mod"))) {
60
167
  return { nodes: goLayoutNodes(projectDirectory) };
61
168
  }
62
- return { nodes: sourceNodes };
169
+ const libraryNodes = enumerateJsSourceRoot(
170
+ nodePath.join(projectDirectory, "lib"),
171
+ (name) => `lib/${name}`
172
+ );
173
+ if (libraryNodes.length > 0) return { nodes: libraryNodes };
174
+ if (declaresWorkspaces(projectDirectory)) return { nodes: [] };
175
+ return { nodes: topLevelJsModuleNodes(projectDirectory) };
176
+ }
177
+ function declaresWorkspaces(projectDirectory) {
178
+ if (exists(nodePath.join(projectDirectory, "pnpm-workspace.yaml"))) return true;
179
+ if (exists(nodePath.join(projectDirectory, "go.work"))) return true;
180
+ const manifest = readJson(nodePath.join(projectDirectory, "package.json"));
181
+ if (manifest !== null && typeof manifest === "object" && manifest.workspaces !== void 0) {
182
+ return true;
183
+ }
184
+ const pyproject = readFileSafe(nodePath.join(projectDirectory, "pyproject.toml"));
185
+ return pyproject !== void 0 && hasTomlTable(pyproject, "tool.uv.workspace");
63
186
  }
64
- function enumerateModuleDirectories(directory, pathFor) {
187
+ function enumerateJsSourceRoot(directory, pathFor) {
65
188
  let entries;
66
189
  try {
67
190
  entries = readdirSync(directory, { withFileTypes: true });
68
191
  } catch {
69
192
  return [];
70
193
  }
71
- return entries.filter((entry) => entry.isDirectory()).map((entry) => ({ name: entry.name, path: pathFor(entry.name), purpose: PURPOSE_PLACEHOLDER })).toSorted(byNodeName);
194
+ const directories = entries.filter((entry) => entry.isDirectory());
195
+ if (directories.length > 0) {
196
+ return directories.map((entry) => ({ name: entry.name, path: pathFor(entry.name), purpose: PURPOSE_PLACEHOLDER })).toSorted(byNodeName);
197
+ }
198
+ return jsFileNodes(entries, pathFor);
199
+ }
200
+ function topLevelJsModuleNodes(projectDirectory) {
201
+ let entries;
202
+ try {
203
+ entries = readdirSync(projectDirectory, { withFileTypes: true });
204
+ } catch {
205
+ return [];
206
+ }
207
+ return jsFileNodes(entries, (name) => name);
208
+ }
209
+ function jsFileNodes(entries, pathFor) {
210
+ const files = entries.filter((entry) => entry.isFile() && isJsSourceModuleFile(entry.name)).toSorted(
211
+ (a, b) => extensionPriority(a.name) - extensionPriority(b.name) || a.name.localeCompare(b.name)
212
+ );
213
+ const byName = /* @__PURE__ */ new Map();
214
+ for (const entry of files) {
215
+ const name = jsModuleName(entry.name);
216
+ if (!byName.has(name)) {
217
+ byName.set(name, { name, path: pathFor(entry.name), purpose: PURPOSE_PLACEHOLDER });
218
+ }
219
+ }
220
+ return byName.values().toArray().toSorted(byNodeName);
221
+ }
222
+ function extensionPriority(filename) {
223
+ return JS_SOURCE_EXTENSIONS.findIndex((extension) => filename.endsWith(extension));
224
+ }
225
+ function isJsSourceModuleFile(name) {
226
+ if (name.startsWith(".")) return false;
227
+ if (/\.d\.[mc]?ts$/.test(name)) return false;
228
+ if (/\.config\.[mc]?[jt]sx?$/.test(name)) return false;
229
+ return JS_SOURCE_EXTENSIONS.some((extension) => name.endsWith(extension));
230
+ }
231
+ function jsModuleName(filename) {
232
+ return filename.slice(0, filename.lastIndexOf("."));
72
233
  }
73
234
  function goLayoutNodes(projectDirectory) {
74
235
  return GO_LAYOUT_DIRECTORIES.filter((name) => isDirectory(nodePath.join(projectDirectory, name))).map((name) => ({ name, path: name, purpose: PURPOSE_PLACEHOLDER })).toSorted(byNodeName);
@@ -161,105 +322,6 @@ function readBoundaryConfig(projectDirectory) {
161
322
  return "";
162
323
  }
163
324
 
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);
261
- }
262
-
263
325
  // src/utils/cargo-manifest.ts
264
326
  var DEPENDENCY_TABLES = ["dependencies", "dev-dependencies", "build-dependencies"];
265
327
  function readCargoWorkspaceMembers(content) {
@@ -484,13 +546,58 @@ function discoverWorkspaces(projectDirectory) {
484
546
  patterns: detections.flatMap(
485
547
  (detection) => detection.status === "parsed" ? detection.patterns : []
486
548
  ),
487
- unreadable: detections.flatMap(
488
- (detection) => detection.status === "unreadable" ? [{ manager: detection.manager, config: detection.config }] : []
489
- )
549
+ unreadable: detections.filter((detection) => detection.status === "unreadable").map((detection) => ({ manager: detection.manager, config: detection.config }))
490
550
  };
491
551
  }
492
552
  function discoverLeafDirectories(projectDirectory) {
493
- return resolveLeafDirectories(projectDirectory, discoverWorkspaces(projectDirectory).patterns);
553
+ return leafDirectoriesFrom(projectDirectory, discoverWorkspaces(projectDirectory).patterns);
554
+ }
555
+ function leafDirectoriesFrom(projectDirectory, patterns) {
556
+ const workspaceLeaves = resolveLeafDirectories(projectDirectory, patterns);
557
+ const orphanLeaves = discoverOrphanManifestDirectories(projectDirectory);
558
+ return [.../* @__PURE__ */ new Set([...workspaceLeaves, ...orphanLeaves])].toSorted(byString2);
559
+ }
560
+ var ORPHAN_MANIFESTS = [
561
+ { packs: ["golang", "go"], filename: "go.mod", declaresPackage: (content) => hasGoModule(content) },
562
+ {
563
+ packs: ["python"],
564
+ filename: "pyproject.toml",
565
+ // A real package declares [project] (PEP 621) or [tool.poetry]; a pyproject that
566
+ // only carries [tool.ruff]/[tool.black] tooling config is not a package leaf.
567
+ declaresPackage: (content) => hasTomlTable(content, "project") || hasTomlTable(content, "tool.poetry")
568
+ },
569
+ {
570
+ packs: ["rust"],
571
+ filename: "Cargo.toml",
572
+ // A member crate declares [package]; a bare [workspace] root is not a leaf.
573
+ declaresPackage: (content) => hasTomlTable(content, "package")
574
+ }
575
+ ];
576
+ function discoverOrphanManifestDirectories(projectDirectory) {
577
+ const installed = installedPacks(projectDirectory);
578
+ const directories = /* @__PURE__ */ new Set();
579
+ for (const manifest of ORPHAN_MANIFESTS) {
580
+ if (manifest.packs.every((pack) => !installed.has(pack))) continue;
581
+ for (const dir of orphanDirectoriesFor(projectDirectory, manifest)) directories.add(dir);
582
+ }
583
+ return [...directories];
584
+ }
585
+ function orphanDirectoriesFor(projectDirectory, manifest) {
586
+ return findAllInTree(projectDirectory, manifest.filename).filter((dir) => {
587
+ if (dir === projectDirectory) return false;
588
+ const content = readFileSafe(nodePath4.join(dir, manifest.filename));
589
+ return content !== void 0 && manifest.declaresPackage(content);
590
+ });
591
+ }
592
+ function hasGoModule(content) {
593
+ return /^module\s+\S+/m.test(content);
594
+ }
595
+ function installedPacks(projectDirectory) {
596
+ const config = readJson(nodePath4.join(projectDirectory, ".safeword", "config.json"));
597
+ if (config === null || typeof config !== "object") return /* @__PURE__ */ new Set();
598
+ const packs = config.installedPacks;
599
+ if (!Array.isArray(packs)) return /* @__PURE__ */ new Set();
600
+ return new Set(packs.filter((pack) => typeof pack === "string"));
494
601
  }
495
602
  function discoverUnreadableWorkspaces(projectDirectory) {
496
603
  return discoverWorkspaces(projectDirectory).unreadable;
@@ -534,7 +641,7 @@ function hasRecognizedManifest(directory) {
534
641
  }
535
642
  function extractMonorepoModel(projectDirectory) {
536
643
  const { patterns, unreadable: unreadableWorkspaces } = discoverWorkspaces(projectDirectory);
537
- const packages = resolveLeafDirectories(projectDirectory, patterns).map((dir) => ({
644
+ const packages = leafDirectoriesFrom(projectDirectory, patterns).map((dir) => ({
538
645
  name: packageName(dir),
539
646
  dir,
540
647
  purpose: PURPOSE_PLACEHOLDER,
@@ -552,8 +659,7 @@ function extractMonorepoModel(projectDirectory) {
552
659
  edges.sort((a, b) => byString2(a.from, b.from) || byString2(a.to, b.to));
553
660
  return { packages, edges, unreadableWorkspaces };
554
661
  }
555
- function monorepoFingerprint(projectDirectory) {
556
- const model = extractMonorepoModel(projectDirectory);
662
+ function monorepoFingerprintOf(projectDirectory, model) {
557
663
  const inputs = {
558
664
  // Introspection status is part of the root shape: a package gaining or losing
559
665
  // a `src/` tree flips its root-index line (marker ↔ described), so the root
@@ -753,8 +859,8 @@ function leafTarget(packageDirectory) {
753
859
  );
754
860
  }
755
861
  function rootIndexTarget(projectDirectory) {
756
- const fingerprint = monorepoFingerprint(projectDirectory);
757
862
  const model = extractMonorepoModel(projectDirectory);
863
+ const fingerprint = monorepoFingerprintOf(projectDirectory, model);
758
864
  return {
759
865
  path: resolveGeneratedArchitecturePath(projectDirectory),
760
866
  fingerprint,
@@ -902,7 +1008,7 @@ ${items}
902
1008
  `;
903
1009
  }
904
1010
  function renderPackageSection(node, stamp) {
905
- const body = node.introspected ? node.purpose : "> \u26A0 not introspected \u2014 no recognized source layout";
1011
+ const body = node.introspected ? node.purpose : "> \u26A0 not introspected \u2014 no source modules to map";
906
1012
  return `### ${node.name}
907
1013
 
908
1014
  ${RECONCILED_PREFIX} ${stamp} -->
@@ -911,6 +1017,54 @@ ${body}
911
1017
  `;
912
1018
  }
913
1019
 
1020
+ // src/utils/architecture-narrative-drift.ts
1021
+ var ADVISORY_PACKAGE_CAP = 6;
1022
+ function architectureNarrativeDriftAdvisoryForProject(projectDirectory) {
1023
+ const narrative = resolveArchitectureNarrative(projectDirectory);
1024
+ const records = listArchitectureRecords(narrative.absolutePath).records;
1025
+ if (records.length === 0) return void 0;
1026
+ const generatedDocument = readFileSafe(resolveGeneratedArchitecturePath(projectDirectory));
1027
+ if (generatedDocument === void 0) return void 0;
1028
+ const narrativeText = records.map((record) => readFileSafe(record) ?? "").join("\n");
1029
+ return narrativeDriftAdvisory(
1030
+ extractRootIndexPackages(generatedDocument),
1031
+ narrativeText,
1032
+ narrative.displayPath
1033
+ );
1034
+ }
1035
+ function extractRootIndexPackages(generatedDocument) {
1036
+ const packages = [];
1037
+ let isInPackagesSection = false;
1038
+ for (const line of generatedDocument.split(/\r?\n/)) {
1039
+ if (line.startsWith("## ")) {
1040
+ isInPackagesSection = line.slice(3).trim() === "Packages";
1041
+ continue;
1042
+ }
1043
+ if (isInPackagesSection && line.startsWith("### ")) {
1044
+ const name = line.slice(4).trim();
1045
+ if (name.length > 0) packages.push(name);
1046
+ }
1047
+ }
1048
+ return packages;
1049
+ }
1050
+ function isMentioned(packageName2, narrativeText) {
1051
+ const tail = packageName2.includes("/") ? packageName2.split("/").at(-1) : void 0;
1052
+ const candidates = tail !== void 0 && tail !== "" ? [packageName2, tail] : [packageName2];
1053
+ return candidates.some((candidate) => nameBoundaryPattern(candidate).test(narrativeText));
1054
+ }
1055
+ function nameBoundaryPattern(name) {
1056
+ const escaped = name.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
1057
+ return new RegExp(String.raw`(?<![\w@/.-])${escaped}(?!\w|[.-]\w)`, "i");
1058
+ }
1059
+ function narrativeDriftAdvisory(packageNames, narrativeText, narrativeDisplayPath) {
1060
+ const missing = packageNames.filter((name) => !isMentioned(name, narrativeText));
1061
+ if (missing.length === 0) return void 0;
1062
+ const named = missing.slice(0, ADVISORY_PACKAGE_CAP);
1063
+ const remainder = missing.length - named.length;
1064
+ const listing = remainder > 0 ? `${named.join(", ")} and ${remainder} more` : named.join(", ");
1065
+ return `Architecture narrative (${narrativeDisplayPath}) does not mention ${missing.length} generated package(s): ${listing} \u2014 run /audit to reconcile against architecture.generated.md. (Advisory; nothing is blocked.)`;
1066
+ }
1067
+
914
1068
  // src/commands/architecture.ts
915
1069
  function architecture(cwd = process.cwd(), options = {}) {
916
1070
  if (options.check) {
@@ -924,8 +1078,13 @@ function architecture(cwd = process.cwd(), options = {}) {
924
1078
  success(`Architecture state document ${result.action}: ${result.path}`);
925
1079
  }
926
1080
  warnUnreadableWorkspaces(cwd);
1081
+ warnNarrativeDrift(cwd);
927
1082
  return Promise.resolve();
928
1083
  }
1084
+ function warnNarrativeDrift(cwd) {
1085
+ const advisory = architectureNarrativeDriftAdvisoryForProject(cwd);
1086
+ if (advisory !== void 0) warn(advisory);
1087
+ }
929
1088
  function warnUnreadableWorkspaces(cwd) {
930
1089
  for (const workspace of discoverUnreadableWorkspaces(cwd)) {
931
1090
  warn(
@@ -937,17 +1096,19 @@ function architectureStage(cwd) {
937
1096
  warnUnreadableWorkspaces(cwd);
938
1097
  if (!isArchitectureDocumentEnforcementEnabled(cwd)) {
939
1098
  success("Architecture doc enforcement is opted out (architectureDocEnforcement: false).");
1099
+ warnNarrativeDrift(cwd);
940
1100
  return Promise.resolve();
941
1101
  }
942
1102
  const changed = selfHealProject(cwd).filter((result) => isWouldChangeAction(result.action));
943
1103
  if (changed.length === 0) {
944
1104
  success("Architecture docs need no change.");
945
- return Promise.resolve();
946
- }
947
- for (const result of changed) {
948
- stageDocument(cwd, result);
949
- success(`Architecture doc ${result.action} and staged: ${result.path}`);
1105
+ } else {
1106
+ for (const result of changed) {
1107
+ stageDocument(cwd, result);
1108
+ success(`Architecture doc ${result.action} and staged: ${result.path}`);
1109
+ }
950
1110
  }
1111
+ warnNarrativeDrift(cwd);
951
1112
  return Promise.resolve();
952
1113
  }
953
1114
  function stageDocument(cwd, result) {
@@ -959,6 +1120,7 @@ function stageDocument(cwd, result) {
959
1120
  }
960
1121
  function architectureCheck(cwd) {
961
1122
  warnUnreadableWorkspaces(cwd);
1123
+ warnNarrativeDrift(cwd);
962
1124
  if (!isArchitectureDocumentEnforcementEnabled(cwd)) {
963
1125
  success("Architecture doc enforcement is opted out (architectureDocEnforcement: false).");
964
1126
  return Promise.resolve();
@@ -976,4 +1138,4 @@ function architectureCheck(cwd) {
976
1138
  export {
977
1139
  architecture
978
1140
  };
979
- //# sourceMappingURL=architecture-T4RR7Q5J.js.map
1141
+ //# sourceMappingURL=architecture-K67T4PSI.js.map