@fenglimg/fabric-cli 2.0.0 → 2.0.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.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -5
  3. package/dist/chunk-BATF4PEJ.js +361 -0
  4. package/dist/{chunk-OBQU6NHO.js → chunk-COI5VDFU.js} +0 -18
  5. package/dist/chunk-D25XJ4BC.js +880 -0
  6. package/dist/chunk-MF3OTILQ.js +544 -0
  7. package/dist/chunk-PWLW3B57.js +18 -0
  8. package/dist/config-XJIPZNUP.js +13 -0
  9. package/dist/doctor-EJDSEJSS.js +810 -0
  10. package/dist/index.js +15 -8
  11. package/dist/{init-BIRSIOXO.js → install-EKWMFLUU.js} +622 -711
  12. package/dist/metrics-ACEQFPDU.js +122 -0
  13. package/dist/onboard-coverage-MFCAEBDO.js +220 -0
  14. package/dist/{plan-context-hint-QMUPAXIB.js → plan-context-hint-FC6P3WFE.js} +34 -28
  15. package/dist/uninstall-MH7ZIB6M.js +1064 -0
  16. package/package.json +30 -5
  17. package/templates/hooks/cite-policy-evict.cjs +231 -0
  18. package/templates/hooks/configs/README.md +29 -6
  19. package/templates/hooks/configs/claude-code.json +14 -3
  20. package/templates/hooks/configs/codex-hooks.json +6 -3
  21. package/templates/hooks/configs/cursor-hooks.json +8 -10
  22. package/templates/hooks/fabric-hint.cjs +833 -105
  23. package/templates/hooks/knowledge-hint-broad.cjs +509 -135
  24. package/templates/hooks/knowledge-hint-narrow.cjs +791 -26
  25. package/templates/hooks/lib/banner-i18n.cjs +309 -0
  26. package/templates/hooks/lib/cite-contract-reminder.cjs +173 -0
  27. package/templates/hooks/lib/cite-line-parser.cjs +158 -0
  28. package/templates/hooks/lib/client-adapter.cjs +106 -0
  29. package/templates/hooks/lib/config-cache.cjs +107 -0
  30. package/templates/hooks/lib/state-store.cjs +84 -0
  31. package/templates/hooks/lib/summary-fallback.cjs +210 -0
  32. package/templates/skills/fabric-archive/SKILL.md +93 -419
  33. package/templates/skills/fabric-archive/ref/dry-run-scope.md +16 -0
  34. package/templates/skills/fabric-archive/ref/e5-cron-recap.md +58 -0
  35. package/templates/skills/fabric-archive/ref/i18n-policy.md +86 -0
  36. package/templates/skills/fabric-archive/ref/phase-0-range-resolution.md +156 -0
  37. package/templates/skills/fabric-archive/ref/phase-1-5-onboard.md +218 -0
  38. package/templates/skills/fabric-archive/ref/phase-1-cross-session.md +62 -0
  39. package/templates/skills/fabric-archive/ref/phase-2-5-viability.md +68 -0
  40. package/templates/skills/fabric-archive/ref/phase-3-5-scope.md +108 -0
  41. package/templates/skills/fabric-archive/ref/phase-3-classify.md +63 -0
  42. package/templates/skills/fabric-archive/ref/phase-4-5-emit.md +78 -0
  43. package/templates/skills/fabric-archive/ref/phase-4-mcp-persist.md +89 -0
  44. package/templates/skills/fabric-archive/ref/rc-history.md +38 -0
  45. package/templates/skills/fabric-archive/ref/worked-examples.md +78 -0
  46. package/templates/skills/fabric-import/SKILL.md +75 -516
  47. package/templates/skills/fabric-import/ref/checkpoint-state.md +85 -0
  48. package/templates/skills/fabric-import/ref/i18n-policy.md +79 -0
  49. package/templates/skills/fabric-import/ref/output-contract.md +61 -0
  50. package/templates/skills/fabric-import/ref/phase-2-mining.md +213 -0
  51. package/templates/skills/fabric-import/ref/phase-3-dedup.md +75 -0
  52. package/templates/skills/fabric-import/ref/state-recovery.md +57 -0
  53. package/templates/skills/fabric-import/ref/worked-examples.md +127 -0
  54. package/templates/skills/fabric-review/SKILL.md +86 -284
  55. package/templates/skills/fabric-review/ref/askuserquestion-policy.md +66 -0
  56. package/templates/skills/fabric-review/ref/i18n-policy.md +111 -0
  57. package/templates/skills/fabric-review/ref/modify-flow.md +103 -0
  58. package/templates/skills/fabric-review/ref/output-contract.md +58 -0
  59. package/templates/skills/fabric-review/ref/per-mode-flows.md +155 -0
  60. package/templates/skills/fabric-review/ref/semantic-check.md +26 -0
  61. package/templates/skills/fabric-review/ref/worked-examples.md +95 -0
  62. package/templates/skills/lib/shared-policy.md +69 -0
  63. package/dist/chunk-6ICJICVU.js +0 -10
  64. package/dist/chunk-74SZWYPH.js +0 -658
  65. package/dist/chunk-EYIDD2YS.js +0 -1000
  66. package/dist/doctor-T7JWODKG.js +0 -282
  67. package/dist/hooks-Y74Y5LQS.js +0 -12
  68. package/dist/scan-LMK3UCWL.js +0 -22
  69. package/dist/serve-H554BHLG.js +0 -124
  70. package/templates/agents-md/AGENTS.md.template +0 -59
  71. package/templates/bootstrap/CLAUDE.md +0 -8
  72. package/templates/bootstrap/codex-AGENTS-header.md +0 -6
  73. package/templates/bootstrap/cursor-fabric-bootstrap.mdc +0 -10
@@ -1,1000 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- paint,
4
- symbol
5
- } from "./chunk-WWNXR34K.js";
6
- import {
7
- t
8
- } from "./chunk-6ICJICVU.js";
9
- import {
10
- createDebugLogger,
11
- readFabricConfig,
12
- resolveDevMode
13
- } from "./chunk-OBQU6NHO.js";
14
-
15
- // src/commands/scan.ts
16
- import { createHash } from "crypto";
17
- import { existsSync, readdirSync, readFileSync, statSync } from "fs";
18
- import { mkdir, readFile } from "fs/promises";
19
- import { dirname, isAbsolute, join, relative, resolve, sep } from "path";
20
- import { defineCommand } from "citty";
21
- import {
22
- KnowledgeIdAllocator,
23
- appendEventLedgerEvent,
24
- writeKnowledgeMeta
25
- } from "@fenglimg/fabric-server";
26
- import {
27
- formatKnowledgeId
28
- } from "@fenglimg/fabric-shared";
29
- import { atomicWriteJson, atomicWriteText } from "@fenglimg/fabric-shared/node/atomic-write";
30
-
31
- // src/scanner/detector.ts
32
- import { detectFramework } from "@fenglimg/fabric-shared/node";
33
-
34
- // src/scanner/ignores.ts
35
- var DEFAULT_IGNORES = [
36
- "**/*.meta",
37
- "library/**",
38
- "temp/**",
39
- "build/**",
40
- "settings/**",
41
- "profiles/**",
42
- "node_modules/**",
43
- "dist/**",
44
- ".git/**",
45
- ".fabric/**"
46
- ];
47
- function resolveIgnores(fabricConfig) {
48
- return [...DEFAULT_IGNORES, ...fabricConfig?.scanIgnores ?? []];
49
- }
50
-
51
- // src/commands/scan.ts
52
- async function createScanReport(targetInput = process.cwd(), fabricConfig) {
53
- const target = normalizeTarget(targetInput);
54
- const framework = detectFramework(target);
55
- const readmeQuality = getReadmeQuality(target);
56
- const hasContributing = existsSync(join(target, "CONTRIBUTING.md"));
57
- const hasExistingFabric = existsSync(join(target, ".fabric", "bootstrap", "README.md")) || existsSync(join(target, ".fabric"));
58
- const walkResult = walkFiles(target, resolveIgnores(fabricConfig));
59
- return {
60
- target,
61
- framework,
62
- readmeQuality,
63
- hasContributing,
64
- fileCount: walkResult.fileCount,
65
- ignoredCount: walkResult.ignoredCount,
66
- hasExistingFabric,
67
- recommendations: buildRecommendations({
68
- framework,
69
- readmeQuality,
70
- hasContributing,
71
- hasExistingFabric
72
- })
73
- };
74
- }
75
- var KNOWLEDGE_DIR = ".fabric/knowledge";
76
- var SCAN_STATE_FILE = ".scan-state.json";
77
- var FORENSIC_FILE = ".fabric/forensic.json";
78
- var AGENTS_META_FILE = ".fabric/agents.meta.json";
79
- var LAYER_REASON = "project artifact (deterministic init scan)";
80
- async function runInitScan(targetInput, options = {}) {
81
- const startTs = Date.now();
82
- const target = normalizeTarget(targetInput);
83
- const forensicPath = join(target, FORENSIC_FILE);
84
- if (!existsSync(forensicPath)) {
85
- throw new Error(t("cli.scan.error.missing-forensic", { path: forensicPath }));
86
- }
87
- const forensic = await readForensic(forensicPath);
88
- const nowIso = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
89
- const tags = deriveTagsFromForensic(forensic);
90
- const fabricConfig = readFabricConfig(target);
91
- const knowledgeLanguage = fabricConfig.knowledge_language ?? "match-existing";
92
- const resolvedLanguage = resolveKnowledgeLanguage(knowledgeLanguage, target);
93
- const candidates = [
94
- buildTechStackEntry(forensic, nowIso, tags, resolvedLanguage),
95
- buildModuleStructureEntry(forensic, nowIso, tags, resolvedLanguage),
96
- buildBuildConfigEntry(forensic, nowIso, tags, resolvedLanguage),
97
- buildCodeStyleEntry(forensic, nowIso, tags, resolvedLanguage),
98
- buildCIConfigEntry(forensic, nowIso, tags),
99
- buildReadmeFirstParaEntry(target, forensic, nowIso, tags, resolvedLanguage),
100
- buildProjectBriefEntry(target, forensic, nowIso, tags)
101
- ];
102
- const entries = candidates.filter((e) => e !== null);
103
- const sidecarPath = join(target, KNOWLEDGE_DIR, SCAN_STATE_FILE);
104
- const sidecar = await readScanState(sidecarPath);
105
- const allocator = new KnowledgeIdAllocator(join(target, AGENTS_META_FILE));
106
- const written = [];
107
- const skipped = [];
108
- const placedEntries = [];
109
- for (const entry of entries) {
110
- const targetPath = join(target, KNOWLEDGE_DIR, entry.target_subdir, `${entry.slug}.md`);
111
- const existingId = findExistingIdForFile(sidecar, targetPath, target);
112
- const id = existingId ?? await allocator.allocate(entry.layer, entry.type);
113
- const built = { ...entry, id };
114
- placedEntries.push(built);
115
- const fullContent = renderMarkdown(built);
116
- const bodyHash = sha256(stripFrontmatter(fullContent));
117
- const sidecarKey = id;
118
- if (sidecar[sidecarKey] === bodyHash && existsSync(targetPath)) {
119
- skipped.push(id);
120
- continue;
121
- }
122
- await ensureParentDirectory(targetPath);
123
- await atomicWriteText(targetPath, fullContent);
124
- sidecar[sidecarKey] = bodyHash;
125
- written.push(id);
126
- }
127
- await ensureParentDirectory(sidecarPath);
128
- await atomicWriteJson(sidecarPath, sidecar);
129
- await registerKnowledgeNodesInMeta(target, placedEntries);
130
- await writeKnowledgeMeta(target, { source: "doctor_fix" });
131
- const durationMs = Date.now() - startTs;
132
- await appendEventLedgerEvent(target, {
133
- event_type: "init_scan_completed",
134
- written_stable_ids: written,
135
- duration_ms: durationMs,
136
- source: options.source ?? "scan"
137
- });
138
- return {
139
- written_stable_ids: written,
140
- skipped_stable_ids: skipped,
141
- total_entries: entries.length,
142
- duration_ms: durationMs
143
- };
144
- }
145
- var scanCommand = defineCommand({
146
- meta: {
147
- name: "scan",
148
- description: t("cli.scan.description")
149
- },
150
- args: {
151
- target: {
152
- type: "string",
153
- description: t("cli.scan.args.target.description")
154
- },
155
- debug: {
156
- type: "boolean",
157
- description: t("cli.scan.args.debug.description"),
158
- default: false
159
- },
160
- json: {
161
- type: "boolean",
162
- description: t("cli.scan.args.json.description"),
163
- default: false
164
- }
165
- },
166
- async run({ args }) {
167
- const workspaceRoot = process.cwd();
168
- const logger = createDebugLogger(args.debug);
169
- const resolution = resolveDevMode(args.target, workspaceRoot);
170
- logger(`scan target source: ${resolution.source}`);
171
- for (const step of resolution.chain) {
172
- logger(step);
173
- }
174
- try {
175
- const result = await runInitScan(resolution.target, { source: "scan" });
176
- if (args.json) {
177
- console.log(JSON.stringify(result, null, 2));
178
- return;
179
- }
180
- printPrettyResult(result);
181
- } catch (error) {
182
- const message = error instanceof Error ? error.message : String(error);
183
- console.error(`${symbol.warn} ${paint.warn(message)}`);
184
- process.exitCode = 1;
185
- }
186
- }
187
- });
188
- var scan_default = scanCommand;
189
- var BASELINE_TEMPLATES = {
190
- en: {
191
- "tech-stack": {
192
- title: ({ frameworkSummary }) => `Tech stack: ${frameworkSummary}`,
193
- build: ({ projectName, frameworkSummary, topExtensionsLine, evidenceLines }) => ({
194
- mission: `Track the primary tech stack and runtime surface used by ${projectName}.`,
195
- context: [
196
- `Framework: ${frameworkSummary}`,
197
- `Top file extensions: ${topExtensionsLine}`,
198
- `Evidence:`,
199
- ...evidenceLines.map((line) => `- ${line}`)
200
- ].join("\n")
201
- })
202
- },
203
- "module-structure": {
204
- title: "Module structure",
205
- build: ({ projectName, totalFiles, maxDepth, dirsBlock, entriesBlock }) => ({
206
- mission: `Map the high-level module layout and primary entry points of ${projectName}.`,
207
- context: [
208
- `Total files: ${totalFiles}`,
209
- `Max directory depth: ${maxDepth}`,
210
- "",
211
- "Key directories:",
212
- dirsBlock,
213
- "",
214
- "Entry points:",
215
- entriesBlock
216
- ].join("\n")
217
- })
218
- },
219
- "build-config": {
220
- title: "Build configuration",
221
- build: ({ projectName, framework, configBlock }) => ({
222
- mission: `Document the deterministic build/bootstrap configuration anchoring ${projectName}.`,
223
- businessLogic: [
224
- `1. Detect framework: \`${framework}\`.`,
225
- `2. Read configuration files in declared order.`,
226
- `3. Honor compiler/bundler boundaries before generating new code.`,
227
- `4. Treat config drift as a fact-check signal \u2014 re-run \`fab scan\` after edits.`
228
- ].join("\n"),
229
- context: [
230
- `Framework: ${framework}`,
231
- "",
232
- "Configuration files:",
233
- configBlock
234
- ].join("\n")
235
- })
236
- },
237
- "code-style": {
238
- title: "Code style guidelines",
239
- build: ({ projectName, rulesBlock, patternsBlock }) => ({
240
- mission: `Codify the recurring authoring conventions observed in ${projectName}.`,
241
- mandatoryInjection: [
242
- "When generating or modifying source files in this repo, AI agents MUST:",
243
- rulesBlock
244
- ].join("\n"),
245
- context: [
246
- "Detected patterns:",
247
- patternsBlock
248
- ].join("\n")
249
- })
250
- },
251
- "readme-first-paragraph": {
252
- title: "README first paragraph",
253
- build: ({ lineCount, quality, excerpt }) => ({
254
- mission: `Preserve the README headline and first paragraph as the canonical project elevator pitch.`,
255
- context: [
256
- `Source: README.md (${lineCount} lines, quality=${quality})`,
257
- "",
258
- "Excerpt:",
259
- "> " + excerpt.split("\n").join("\n> ")
260
- ].join("\n")
261
- })
262
- }
263
- },
264
- "zh-CN": {
265
- "tech-stack": {
266
- title: ({ frameworkSummary }) => `Tech stack: ${frameworkSummary}`,
267
- build: ({ projectName, frameworkSummary, topExtensionsLine, evidenceLines }) => ({
268
- mission: `\u8BB0\u5F55 ${projectName} \u6240\u4F7F\u7528\u7684\u4E3B\u8981 tech stack \u4E0E\u8FD0\u884C\u65F6\u9762\u3002`,
269
- context: [
270
- `Framework\uFF1A${frameworkSummary}`,
271
- `\u4E3B\u8981\u6587\u4EF6\u540E\u7F00\uFF1A${topExtensionsLine}`,
272
- `\u8BC1\u636E\uFF1A`,
273
- ...evidenceLines.map((line) => `- ${line}`)
274
- ].join("\n")
275
- })
276
- },
277
- "module-structure": {
278
- title: "Module structure",
279
- build: ({ projectName, totalFiles, maxDepth, dirsBlock, entriesBlock }) => ({
280
- mission: `\u68B3\u7406 ${projectName} \u7684\u9AD8\u5C42 module \u5E03\u5C40\u4E0E\u4E3B\u8981 entry point\u3002`,
281
- context: [
282
- `\u6587\u4EF6\u603B\u6570\uFF1A${totalFiles}`,
283
- `\u6700\u5927\u76EE\u5F55\u6DF1\u5EA6\uFF1A${maxDepth}`,
284
- "",
285
- "\u5173\u952E\u76EE\u5F55\uFF1A",
286
- dirsBlock,
287
- "",
288
- "Entry points\uFF1A",
289
- entriesBlock
290
- ].join("\n")
291
- })
292
- },
293
- "build-config": {
294
- title: "Build configuration",
295
- build: ({ projectName, framework, configBlock }) => ({
296
- mission: `\u8BB0\u5F55 ${projectName} \u6240\u4F9D\u8D56\u7684\u3001\u786E\u5B9A\u6027\u7684 build / bootstrap \u914D\u7F6E\u3002`,
297
- businessLogic: [
298
- `1. \u63A2\u6D4B framework\uFF1A\`${framework}\`\u3002`,
299
- `2. \u6309\u58F0\u660E\u987A\u5E8F\u8BFB\u53D6 configuration files\u3002`,
300
- `3. \u5728\u751F\u6210\u65B0\u4EE3\u7801\u4E4B\u524D\uFF0C\u5C0A\u91CD compiler / bundler \u7684\u8FB9\u754C\u3002`,
301
- `4. \u628A config \u6F02\u79FB\u89C6\u4E3A fact-check \u4FE1\u53F7 \u2014\u2014 \u4FEE\u6539\u540E\u91CD\u65B0\u8FD0\u884C \`fab scan\`\u3002`
302
- ].join("\n"),
303
- context: [
304
- `Framework\uFF1A${framework}`,
305
- "",
306
- "Configuration files\uFF1A",
307
- configBlock
308
- ].join("\n")
309
- })
310
- },
311
- "code-style": {
312
- title: "Code style guidelines",
313
- build: ({ projectName, rulesBlock, patternsBlock }) => ({
314
- mission: `\u56FA\u5316 ${projectName} \u4E2D\u53CD\u590D\u51FA\u73B0\u7684\u5199\u7801\u7EA6\u5B9A\u3002`,
315
- mandatoryInjection: [
316
- "\u5728\u672C\u4ED3\u5E93\u5185\u751F\u6210\u6216\u4FEE\u6539\u6E90\u7801\u6587\u4EF6\u65F6\uFF0CAI agent \u5FC5\u987B\uFF1A",
317
- rulesBlock
318
- ].join("\n"),
319
- context: [
320
- "\u89C2\u5BDF\u5230\u7684\u6A21\u5F0F\uFF1A",
321
- patternsBlock
322
- ].join("\n")
323
- })
324
- },
325
- "readme-first-paragraph": {
326
- title: "README first paragraph",
327
- build: ({ lineCount, quality, excerpt }) => ({
328
- mission: `\u628A README \u7684\u6807\u9898\u4E0E\u9996\u6BB5\u4FDD\u7559\u4E3A\u9879\u76EE\u5BF9\u5916\u7684 canonical elevator pitch\u3002`,
329
- context: [
330
- `\u6765\u6E90\uFF1AREADME.md\uFF08${lineCount} \u884C\uFF0Cquality=${quality}\uFF09`,
331
- "",
332
- "\u6458\u5F55\uFF1A",
333
- "> " + excerpt.split("\n").join("\n> ")
334
- ].join("\n")
335
- })
336
- }
337
- }
338
- };
339
- function resolveTemplateTitle(template, inputs) {
340
- return typeof template.title === "function" ? template.title(inputs) : template.title;
341
- }
342
- function resolveKnowledgeLanguage(configured, target) {
343
- if (configured === "en" || configured === "zh-CN") {
344
- return configured;
345
- }
346
- return detectExistingLanguage(target);
347
- }
348
- function detectExistingLanguage(target) {
349
- const ZH_CN_RATIO_THRESHOLD = 0.3;
350
- const samples = [];
351
- const readmePath = join(target, "README.md");
352
- if (existsSync(readmePath)) {
353
- try {
354
- samples.push(readFileSync(readmePath, "utf8"));
355
- } catch {
356
- }
357
- }
358
- const docsDir = join(target, "docs");
359
- if (existsSync(docsDir)) {
360
- try {
361
- const stat = statSync(docsDir);
362
- if (stat.isDirectory()) {
363
- for (const entry of readdirSync(docsDir, { withFileTypes: true })) {
364
- if (!entry.isFile()) continue;
365
- if (!/\.(md|mdx|txt)$/iu.test(entry.name)) continue;
366
- try {
367
- samples.push(readFileSync(join(docsDir, entry.name), "utf8"));
368
- } catch {
369
- }
370
- }
371
- }
372
- } catch {
373
- }
374
- }
375
- if (samples.length === 0) {
376
- return "en";
377
- }
378
- let cjkCount = 0;
379
- let asciiLetterCount = 0;
380
- for (const sample of samples) {
381
- for (const ch of sample) {
382
- const code = ch.codePointAt(0) ?? 0;
383
- if (code >= 19968 && code <= 40959) {
384
- cjkCount += 1;
385
- } else if (code >= 65 && code <= 90 || code >= 97 && code <= 122) {
386
- asciiLetterCount += 1;
387
- }
388
- }
389
- }
390
- const denominator = cjkCount + asciiLetterCount;
391
- if (denominator === 0) {
392
- return "en";
393
- }
394
- const ratio = cjkCount / denominator;
395
- return ratio > ZH_CN_RATIO_THRESHOLD ? "zh-CN" : "en";
396
- }
397
- function buildTechStackEntry(forensic, nowIso, tags, language = "en") {
398
- const framework = forensic.framework;
399
- const byExt = forensic.topology.by_ext ?? {};
400
- const topExtensions = Object.entries(byExt).sort(([, a], [, b]) => b - a).slice(0, 5).map(([ext, count]) => `${ext} (${count})`);
401
- const frameworkSummary = `${framework.kind}${framework.version ? ` ${framework.version}` : ""}${framework.subkind ? ` / ${framework.subkind}` : ""}`;
402
- const topExtensionsLine = topExtensions.length > 0 ? topExtensions.join(", ") : "(none)";
403
- const evidenceLines = framework.evidence.length > 0 ? framework.evidence.slice(0, 6) : ["(no explicit framework evidence)"];
404
- const inputs = {
405
- projectName: forensic.project_name,
406
- frameworkSummary,
407
- topExtensionsLine,
408
- evidenceLines
409
- };
410
- const template = BASELINE_TEMPLATES[language]["tech-stack"];
411
- const sections = template.build(inputs);
412
- const body = renderSections(sections);
413
- const relevancePaths = ["package.json", "pnpm-workspace.yaml"];
414
- return {
415
- type: "model",
416
- layer: "team",
417
- maturity: "verified",
418
- layer_reason: LAYER_REASON,
419
- created_at: nowIso,
420
- title: resolveTemplateTitle(template, inputs),
421
- body,
422
- target_subdir: "models",
423
- slug: "tech-stack",
424
- tags,
425
- relevance_scope: "narrow",
426
- relevance_paths: relevancePaths
427
- };
428
- }
429
- function buildModuleStructureEntry(forensic, nowIso, tags, language = "en") {
430
- const keyDirs = forensic.topology.key_dirs ?? [];
431
- const entryPoints = forensic.entry_points ?? [];
432
- const totalFiles = forensic.topology.total_files ?? 0;
433
- const dirsBlock = keyDirs.length > 0 ? keyDirs.slice(0, 12).map((dir) => `- ${dir}`).join("\n") : "- (no key directories detected)";
434
- const entriesBlock = entryPoints.length > 0 ? entryPoints.slice(0, 8).map((ep) => `- ${ep.path} \u2014 ${ep.reason}`).join("\n") : "- (no entry points detected)";
435
- const inputs = {
436
- projectName: forensic.project_name,
437
- totalFiles,
438
- maxDepth: forensic.topology.max_depth ?? 0,
439
- dirsBlock,
440
- entriesBlock
441
- };
442
- const template = BASELINE_TEMPLATES[language]["module-structure"];
443
- const body = renderSections(template.build(inputs));
444
- const relevancePaths = ["packages/**/package.json"];
445
- return {
446
- type: "model",
447
- layer: "team",
448
- maturity: "verified",
449
- layer_reason: LAYER_REASON,
450
- created_at: nowIso,
451
- title: resolveTemplateTitle(template, inputs),
452
- body,
453
- target_subdir: "models",
454
- slug: "module-structure",
455
- tags,
456
- relevance_scope: "narrow",
457
- relevance_paths: relevancePaths
458
- };
459
- }
460
- function buildBuildConfigEntry(forensic, nowIso, tags, language = "en") {
461
- const configFiles = (forensic.candidate_files ?? []).filter((entry) => entry.family === "config").map((entry) => entry.path);
462
- const framework = forensic.framework.kind;
463
- const configBlock = configFiles.length > 0 ? configFiles.map((file) => `- ${file}`).join("\n") : "- (no config files detected)";
464
- const inputs = {
465
- projectName: forensic.project_name,
466
- framework,
467
- configBlock
468
- };
469
- const template = BASELINE_TEMPLATES[language]["build-config"];
470
- const body = renderSections(template.build(inputs));
471
- const discovered = configFiles.filter((path) => isBuildConfigPath(path));
472
- const relevancePaths = discovered.length > 0 ? Array.from(new Set(discovered)) : ["tsconfig.json", "tsconfig.*.json", "vite.config.*", "rollup.config.*", "webpack.config.*"];
473
- return {
474
- type: "process",
475
- layer: "team",
476
- maturity: "verified",
477
- layer_reason: LAYER_REASON,
478
- created_at: nowIso,
479
- title: resolveTemplateTitle(template, inputs),
480
- body,
481
- target_subdir: "processes",
482
- slug: "build-config",
483
- tags,
484
- relevance_scope: "narrow",
485
- relevance_paths: relevancePaths
486
- };
487
- }
488
- function buildCodeStyleEntry(forensic, nowIso, tags, language = "en") {
489
- const dominantPatterns = (forensic.assertions ?? []).filter((a) => a.type === "pattern" || a.type === "domain").slice(0, 4).map((a) => `- ${a.statement}`);
490
- const proposedRules = (forensic.assertions ?? []).map((a) => a.proposed_rule).filter((rule) => typeof rule === "string" && rule.length > 0).slice(0, 4);
491
- const patternsBlock = dominantPatterns.length > 0 ? dominantPatterns.join("\n") : "- (no dominant patterns detected)";
492
- const rulesBlock = proposedRules.length > 0 ? proposedRules.map((rule) => `- ${rule}`).join("\n") : "- Follow existing module/file patterns; do not introduce new conventions without team agreement.";
493
- const inputs = {
494
- projectName: forensic.project_name,
495
- rulesBlock,
496
- patternsBlock
497
- };
498
- const template = BASELINE_TEMPLATES[language]["code-style"];
499
- const body = renderSections(template.build(inputs));
500
- const relevancePaths = [
501
- ".prettierrc",
502
- ".prettierrc.*",
503
- ".editorconfig",
504
- "eslint.config.*",
505
- ".eslintrc",
506
- ".eslintrc.*"
507
- ];
508
- return {
509
- type: "guideline",
510
- layer: "team",
511
- maturity: "verified",
512
- layer_reason: LAYER_REASON,
513
- created_at: nowIso,
514
- title: resolveTemplateTitle(template, inputs),
515
- body,
516
- target_subdir: "guidelines",
517
- slug: "code-style",
518
- tags,
519
- relevance_scope: "narrow",
520
- relevance_paths: relevancePaths
521
- };
522
- }
523
- function buildCIConfigEntry(forensic, nowIso, tags) {
524
- const ciFiles = (forensic.candidate_files ?? []).map((entry) => entry.path).filter((path) => isCIConfigPath(path));
525
- const ciExtensions = forensic.topology.by_ext ?? {};
526
- 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));
527
- if (!hasCISignal) {
528
- return null;
529
- }
530
- const filesBlock = ciFiles.length > 0 ? ciFiles.map((file) => `- ${file}`).join("\n") : "- (CI configuration inferred from repository topology)";
531
- const body = renderSections({
532
- mission: `Document the CI / continuous-verification pipeline guarding ${forensic.project_name}.`,
533
- businessLogic: [
534
- "1. Pull request opens \u2192 CI workflow triggers.",
535
- "2. Lint + typecheck + unit tests must pass before review.",
536
- "3. Failing checks block merge until resolved.",
537
- "4. Updates to CI configuration should accompany the change they enable."
538
- ].join("\n"),
539
- context: [
540
- "CI configuration sources:",
541
- filesBlock
542
- ].join("\n")
543
- });
544
- const relevancePaths = [
545
- ".github/workflows/**",
546
- ".gitlab-ci.yml",
547
- ".circleci/**",
548
- "Jenkinsfile"
549
- ];
550
- return {
551
- type: "process",
552
- layer: "team",
553
- maturity: "verified",
554
- layer_reason: LAYER_REASON,
555
- created_at: nowIso,
556
- title: "CI configuration",
557
- body,
558
- target_subdir: "processes",
559
- slug: "ci-config",
560
- tags,
561
- relevance_scope: "narrow",
562
- relevance_paths: relevancePaths
563
- };
564
- }
565
- function buildReadmeFirstParaEntry(target, forensic, nowIso, tags, language = "en") {
566
- if (forensic.readme.quality === "missing") {
567
- return null;
568
- }
569
- const readmePath = join(target, "README.md");
570
- if (!existsSync(readmePath)) {
571
- return null;
572
- }
573
- const readme = readFileSync(readmePath, "utf8");
574
- const firstPara = extractFirstParagraph(readme);
575
- if (firstPara === null) {
576
- return null;
577
- }
578
- const inputs = {
579
- lineCount: forensic.readme.line_count,
580
- quality: forensic.readme.quality,
581
- excerpt: firstPara
582
- };
583
- const template = BASELINE_TEMPLATES[language]["readme-first-paragraph"];
584
- const body = renderSections(template.build(inputs));
585
- return {
586
- type: "model",
587
- layer: "team",
588
- maturity: "verified",
589
- layer_reason: LAYER_REASON,
590
- created_at: nowIso,
591
- title: resolveTemplateTitle(template, inputs),
592
- body,
593
- target_subdir: "models",
594
- slug: "readme-first-paragraph",
595
- tags,
596
- // v2.0-rc.7 T2: broad by design — single repo-root file, the Phase 1.5
597
- // PreToolUse blacklist already covers README. Anchoring this entry to
598
- // README.md would surface it on every README edit, which is noise.
599
- relevance_scope: "broad",
600
- relevance_paths: []
601
- };
602
- }
603
- function buildProjectBriefEntry(target, forensic, nowIso, tags) {
604
- if (forensic.readme.quality === "missing") {
605
- return null;
606
- }
607
- const readmePath = join(target, "README.md");
608
- if (!existsSync(readmePath)) {
609
- return null;
610
- }
611
- const readme = readFileSync(readmePath, "utf8");
612
- const description = extractExplicitDescription(readme);
613
- if (description === null) {
614
- return null;
615
- }
616
- const body = renderSections({
617
- mission: `Capture the explicit project description declared by README.md.`,
618
- context: [
619
- `Project: ${forensic.project_name}`,
620
- "",
621
- "Declared description:",
622
- "> " + description.split("\n").join("\n> ")
623
- ].join("\n")
624
- });
625
- return {
626
- type: "model",
627
- layer: "team",
628
- maturity: "verified",
629
- layer_reason: LAYER_REASON,
630
- created_at: nowIso,
631
- title: "Project brief",
632
- body,
633
- target_subdir: "models",
634
- slug: "project-brief",
635
- tags,
636
- // v2.0-rc.7 T2: broad — project brief is a cross-cutting description
637
- // with no path anchor. Narrowing it to README.md would duplicate the
638
- // readme-first-paragraph surface; keeping it broad lets the
639
- // SessionStart broad hint do the right thing.
640
- relevance_scope: "broad",
641
- relevance_paths: []
642
- };
643
- }
644
- function renderMarkdown(entry) {
645
- const frontmatter = renderFrontmatter(entry);
646
- return `${frontmatter}
647
-
648
- # ${entry.title}
649
-
650
- ${entry.body}
651
- `;
652
- }
653
- function renderFrontmatter(entry) {
654
- const tagsLine = entry.tags.length > 0 ? `tags: [${entry.tags.join(", ")}]` : "tags: []";
655
- const relevancePathsLine = entry.relevance_paths.length > 0 ? `relevance_paths: [${entry.relevance_paths.map((p) => quoteIfNeeded(p)).join(", ")}]` : "relevance_paths: []";
656
- const lines = [
657
- "---",
658
- `id: ${entry.id}`,
659
- `type: ${entry.type}`,
660
- `layer: ${entry.layer}`,
661
- `maturity: ${entry.maturity}`,
662
- `layer_reason: ${quoteIfNeeded(entry.layer_reason)}`,
663
- `created_at: ${entry.created_at}`,
664
- tagsLine,
665
- `relevance_scope: ${entry.relevance_scope}`,
666
- relevancePathsLine,
667
- "---"
668
- ];
669
- return lines.join("\n");
670
- }
671
- function renderSections(input) {
672
- const parts = [];
673
- parts.push(`## [MISSION_STATEMENT]
674
-
675
- ${input.mission}`);
676
- if (input.mandatoryInjection !== void 0) {
677
- parts.push(`## [MANDATORY_INJECTION]
678
-
679
- ${input.mandatoryInjection}`);
680
- }
681
- if (input.businessLogic !== void 0) {
682
- parts.push(`## [BUSINESS_LOGIC_CHUNKS]
683
-
684
- ${input.businessLogic}`);
685
- }
686
- parts.push(`## [CONTEXT_INFO]
687
-
688
- ${input.context}`);
689
- return parts.join("\n\n");
690
- }
691
- function quoteIfNeeded(value) {
692
- return `"${value.replace(/"/g, '\\"')}"`;
693
- }
694
- function stripFrontmatter(content) {
695
- return content.replace(/^---[\s\S]*?\r?\n---\s*\r?\n?/u, "");
696
- }
697
- function deriveTagsFromForensic(forensic) {
698
- const MAX_TAGS = 5;
699
- const seen = /* @__PURE__ */ new Set();
700
- const tags = [];
701
- function add(raw) {
702
- const normalized = raw.toLowerCase().trim().replace(/\s+/gu, "-");
703
- if (normalized.length > 0 && !seen.has(normalized)) {
704
- seen.add(normalized);
705
- tags.push(normalized);
706
- }
707
- }
708
- if (forensic.framework.kind) {
709
- add(forensic.framework.kind);
710
- }
711
- const SKIP_EXTS = /* @__PURE__ */ new Set([".json", ".md", ".lock", ".yaml", ".yml", ".txt", ".env"]);
712
- const EXT_MAP = {
713
- ".ts": "typescript",
714
- ".tsx": "typescript",
715
- ".js": "javascript",
716
- ".jsx": "javascript",
717
- ".mjs": "javascript",
718
- ".cjs": "javascript",
719
- ".py": "python",
720
- ".go": "go",
721
- ".rs": "rust",
722
- ".java": "java",
723
- ".cs": "csharp",
724
- ".rb": "ruby",
725
- ".php": "php",
726
- ".swift": "swift",
727
- ".kt": "kotlin"
728
- };
729
- const byExt = forensic.topology.by_ext ?? {};
730
- const sorted = Object.entries(byExt).filter(([ext]) => !SKIP_EXTS.has(ext)).sort(([, a], [, b]) => b - a);
731
- for (const [ext] of sorted) {
732
- if (tags.length >= MAX_TAGS) break;
733
- const mapped = EXT_MAP[ext] ?? ext.replace(/^\./u, "");
734
- add(mapped);
735
- }
736
- return tags.slice(0, MAX_TAGS);
737
- }
738
- async function readForensic(forensicPath) {
739
- const raw = await readFile(forensicPath, "utf8");
740
- return JSON.parse(raw);
741
- }
742
- async function readScanState(sidecarPath) {
743
- if (!existsSync(sidecarPath)) {
744
- return {};
745
- }
746
- try {
747
- const raw = await readFile(sidecarPath, "utf8");
748
- const parsed = JSON.parse(raw);
749
- if (parsed === null || typeof parsed !== "object") {
750
- return {};
751
- }
752
- const result = {};
753
- for (const [key, value] of Object.entries(parsed)) {
754
- if (typeof value === "string") {
755
- result[key] = value;
756
- }
757
- }
758
- return result;
759
- } catch {
760
- return {};
761
- }
762
- }
763
- function findExistingIdForFile(sidecar, targetPath, target) {
764
- if (!existsSync(targetPath)) {
765
- return null;
766
- }
767
- try {
768
- const raw = readFileSync(targetPath, "utf8");
769
- const match = /^---\r?\n([\s\S]*?)\r?\n---/u.exec(raw);
770
- if (match === null) {
771
- return null;
772
- }
773
- const idLine = /^id:\s*(.+)$/mu.exec(match[1]);
774
- if (idLine === null) {
775
- return null;
776
- }
777
- const candidate = idLine[1].replace(/^["'](.*)["']$/u, "$1").trim();
778
- if (/^K[PT]-(MOD|DEC|GLD|PIT|PRO)-\d{4,}$/.test(candidate) && sidecar[candidate] !== void 0) {
779
- return candidate;
780
- }
781
- return null;
782
- } catch {
783
- void target;
784
- return null;
785
- }
786
- }
787
- function isCIConfigPath(path) {
788
- return path.startsWith(".github/workflows/") || path.startsWith(".gitlab-ci") || path === "azure-pipelines.yml" || path === ".circleci/config.yml" || path === "Jenkinsfile" || path === ".travis.yml";
789
- }
790
- function isBuildConfigPath(path) {
791
- const lower = path.toLowerCase();
792
- const basename = lower.split("/").pop() ?? lower;
793
- if (basename.startsWith("tsconfig") && basename.endsWith(".json")) return true;
794
- if (basename === "package.json") return true;
795
- if (basename === "pnpm-workspace.yaml" || basename === "pnpm-workspace.yml") return true;
796
- if (basename.startsWith("vite.config.")) return true;
797
- if (basename.startsWith("rollup.config.")) return true;
798
- if (basename.startsWith("webpack.config.")) return true;
799
- if (basename.startsWith("vitest.config.")) return true;
800
- if (basename === "turbo.json" || basename === "nx.json") return true;
801
- return false;
802
- }
803
- function extractFirstParagraph(readme) {
804
- const lines = readme.split(/\r?\n/);
805
- let i = 0;
806
- while (i < lines.length && lines[i].trim().length === 0) i += 1;
807
- while (i < lines.length && /^#{1,2}\s/.test(lines[i].trim())) {
808
- i += 1;
809
- while (i < lines.length && lines[i].trim().length === 0) i += 1;
810
- }
811
- if (i >= lines.length) {
812
- return null;
813
- }
814
- const collected = [];
815
- while (i < lines.length && lines[i].trim().length > 0) {
816
- if (/^#{1,6}\s/.test(lines[i].trim())) break;
817
- collected.push(lines[i]);
818
- i += 1;
819
- }
820
- const paragraph = collected.join("\n").trim();
821
- return paragraph.length > 0 ? paragraph : null;
822
- }
823
- function extractExplicitDescription(readme) {
824
- 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);
825
- if (headingMatch !== null) {
826
- const text = headingMatch[1].trim();
827
- if (text.length > 0) return text;
828
- }
829
- const labelMatch = /^\*\*(?:Description|About|Overview|Summary)\*\*\s*:?\s*(.+?)(?:\r?\n\r?\n|$)/imu.exec(readme);
830
- if (labelMatch !== null) {
831
- const text = labelMatch[1].trim();
832
- if (text.length > 0) return text;
833
- }
834
- return null;
835
- }
836
- function sha256(content) {
837
- return `sha256:${createHash("sha256").update(content).digest("hex")}`;
838
- }
839
- async function registerKnowledgeNodesInMeta(target, entries) {
840
- if (entries.length === 0) {
841
- return;
842
- }
843
- const metaPath = join(target, AGENTS_META_FILE);
844
- let meta;
845
- try {
846
- const raw = await readFile(metaPath, "utf8");
847
- meta = JSON.parse(raw);
848
- } catch {
849
- meta = {};
850
- }
851
- const nodes = typeof meta.nodes === "object" && meta.nodes !== null ? meta.nodes : {};
852
- for (const entry of entries) {
853
- const contentRef = `${KNOWLEDGE_DIR}/${entry.target_subdir}/${entry.slug}.md`;
854
- const absPath = join(target, contentRef);
855
- let hash = "";
856
- try {
857
- const raw = readFileSync(absPath, "utf8");
858
- hash = sha256(raw);
859
- } catch {
860
- }
861
- nodes[entry.id] = {
862
- file: contentRef,
863
- content_ref: contentRef,
864
- scope_glob: "**",
865
- deps: [],
866
- priority: "medium",
867
- level: "L1",
868
- layer: "L1",
869
- topology_type: "cross-cutting",
870
- hash,
871
- stable_id: entry.id,
872
- identity_source: "declared"
873
- };
874
- }
875
- meta.nodes = nodes;
876
- await ensureParentDirectory(metaPath);
877
- await atomicWriteJson(metaPath, meta);
878
- }
879
- async function ensureParentDirectory(filePath) {
880
- await mkdir(dirname(filePath), { recursive: true });
881
- }
882
- function printPrettyResult(result) {
883
- const writtenCount = result.written_stable_ids.length;
884
- const skippedCount = result.skipped_stable_ids.length;
885
- if (writtenCount === 0) {
886
- console.log(`${symbol.ok} ${paint.success(t("cli.scan.summary.skipped", { count: String(skippedCount) }))}`);
887
- return;
888
- }
889
- console.log(`${symbol.ok} ${paint.success(t("cli.scan.summary.created", { count: String(writtenCount) }))}`);
890
- for (const id of result.written_stable_ids) {
891
- console.log(` - ${paint.ai(id)}`);
892
- }
893
- if (skippedCount > 0) {
894
- console.log(paint.muted(`(${skippedCount} unchanged, skipped)`));
895
- }
896
- }
897
- function normalizeTarget(targetInput) {
898
- return isAbsolute(targetInput) ? targetInput : resolve(process.cwd(), targetInput);
899
- }
900
- function getReadmeQuality(target) {
901
- const readmePath = join(target, "README.md");
902
- if (!existsSync(readmePath)) {
903
- return "stub";
904
- }
905
- const wordCount = readFileSync(readmePath, "utf8").trim().split(/\s+/).filter(Boolean).length;
906
- return wordCount >= 200 ? "ok" : "stub";
907
- }
908
- function walkFiles(root, ignorePatterns) {
909
- if (!existsSync(root) || !statSync(root).isDirectory()) {
910
- throw new Error(t("cli.shared.target-invalid", { target: root }));
911
- }
912
- let fileCount = 0;
913
- let ignoredCount = 0;
914
- const stack = [root];
915
- while (stack.length > 0) {
916
- const current = stack.pop();
917
- if (current === void 0) {
918
- continue;
919
- }
920
- for (const entry of readdirSync(current, { withFileTypes: true })) {
921
- const absolutePath = join(current, entry.name);
922
- const relativePath = toPosixPath(relative(root, absolutePath));
923
- if (shouldIgnore(relativePath, entry.isDirectory(), ignorePatterns)) {
924
- ignoredCount += 1;
925
- continue;
926
- }
927
- if (entry.isDirectory()) {
928
- stack.push(absolutePath);
929
- } else if (entry.isFile()) {
930
- fileCount += 1;
931
- }
932
- }
933
- }
934
- return { fileCount, ignoredCount };
935
- }
936
- function shouldIgnore(relativePath, isDirectory, ignorePatterns) {
937
- return ignorePatterns.some((pattern) => matchesIgnorePattern(relativePath, isDirectory, pattern));
938
- }
939
- function matchesIgnorePattern(relativePath, isDirectory, pattern) {
940
- const normalizedPattern = toPosixPath(pattern);
941
- if (normalizedPattern === "**/*.meta") {
942
- return relativePath.endsWith(".meta");
943
- }
944
- if (normalizedPattern.endsWith("/**")) {
945
- const directoryPrefix = normalizedPattern.slice(0, -3);
946
- return relativePath === directoryPrefix || relativePath.startsWith(`${directoryPrefix}/`) || isDirectory && `${relativePath}/` === directoryPrefix;
947
- }
948
- return relativePath === normalizedPattern;
949
- }
950
- function toPosixPath(path) {
951
- return path.split(sep).join("/");
952
- }
953
- function buildRecommendations(input) {
954
- const recommendations = [];
955
- if (!input.hasExistingFabric) {
956
- recommendations.push(t("cli.scan.recommendation.init"));
957
- }
958
- if (input.readmeQuality === "stub") {
959
- recommendations.push(t("cli.scan.recommendation.readme"));
960
- }
961
- if (!input.hasContributing) {
962
- recommendations.push(t("cli.scan.recommendation.contributing"));
963
- }
964
- if (input.framework.kind === "unknown") {
965
- recommendations.push(t("cli.scan.recommendation.unknown-framework"));
966
- } else {
967
- recommendations.push(t("cli.scan.recommendation.framework-dirs", { framework: input.framework.kind }));
968
- }
969
- return recommendations;
970
- }
971
- var __testing__ = {
972
- buildTechStackEntry,
973
- buildModuleStructureEntry,
974
- buildBuildConfigEntry,
975
- buildCodeStyleEntry,
976
- buildCIConfigEntry,
977
- buildReadmeFirstParaEntry,
978
- buildProjectBriefEntry,
979
- renderMarkdown,
980
- stripFrontmatter,
981
- isCIConfigPath,
982
- isBuildConfigPath,
983
- extractFirstParagraph,
984
- extractExplicitDescription,
985
- // TASK-008: bilingual template registry + language detection
986
- detectExistingLanguage,
987
- resolveKnowledgeLanguage,
988
- BASELINE_TEMPLATES
989
- };
990
-
991
- export {
992
- detectFramework,
993
- formatKnowledgeId,
994
- createScanReport,
995
- runInitScan,
996
- scanCommand,
997
- scan_default,
998
- deriveTagsFromForensic,
999
- __testing__
1000
- };