@fenglimg/fabric-cli 2.0.0-rc.22 → 2.0.0-rc.23

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