safeword 0.52.1 → 0.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/architecture-CYFAXY2U.js +256 -0
- package/dist/architecture-CYFAXY2U.js.map +1 -0
- package/dist/{check-K6M6MIVS.js → check-TKA2IIC7.js} +10 -10
- package/dist/{chunk-JLFYAVLP.js → chunk-5ES7OYBI.js} +22 -19
- package/dist/chunk-5ES7OYBI.js.map +1 -0
- package/dist/{chunk-MPEK5NNA.js → chunk-G3BDQLVU.js} +7 -7
- package/dist/chunk-G3BDQLVU.js.map +1 -0
- package/dist/{chunk-WE7ZQLCT.js → chunk-GT7KMCFG.js} +23 -8
- package/dist/chunk-GT7KMCFG.js.map +1 -0
- package/dist/{chunk-MPYFFJBF.js → chunk-I2GV5QKO.js} +10 -3
- package/dist/{chunk-MPYFFJBF.js.map → chunk-I2GV5QKO.js.map} +1 -1
- package/dist/{chunk-3RM6NYZT.js → chunk-IE32BCVN.js} +22 -19
- package/dist/chunk-IE32BCVN.js.map +1 -0
- package/dist/{chunk-OJNF2CHC.js → chunk-JQVVJCLH.js} +476 -282
- package/dist/chunk-JQVVJCLH.js.map +1 -0
- package/dist/{chunk-LODQOJEK.js → chunk-PHR2K2Y3.js} +41 -29
- package/dist/chunk-PHR2K2Y3.js.map +1 -0
- package/dist/{chunk-SF3CPJUX.js → chunk-SLPSZC2D.js} +12 -12
- package/dist/chunk-SLPSZC2D.js.map +1 -0
- package/dist/{chunk-SD4UB7HJ.js → chunk-VLK2DXJ7.js} +55 -33
- package/dist/chunk-VLK2DXJ7.js.map +1 -0
- package/dist/{chunk-2WUL76K5.js → chunk-WJOSBJ37.js} +7 -2
- package/dist/{chunk-2WUL76K5.js.map → chunk-WJOSBJ37.js.map} +1 -1
- package/dist/{chunk-QLXFPFIC.js → chunk-XTLCJKGE.js} +5 -5
- package/dist/chunk-XTLCJKGE.js.map +1 -0
- package/dist/{chunk-IGULTNHR.js → chunk-YXNI7W5D.js} +14 -12
- package/dist/chunk-YXNI7W5D.js.map +1 -0
- package/dist/cli.js +18 -12
- package/dist/cli.js.map +1 -1
- package/dist/{codify-YUGZVCR4.js → codify-64DSX5YK.js} +14 -12
- package/dist/codify-64DSX5YK.js.map +1 -0
- package/dist/{diff-WAQXK32P.js → diff-WFF3TQH2.js} +33 -32
- package/dist/diff-WFF3TQH2.js.map +1 -0
- package/dist/index.js +2 -2
- package/dist/{lint-gherkin-KJH3GNJQ.js → lint-gherkin-FLJEH3EJ.js} +2 -2
- package/dist/presets/typescript/index.d.ts +11 -11
- package/dist/presets/typescript/index.js +2 -2
- package/dist/{reset-AT5CGCYX.js → reset-GY7TFOIB.js} +15 -17
- package/dist/reset-GY7TFOIB.js.map +1 -0
- package/dist/{setup-C3FCNOW7.js → setup-VVQ3EKND.js} +29 -26
- package/dist/setup-VVQ3EKND.js.map +1 -0
- package/dist/{sync-config-5FCJLGMW.js → sync-config-IDVS7DTC.js} +3 -3
- package/dist/{sync-learnings-C7GXSMKZ.js → sync-learnings-RY6SVKZ2.js} +3 -3
- package/dist/{sync-tickets-SDX4XPVS.js → sync-tickets-LMQKAABL.js} +4 -4
- package/dist/{test-plan-4D3WNSQP.js → test-plan-PV6C5IUO.js} +18 -10
- package/dist/test-plan-PV6C5IUO.js.map +1 -0
- package/dist/{ticket-new-CVBD3MVR.js → ticket-new-BK5VDS4X.js} +4 -4
- package/dist/{ticket-new-CVBD3MVR.js.map → ticket-new-BK5VDS4X.js.map} +1 -1
- package/dist/{upgrade-25PKQQCY.js → upgrade-IQNLPR5G.js} +48 -29
- package/dist/upgrade-IQNLPR5G.js.map +1 -0
- package/package.json +14 -16
- package/templates/SAFEWORD.md +2 -0
- package/templates/codex/config.toml +7 -0
- package/templates/commands/audit.md +10 -3
- package/templates/commands/verify.md +2 -2
- package/templates/doc-templates/test-definitions-feature.md +1 -1
- package/templates/hooks/lib/dependency-readiness.ts +228 -20
- package/templates/hooks/lib/ledger-validation.ts +31 -7
- package/templates/hooks/lib/lint-config.ts +55 -0
- package/templates/hooks/lib/lint.ts +16 -8
- package/templates/hooks/lib/quality-state.ts +22 -1
- package/templates/hooks/lib/readiness-pointer.ts +18 -0
- package/templates/hooks/lib/skill-invocation-log.ts +12 -4
- package/templates/hooks/lib/test-runner.ts +23 -2
- package/templates/hooks/lib/update-cache.ts +98 -2
- package/templates/hooks/post-tool-bypass-warn.ts +1 -1
- package/templates/hooks/post-tool-lint.ts +1 -1
- package/templates/hooks/pre-tool-dependency-readiness.ts +2 -0
- package/templates/hooks/pre-tool-quality.ts +5 -21
- package/templates/hooks/prompt-questions.ts +16 -1
- package/templates/hooks/record-skill-invocation.ts +7 -1
- package/templates/hooks/session-architecture-heal.ts +30 -0
- package/templates/hooks/session-auto-upgrade.ts +193 -48
- package/templates/hooks/session-bun-check.sh +1 -1
- package/templates/hooks/session-compact-context.ts +4 -15
- package/templates/hooks/session-dependency-readiness.ts +5 -2
- package/templates/hooks/session-lint-check.ts +11 -3
- package/templates/hooks/stop-quality.ts +81 -64
- package/templates/hooks/stop-reentry.ts +24 -19
- package/templates/skills/audit/SKILL.md +10 -3
- package/templates/skills/bdd/SKILL.md +6 -2
- package/templates/skills/bdd/TDD.md +14 -4
- package/templates/skills/bdd/VERIFY.md +1 -1
- package/templates/skills/debug/SKILL.md +29 -15
- package/templates/skills/elicit/SKILL.md +4 -0
- package/templates/skills/figure-it-out/SKILL.md +6 -0
- package/templates/skills/quality-review/SKILL.md +41 -9
- package/templates/skills/refactor/SKILL.md +26 -11
- package/templates/skills/tdd-review/SKILL.md +29 -23
- package/templates/skills/verify/SKILL.md +2 -2
- package/dist/chunk-3RM6NYZT.js.map +0 -1
- package/dist/chunk-IGULTNHR.js.map +0 -1
- package/dist/chunk-JLFYAVLP.js.map +0 -1
- package/dist/chunk-LODQOJEK.js.map +0 -1
- package/dist/chunk-MPEK5NNA.js.map +0 -1
- package/dist/chunk-OJNF2CHC.js.map +0 -1
- package/dist/chunk-QLXFPFIC.js.map +0 -1
- package/dist/chunk-SD4UB7HJ.js.map +0 -1
- package/dist/chunk-SF3CPJUX.js.map +0 -1
- package/dist/chunk-WE7ZQLCT.js.map +0 -1
- package/dist/codify-YUGZVCR4.js.map +0 -1
- package/dist/diff-WAQXK32P.js.map +0 -1
- package/dist/reset-AT5CGCYX.js.map +0 -1
- package/dist/setup-C3FCNOW7.js.map +0 -1
- package/dist/test-plan-4D3WNSQP.js.map +0 -1
- package/dist/upgrade-25PKQQCY.js.map +0 -1
- package/templates/hooks/session-update-check.ts +0 -81
- /package/dist/{check-K6M6MIVS.js.map → check-TKA2IIC7.js.map} +0 -0
- /package/dist/{lint-gherkin-KJH3GNJQ.js.map → lint-gherkin-FLJEH3EJ.js.map} +0 -0
- /package/dist/{sync-config-5FCJLGMW.js.map → sync-config-IDVS7DTC.js.map} +0 -0
- /package/dist/{sync-learnings-C7GXSMKZ.js.map → sync-learnings-RY6SVKZ2.js.map} +0 -0
- /package/dist/{sync-tickets-SDX4XPVS.js.map → sync-tickets-LMQKAABL.js.map} +0 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveGeneratedArchitecturePath
|
|
3
|
+
} from "./chunk-WJOSBJ37.js";
|
|
4
|
+
import {
|
|
5
|
+
success
|
|
6
|
+
} from "./chunk-I2GV5QKO.js";
|
|
7
|
+
|
|
8
|
+
// src/commands/architecture.ts
|
|
9
|
+
import process from "process";
|
|
10
|
+
|
|
11
|
+
// src/utils/architecture-document.ts
|
|
12
|
+
import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
13
|
+
import nodePath3 from "path";
|
|
14
|
+
|
|
15
|
+
// src/utils/architecture-fingerprint.ts
|
|
16
|
+
import { createHash } from "crypto";
|
|
17
|
+
import { readdirSync as readdirSync2, readFileSync } from "fs";
|
|
18
|
+
import nodePath2 from "path";
|
|
19
|
+
|
|
20
|
+
// src/utils/architecture-skeleton.ts
|
|
21
|
+
import { readdirSync } from "fs";
|
|
22
|
+
import nodePath from "path";
|
|
23
|
+
var PURPOSE_PLACEHOLDER = "No description yet \u2014 awaiting prose.";
|
|
24
|
+
function extractSkeleton(projectDirectory) {
|
|
25
|
+
const sourceDirectory = nodePath.join(projectDirectory, "src");
|
|
26
|
+
let entries;
|
|
27
|
+
try {
|
|
28
|
+
entries = readdirSync(sourceDirectory, { withFileTypes: true });
|
|
29
|
+
} catch {
|
|
30
|
+
return { nodes: [] };
|
|
31
|
+
}
|
|
32
|
+
const nodes = entries.filter((entry) => entry.isDirectory()).map((entry) => ({
|
|
33
|
+
name: entry.name,
|
|
34
|
+
// Forward slashes always — the rendered doc and fingerprint must be
|
|
35
|
+
// platform-stable (the fingerprint normalizes paths the same way).
|
|
36
|
+
path: `src/${entry.name}`,
|
|
37
|
+
purpose: PURPOSE_PLACEHOLDER
|
|
38
|
+
})).toSorted((a, b) => a.name.localeCompare(b.name));
|
|
39
|
+
return { nodes };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/utils/architecture-fingerprint.ts
|
|
43
|
+
var DEPENDENCY_CRUISER_CONFIG_NAMES = [
|
|
44
|
+
".dependency-cruiser.cjs",
|
|
45
|
+
".dependency-cruiser.js",
|
|
46
|
+
".dependency-cruiser.mjs",
|
|
47
|
+
".dependency-cruiser.json"
|
|
48
|
+
];
|
|
49
|
+
var SCHEMA_EXTENSIONS = /* @__PURE__ */ new Set([".sql", ".prisma"]);
|
|
50
|
+
var SHAPE_SCAN_EXCLUDED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
51
|
+
".git",
|
|
52
|
+
".project",
|
|
53
|
+
".safeword",
|
|
54
|
+
"dist",
|
|
55
|
+
"node_modules"
|
|
56
|
+
]);
|
|
57
|
+
var DEPENDENCY_SECTIONS = [
|
|
58
|
+
"dependencies",
|
|
59
|
+
"devDependencies",
|
|
60
|
+
"peerDependencies",
|
|
61
|
+
"optionalDependencies"
|
|
62
|
+
];
|
|
63
|
+
var byString = (a, b) => a.localeCompare(b);
|
|
64
|
+
function collectShapeInputs(projectDirectory) {
|
|
65
|
+
const moduleNames = extractSkeleton(projectDirectory).nodes.map((node) => node.name).toSorted(byString);
|
|
66
|
+
return {
|
|
67
|
+
moduleNames,
|
|
68
|
+
dependencyNames: readDependencyNames(projectDirectory),
|
|
69
|
+
boundaryConfig: readBoundaryConfig(projectDirectory),
|
|
70
|
+
schemaFiles: collectSchemaFiles(projectDirectory)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function shapeFingerprint(projectDirectory) {
|
|
74
|
+
const inputs = collectShapeInputs(projectDirectory);
|
|
75
|
+
return createHash("sha256").update(JSON.stringify(inputs)).digest("hex");
|
|
76
|
+
}
|
|
77
|
+
function readDependencyNames(projectDirectory) {
|
|
78
|
+
const manifest = readJson(nodePath2.join(projectDirectory, "package.json"));
|
|
79
|
+
if (manifest === void 0) return [];
|
|
80
|
+
const names = /* @__PURE__ */ new Set();
|
|
81
|
+
for (const section of DEPENDENCY_SECTIONS) {
|
|
82
|
+
const entry = manifest[section];
|
|
83
|
+
if (entry !== null && typeof entry === "object") {
|
|
84
|
+
for (const name of Object.keys(entry)) names.add(name);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return [...names].toSorted(byString);
|
|
88
|
+
}
|
|
89
|
+
function readBoundaryConfig(projectDirectory) {
|
|
90
|
+
for (const name of DEPENDENCY_CRUISER_CONFIG_NAMES) {
|
|
91
|
+
try {
|
|
92
|
+
return readFileSync(nodePath2.join(projectDirectory, name), "utf8");
|
|
93
|
+
} catch {
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return "";
|
|
97
|
+
}
|
|
98
|
+
function collectSchemaFiles(projectDirectory) {
|
|
99
|
+
const schemaFiles = [];
|
|
100
|
+
const pending = [projectDirectory];
|
|
101
|
+
while (pending.length > 0) {
|
|
102
|
+
const directory = pending.pop();
|
|
103
|
+
if (directory !== void 0) {
|
|
104
|
+
scanDirectoryForSchema(projectDirectory, directory, schemaFiles, pending);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return schemaFiles.toSorted(byString);
|
|
108
|
+
}
|
|
109
|
+
function scanDirectoryForSchema(projectDirectory, directory, schemaFiles, pending) {
|
|
110
|
+
let entries;
|
|
111
|
+
try {
|
|
112
|
+
entries = readdirSync2(directory, { withFileTypes: true });
|
|
113
|
+
} catch {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
for (const entry of entries) {
|
|
117
|
+
if (entry.isDirectory()) {
|
|
118
|
+
if (!SHAPE_SCAN_EXCLUDED_DIRECTORIES.has(entry.name)) {
|
|
119
|
+
pending.push(nodePath2.join(directory, entry.name));
|
|
120
|
+
}
|
|
121
|
+
} else if (SCHEMA_EXTENSIONS.has(nodePath2.extname(entry.name))) {
|
|
122
|
+
const absolutePath = nodePath2.join(directory, entry.name);
|
|
123
|
+
schemaFiles.push(nodePath2.relative(projectDirectory, absolutePath).replaceAll("\\", "/"));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function readJson(filePath) {
|
|
128
|
+
try {
|
|
129
|
+
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
130
|
+
} catch {
|
|
131
|
+
return void 0;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/utils/architecture-reconcile.ts
|
|
136
|
+
function reconcileSections(input) {
|
|
137
|
+
const verdicts = input.nodeNames.map((node) => ({
|
|
138
|
+
node,
|
|
139
|
+
status: liveNodeStatus(input.priorStamps[node], input.fingerprint)
|
|
140
|
+
}));
|
|
141
|
+
const present = new Set(input.nodeNames);
|
|
142
|
+
for (const node of Object.keys(input.priorStamps)) {
|
|
143
|
+
if (!present.has(node)) verdicts.push({ node, status: "orphaned" });
|
|
144
|
+
}
|
|
145
|
+
return verdicts;
|
|
146
|
+
}
|
|
147
|
+
function liveNodeStatus(stamp, fingerprint) {
|
|
148
|
+
if (stamp === void 0) return "placeholder";
|
|
149
|
+
if (stamp !== fingerprint) return "stale";
|
|
150
|
+
return "current";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/utils/architecture-document.ts
|
|
154
|
+
var FINGERPRINT_KEY = "fingerprint";
|
|
155
|
+
var GENERATOR_KEY = "generator";
|
|
156
|
+
var GENERATOR_VALUE = "safeword-architecture";
|
|
157
|
+
function frontmatterBody(content) {
|
|
158
|
+
return /^---\r?\n([\s\S]*?)\r?\n---/.exec(content)?.[1];
|
|
159
|
+
}
|
|
160
|
+
function isSafewordOwned(content) {
|
|
161
|
+
return frontmatterBody(content)?.split(/\r?\n/).includes(`${GENERATOR_KEY}: ${GENERATOR_VALUE}`) ?? false;
|
|
162
|
+
}
|
|
163
|
+
function readDocumentFingerprint(content) {
|
|
164
|
+
const line = frontmatterBody(content)?.split(/\r?\n/).find((candidate) => candidate.startsWith(`${FINGERPRINT_KEY}:`));
|
|
165
|
+
if (line === void 0) return void 0;
|
|
166
|
+
const value = line.slice(FINGERPRINT_KEY.length + 1).trim();
|
|
167
|
+
return value.length > 0 ? value : void 0;
|
|
168
|
+
}
|
|
169
|
+
var RECONCILED_PREFIX = "<!-- reconciled:";
|
|
170
|
+
function selfHeal(projectDirectory) {
|
|
171
|
+
const path = resolveGeneratedArchitecturePath(projectDirectory);
|
|
172
|
+
const fingerprint = shapeFingerprint(projectDirectory);
|
|
173
|
+
const existing = readExisting(path);
|
|
174
|
+
const nodes = extractSkeleton(projectDirectory).nodes;
|
|
175
|
+
const action = decideAction(existing, fingerprint, nodes.length > 0);
|
|
176
|
+
if (action !== "unchanged" && action !== "skipped" && action !== "noop") {
|
|
177
|
+
mkdirSync(nodePath3.dirname(path), { recursive: true });
|
|
178
|
+
const priorStamps = existing === void 0 ? /* @__PURE__ */ new Map() : parseSectionStamps(existing);
|
|
179
|
+
writeFileSync(path, renderDocument(nodes, fingerprint, priorStamps));
|
|
180
|
+
}
|
|
181
|
+
return { action, path };
|
|
182
|
+
}
|
|
183
|
+
function readExisting(path) {
|
|
184
|
+
try {
|
|
185
|
+
return readFileSync2(path, "utf8");
|
|
186
|
+
} catch {
|
|
187
|
+
return void 0;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function decideAction(existing, fingerprint, hasModules) {
|
|
191
|
+
if (existing === void 0) return hasModules ? "created" : "noop";
|
|
192
|
+
if (!isSafewordOwned(existing)) return "skipped";
|
|
193
|
+
const recorded = readDocumentFingerprint(existing);
|
|
194
|
+
if (recorded === void 0) return "regenerated";
|
|
195
|
+
if (recorded !== fingerprint) return "healed";
|
|
196
|
+
return "unchanged";
|
|
197
|
+
}
|
|
198
|
+
function parseSectionStamps(content) {
|
|
199
|
+
const stamps = /* @__PURE__ */ new Map();
|
|
200
|
+
const pattern = /^### (.+)\n+<!-- reconciled: (\S+) -->/gm;
|
|
201
|
+
for (const match of content.matchAll(pattern)) {
|
|
202
|
+
const name = match[1];
|
|
203
|
+
const stamp = match[2];
|
|
204
|
+
if (name !== void 0 && stamp !== void 0) stamps.set(name.trim(), stamp);
|
|
205
|
+
}
|
|
206
|
+
return stamps;
|
|
207
|
+
}
|
|
208
|
+
function renderDocument(nodes, fingerprint, priorStamps) {
|
|
209
|
+
const verdicts = reconcileSections({
|
|
210
|
+
priorStamps: Object.fromEntries(priorStamps),
|
|
211
|
+
nodeNames: nodes.map((node) => node.name),
|
|
212
|
+
fingerprint
|
|
213
|
+
});
|
|
214
|
+
const nodeByName = new Map(nodes.map((node) => [node.name, node]));
|
|
215
|
+
const sections = verdicts.map((verdict) => {
|
|
216
|
+
const node = nodeByName.get(verdict.node);
|
|
217
|
+
const stamp = priorStamps.get(verdict.node) ?? fingerprint;
|
|
218
|
+
return node === void 0 ? renderOrphanSection(verdict.node) : renderSection(node, stamp, verdict.status);
|
|
219
|
+
}).join("\n");
|
|
220
|
+
return `---
|
|
221
|
+
${GENERATOR_KEY}: ${GENERATOR_VALUE}
|
|
222
|
+
${FINGERPRINT_KEY}: ${fingerprint}
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
# Architecture
|
|
226
|
+
|
|
227
|
+
## Modules
|
|
228
|
+
|
|
229
|
+
${sections}`;
|
|
230
|
+
}
|
|
231
|
+
function renderSection(node, stamp, status) {
|
|
232
|
+
const marker = status === "stale" ? "\n> \u26A0 stale: structure changed since this section was reconciled.\n" : "";
|
|
233
|
+
return `### ${node.name}
|
|
234
|
+
|
|
235
|
+
${RECONCILED_PREFIX} ${stamp} -->
|
|
236
|
+
|
|
237
|
+
\`${node.path}\` \u2014 ${node.purpose}
|
|
238
|
+
${marker}`;
|
|
239
|
+
}
|
|
240
|
+
function renderOrphanSection(name) {
|
|
241
|
+
return `### ${name}
|
|
242
|
+
|
|
243
|
+
> \u26A0 orphaned: this section describes a module that no longer exists.
|
|
244
|
+
`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/commands/architecture.ts
|
|
248
|
+
function architecture(cwd = process.cwd()) {
|
|
249
|
+
const result = selfHeal(cwd);
|
|
250
|
+
success(`Architecture state document ${result.action}: ${result.path}`);
|
|
251
|
+
return Promise.resolve();
|
|
252
|
+
}
|
|
253
|
+
export {
|
|
254
|
+
architecture
|
|
255
|
+
};
|
|
256
|
+
//# sourceMappingURL=architecture-CYFAXY2U.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/architecture.ts","../src/utils/architecture-document.ts","../src/utils/architecture-fingerprint.ts","../src/utils/architecture-skeleton.ts","../src/utils/architecture-reconcile.ts"],"sourcesContent":["/**\n * `safeword architecture` — refresh the architecture state document (ticket\n * QD5DTT, Slice 1).\n *\n * Thin CLI entry over `selfHeal`: re-extracts the skeleton and reconciles prose\n * markers at the generated `.project/architecture.generated.md`. The SessionStart hook shells\n * out to this command so the heal logic lives in one place (the CLI), not\n * duplicated into the hook lib.\n */\n\nimport process from 'node:process';\n\nimport { selfHeal } from '../utils/architecture-document.js';\nimport { success } from '../utils/output.js';\n\nexport function architecture(cwd: string = process.cwd()): Promise<void> {\n const result = selfHeal(cwd);\n success(`Architecture state document ${result.action}: ${result.path}`);\n return Promise.resolve();\n}\n","/**\n * Architecture state-document self-heal (ticket QD5DTT, Slice 1).\n *\n * Reads the generated architecture state document at the fixed\n * `<namespace-root>/architecture.generated.md`, compares its recorded\n * shape-fingerprint against the live one, and deterministically\n * (LLM-free) re-extracts the skeleton when they differ — creating the document\n * when absent and regenerating it when its fingerprint is missing or corrupt.\n * This is the SessionStart entry point that keeps structural facts fresh,\n * including after out-of-band human edits.\n */\n\nimport { mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport nodePath from 'node:path';\n\nimport { shapeFingerprint } from './architecture-fingerprint.js';\nimport { reconcileSections, type SectionStatus } from './architecture-reconcile.js';\nimport { extractSkeleton, type SkeletonNode } from './architecture-skeleton.js';\nimport { resolveGeneratedArchitecturePath } from './configured-paths.js';\n\ntype SelfHealAction = 'created' | 'healed' | 'unchanged' | 'regenerated' | 'skipped' | 'noop';\n\nexport interface SelfHealResult {\n action: SelfHealAction;\n path: string;\n}\n\nconst FINGERPRINT_KEY = 'fingerprint';\n\n/** Frontmatter ownership marker — only documents carrying it are safeword's to rewrite. */\nconst GENERATOR_KEY = 'generator';\nconst GENERATOR_VALUE = 'safeword-architecture';\n\n/** The frontmatter body (between the `---` fences), CRLF-tolerant, or undefined. */\nfunction frontmatterBody(content: string): string | undefined {\n return /^---\\r?\\n([\\s\\S]*?)\\r?\\n---/.exec(content)?.[1];\n}\n\n/**\n * Whether safeword owns this document, i.e. it carries the generator marker.\n * A document without it is hand-authored (or foreign) and must never be\n * overwritten — the marker survives even when the fingerprint is corrupted.\n * Exact-line match so a different generator (e.g. `safeword-architecture-v2`)\n * is not mistaken for this one.\n */\nfunction isSafewordOwned(content: string): boolean {\n return (\n frontmatterBody(content)?.split(/\\r?\\n/).includes(`${GENERATOR_KEY}: ${GENERATOR_VALUE}`) ??\n false\n );\n}\n\n/** Parse the recorded fingerprint from a document's frontmatter, or undefined. */\nexport function readDocumentFingerprint(content: string): string | undefined {\n const line = frontmatterBody(content)\n ?.split(/\\r?\\n/)\n .find(candidate => candidate.startsWith(`${FINGERPRINT_KEY}:`));\n if (line === undefined) return undefined;\n\n const value = line.slice(FINGERPRINT_KEY.length + 1).trim();\n return value.length > 0 ? value : undefined;\n}\n\nconst RECONCILED_PREFIX = '<!-- reconciled:';\n\nexport function selfHeal(projectDirectory: string): SelfHealResult {\n const path = resolveGeneratedArchitecturePath(projectDirectory);\n const fingerprint = shapeFingerprint(projectDirectory);\n const existing = readExisting(path);\n const nodes = extractSkeleton(projectDirectory).nodes;\n const action = decideAction(existing, fingerprint, nodes.length > 0);\n\n if (action !== 'unchanged' && action !== 'skipped' && action !== 'noop') {\n mkdirSync(nodePath.dirname(path), { recursive: true });\n const priorStamps = existing === undefined ? new Map() : parseSectionStamps(existing);\n writeFileSync(path, renderDocument(nodes, fingerprint, priorStamps));\n }\n\n return { action, path };\n}\n\nfunction readExisting(path: string): string | undefined {\n try {\n return readFileSync(path, 'utf8');\n } catch {\n return undefined;\n }\n}\n\nfunction decideAction(\n existing: string | undefined,\n fingerprint: string,\n hasModules: boolean,\n): SelfHealAction {\n // Don't birth an empty doc: a contentless \"## Modules\" implies \"no modules\",\n // which is false for a monorepo the single-repo extractor can't read yet.\n // An existing doc still heals toward empty (orphan markers show real removals).\n if (existing === undefined) return hasModules ? 'created' : 'noop';\n\n // Never touch a document safeword does not own — a hand-written architecture\n // doc has no generator marker and must be left exactly as-is.\n if (!isSafewordOwned(existing)) return 'skipped';\n\n const recorded = readDocumentFingerprint(existing);\n if (recorded === undefined) return 'regenerated';\n if (recorded !== fingerprint) return 'healed';\n return 'unchanged';\n}\n\n/**\n * Map each section's node name to the fingerprint it was last reconciled\n * against, so a heal can preserve prior stamps and mark prose that lags the\n * new structure instead of silently bumping it current.\n */\nfunction parseSectionStamps(content: string): Map<string, string> {\n const stamps = new Map<string, string>();\n const pattern = /^### (.+)\\n+<!-- reconciled: (\\S+) -->/gm;\n\n for (const match of content.matchAll(pattern)) {\n const name = match[1];\n const stamp = match[2];\n if (name !== undefined && stamp !== undefined) stamps.set(name.trim(), stamp);\n }\n\n return stamps;\n}\n\nfunction renderDocument(\n nodes: SkeletonNode[],\n fingerprint: string,\n priorStamps: Map<string, string>,\n): string {\n // reconcileSections is the single source of truth for per-section status;\n // this layer only renders markers from its verdicts.\n const verdicts = reconcileSections({\n priorStamps: Object.fromEntries(priorStamps),\n nodeNames: nodes.map(node => node.name),\n fingerprint,\n });\n const nodeByName = new Map(nodes.map(node => [node.name, node]));\n\n const sections = verdicts\n .map(verdict => {\n const node = nodeByName.get(verdict.node);\n // A section the heal has seen before keeps its prior stamp; a brand-new\n // node is stamped current (a placeholder awaiting prose, not stale).\n const stamp = priorStamps.get(verdict.node) ?? fingerprint;\n return node === undefined\n ? renderOrphanSection(verdict.node)\n : renderSection(node, stamp, verdict.status);\n })\n .join('\\n');\n\n return `---\\n${GENERATOR_KEY}: ${GENERATOR_VALUE}\\n${FINGERPRINT_KEY}: ${fingerprint}\\n---\\n\\n# Architecture\\n\\n## Modules\\n\\n${sections}`;\n}\n\nfunction renderSection(node: SkeletonNode, stamp: string, status: SectionStatus): string {\n const marker =\n status === 'stale' ? '\\n> ⚠ stale: structure changed since this section was reconciled.\\n' : '';\n\n return `### ${node.name}\\n\\n${RECONCILED_PREFIX} ${stamp} -->\\n\\n\\`${node.path}\\` — ${node.purpose}\\n${marker}`;\n}\n\nfunction renderOrphanSection(name: string): string {\n return `### ${name}\\n\\n> ⚠ orphaned: this section describes a module that no longer exists.\\n`;\n}\n","/**\n * Shape-fingerprint of a project's architecture-relevant structure (ticket\n * QD5DTT, Slice 1).\n *\n * Hashes the *shape* — top-level module names, dependency names (not versions),\n * the dependency-cruiser boundary config, and schema files — never source-file\n * bytes. So a structural change moves the fingerprint while semantics-preserving\n * noise (a version bump, a comment edit) does not. This is the cheap, LLM-free\n * drift signal the self-heal path compares against the recorded value.\n */\n\nimport { createHash } from 'node:crypto';\nimport { type Dirent, readdirSync, readFileSync } from 'node:fs';\nimport nodePath from 'node:path';\n\nimport { extractSkeleton } from './architecture-skeleton.js';\n\n/** Candidate dependency-cruiser config filenames, in resolution order. */\nconst DEPENDENCY_CRUISER_CONFIG_NAMES = [\n '.dependency-cruiser.cjs',\n '.dependency-cruiser.js',\n '.dependency-cruiser.mjs',\n '.dependency-cruiser.json',\n];\n\n/** File extensions treated as schema definitions. */\nconst SCHEMA_EXTENSIONS = new Set(['.sql', '.prisma']);\n\n/** Directories never walked when collecting schema files. */\nconst SHAPE_SCAN_EXCLUDED_DIRECTORIES = new Set([\n '.git',\n '.project',\n '.safeword',\n 'dist',\n 'node_modules',\n]);\n\n/** Dependency manifest sections whose *keys* contribute to the shape. */\nconst DEPENDENCY_SECTIONS = [\n 'dependencies',\n 'devDependencies',\n 'peerDependencies',\n 'optionalDependencies',\n] as const;\n\n/** Stable string ordering for the fingerprint's sorted inputs. */\nconst byString = (a: string, b: string): number => a.localeCompare(b);\n\ninterface ShapeInputs {\n /** Top-level module names. */\n moduleNames: string[];\n /** Dependency names (keys only — versions are deliberately excluded). */\n dependencyNames: string[];\n /** Raw dependency-cruiser boundary config, or '' when none is present. */\n boundaryConfig: string;\n /** Schema file paths, relative to the project root. */\n schemaFiles: string[];\n}\n\nfunction collectShapeInputs(projectDirectory: string): ShapeInputs {\n const moduleNames = extractSkeleton(projectDirectory)\n .nodes.map(node => node.name)\n .toSorted(byString);\n\n return {\n moduleNames,\n dependencyNames: readDependencyNames(projectDirectory),\n boundaryConfig: readBoundaryConfig(projectDirectory),\n schemaFiles: collectSchemaFiles(projectDirectory),\n };\n}\n\nexport function shapeFingerprint(projectDirectory: string): string {\n const inputs = collectShapeInputs(projectDirectory);\n return createHash('sha256').update(JSON.stringify(inputs)).digest('hex');\n}\n\nfunction readDependencyNames(projectDirectory: string): string[] {\n const manifest = readJson(nodePath.join(projectDirectory, 'package.json'));\n if (manifest === undefined) return [];\n\n const names = new Set<string>();\n for (const section of DEPENDENCY_SECTIONS) {\n const entry = manifest[section];\n if (entry !== null && typeof entry === 'object') {\n for (const name of Object.keys(entry)) names.add(name);\n }\n }\n\n return [...names].toSorted(byString);\n}\n\nfunction readBoundaryConfig(projectDirectory: string): string {\n for (const name of DEPENDENCY_CRUISER_CONFIG_NAMES) {\n try {\n return readFileSync(nodePath.join(projectDirectory, name), 'utf8');\n } catch {\n // Try the next candidate name.\n }\n }\n return '';\n}\n\nfunction collectSchemaFiles(projectDirectory: string): string[] {\n const schemaFiles: string[] = [];\n const pending: string[] = [projectDirectory];\n\n while (pending.length > 0) {\n const directory = pending.pop();\n if (directory !== undefined) {\n scanDirectoryForSchema(projectDirectory, directory, schemaFiles, pending);\n }\n }\n\n return schemaFiles.toSorted(byString);\n}\n\nfunction scanDirectoryForSchema(\n projectDirectory: string,\n directory: string,\n schemaFiles: string[],\n pending: string[],\n): void {\n let entries: Dirent[];\n try {\n entries = readdirSync(directory, { withFileTypes: true });\n } catch {\n return;\n }\n\n for (const entry of entries) {\n if (entry.isDirectory()) {\n if (!SHAPE_SCAN_EXCLUDED_DIRECTORIES.has(entry.name)) {\n pending.push(nodePath.join(directory, entry.name));\n }\n } else if (SCHEMA_EXTENSIONS.has(nodePath.extname(entry.name))) {\n const absolutePath = nodePath.join(directory, entry.name);\n schemaFiles.push(nodePath.relative(projectDirectory, absolutePath).replaceAll('\\\\', '/'));\n }\n }\n}\n\nfunction readJson(filePath: string): Record<string, unknown> | undefined {\n try {\n return JSON.parse(readFileSync(filePath, 'utf8')) as Record<string, unknown>;\n } catch {\n return undefined;\n }\n}\n","/**\n * Deterministic architecture skeleton extractor (ticket QD5DTT, Slice 1).\n *\n * Enumerates the structural facts of a single-repo project — the top-level\n * `src/` modules, each with a code reference and a one-line purpose floor —\n * with zero language-model involvement, so the architecture state doc can\n * never hallucinate structure. The fingerprint and reconcile layers build on\n * the model this returns.\n */\n\nimport { type Dirent, readdirSync } from 'node:fs';\nimport nodePath from 'node:path';\n\n/** Placeholder purpose for a freshly extracted node awaiting human prose. */\nconst PURPOSE_PLACEHOLDER = 'No description yet — awaiting prose.';\n\nexport interface SkeletonNode {\n /** Module name — the top-level `src/` subdirectory. */\n name: string;\n /** Code reference: the module's path relative to the project root. */\n path: string;\n /** One-line purpose (the purpose floor); a placeholder until prose is written. */\n purpose: string;\n}\n\nexport interface Skeleton {\n /** Structural nodes, one per top-level module. */\n nodes: SkeletonNode[];\n}\n\nexport function extractSkeleton(projectDirectory: string): Skeleton {\n const sourceDirectory = nodePath.join(projectDirectory, 'src');\n\n let entries: Dirent[];\n try {\n entries = readdirSync(sourceDirectory, { withFileTypes: true });\n } catch {\n return { nodes: [] };\n }\n\n const nodes = entries\n .filter(entry => entry.isDirectory())\n .map(entry => ({\n name: entry.name,\n // Forward slashes always — the rendered doc and fingerprint must be\n // platform-stable (the fingerprint normalizes paths the same way).\n path: `src/${entry.name}`,\n purpose: PURPOSE_PLACEHOLDER,\n }))\n // Sort by name so the rendered document is deterministic across\n // filesystems (readdirSync order is not guaranteed), like the fingerprint.\n .toSorted((a, b) => a.name.localeCompare(b.name));\n\n return { nodes };\n}\n\n/**\n * The names of nodes that violate the purpose floor — every skeleton node must\n * carry a non-empty one-line purpose. Catches a doc whose purpose was blanked\n * (e.g. hand-edited away), which would otherwise leave the floor unenforced.\n */\nexport function purposeFloorViolations(nodes: SkeletonNode[]): string[] {\n return nodes.filter(node => node.purpose.trim().length === 0).map(node => node.name);\n}\n","/**\n * Reconcile prose sections against the current structure (ticket QD5DTT,\n * Slice 1).\n *\n * Deterministic, LLM-free: the single source of truth for per-section status.\n * Given each section's recorded `reconciled` stamp and the live skeleton, it\n * classifies every node — current, stale, orphaned, or placeholder — and the\n * document layer renders markers from these verdicts. This is what lets the\n * doc be incomplete (prose lagging, visibly marked) yet never silently wrong.\n */\n\nexport type SectionStatus = 'current' | 'stale' | 'orphaned' | 'placeholder';\n\nexport interface ReconcileInput {\n /** Node name → the fingerprint its section was last reconciled against. */\n priorStamps: Record<string, string>;\n /** Names of the nodes in the current skeleton. */\n nodeNames: string[];\n /** The current live skeleton fingerprint. */\n fingerprint: string;\n}\n\nexport interface SectionVerdict {\n node: string;\n status: SectionStatus;\n}\n\nexport function reconcileSections(input: ReconcileInput): SectionVerdict[] {\n const verdicts: SectionVerdict[] = input.nodeNames.map(node => ({\n node,\n status: liveNodeStatus(input.priorStamps[node], input.fingerprint),\n }));\n\n // A prior section whose node is gone is orphaned — surfaced, never dropped.\n const present = new Set(input.nodeNames);\n for (const node of Object.keys(input.priorStamps)) {\n if (!present.has(node)) verdicts.push({ node, status: 'orphaned' });\n }\n\n return verdicts;\n}\n\nfunction liveNodeStatus(stamp: string | undefined, fingerprint: string): SectionStatus {\n // No prior stamp → the node is new this reconcile: a placeholder awaiting\n // prose, not stale. A surviving section is stale exactly when its stamp has\n // fallen behind the live fingerprint.\n if (stamp === undefined) return 'placeholder';\n if (stamp !== fingerprint) return 'stale';\n return 'current';\n}\n"],"mappings":"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","names":["readFileSync","nodePath","readdirSync","nodePath","nodePath","readdirSync","nodePath","readFileSync"]}
|
|
@@ -4,17 +4,17 @@ import {
|
|
|
4
4
|
import {
|
|
5
5
|
checkHealth,
|
|
6
6
|
reportHealthSummary
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-IE32BCVN.js";
|
|
8
8
|
import {
|
|
9
9
|
syncTickets
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-5ES7OYBI.js";
|
|
11
11
|
import "./chunk-NHXVS5FL.js";
|
|
12
|
-
import "./chunk-
|
|
13
|
-
import "./chunk-
|
|
14
|
-
import "./chunk-
|
|
15
|
-
import "./chunk-
|
|
16
|
-
import "./chunk-
|
|
17
|
-
import "./chunk-
|
|
12
|
+
import "./chunk-YXNI7W5D.js";
|
|
13
|
+
import "./chunk-XTLCJKGE.js";
|
|
14
|
+
import "./chunk-JQVVJCLH.js";
|
|
15
|
+
import "./chunk-WJOSBJ37.js";
|
|
16
|
+
import "./chunk-GT7KMCFG.js";
|
|
17
|
+
import "./chunk-PHR2K2Y3.js";
|
|
18
18
|
import "./chunk-HSC7TELY.js";
|
|
19
19
|
import {
|
|
20
20
|
header,
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
keyValue,
|
|
23
23
|
success,
|
|
24
24
|
warn
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-I2GV5QKO.js";
|
|
26
26
|
|
|
27
27
|
// src/commands/check.ts
|
|
28
28
|
import process from "process";
|
|
@@ -110,4 +110,4 @@ async function check(options) {
|
|
|
110
110
|
export {
|
|
111
111
|
check
|
|
112
112
|
};
|
|
113
|
-
//# sourceMappingURL=check-
|
|
113
|
+
//# sourceMappingURL=check-TKA2IIC7.js.map
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
} from "./chunk-NHXVS5FL.js";
|
|
4
4
|
import {
|
|
5
5
|
resolveTicketsDirectory
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-WJOSBJ37.js";
|
|
7
7
|
|
|
8
8
|
// src/ticket-sync/index.ts
|
|
9
9
|
import { existsSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -42,22 +42,25 @@ function findTicketsInCycles(nodes) {
|
|
|
42
42
|
const edges = new Map(nodes.map((node) => [node.id, node.dependsOn]));
|
|
43
43
|
const inCycle = /* @__PURE__ */ new Set();
|
|
44
44
|
for (const start of edges.keys()) {
|
|
45
|
-
|
|
46
|
-
const seen = /* @__PURE__ */ new Set();
|
|
47
|
-
while (stack.length > 0) {
|
|
48
|
-
const next = stack.pop();
|
|
49
|
-
if (next === void 0) continue;
|
|
50
|
-
if (next === start) {
|
|
51
|
-
inCycle.add(start);
|
|
52
|
-
break;
|
|
53
|
-
}
|
|
54
|
-
if (seen.has(next)) continue;
|
|
55
|
-
seen.add(next);
|
|
56
|
-
stack.push(...edges.get(next) ?? []);
|
|
57
|
-
}
|
|
45
|
+
if (reachesSelf(start, edges)) inCycle.add(start);
|
|
58
46
|
}
|
|
59
47
|
return [...inCycle].toSorted((a, b) => a.localeCompare(b));
|
|
60
48
|
}
|
|
49
|
+
function reachesSelf(start, edges) {
|
|
50
|
+
const stack = [...edges.get(start) ?? []];
|
|
51
|
+
const seen = /* @__PURE__ */ new Set();
|
|
52
|
+
while (stack.length > 0) {
|
|
53
|
+
const next = stack.pop();
|
|
54
|
+
if (next === void 0) continue;
|
|
55
|
+
if (next === start) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
if (seen.has(next)) continue;
|
|
59
|
+
seen.add(next);
|
|
60
|
+
stack.push(...edges.get(next) ?? []);
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
61
64
|
|
|
62
65
|
// src/ticket-sync/index.ts
|
|
63
66
|
var TICKETS_RELATIVE_PATH = "<namespace-root>/tickets";
|
|
@@ -182,7 +185,7 @@ function groupByEpic(entries) {
|
|
|
182
185
|
if (bucket) bucket.push(entry);
|
|
183
186
|
else groups.set(key, [entry]);
|
|
184
187
|
}
|
|
185
|
-
return [...groups
|
|
188
|
+
return [...groups].toSorted(([a], [b]) => {
|
|
186
189
|
if (a === NO_EPIC_GROUP) return 1;
|
|
187
190
|
if (b === NO_EPIC_GROUP) return -1;
|
|
188
191
|
return a.localeCompare(b);
|
|
@@ -225,11 +228,11 @@ function syncTickets(cwd) {
|
|
|
225
228
|
return { wrote: false, active: [], completed: [], skipped: [], indexPath, completedIndexPath };
|
|
226
229
|
}
|
|
227
230
|
const { active, completed, skipped } = readTickets(ticketsDirectory, relativeLabel);
|
|
228
|
-
const
|
|
231
|
+
const isWroteActive = writeIfChanged(indexPath, buildIndexContent(active, { variant: "active" }));
|
|
229
232
|
const completedDirectory = nodePath.join(ticketsDirectory, COMPLETED_DIRNAME);
|
|
230
|
-
const
|
|
233
|
+
const isWroteCompleted = completed.length > 0 || existsSync(completedDirectory) ? writeIfChanged(completedIndexPath, buildIndexContent(completed, { variant: "completed" })) : false;
|
|
231
234
|
return {
|
|
232
|
-
wrote:
|
|
235
|
+
wrote: isWroteActive || isWroteCompleted,
|
|
233
236
|
active,
|
|
234
237
|
completed,
|
|
235
238
|
skipped,
|
|
@@ -244,4 +247,4 @@ export {
|
|
|
244
247
|
readTickets,
|
|
245
248
|
syncTickets
|
|
246
249
|
};
|
|
247
|
-
//# sourceMappingURL=chunk-
|
|
250
|
+
//# sourceMappingURL=chunk-5ES7OYBI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ticket-sync/index.ts","../src/utils/ticket-relations.ts"],"sourcesContent":["/**\n * Ticket sync — generates capability-discovery indexes over the ticket corpus:\n * `<namespace-root>/tickets/INDEX.md` (active tickets, grouped by epic) and\n * `INDEX-completed.md` (the `completed/` archive). Mirrors `learning-sync`\n * (plain markdown + grep, no skill-description char cap) so \"is there already\n * a ticket for X?\" is one grep instead of a hundreds-of-folders hunt.\n *\n * Fired manually via `safeword sync-tickets`, as a `safeword check` step, and\n * after `ticket new`.\n *\n * Ticket 1GGD28.\n */\n\nimport { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport nodePath from 'node:path';\n\nimport { resolveTicketsDirectory } from '../utils/configured-paths.js';\nimport { formatTicketReference } from '../utils/ticket-reference.js';\nimport { deriveBlocks, parseTicketIdList } from '../utils/ticket-relations.js';\n\n/** Placeholder label for callers that read a directory without a project cwd. */\nexport const TICKETS_RELATIVE_PATH = '<namespace-root>/tickets';\nexport const INDEX_FILENAME = 'INDEX.md';\nexport const COMPLETED_INDEX_FILENAME = 'INDEX-completed.md';\nexport const COMPLETED_DIRNAME = 'completed';\n\nconst NO_EPIC_GROUP = '(no epic)';\nconst SKIP_DIRECTORIES = new Set([COMPLETED_DIRNAME, 'tmp']);\n\nexport interface TicketEntry {\n id: string;\n folder: string; // folder name, e.g. 1GGD28-ticket-discovery-index\n relativePath: string; // e.g. <namespace-root>/tickets/1GGD28-ticket-discovery-index\n title: string;\n status: string;\n epic: string | undefined; // undefined → grouped under \"(no epic)\"\n goal: string | undefined; // the **Goal:** one-liner, when present\n dependsOn: string[]; // ticket ids this one depends on (directed edge); [] when none\n}\n\nexport interface TicketSyncResult {\n wrote: boolean;\n active: TicketEntry[];\n completed: TicketEntry[];\n skipped: { folder: string; reason: string }[];\n indexPath: string;\n completedIndexPath: string;\n}\n\n/** Strip a single layer of matching surrounding quotes. */\nfunction stripQuotes(value: string): string {\n if (\n value.length >= 2 &&\n ((value.startsWith(\"'\") && value.endsWith(\"'\")) ||\n (value.startsWith('\"') && value.endsWith('\"')))\n ) {\n return value.slice(1, -1);\n }\n return value;\n}\n\n/** Parse the leading `--- … ---` frontmatter block into a key→value map. */\nfunction parseFrontmatter(content: string): { fields: Map<string, string>; bodyStart: number } {\n const lines = content.split('\\n');\n const fields = new Map<string, string>();\n if (lines[0]?.trim() !== '---') return { fields, bodyStart: 0 };\n\n for (let index = 1; index < lines.length; index += 1) {\n const line = lines[index] ?? '';\n if (line.trim() === '---') return { fields, bodyStart: index + 1 };\n const match = /^([a-z_][\\w-]*):(.*)$/i.exec(line);\n if (match?.[1] !== undefined) fields.set(match[1], stripQuotes((match[2] ?? '').trim()));\n }\n return { fields, bodyStart: 0 };\n}\n\n/** First `# H1` heading text in the body, if any. */\nfunction firstHeading(bodyLines: string[]): string | undefined {\n for (const line of bodyLines) {\n if (line.startsWith('# ')) return line.slice(2).trim();\n }\n return undefined;\n}\n\n/** The `**Goal:**` one-liner from the body, label stripped, if present. */\nfunction goalLine(bodyLines: string[]): string | undefined {\n for (const line of bodyLines) {\n const match = /^\\*\\*Goal:\\*\\*(.*)$/.exec(line.trim());\n if (match?.[1] !== undefined) {\n const goal = match[1].trim();\n if (goal.length > 0) return goal;\n }\n }\n return undefined;\n}\n\n/**\n * Parse a single ticket.md. Returns the entry (minus relativePath) when it has\n * an `id:`, or a skip reason. Title resolves frontmatter `title` → first H1 →\n * frontmatter `slug` → folder name.\n */\nfunction parseTicket(\n filePath: string,\n folder: string,\n): { ok: true; entry: Omit<TicketEntry, 'relativePath'> } | { ok: false; reason: string } {\n const content = readFileSync(filePath, 'utf8');\n const { fields, bodyStart } = parseFrontmatter(content);\n\n const id = fields.get('id');\n if (id === undefined || id.length === 0) {\n return { ok: false, reason: 'missing id: in frontmatter' };\n }\n\n const bodyLines = content.split('\\n').slice(bodyStart);\n const title = fields.get('title') ?? firstHeading(bodyLines) ?? fields.get('slug') ?? folder;\n const status = fields.get('status') ?? '—';\n const epic = fields.get('epic');\n\n return {\n ok: true,\n entry: {\n id,\n folder,\n title,\n status,\n epic,\n goal: goalLine(bodyLines),\n dependsOn: parseTicketIdList(fields.get('depends_on')),\n },\n };\n}\n\n/** Parse every ticket folder directly under `directory`, returning entries +\n * skip reasons. Folders without a ticket.md are silently ignored (not skipped).\n * `pathPrefix` is prepended to the folder for the entry's relativePath. */\nfunction readTicketFolders(\n directory: string,\n pathPrefix: string,\n): { entries: TicketEntry[]; skipped: { folder: string; reason: string }[] } {\n if (!existsSync(directory)) return { entries: [], skipped: [] };\n\n const entries: TicketEntry[] = [];\n const skipped: { folder: string; reason: string }[] = [];\n\n const folders = readdirSync(directory, { withFileTypes: true })\n .filter(dirent => dirent.isDirectory() && !SKIP_DIRECTORIES.has(dirent.name))\n .map(dirent => dirent.name)\n .toSorted((a, b) => a.localeCompare(b));\n\n for (const folder of folders) {\n const ticketPath = nodePath.join(directory, folder, 'ticket.md');\n if (!existsSync(ticketPath)) continue; // not a ticket folder — ignore\n const parsed = parseTicket(ticketPath, folder);\n if (parsed.ok) {\n entries.push({ ...parsed.entry, relativePath: `${pathPrefix}/${folder}` });\n } else {\n skipped.push({ folder, reason: parsed.reason });\n }\n }\n\n return { entries, skipped };\n}\n\n/**\n * Read the corpus into active (top-level) and completed (`completed/`) entries,\n * each sorted by id, plus any skipped folders. INDEX*.md are files, so the\n * directory filter excludes them from being parsed as tickets.\n */\nexport function readTickets(\n ticketsDirectory: string,\n relativeLabel: string = TICKETS_RELATIVE_PATH,\n): {\n active: TicketEntry[];\n completed: TicketEntry[];\n skipped: { folder: string; reason: string }[];\n} {\n const active = readTicketFolders(ticketsDirectory, relativeLabel);\n const completed = readTicketFolders(\n nodePath.join(ticketsDirectory, COMPLETED_DIRNAME),\n `${relativeLabel}/${COMPLETED_DIRNAME}`,\n );\n\n const byId = (a: TicketEntry, b: TicketEntry) => a.id.localeCompare(b.id);\n return {\n active: active.entries.toSorted(byId),\n completed: completed.entries.toSorted(byId),\n skipped: [...active.skipped, ...completed.skipped],\n };\n}\n\n/** Render a list of related ticket ids slug-first, falling back to the bare id\n * for targets outside this index (cross-variant or not-yet-created). */\nfunction renderRelation(ids: string[], labelById: Map<string, string>): string {\n return ids\n .map(id => {\n const title = labelById.get(id);\n return title === undefined ? id : formatTicketReference(id, title);\n })\n .join(', ');\n}\n\n/** Render one entry as a block: header, optional goal, relation edges, path. */\nfunction renderEntry(\n entry: TicketEntry,\n blocks: Map<string, string[]>,\n labelById: Map<string, string>,\n): string[] {\n const epic = entry.epic ?? '—';\n const lines = [\n `- **${formatTicketReference(entry.id, entry.title)}** (${entry.status}, epic: ${epic})`,\n ];\n if (entry.goal !== undefined) lines.push(` ${entry.goal}`);\n if (entry.dependsOn.length > 0) {\n lines.push(` blocked by: ${renderRelation(entry.dependsOn, labelById)}`);\n }\n const blocking = blocks.get(entry.id) ?? [];\n if (blocking.length > 0) lines.push(` blocks: ${renderRelation(blocking, labelById)}`);\n lines.push(` → \\`${entry.relativePath}\\``);\n return lines;\n}\n\n/** Group entries by epic; \"(no epic)\" sorts last, every other group alphabetical. */\nfunction groupByEpic(entries: TicketEntry[]): [string, TicketEntry[]][] {\n const groups = new Map<string, TicketEntry[]>();\n for (const entry of entries) {\n const key = entry.epic ?? NO_EPIC_GROUP;\n const bucket = groups.get(key);\n if (bucket) bucket.push(entry);\n else groups.set(key, [entry]);\n }\n return [...groups].toSorted(([a], [b]) => {\n if (a === NO_EPIC_GROUP) return 1;\n if (b === NO_EPIC_GROUP) return -1;\n return a.localeCompare(b);\n });\n}\n\n/**\n * Render the full index for one variant. Deterministic: same entries → same\n * bytes. No size cap — agents Read or grep the file.\n */\nexport function buildIndexContent(\n entries: TicketEntry[],\n options: { variant: 'active' | 'completed' },\n): string {\n const isActive = options.variant === 'active';\n const header = [\n isActive ? '# Project Tickets — Index' : '# Project Tickets — Completed Archive',\n '',\n '<!-- Auto-generated by `safeword sync-tickets`. Do not edit by hand. -->',\n isActive\n ? '<!-- Active tickets, grouped by epic. Completed tickets live in INDEX-completed.md. -->'\n : '<!-- Completed tickets (the completed/ archive), grouped by epic. -->',\n '',\n ];\n\n if (entries.length === 0) {\n return [...header, isActive ? 'No active tickets.' : 'No completed tickets.', ''].join('\\n');\n }\n\n const blocks = deriveBlocks(entries);\n const labelById = new Map(entries.map(entry => [entry.id, entry.title]));\n\n const lines = [...header, `## Tickets (${entries.length})`, ''];\n for (const [epic, group] of groupByEpic(entries)) {\n lines.push(`### ${epic}`, '');\n for (const entry of group) lines.push(...renderEntry(entry, blocks, labelById));\n lines.push('');\n }\n return lines.join('\\n');\n}\n\n/** Write `content` to `path` only when it differs; report whether it wrote. */\nfunction writeIfChanged(path: string, content: string): boolean {\n const previous = existsSync(path) ? readFileSync(path, 'utf8') : undefined;\n if (previous === content) return false;\n writeFileSync(path, content);\n return true;\n}\n\n/**\n * Generate/update both ticket indexes from the corpus. No-op (creates nothing)\n * when the tickets directory is absent. The completed archive is written when\n * a `completed/` directory exists or completed entries are present.\n */\nexport function syncTickets(cwd: string): TicketSyncResult {\n const ticketsDirectory = resolveTicketsDirectory(cwd);\n const relativeLabel = nodePath.relative(cwd, ticketsDirectory) || TICKETS_RELATIVE_PATH;\n const indexPath = nodePath.join(ticketsDirectory, INDEX_FILENAME);\n const completedIndexPath = nodePath.join(ticketsDirectory, COMPLETED_INDEX_FILENAME);\n\n if (!existsSync(ticketsDirectory)) {\n return { wrote: false, active: [], completed: [], skipped: [], indexPath, completedIndexPath };\n }\n\n const { active, completed, skipped } = readTickets(ticketsDirectory, relativeLabel);\n\n const isWroteActive = writeIfChanged(indexPath, buildIndexContent(active, { variant: 'active' }));\n\n const completedDirectory = nodePath.join(ticketsDirectory, COMPLETED_DIRNAME);\n const isWroteCompleted =\n completed.length > 0 || existsSync(completedDirectory)\n ? writeIfChanged(completedIndexPath, buildIndexContent(completed, { variant: 'completed' }))\n : false;\n\n return {\n wrote: isWroteActive || isWroteCompleted,\n active,\n completed,\n skipped,\n indexPath,\n completedIndexPath,\n };\n}\n","/**\n * Structured ticket relations (ticket AKZJXC).\n *\n * One canonical directed edge — `depends_on` — stored as an inline-array scalar\n * the hand-rolled frontmatter parser can hold. The inverse (`blocks`) is always\n * derived across the corpus; cycles and dangling refs surface as warnings, never\n * errors (mirrors safeword's tolerant ID resolution).\n */\n\n/** A ticket reduced to its id and its outgoing `depends_on` edges. */\nexport interface TicketNode {\n id: string;\n dependsOn: string[];\n}\n\n/**\n * Parse a `depends_on` frontmatter scalar into ticket ids. Accepts the inline\n * array form (`[A, B]`) or a bare comma list (`A, B`); trims each id and drops\n * empties. Missing/empty input → `[]`.\n * @param raw the raw frontmatter value, or undefined when the key is absent\n */\nexport function parseTicketIdList(raw?: string): string[] {\n if (raw === undefined) return [];\n const inner = raw.trim().replace(/^\\[/, '').replace(/\\]$/, '');\n return inner\n .split(',')\n .map(id => id.trim())\n .filter(id => id.length > 0);\n}\n\n/**\n * Invert the `depends_on` graph into `id → ids that depend on it` (the derived\n * `blocks` edges). Only ids that block something appear as keys; each value\n * preserves corpus order.\n * @param nodes every ticket's id + depends_on edges\n */\nexport function deriveBlocks(nodes: TicketNode[]): Map<string, string[]> {\n const blocks = new Map<string, string[]>();\n for (const node of nodes) {\n for (const target of node.dependsOn) {\n const blockers = blocks.get(target) ?? [];\n blockers.push(node.id);\n blocks.set(target, blockers);\n }\n }\n return blocks;\n}\n\n/**\n * `depends_on` targets absent from the corpus, as `{from, missing}` pairs sorted\n * by from then missing. Warn-only — a target may live on another branch or in\n * completed/.\n * @param nodes every ticket's id + depends_on edges\n */\nexport function findDanglingDependencies(nodes: TicketNode[]): { from: string; missing: string }[] {\n const known = new Set(nodes.map(node => node.id));\n const dangling: { from: string; missing: string }[] = [];\n for (const node of nodes) {\n for (const target of node.dependsOn) {\n if (!known.has(target)) dangling.push({ from: node.id, missing: target });\n }\n }\n return dangling.toSorted(\n (a, b) => a.from.localeCompare(b.from) || a.missing.localeCompare(b.missing),\n );\n}\n\n/**\n * Sorted ids of tickets that participate in any `depends_on` cycle (a node\n * reachable from itself, including a self-edge). Warn-only. Dangling targets are\n * inert — they have no outgoing edges, so they can't form a cycle.\n * @param nodes every ticket's id + depends_on edges\n */\nexport function findTicketsInCycles(nodes: TicketNode[]): string[] {\n const edges = new Map(nodes.map(node => [node.id, node.dependsOn]));\n const inCycle = new Set<string>();\n\n for (const start of edges.keys()) {\n if (reachesSelf(start, edges)) inCycle.add(start);\n }\n\n return [...inCycle].toSorted((a, b) => a.localeCompare(b));\n}\n\n/**\n * DFS along depends_on edges from `start`; returns true when `start` is\n * reachable from itself (it lies on a cycle, including via a self-edge).\n */\nfunction reachesSelf(start: string, edges: Map<string, string[]>): boolean {\n const stack = [...(edges.get(start) ?? [])];\n const seen = new Set<string>();\n while (stack.length > 0) {\n const next = stack.pop();\n if (next === undefined) continue;\n if (next === start) {\n return true;\n }\n if (seen.has(next)) continue;\n seen.add(next);\n stack.push(...(edges.get(next) ?? []));\n }\n return false;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
info,
|
|
5
5
|
readJson,
|
|
6
6
|
success
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-I2GV5QKO.js";
|
|
8
8
|
|
|
9
9
|
// src/commands/sync-config.ts
|
|
10
10
|
import { readFileSync, writeFileSync } from "fs";
|
|
@@ -132,7 +132,7 @@ function generateMonorepoRules(workspaces) {
|
|
|
132
132
|
}
|
|
133
133
|
return rules.join(",\n");
|
|
134
134
|
}
|
|
135
|
-
function
|
|
135
|
+
function generateDependencyCruiseConfigFile(arch) {
|
|
136
136
|
const monorepoRules = arch.workspaces ? generateMonorepoRules(arch.workspaces) : "";
|
|
137
137
|
const hasMonorepoRules = monorepoRules.length > 0;
|
|
138
138
|
return String.raw`module.exports = {
|
|
@@ -217,7 +217,7 @@ ${monorepoRules},` : ""}
|
|
|
217
217
|
};
|
|
218
218
|
`;
|
|
219
219
|
}
|
|
220
|
-
function
|
|
220
|
+
function generateDependencyCruiseMainConfig() {
|
|
221
221
|
return `/**
|
|
222
222
|
* Dependency Cruiser Configuration
|
|
223
223
|
*
|
|
@@ -249,12 +249,12 @@ function syncConfigCore(cwd, arch) {
|
|
|
249
249
|
createdMainConfig: false
|
|
250
250
|
};
|
|
251
251
|
const generatedConfigPath = nodePath3.join(safewordDirectory, "depcruise-config.cjs");
|
|
252
|
-
const generatedConfig =
|
|
252
|
+
const generatedConfig = generateDependencyCruiseConfigFile(arch);
|
|
253
253
|
writeFileSync(generatedConfigPath, generatedConfig);
|
|
254
254
|
result.generatedConfig = true;
|
|
255
255
|
const mainConfigPath = nodePath3.join(cwd, ".dependency-cruiser.cjs");
|
|
256
256
|
if (!exists(mainConfigPath)) {
|
|
257
|
-
const mainConfig =
|
|
257
|
+
const mainConfig = generateDependencyCruiseMainConfig();
|
|
258
258
|
writeFileSync(mainConfigPath, mainConfig);
|
|
259
259
|
result.createdMainConfig = true;
|
|
260
260
|
}
|
|
@@ -273,7 +273,7 @@ function checkConfig(cwd, arch) {
|
|
|
273
273
|
if (!exists(generatedConfigPath)) {
|
|
274
274
|
return { matches: false, reason: "missing" };
|
|
275
275
|
}
|
|
276
|
-
const generated =
|
|
276
|
+
const generated = generateDependencyCruiseConfigFile(arch);
|
|
277
277
|
const onDisk = readFileSync(generatedConfigPath, "utf8");
|
|
278
278
|
return generated === onDisk ? { matches: true } : { matches: false, reason: "drifted" };
|
|
279
279
|
}
|
|
@@ -312,4 +312,4 @@ export {
|
|
|
312
312
|
hasArchitectureDetected,
|
|
313
313
|
syncConfig
|
|
314
314
|
};
|
|
315
|
-
//# sourceMappingURL=chunk-
|
|
315
|
+
//# sourceMappingURL=chunk-G3BDQLVU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/sync-config.ts","../src/utils/boundaries.ts","../src/utils/depcruise-config.ts"],"sourcesContent":["/**\n * Sync Config command - Regenerate depcruise config from current project structure\n *\n * Default mode writes config to disk. `--check` mode reports drift without writing —\n * used by `/audit` to detect stale config without polluting the working tree.\n */\n\nimport { readFileSync, writeFileSync } from 'node:fs';\nimport nodePath from 'node:path';\n\nimport { detectArchitecture } from '../utils/boundaries.js';\nimport {\n type DependencyCruiseArchitecture,\n detectWorkspaces,\n generateDependencyCruiseConfigFile,\n generateDependencyCruiseMainConfig,\n} from '../utils/depcruise-config.js';\nimport { exists } from '../utils/fs.js';\nimport { error, info, success } from '../utils/output.js';\n\ninterface SyncConfigResult {\n generatedConfig: boolean;\n createdMainConfig: boolean;\n}\n\n/**\n * Core sync logic - writes depcruise configs to disk\n * Can be called from setup or as standalone command\n */\nexport function syncConfigCore(cwd: string, arch: DependencyCruiseArchitecture): SyncConfigResult {\n const safewordDirectory = nodePath.join(cwd, '.safeword');\n const result: SyncConfigResult = {\n generatedConfig: false,\n createdMainConfig: false,\n };\n\n // Generate and write .safeword/depcruise-config.cjs (CJS for compatibility)\n const generatedConfigPath = nodePath.join(safewordDirectory, 'depcruise-config.cjs');\n const generatedConfig = generateDependencyCruiseConfigFile(arch);\n writeFileSync(generatedConfigPath, generatedConfig);\n result.generatedConfig = true;\n\n // Create main config if not exists (self-healing)\n // Use .cjs extension to work in ESM projects (type: \"module\")\n const mainConfigPath = nodePath.join(cwd, '.dependency-cruiser.cjs');\n if (!exists(mainConfigPath)) {\n const mainConfig = generateDependencyCruiseMainConfig();\n writeFileSync(mainConfigPath, mainConfig);\n result.createdMainConfig = true;\n }\n\n return result;\n}\n\n/**\n * Build full architecture info by combining detected layers with workspaces\n */\nexport function buildArchitecture(cwd: string): DependencyCruiseArchitecture {\n const arch = detectArchitecture(cwd);\n const workspaces = detectWorkspaces(cwd);\n return { ...arch, workspaces };\n}\n\n/**\n * Check if architecture was detected (layers, monorepo structure, or workspaces)\n */\nexport function hasArchitectureDetected(arch: DependencyCruiseArchitecture): boolean {\n return arch.elements.length > 0 || arch.isMonorepo || (arch.workspaces?.length ?? 0) > 0;\n}\n\n/**\n * Check if generated config matches on-disk content. No writes.\n * Returns { matches: true } when bytes are byte-equal.\n * Returns { matches: false, reason } when on-disk is missing or differs.\n */\nfunction checkConfig(\n cwd: string,\n arch: DependencyCruiseArchitecture,\n): { matches: true } | { matches: false; reason: 'missing' | 'drifted' } {\n const generatedConfigPath = nodePath.join(cwd, '.safeword', 'depcruise-config.cjs');\n if (!exists(generatedConfigPath)) {\n return { matches: false, reason: 'missing' };\n }\n const generated = generateDependencyCruiseConfigFile(arch);\n const onDisk = readFileSync(generatedConfigPath, 'utf8');\n return generated === onDisk ? { matches: true } : { matches: false, reason: 'drifted' };\n}\n\n/**\n * CLI command: Sync depcruise config with current project structure\n */\n\nexport async function syncConfig(options: { check?: boolean } = {}): Promise<void> {\n // Public CLI command contract is Promise<void>; body is sync today but the\n // signature reserves room for async I/O. Token await keeps the contract honest.\n await Promise.resolve();\n const cwd = process.cwd();\n const safewordDirectory = nodePath.join(cwd, '.safeword');\n\n // Check if .safeword exists\n if (!exists(safewordDirectory)) {\n error('Not configured. Run `safeword setup` first.');\n process.exit(1);\n }\n\n const arch = buildArchitecture(cwd);\n\n if (options.check) {\n const result = checkConfig(cwd, arch);\n if (result.matches) {\n success('Config in sync');\n return;\n }\n const message =\n result.reason === 'missing'\n ? 'Missing .safeword/depcruise-config.cjs — run `safeword sync-config` to generate it.'\n : 'Stale .safeword/depcruise-config.cjs — run `safeword sync-config` to refresh.';\n error(message);\n process.exit(1);\n }\n\n const result = syncConfigCore(cwd, arch);\n\n if (result.generatedConfig) {\n info('Generated .safeword/depcruise-config.cjs');\n }\n if (result.createdMainConfig) {\n info('Created .dependency-cruiser.cjs');\n }\n\n success('Config synced');\n}\n","/**\n * Architecture boundaries detection\n *\n * Auto-detects common architecture directories for use by\n * dependency-cruiser layer enforcement.\n *\n * Supports:\n * - Standard projects (src/utils, utils/)\n * - Monorepos (packages/*, apps/*)\n * - Various naming conventions (helpers, shared, core, etc.)\n */\n\nimport { readdirSync } from 'node:fs';\nimport nodePath from 'node:path';\n\nimport { exists } from './fs.js';\n\n/**\n * Architecture layer definitions with alternative names.\n * Each layer maps to equivalent directory names.\n * Order defines hierarchy: earlier = lower layer.\n */\nconst ARCHITECTURE_LAYERS = [\n // Layer 0: Pure types (no imports)\n { layer: 'types', dirs: ['types', 'interfaces', 'schemas'] },\n // Layer 1: Utilities (only types)\n { layer: 'utils', dirs: ['utils', 'helpers', 'shared', 'common', 'core'] },\n // Layer 2: Libraries (types, utils)\n { layer: 'lib', dirs: ['lib', 'libraries'] },\n // Layer 3: State & logic (types, utils, lib)\n { layer: 'hooks', dirs: ['hooks', 'composables'] },\n { layer: 'services', dirs: ['services', 'api', 'stores', 'state'] },\n // Layer 4: UI components (all above)\n { layer: 'components', dirs: ['components', 'ui'] },\n // Layer 5: Features (all above)\n { layer: 'features', dirs: ['features', 'modules', 'domains'] },\n // Layer 6: Entry points (can import everything)\n { layer: 'app', dirs: ['app', 'pages', 'views', 'routes', 'commands'] },\n] as const;\n\ntype Layer = (typeof ARCHITECTURE_LAYERS)[number]['layer'];\n\ninterface DetectedElement {\n layer: Layer;\n pattern: string; // glob pattern for boundaries config\n location: string; // human-readable location\n}\n\nexport interface DetectedArchitecture {\n elements: DetectedElement[];\n isMonorepo: boolean;\n}\n\n/**\n * Find monorepo package directories\n * @param projectDirectory\n */\nfunction findMonorepoPackages(projectDirectory: string): string[] {\n const packages: string[] = [];\n\n // Check common monorepo patterns\n const monorepoRoots = ['packages', 'apps', 'libs', 'modules'];\n\n for (const root of monorepoRoots) {\n const rootPath = nodePath.join(projectDirectory, root);\n if (!exists(rootPath)) continue;\n\n try {\n const entries = readdirSync(rootPath, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.isDirectory() && !entry.name.startsWith('.')) {\n packages.push(nodePath.join(root, entry.name));\n }\n }\n } catch {\n // Directory not readable, skip\n }\n }\n\n return packages;\n}\n\n/**\n * Check if a layer already exists for this path prefix\n * @param elements\n * @param layer\n * @param pathPrefix\n */\nfunction hasLayerForPrefix(elements: DetectedElement[], layer: Layer, pathPrefix: string): boolean {\n return elements.some(\n element => element.layer === layer && element.pattern.startsWith(pathPrefix),\n );\n}\n\n/**\n * Scan a single search path for architecture layers\n * @param projectDirectory\n * @param searchPath\n * @param pathPrefix\n * @param elements\n */\n/**\n * A `features/` directory holding Gherkin `.feature` files is safeword's BDD\n * acceptance lane (scaffolded by setup, ticket 102b), not a feature-sliced\n * architecture layer — without this guard every safeword project would\n * \"detect\" architecture and get depcruise configs it doesn't need.\n */\nfunction isGherkinDirectory(fullPath: string): boolean {\n try {\n return readdirSync(fullPath).some(entry => entry.endsWith('.feature'));\n } catch {\n return false;\n }\n}\n\nfunction scanSearchPath(\n projectDirectory: string,\n searchPath: string,\n pathPrefix: string,\n elements: DetectedElement[],\n): void {\n for (const layerDefinition of ARCHITECTURE_LAYERS) {\n for (const dirName of layerDefinition.dirs) {\n const fullPath = nodePath.join(projectDirectory, searchPath, dirName);\n if (\n exists(fullPath) &&\n !isGherkinDirectory(fullPath) &&\n !hasLayerForPrefix(elements, layerDefinition.layer, pathPrefix)\n ) {\n elements.push({\n layer: layerDefinition.layer,\n pattern: `${pathPrefix}${dirName}/**`,\n location: `${pathPrefix}${dirName}`,\n });\n }\n }\n }\n}\n\n/**\n * Scan a directory for architecture layers\n * @param projectDirectory\n * @param basePath\n */\nfunction scanForLayers(projectDirectory: string, basePath: string): DetectedElement[] {\n const elements: DetectedElement[] = [];\n const prefix = basePath ? `${basePath}/` : '';\n\n // Check src/ and root level\n scanSearchPath(projectDirectory, nodePath.join(basePath, 'src'), `${prefix}src/`, elements);\n scanSearchPath(projectDirectory, basePath, prefix, elements);\n\n return elements;\n}\n\n/**\n * Detects architecture directories in the project\n * Handles both standard projects and monorepos\n * @param projectDirectory\n */\nexport function detectArchitecture(projectDirectory: string): DetectedArchitecture {\n const elements: DetectedElement[] = [];\n\n // First, check for monorepo packages\n const packages = findMonorepoPackages(projectDirectory);\n const isMonorepo = packages.length > 0;\n\n if (isMonorepo) {\n // Scan each package\n for (const pkg of packages) {\n elements.push(...scanForLayers(projectDirectory, pkg));\n }\n }\n\n // Also scan root level (works for both monorepo root and standard projects)\n elements.push(...scanForLayers(projectDirectory, ''));\n\n // Deduplicate by pattern\n const seen = new Set<string>();\n const uniqueElements = elements.filter(element => {\n if (seen.has(element.pattern)) return false;\n seen.add(element.pattern);\n return true;\n });\n\n return { elements: uniqueElements, isMonorepo };\n}\n","/**\n * Dependency-cruiser config generator\n *\n * Generates dependency-cruiser configuration from detected architecture.\n * Used by `safeword sync-config` command and `/audit` slash command.\n */\n\nimport nodePath from 'node:path';\n\nimport type { DetectedArchitecture } from './boundaries.js';\nimport { readJson } from './fs.js';\n\nexport interface DependencyCruiseArchitecture extends DetectedArchitecture {\n workspaces?: string[];\n}\n\ninterface PackageJson {\n workspaces?: string[] | { packages?: string[] };\n}\n\n/**\n * Detect workspaces from package.json\n * Supports both array format and object format (yarn workspaces)\n */\nexport function detectWorkspaces(cwd: string): string[] | undefined {\n const packageJsonPath = nodePath.join(cwd, 'package.json');\n const packageJson = readJson(packageJsonPath) as PackageJson | undefined;\n\n if (!packageJson?.workspaces) return undefined;\n\n // Handle both formats: string[] or { packages: string[] }\n const workspaces = Array.isArray(packageJson.workspaces)\n ? packageJson.workspaces\n : packageJson.workspaces.packages;\n\n return workspaces && workspaces.length > 0 ? workspaces : undefined;\n}\n\n/**\n * Generate monorepo hierarchy rules based on workspace patterns\n */\nfunction generateMonorepoRules(workspaces: string[]): string {\n const rules: string[] = [];\n\n const hasLibs = workspaces.some(w => w.startsWith('libs'));\n const hasPackages = workspaces.some(w => w.startsWith('packages'));\n const hasApps = workspaces.some(w => w.startsWith('apps'));\n\n // libs cannot import packages or apps\n if (hasLibs && (hasPackages || hasApps)) {\n rules.push(` {\n name: 'libs-cannot-import-packages-or-apps',\n severity: 'error',\n from: { path: '^libs/' },\n to: { path: '^(packages|apps)/' },\n }`);\n }\n\n // packages cannot import apps\n if (hasPackages && hasApps) {\n rules.push(` {\n name: 'packages-cannot-import-apps',\n severity: 'error',\n from: { path: '^packages/' },\n to: { path: '^apps/' },\n }`);\n }\n\n return rules.join(',\\n');\n}\n\n/**\n * Generate .safeword/depcruise-config.cjs content (forbidden rules + options)\n */\nexport function generateDependencyCruiseConfigFile(arch: DependencyCruiseArchitecture): string {\n const monorepoRules = arch.workspaces ? generateMonorepoRules(arch.workspaces) : '';\n const hasMonorepoRules = monorepoRules.length > 0;\n\n return String.raw`module.exports = {\n forbidden: [\n // =========================================================================\n // ERROR RULES (block on violations)\n // =========================================================================\n {\n name: 'no-circular',\n // Runtime cycles cause initialization-order bugs and make code hard to reason about.\n // Type-only edges (import type) are erased at compile time and cannot cause runtime\n // cycles - TypeScript designed import type for exactly this case, and depcruise\n // documents viaOnly + dependencyTypesNot: ['type-only'] as the canonical opt-in.\n comment: 'Circular dependencies cause runtime issues and make code hard to reason about',\n severity: 'error',\n from: {},\n to: { circular: true, viaOnly: { dependencyTypesNot: ['type-only'] } },\n },\n {\n name: 'no-deprecated-deps',\n comment: 'Deprecated npm packages should be replaced - they may have security issues or be unmaintained',\n severity: 'error',\n from: {},\n to: { dependencyTypes: ['deprecated'] },\n },${hasMonorepoRules ? `\\n${monorepoRules},` : ''}\n\n // =========================================================================\n // WARNING RULES (flag issues but don't block)\n // =========================================================================\n {\n name: 'no-dev-deps-in-src',\n comment: 'Production code should not import devDependencies - may cause runtime failures',\n severity: 'warn',\n from: {\n path: ['^src', '^packages/[^/]+/src'],\n pathNot: '\\\\.test\\\\.[tj]sx?$',\n },\n to: { dependencyTypes: ['npm-dev'] },\n },\n {\n name: 'no-orphans',\n comment: 'Orphan modules are not imported anywhere - may be dead code',\n severity: 'warn',\n from: {\n orphan: true,\n pathNot: [\n // Entry points\n '(^|/)index\\\\.[tj]sx?$',\n '(^|/)main\\\\.[tj]sx?$',\n '(^|/)cli\\\\.[tj]s$',\n '(^|/)cucumber\\\\.mjs$',\n '\\\\.config\\\\.[tj]s$',\n '\\\\.config\\\\.mjs$',\n // Test files\n '\\\\.test\\\\.[tj]sx?$',\n '\\\\.spec\\\\.[tj]sx?$',\n '/tests/',\n '/__tests__/',\n // Astro/Next.js pages and content\n '/src/content/',\n '/src/pages/',\n '/app/',\n ],\n },\n to: {},\n },\n ],\n options: {\n doNotFollow: { path: ['node_modules', '.safeword'] },\n exclude: {\n path: ['node_modules', 'dist', 'build', 'coverage', '\\\\.d\\\\.ts$'],\n },\n tsPreCompilationDeps: true,\n tsConfig: { fileName: 'tsconfig.json' },\n enhancedResolveOptions: {\n extensions: ['.ts', '.tsx', '.js', '.jsx'],\n exportsFields: ['exports'],\n conditionNames: ['import', 'require', 'node', 'default'],\n },\n },\n};\n`;\n}\n\n/**\n * Generate .dependency-cruiser.js (main config that imports generated)\n */\nexport function generateDependencyCruiseMainConfig(): string {\n return `/**\n * Dependency Cruiser Configuration\n *\n * Imports auto-generated rules from .safeword/depcruise-config.cjs\n * ADD YOUR CUSTOM RULES BELOW the spread operator.\n */\n\nconst generated = require('./.safeword/depcruise-config.cjs');\n\nmodule.exports = {\n forbidden: [\n ...generated.forbidden,\n // ADD YOUR CUSTOM RULES BELOW:\n // { name: 'no-legacy', from: { path: 'legacy/' }, to: { path: 'new/' } },\n ],\n options: {\n ...generated.options,\n // Your overrides here\n },\n};\n`;\n}\n"],"mappings":"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","names":["nodePath","nodePath","nodePath","nodePath","result"]}
|