@fenglimg/fabric-cli 1.8.0-rc.3 → 2.0.0-rc.1
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/{chunk-QPCRBQ5Y.js → chunk-5LOYBXWD.js} +0 -1
- package/dist/chunk-UHNP7T7W.js +740 -0
- package/dist/{doctor-F52XWWZC.js → doctor-DUHWLAYD.js} +1 -1
- package/dist/index.js +5 -5
- package/dist/{init-7EYGUJNJ.js → init-DRHUYHYA.js} +224 -875
- package/dist/scan-HU2EGITF.js +20 -0
- package/dist/{serve-466QXQ5Q.js → serve-3LXXSBFR.js} +1 -1
- package/package.json +3 -3
- package/templates/agents-md/AGENTS.md.template +55 -17
- package/dist/chunk-NMMUETVK.js +0 -216
- package/dist/scan-NNBNGIZG.js +0 -12
- package/templates/agents-md/variants/cocos.md +0 -20
- package/templates/agents-md/variants/next.md +0 -20
- package/templates/agents-md/variants/vite.md +0 -20
- package/templates/bootstrap/GEMINI.md +0 -8
- package/templates/bootstrap/roo-fabric.md +0 -5
- package/templates/bootstrap/windsurf-fabric.md +0 -5
- package/templates/claude-hooks/fabric-init-reminder.cjs +0 -18
- package/templates/claude-skills/fabric-init/SKILL.md +0 -163
- package/templates/codex-hooks/fabric-session-start.cjs +0 -19
- package/templates/codex-hooks/fabric-stop-reminder.cjs +0 -18
- package/templates/codex-skills/fabric-init/SKILL.md +0 -162
- package/templates/husky/pre-commit +0 -9
- package/templates/skill-source/fabric-init/SOURCE.md +0 -157
- package/templates/skill-source/fabric-init/clients.json +0 -17
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
createDebugLogger,
|
|
4
|
+
paint,
|
|
5
|
+
resolveDevMode,
|
|
6
|
+
symbol,
|
|
7
|
+
t
|
|
8
|
+
} from "./chunk-5LOYBXWD.js";
|
|
9
|
+
|
|
10
|
+
// src/commands/scan.ts
|
|
11
|
+
import { createHash } from "crypto";
|
|
12
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
13
|
+
import { mkdir, readFile } from "fs/promises";
|
|
14
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "path";
|
|
15
|
+
import { defineCommand } from "citty";
|
|
16
|
+
import {
|
|
17
|
+
KnowledgeIdAllocator,
|
|
18
|
+
appendEventLedgerEvent,
|
|
19
|
+
writeRuleMeta
|
|
20
|
+
} from "@fenglimg/fabric-server";
|
|
21
|
+
import {
|
|
22
|
+
formatKnowledgeId
|
|
23
|
+
} from "@fenglimg/fabric-shared";
|
|
24
|
+
import { atomicWriteJson, atomicWriteText } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
25
|
+
|
|
26
|
+
// src/scanner/detector.ts
|
|
27
|
+
import { detectFramework } from "@fenglimg/fabric-shared/node";
|
|
28
|
+
|
|
29
|
+
// src/scanner/ignores.ts
|
|
30
|
+
var DEFAULT_IGNORES = [
|
|
31
|
+
"**/*.meta",
|
|
32
|
+
"library/**",
|
|
33
|
+
"temp/**",
|
|
34
|
+
"build/**",
|
|
35
|
+
"settings/**",
|
|
36
|
+
"profiles/**",
|
|
37
|
+
"node_modules/**",
|
|
38
|
+
"dist/**",
|
|
39
|
+
".git/**",
|
|
40
|
+
".fabric/**"
|
|
41
|
+
];
|
|
42
|
+
function resolveIgnores(fabricConfig) {
|
|
43
|
+
return [...DEFAULT_IGNORES, ...fabricConfig?.scanIgnores ?? []];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/commands/scan.ts
|
|
47
|
+
async function createScanReport(targetInput = process.cwd(), fabricConfig) {
|
|
48
|
+
const target = normalizeTarget(targetInput);
|
|
49
|
+
const framework = detectFramework(target);
|
|
50
|
+
const readmeQuality = getReadmeQuality(target);
|
|
51
|
+
const hasContributing = existsSync(join(target, "CONTRIBUTING.md"));
|
|
52
|
+
const hasExistingFabric = existsSync(join(target, ".fabric", "bootstrap", "README.md")) || existsSync(join(target, ".fabric"));
|
|
53
|
+
const walkResult = walkFiles(target, resolveIgnores(fabricConfig));
|
|
54
|
+
return {
|
|
55
|
+
target,
|
|
56
|
+
framework,
|
|
57
|
+
readmeQuality,
|
|
58
|
+
hasContributing,
|
|
59
|
+
fileCount: walkResult.fileCount,
|
|
60
|
+
ignoredCount: walkResult.ignoredCount,
|
|
61
|
+
hasExistingFabric,
|
|
62
|
+
recommendations: buildRecommendations({
|
|
63
|
+
framework,
|
|
64
|
+
readmeQuality,
|
|
65
|
+
hasContributing,
|
|
66
|
+
hasExistingFabric
|
|
67
|
+
})
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
var KNOWLEDGE_DIR = ".fabric/knowledge";
|
|
71
|
+
var SCAN_STATE_FILE = ".scan-state.json";
|
|
72
|
+
var FORENSIC_FILE = ".fabric/forensic.json";
|
|
73
|
+
var AGENTS_META_FILE = ".fabric/agents.meta.json";
|
|
74
|
+
var LAYER_REASON = "project artifact (deterministic init scan)";
|
|
75
|
+
async function runInitScan(targetInput, options = {}) {
|
|
76
|
+
const startTs = Date.now();
|
|
77
|
+
const target = normalizeTarget(targetInput);
|
|
78
|
+
const forensicPath = join(target, FORENSIC_FILE);
|
|
79
|
+
if (!existsSync(forensicPath)) {
|
|
80
|
+
throw new Error(t("cli.scan.error.missing-forensic", { path: forensicPath }));
|
|
81
|
+
}
|
|
82
|
+
const forensic = await readForensic(forensicPath);
|
|
83
|
+
const nowIso = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
84
|
+
const tags = deriveTagsFromForensic(forensic);
|
|
85
|
+
const candidates = [
|
|
86
|
+
buildTechStackEntry(forensic, nowIso, tags),
|
|
87
|
+
buildModuleStructureEntry(forensic, nowIso, tags),
|
|
88
|
+
buildBuildConfigEntry(forensic, nowIso, tags),
|
|
89
|
+
buildCodeStyleEntry(forensic, nowIso, tags),
|
|
90
|
+
buildCIConfigEntry(forensic, nowIso, tags),
|
|
91
|
+
buildReadmeFirstParaEntry(target, forensic, nowIso, tags),
|
|
92
|
+
buildProjectBriefEntry(target, forensic, nowIso, tags)
|
|
93
|
+
];
|
|
94
|
+
const entries = candidates.filter((e) => e !== null);
|
|
95
|
+
const sidecarPath = join(target, KNOWLEDGE_DIR, SCAN_STATE_FILE);
|
|
96
|
+
const sidecar = await readScanState(sidecarPath);
|
|
97
|
+
const allocator = new KnowledgeIdAllocator(join(target, AGENTS_META_FILE));
|
|
98
|
+
const written = [];
|
|
99
|
+
const skipped = [];
|
|
100
|
+
const placedEntries = [];
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
const targetPath = join(target, KNOWLEDGE_DIR, entry.target_subdir, `${entry.slug}.md`);
|
|
103
|
+
const existingId = findExistingIdForFile(sidecar, targetPath, target);
|
|
104
|
+
const id = existingId ?? await allocator.allocate(entry.layer, entry.type);
|
|
105
|
+
const built = { ...entry, id };
|
|
106
|
+
placedEntries.push(built);
|
|
107
|
+
const fullContent = renderMarkdown(built);
|
|
108
|
+
const bodyHash = sha256(stripFrontmatter(fullContent));
|
|
109
|
+
const sidecarKey = id;
|
|
110
|
+
if (sidecar[sidecarKey] === bodyHash && existsSync(targetPath)) {
|
|
111
|
+
skipped.push(id);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
await ensureParentDirectory(targetPath);
|
|
115
|
+
await atomicWriteText(targetPath, fullContent);
|
|
116
|
+
sidecar[sidecarKey] = bodyHash;
|
|
117
|
+
written.push(id);
|
|
118
|
+
}
|
|
119
|
+
await ensureParentDirectory(sidecarPath);
|
|
120
|
+
await atomicWriteJson(sidecarPath, sidecar);
|
|
121
|
+
await registerKnowledgeNodesInMeta(target, placedEntries);
|
|
122
|
+
await writeRuleMeta(target, { source: "doctor_fix" });
|
|
123
|
+
const durationMs = Date.now() - startTs;
|
|
124
|
+
await appendEventLedgerEvent(target, {
|
|
125
|
+
event_type: "init_scan_completed",
|
|
126
|
+
written_stable_ids: written,
|
|
127
|
+
duration_ms: durationMs,
|
|
128
|
+
source: options.source ?? "scan"
|
|
129
|
+
});
|
|
130
|
+
return {
|
|
131
|
+
written_stable_ids: written,
|
|
132
|
+
skipped_stable_ids: skipped,
|
|
133
|
+
total_entries: entries.length,
|
|
134
|
+
duration_ms: durationMs
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
var scanCommand = defineCommand({
|
|
138
|
+
meta: {
|
|
139
|
+
name: "scan",
|
|
140
|
+
description: t("cli.scan.description")
|
|
141
|
+
},
|
|
142
|
+
args: {
|
|
143
|
+
target: {
|
|
144
|
+
type: "string",
|
|
145
|
+
description: t("cli.scan.args.target.description")
|
|
146
|
+
},
|
|
147
|
+
debug: {
|
|
148
|
+
type: "boolean",
|
|
149
|
+
description: t("cli.scan.args.debug.description"),
|
|
150
|
+
default: false
|
|
151
|
+
},
|
|
152
|
+
json: {
|
|
153
|
+
type: "boolean",
|
|
154
|
+
description: t("cli.scan.args.json.description"),
|
|
155
|
+
default: false
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
async run({ args }) {
|
|
159
|
+
const workspaceRoot = process.cwd();
|
|
160
|
+
const logger = createDebugLogger(args.debug);
|
|
161
|
+
const resolution = resolveDevMode(args.target, workspaceRoot);
|
|
162
|
+
logger(`scan target source: ${resolution.source}`);
|
|
163
|
+
for (const step of resolution.chain) {
|
|
164
|
+
logger(step);
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
const result = await runInitScan(resolution.target, { source: "scan" });
|
|
168
|
+
if (args.json) {
|
|
169
|
+
console.log(JSON.stringify(result, null, 2));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
printPrettyResult(result);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
175
|
+
console.error(`${symbol.warn} ${paint.warn(message)}`);
|
|
176
|
+
process.exitCode = 1;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
var scan_default = scanCommand;
|
|
181
|
+
function buildTechStackEntry(forensic, nowIso, tags) {
|
|
182
|
+
const framework = forensic.framework;
|
|
183
|
+
const byExt = forensic.topology.by_ext ?? {};
|
|
184
|
+
const topExtensions = Object.entries(byExt).sort(([, a], [, b]) => b - a).slice(0, 5).map(([ext, count]) => `${ext} (${count})`);
|
|
185
|
+
const evidenceLines = framework.evidence.length > 0 ? framework.evidence.slice(0, 6) : ["(no explicit framework evidence)"];
|
|
186
|
+
const body = renderSections({
|
|
187
|
+
mission: `Track the primary tech stack and runtime surface used by ${forensic.project_name}.`,
|
|
188
|
+
context: [
|
|
189
|
+
`Framework: ${framework.kind}${framework.version ? ` ${framework.version}` : ""}${framework.subkind ? ` / ${framework.subkind}` : ""}`,
|
|
190
|
+
`Top file extensions: ${topExtensions.length > 0 ? topExtensions.join(", ") : "(none)"}`,
|
|
191
|
+
`Evidence:`,
|
|
192
|
+
...evidenceLines.map((line) => `- ${line}`)
|
|
193
|
+
].join("\n")
|
|
194
|
+
});
|
|
195
|
+
return {
|
|
196
|
+
type: "model",
|
|
197
|
+
layer: "team",
|
|
198
|
+
maturity: "verified",
|
|
199
|
+
layer_reason: LAYER_REASON,
|
|
200
|
+
created_at: nowIso,
|
|
201
|
+
title: `Tech stack: ${framework.kind}`,
|
|
202
|
+
body,
|
|
203
|
+
target_subdir: "models",
|
|
204
|
+
slug: "tech-stack",
|
|
205
|
+
tags
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function buildModuleStructureEntry(forensic, nowIso, tags) {
|
|
209
|
+
const keyDirs = forensic.topology.key_dirs ?? [];
|
|
210
|
+
const entryPoints = forensic.entry_points ?? [];
|
|
211
|
+
const totalFiles = forensic.topology.total_files ?? 0;
|
|
212
|
+
const dirsBlock = keyDirs.length > 0 ? keyDirs.slice(0, 12).map((dir) => `- ${dir}`).join("\n") : "- (no key directories detected)";
|
|
213
|
+
const entriesBlock = entryPoints.length > 0 ? entryPoints.slice(0, 8).map((ep) => `- ${ep.path} \u2014 ${ep.reason}`).join("\n") : "- (no entry points detected)";
|
|
214
|
+
const body = renderSections({
|
|
215
|
+
mission: `Map the high-level module layout and primary entry points of ${forensic.project_name}.`,
|
|
216
|
+
context: [
|
|
217
|
+
`Total files: ${totalFiles}`,
|
|
218
|
+
`Max directory depth: ${forensic.topology.max_depth ?? 0}`,
|
|
219
|
+
"",
|
|
220
|
+
"Key directories:",
|
|
221
|
+
dirsBlock,
|
|
222
|
+
"",
|
|
223
|
+
"Entry points:",
|
|
224
|
+
entriesBlock
|
|
225
|
+
].join("\n")
|
|
226
|
+
});
|
|
227
|
+
return {
|
|
228
|
+
type: "model",
|
|
229
|
+
layer: "team",
|
|
230
|
+
maturity: "verified",
|
|
231
|
+
layer_reason: LAYER_REASON,
|
|
232
|
+
created_at: nowIso,
|
|
233
|
+
title: "Module structure",
|
|
234
|
+
body,
|
|
235
|
+
target_subdir: "models",
|
|
236
|
+
slug: "module-structure",
|
|
237
|
+
tags
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function buildBuildConfigEntry(forensic, nowIso, tags) {
|
|
241
|
+
const configFiles = (forensic.candidate_files ?? []).filter((entry) => entry.family === "config").map((entry) => entry.path);
|
|
242
|
+
const framework = forensic.framework.kind;
|
|
243
|
+
const configBlock = configFiles.length > 0 ? configFiles.map((file) => `- ${file}`).join("\n") : "- (no config files detected)";
|
|
244
|
+
const body = renderSections({
|
|
245
|
+
mission: `Document the deterministic build/bootstrap configuration anchoring ${forensic.project_name}.`,
|
|
246
|
+
businessLogic: [
|
|
247
|
+
`1. Detect framework: \`${framework}\`.`,
|
|
248
|
+
`2. Read configuration files in declared order.`,
|
|
249
|
+
`3. Honor compiler/bundler boundaries before generating new code.`,
|
|
250
|
+
`4. Treat config drift as a fact-check signal \u2014 re-run \`fab scan\` after edits.`
|
|
251
|
+
].join("\n"),
|
|
252
|
+
context: [
|
|
253
|
+
`Framework: ${framework}`,
|
|
254
|
+
"",
|
|
255
|
+
"Configuration files:",
|
|
256
|
+
configBlock
|
|
257
|
+
].join("\n")
|
|
258
|
+
});
|
|
259
|
+
return {
|
|
260
|
+
type: "process",
|
|
261
|
+
layer: "team",
|
|
262
|
+
maturity: "verified",
|
|
263
|
+
layer_reason: LAYER_REASON,
|
|
264
|
+
created_at: nowIso,
|
|
265
|
+
title: "Build configuration",
|
|
266
|
+
body,
|
|
267
|
+
target_subdir: "processes",
|
|
268
|
+
slug: "build-config",
|
|
269
|
+
tags
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function buildCodeStyleEntry(forensic, nowIso, tags) {
|
|
273
|
+
const dominantPatterns = (forensic.assertions ?? []).filter((a) => a.type === "pattern" || a.type === "domain").slice(0, 4).map((a) => `- ${a.statement}`);
|
|
274
|
+
const proposedRules = (forensic.assertions ?? []).map((a) => a.proposed_rule).filter((rule) => typeof rule === "string" && rule.length > 0).slice(0, 4);
|
|
275
|
+
const patternsBlock = dominantPatterns.length > 0 ? dominantPatterns.join("\n") : "- (no dominant patterns detected)";
|
|
276
|
+
const rulesBlock = proposedRules.length > 0 ? proposedRules.map((rule) => `- ${rule}`).join("\n") : "- Follow existing module/file patterns; do not introduce new conventions without team agreement.";
|
|
277
|
+
const body = renderSections({
|
|
278
|
+
mission: `Codify the recurring authoring conventions observed in ${forensic.project_name}.`,
|
|
279
|
+
mandatoryInjection: [
|
|
280
|
+
"When generating or modifying source files in this repo, AI agents MUST:",
|
|
281
|
+
rulesBlock
|
|
282
|
+
].join("\n"),
|
|
283
|
+
context: [
|
|
284
|
+
"Detected patterns:",
|
|
285
|
+
patternsBlock
|
|
286
|
+
].join("\n")
|
|
287
|
+
});
|
|
288
|
+
return {
|
|
289
|
+
type: "guideline",
|
|
290
|
+
layer: "team",
|
|
291
|
+
maturity: "verified",
|
|
292
|
+
layer_reason: LAYER_REASON,
|
|
293
|
+
created_at: nowIso,
|
|
294
|
+
title: "Code style guidelines",
|
|
295
|
+
body,
|
|
296
|
+
target_subdir: "guidelines",
|
|
297
|
+
slug: "code-style",
|
|
298
|
+
tags
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
function buildCIConfigEntry(forensic, nowIso, tags) {
|
|
302
|
+
const ciFiles = (forensic.candidate_files ?? []).map((entry) => entry.path).filter((path) => isCIConfigPath(path));
|
|
303
|
+
const ciExtensions = forensic.topology.by_ext ?? {};
|
|
304
|
+
const hasCISignal = ciFiles.length > 0 || Object.keys(ciExtensions).some((ext) => ext === ".yml" || ext === ".yaml") && (forensic.assertions ?? []).some((a) => /ci|workflow|pipeline/i.test(a.statement));
|
|
305
|
+
if (!hasCISignal) {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
const filesBlock = ciFiles.length > 0 ? ciFiles.map((file) => `- ${file}`).join("\n") : "- (CI configuration inferred from repository topology)";
|
|
309
|
+
const body = renderSections({
|
|
310
|
+
mission: `Document the CI / continuous-verification pipeline guarding ${forensic.project_name}.`,
|
|
311
|
+
businessLogic: [
|
|
312
|
+
"1. Pull request opens \u2192 CI workflow triggers.",
|
|
313
|
+
"2. Lint + typecheck + unit tests must pass before review.",
|
|
314
|
+
"3. Failing checks block merge until resolved.",
|
|
315
|
+
"4. Updates to CI configuration should accompany the change they enable."
|
|
316
|
+
].join("\n"),
|
|
317
|
+
context: [
|
|
318
|
+
"CI configuration sources:",
|
|
319
|
+
filesBlock
|
|
320
|
+
].join("\n")
|
|
321
|
+
});
|
|
322
|
+
return {
|
|
323
|
+
type: "process",
|
|
324
|
+
layer: "team",
|
|
325
|
+
maturity: "verified",
|
|
326
|
+
layer_reason: LAYER_REASON,
|
|
327
|
+
created_at: nowIso,
|
|
328
|
+
title: "CI configuration",
|
|
329
|
+
body,
|
|
330
|
+
target_subdir: "processes",
|
|
331
|
+
slug: "ci-config",
|
|
332
|
+
tags
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
function buildReadmeFirstParaEntry(target, forensic, nowIso, tags) {
|
|
336
|
+
if (forensic.readme.quality === "missing") {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
const readmePath = join(target, "README.md");
|
|
340
|
+
if (!existsSync(readmePath)) {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
const readme = readFileSync(readmePath, "utf8");
|
|
344
|
+
const firstPara = extractFirstParagraph(readme);
|
|
345
|
+
if (firstPara === null) {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
const body = renderSections({
|
|
349
|
+
mission: `Preserve the README headline and first paragraph as the canonical project elevator pitch.`,
|
|
350
|
+
context: [
|
|
351
|
+
`Source: README.md (${forensic.readme.line_count} lines, quality=${forensic.readme.quality})`,
|
|
352
|
+
"",
|
|
353
|
+
"Excerpt:",
|
|
354
|
+
"> " + firstPara.split("\n").join("\n> ")
|
|
355
|
+
].join("\n")
|
|
356
|
+
});
|
|
357
|
+
return {
|
|
358
|
+
type: "model",
|
|
359
|
+
layer: "team",
|
|
360
|
+
maturity: "verified",
|
|
361
|
+
layer_reason: LAYER_REASON,
|
|
362
|
+
created_at: nowIso,
|
|
363
|
+
title: "README first paragraph",
|
|
364
|
+
body,
|
|
365
|
+
target_subdir: "models",
|
|
366
|
+
slug: "readme-first-paragraph",
|
|
367
|
+
tags
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
function buildProjectBriefEntry(target, forensic, nowIso, tags) {
|
|
371
|
+
if (forensic.readme.quality === "missing") {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
const readmePath = join(target, "README.md");
|
|
375
|
+
if (!existsSync(readmePath)) {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
const readme = readFileSync(readmePath, "utf8");
|
|
379
|
+
const description = extractExplicitDescription(readme);
|
|
380
|
+
if (description === null) {
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
const body = renderSections({
|
|
384
|
+
mission: `Capture the explicit project description declared by README.md.`,
|
|
385
|
+
context: [
|
|
386
|
+
`Project: ${forensic.project_name}`,
|
|
387
|
+
"",
|
|
388
|
+
"Declared description:",
|
|
389
|
+
"> " + description.split("\n").join("\n> ")
|
|
390
|
+
].join("\n")
|
|
391
|
+
});
|
|
392
|
+
return {
|
|
393
|
+
type: "model",
|
|
394
|
+
layer: "team",
|
|
395
|
+
maturity: "verified",
|
|
396
|
+
layer_reason: LAYER_REASON,
|
|
397
|
+
created_at: nowIso,
|
|
398
|
+
title: "Project brief",
|
|
399
|
+
body,
|
|
400
|
+
target_subdir: "models",
|
|
401
|
+
slug: "project-brief",
|
|
402
|
+
tags
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
function renderMarkdown(entry) {
|
|
406
|
+
const frontmatter = renderFrontmatter(entry);
|
|
407
|
+
return `${frontmatter}
|
|
408
|
+
|
|
409
|
+
# ${entry.title}
|
|
410
|
+
|
|
411
|
+
${entry.body}
|
|
412
|
+
`;
|
|
413
|
+
}
|
|
414
|
+
function renderFrontmatter(entry) {
|
|
415
|
+
const tagsLine = entry.tags.length > 0 ? `tags: [${entry.tags.join(", ")}]` : "tags: []";
|
|
416
|
+
const lines = [
|
|
417
|
+
"---",
|
|
418
|
+
`id: ${entry.id}`,
|
|
419
|
+
`type: ${entry.type}`,
|
|
420
|
+
`layer: ${entry.layer}`,
|
|
421
|
+
`maturity: ${entry.maturity}`,
|
|
422
|
+
`layer_reason: ${quoteIfNeeded(entry.layer_reason)}`,
|
|
423
|
+
`created_at: ${entry.created_at}`,
|
|
424
|
+
tagsLine,
|
|
425
|
+
"---"
|
|
426
|
+
];
|
|
427
|
+
return lines.join("\n");
|
|
428
|
+
}
|
|
429
|
+
function renderSections(input) {
|
|
430
|
+
const parts = [];
|
|
431
|
+
parts.push(`## [MISSION_STATEMENT]
|
|
432
|
+
|
|
433
|
+
${input.mission}`);
|
|
434
|
+
if (input.mandatoryInjection !== void 0) {
|
|
435
|
+
parts.push(`## [MANDATORY_INJECTION]
|
|
436
|
+
|
|
437
|
+
${input.mandatoryInjection}`);
|
|
438
|
+
}
|
|
439
|
+
if (input.businessLogic !== void 0) {
|
|
440
|
+
parts.push(`## [BUSINESS_LOGIC_CHUNKS]
|
|
441
|
+
|
|
442
|
+
${input.businessLogic}`);
|
|
443
|
+
}
|
|
444
|
+
parts.push(`## [CONTEXT_INFO]
|
|
445
|
+
|
|
446
|
+
${input.context}`);
|
|
447
|
+
return parts.join("\n\n");
|
|
448
|
+
}
|
|
449
|
+
function quoteIfNeeded(value) {
|
|
450
|
+
return `"${value.replace(/"/g, '\\"')}"`;
|
|
451
|
+
}
|
|
452
|
+
function stripFrontmatter(content) {
|
|
453
|
+
return content.replace(/^---[\s\S]*?\r?\n---\s*\r?\n?/u, "");
|
|
454
|
+
}
|
|
455
|
+
function deriveTagsFromForensic(forensic) {
|
|
456
|
+
const MAX_TAGS = 5;
|
|
457
|
+
const seen = /* @__PURE__ */ new Set();
|
|
458
|
+
const tags = [];
|
|
459
|
+
function add(raw) {
|
|
460
|
+
const normalized = raw.toLowerCase().trim().replace(/\s+/gu, "-");
|
|
461
|
+
if (normalized.length > 0 && !seen.has(normalized)) {
|
|
462
|
+
seen.add(normalized);
|
|
463
|
+
tags.push(normalized);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (forensic.framework.kind) {
|
|
467
|
+
add(forensic.framework.kind);
|
|
468
|
+
}
|
|
469
|
+
const SKIP_EXTS = /* @__PURE__ */ new Set([".json", ".md", ".lock", ".yaml", ".yml", ".txt", ".env"]);
|
|
470
|
+
const EXT_MAP = {
|
|
471
|
+
".ts": "typescript",
|
|
472
|
+
".tsx": "typescript",
|
|
473
|
+
".js": "javascript",
|
|
474
|
+
".jsx": "javascript",
|
|
475
|
+
".mjs": "javascript",
|
|
476
|
+
".cjs": "javascript",
|
|
477
|
+
".py": "python",
|
|
478
|
+
".go": "go",
|
|
479
|
+
".rs": "rust",
|
|
480
|
+
".java": "java",
|
|
481
|
+
".cs": "csharp",
|
|
482
|
+
".rb": "ruby",
|
|
483
|
+
".php": "php",
|
|
484
|
+
".swift": "swift",
|
|
485
|
+
".kt": "kotlin"
|
|
486
|
+
};
|
|
487
|
+
const byExt = forensic.topology.by_ext ?? {};
|
|
488
|
+
const sorted = Object.entries(byExt).filter(([ext]) => !SKIP_EXTS.has(ext)).sort(([, a], [, b]) => b - a);
|
|
489
|
+
for (const [ext] of sorted) {
|
|
490
|
+
if (tags.length >= MAX_TAGS) break;
|
|
491
|
+
const mapped = EXT_MAP[ext] ?? ext.replace(/^\./u, "");
|
|
492
|
+
add(mapped);
|
|
493
|
+
}
|
|
494
|
+
return tags.slice(0, MAX_TAGS);
|
|
495
|
+
}
|
|
496
|
+
async function readForensic(forensicPath) {
|
|
497
|
+
const raw = await readFile(forensicPath, "utf8");
|
|
498
|
+
return JSON.parse(raw);
|
|
499
|
+
}
|
|
500
|
+
async function readScanState(sidecarPath) {
|
|
501
|
+
if (!existsSync(sidecarPath)) {
|
|
502
|
+
return {};
|
|
503
|
+
}
|
|
504
|
+
try {
|
|
505
|
+
const raw = await readFile(sidecarPath, "utf8");
|
|
506
|
+
const parsed = JSON.parse(raw);
|
|
507
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
508
|
+
return {};
|
|
509
|
+
}
|
|
510
|
+
const result = {};
|
|
511
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
512
|
+
if (typeof value === "string") {
|
|
513
|
+
result[key] = value;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return result;
|
|
517
|
+
} catch {
|
|
518
|
+
return {};
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
function findExistingIdForFile(sidecar, targetPath, target) {
|
|
522
|
+
if (!existsSync(targetPath)) {
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
try {
|
|
526
|
+
const raw = readFileSync(targetPath, "utf8");
|
|
527
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---/u.exec(raw);
|
|
528
|
+
if (match === null) {
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
const idLine = /^id:\s*(.+)$/mu.exec(match[1]);
|
|
532
|
+
if (idLine === null) {
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
const candidate = idLine[1].replace(/^["'](.*)["']$/u, "$1").trim();
|
|
536
|
+
if (/^K[PT]-(MOD|DEC|GLD|PIT|PRO)-\d{4,}$/.test(candidate) && sidecar[candidate] !== void 0) {
|
|
537
|
+
return candidate;
|
|
538
|
+
}
|
|
539
|
+
return null;
|
|
540
|
+
} catch {
|
|
541
|
+
void target;
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
function isCIConfigPath(path) {
|
|
546
|
+
return path.startsWith(".github/workflows/") || path.startsWith(".gitlab-ci") || path === "azure-pipelines.yml" || path === ".circleci/config.yml" || path === "Jenkinsfile" || path === ".travis.yml";
|
|
547
|
+
}
|
|
548
|
+
function extractFirstParagraph(readme) {
|
|
549
|
+
const lines = readme.split(/\r?\n/);
|
|
550
|
+
let i = 0;
|
|
551
|
+
while (i < lines.length && lines[i].trim().length === 0) i += 1;
|
|
552
|
+
while (i < lines.length && /^#{1,2}\s/.test(lines[i].trim())) {
|
|
553
|
+
i += 1;
|
|
554
|
+
while (i < lines.length && lines[i].trim().length === 0) i += 1;
|
|
555
|
+
}
|
|
556
|
+
if (i >= lines.length) {
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
const collected = [];
|
|
560
|
+
while (i < lines.length && lines[i].trim().length > 0) {
|
|
561
|
+
if (/^#{1,6}\s/.test(lines[i].trim())) break;
|
|
562
|
+
collected.push(lines[i]);
|
|
563
|
+
i += 1;
|
|
564
|
+
}
|
|
565
|
+
const paragraph = collected.join("\n").trim();
|
|
566
|
+
return paragraph.length > 0 ? paragraph : null;
|
|
567
|
+
}
|
|
568
|
+
function extractExplicitDescription(readme) {
|
|
569
|
+
const headingMatch = /^#{1,6}\s+(?:Description|About|Overview|Summary)\s*\r?\n+([^#][\s\S]*?)(?:\r?\n\r?\n|\r?\n#{1,6}\s|$)/imu.exec(readme);
|
|
570
|
+
if (headingMatch !== null) {
|
|
571
|
+
const text = headingMatch[1].trim();
|
|
572
|
+
if (text.length > 0) return text;
|
|
573
|
+
}
|
|
574
|
+
const labelMatch = /^\*\*(?:Description|About|Overview|Summary)\*\*\s*:?\s*(.+?)(?:\r?\n\r?\n|$)/imu.exec(readme);
|
|
575
|
+
if (labelMatch !== null) {
|
|
576
|
+
const text = labelMatch[1].trim();
|
|
577
|
+
if (text.length > 0) return text;
|
|
578
|
+
}
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
function sha256(content) {
|
|
582
|
+
return `sha256:${createHash("sha256").update(content).digest("hex")}`;
|
|
583
|
+
}
|
|
584
|
+
async function registerKnowledgeNodesInMeta(target, entries) {
|
|
585
|
+
if (entries.length === 0) {
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
const metaPath = join(target, AGENTS_META_FILE);
|
|
589
|
+
let meta;
|
|
590
|
+
try {
|
|
591
|
+
const raw = await readFile(metaPath, "utf8");
|
|
592
|
+
meta = JSON.parse(raw);
|
|
593
|
+
} catch {
|
|
594
|
+
meta = {};
|
|
595
|
+
}
|
|
596
|
+
const nodes = typeof meta.nodes === "object" && meta.nodes !== null ? meta.nodes : {};
|
|
597
|
+
for (const entry of entries) {
|
|
598
|
+
const contentRef = `${KNOWLEDGE_DIR}/${entry.target_subdir}/${entry.slug}.md`;
|
|
599
|
+
const absPath = join(target, contentRef);
|
|
600
|
+
let hash = "";
|
|
601
|
+
try {
|
|
602
|
+
const raw = readFileSync(absPath, "utf8");
|
|
603
|
+
hash = sha256(raw);
|
|
604
|
+
} catch {
|
|
605
|
+
}
|
|
606
|
+
nodes[entry.id] = {
|
|
607
|
+
file: contentRef,
|
|
608
|
+
content_ref: contentRef,
|
|
609
|
+
scope_glob: "**",
|
|
610
|
+
deps: [],
|
|
611
|
+
priority: "medium",
|
|
612
|
+
level: "L1",
|
|
613
|
+
layer: "L1",
|
|
614
|
+
topology_type: "cross-cutting",
|
|
615
|
+
hash,
|
|
616
|
+
stable_id: entry.id,
|
|
617
|
+
identity_source: "declared"
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
meta.nodes = nodes;
|
|
621
|
+
await ensureParentDirectory(metaPath);
|
|
622
|
+
await atomicWriteJson(metaPath, meta);
|
|
623
|
+
}
|
|
624
|
+
async function ensureParentDirectory(filePath) {
|
|
625
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
626
|
+
}
|
|
627
|
+
function printPrettyResult(result) {
|
|
628
|
+
const writtenCount = result.written_stable_ids.length;
|
|
629
|
+
const skippedCount = result.skipped_stable_ids.length;
|
|
630
|
+
if (writtenCount === 0) {
|
|
631
|
+
console.log(`${symbol.ok} ${paint.success(t("cli.scan.summary.skipped", { count: String(skippedCount) }))}`);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
console.log(`${symbol.ok} ${paint.success(t("cli.scan.summary.created", { count: String(writtenCount) }))}`);
|
|
635
|
+
for (const id of result.written_stable_ids) {
|
|
636
|
+
console.log(` - ${paint.ai(id)}`);
|
|
637
|
+
}
|
|
638
|
+
if (skippedCount > 0) {
|
|
639
|
+
console.log(paint.muted(`(${skippedCount} unchanged, skipped)`));
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
function normalizeTarget(targetInput) {
|
|
643
|
+
return isAbsolute(targetInput) ? targetInput : resolve(process.cwd(), targetInput);
|
|
644
|
+
}
|
|
645
|
+
function getReadmeQuality(target) {
|
|
646
|
+
const readmePath = join(target, "README.md");
|
|
647
|
+
if (!existsSync(readmePath)) {
|
|
648
|
+
return "stub";
|
|
649
|
+
}
|
|
650
|
+
const wordCount = readFileSync(readmePath, "utf8").trim().split(/\s+/).filter(Boolean).length;
|
|
651
|
+
return wordCount >= 200 ? "ok" : "stub";
|
|
652
|
+
}
|
|
653
|
+
function walkFiles(root, ignorePatterns) {
|
|
654
|
+
if (!existsSync(root) || !statSync(root).isDirectory()) {
|
|
655
|
+
throw new Error(t("cli.shared.target-invalid", { target: root }));
|
|
656
|
+
}
|
|
657
|
+
let fileCount = 0;
|
|
658
|
+
let ignoredCount = 0;
|
|
659
|
+
const stack = [root];
|
|
660
|
+
while (stack.length > 0) {
|
|
661
|
+
const current = stack.pop();
|
|
662
|
+
if (current === void 0) {
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
666
|
+
const absolutePath = join(current, entry.name);
|
|
667
|
+
const relativePath = toPosixPath(relative(root, absolutePath));
|
|
668
|
+
if (shouldIgnore(relativePath, entry.isDirectory(), ignorePatterns)) {
|
|
669
|
+
ignoredCount += 1;
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
if (entry.isDirectory()) {
|
|
673
|
+
stack.push(absolutePath);
|
|
674
|
+
} else if (entry.isFile()) {
|
|
675
|
+
fileCount += 1;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return { fileCount, ignoredCount };
|
|
680
|
+
}
|
|
681
|
+
function shouldIgnore(relativePath, isDirectory, ignorePatterns) {
|
|
682
|
+
return ignorePatterns.some((pattern) => matchesIgnorePattern(relativePath, isDirectory, pattern));
|
|
683
|
+
}
|
|
684
|
+
function matchesIgnorePattern(relativePath, isDirectory, pattern) {
|
|
685
|
+
const normalizedPattern = toPosixPath(pattern);
|
|
686
|
+
if (normalizedPattern === "**/*.meta") {
|
|
687
|
+
return relativePath.endsWith(".meta");
|
|
688
|
+
}
|
|
689
|
+
if (normalizedPattern.endsWith("/**")) {
|
|
690
|
+
const directoryPrefix = normalizedPattern.slice(0, -3);
|
|
691
|
+
return relativePath === directoryPrefix || relativePath.startsWith(`${directoryPrefix}/`) || isDirectory && `${relativePath}/` === directoryPrefix;
|
|
692
|
+
}
|
|
693
|
+
return relativePath === normalizedPattern;
|
|
694
|
+
}
|
|
695
|
+
function toPosixPath(path) {
|
|
696
|
+
return path.split(sep).join("/");
|
|
697
|
+
}
|
|
698
|
+
function buildRecommendations(input) {
|
|
699
|
+
const recommendations = [];
|
|
700
|
+
if (!input.hasExistingFabric) {
|
|
701
|
+
recommendations.push(t("cli.scan.recommendation.init"));
|
|
702
|
+
}
|
|
703
|
+
if (input.readmeQuality === "stub") {
|
|
704
|
+
recommendations.push(t("cli.scan.recommendation.readme"));
|
|
705
|
+
}
|
|
706
|
+
if (!input.hasContributing) {
|
|
707
|
+
recommendations.push(t("cli.scan.recommendation.contributing"));
|
|
708
|
+
}
|
|
709
|
+
if (input.framework.kind === "unknown") {
|
|
710
|
+
recommendations.push(t("cli.scan.recommendation.unknown-framework"));
|
|
711
|
+
} else {
|
|
712
|
+
recommendations.push(t("cli.scan.recommendation.framework-dirs", { framework: input.framework.kind }));
|
|
713
|
+
}
|
|
714
|
+
return recommendations;
|
|
715
|
+
}
|
|
716
|
+
var __testing__ = {
|
|
717
|
+
buildTechStackEntry,
|
|
718
|
+
buildModuleStructureEntry,
|
|
719
|
+
buildBuildConfigEntry,
|
|
720
|
+
buildCodeStyleEntry,
|
|
721
|
+
buildCIConfigEntry,
|
|
722
|
+
buildReadmeFirstParaEntry,
|
|
723
|
+
buildProjectBriefEntry,
|
|
724
|
+
renderMarkdown,
|
|
725
|
+
stripFrontmatter,
|
|
726
|
+
isCIConfigPath,
|
|
727
|
+
extractFirstParagraph,
|
|
728
|
+
extractExplicitDescription
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
export {
|
|
732
|
+
detectFramework,
|
|
733
|
+
formatKnowledgeId,
|
|
734
|
+
createScanReport,
|
|
735
|
+
runInitScan,
|
|
736
|
+
scanCommand,
|
|
737
|
+
scan_default,
|
|
738
|
+
deriveTagsFromForensic,
|
|
739
|
+
__testing__
|
|
740
|
+
};
|