@fenglimg/fabric-cli 2.0.0-rc.21 → 2.0.0-rc.22
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-FNO7CQDG.js → chunk-PSVKSMRO.js} +180 -82
- package/dist/{doctor-L6TIXXIX.js → doctor-HIX2FFEP.js} +1 -1
- package/dist/index.js +4 -4
- package/dist/{install-DNZXGFHJ.js → install-WJZQZM7D.js} +2 -2
- package/dist/{plan-context-hint-CFDGXHCA.js → plan-context-hint-RYVSMULL.js} +8 -1
- package/package.json +3 -3
- package/templates/hooks/knowledge-hint-broad.cjs +40 -0
- package/templates/hooks/lib/banner-i18n.cjs +30 -0
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
// src/commands/scan.ts
|
|
10
10
|
import { createHash } from "crypto";
|
|
11
11
|
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
12
|
-
import { mkdir, readFile } from "fs/promises";
|
|
12
|
+
import { mkdir, readFile, unlink } from "fs/promises";
|
|
13
13
|
import { dirname, isAbsolute, join, resolve } from "path";
|
|
14
14
|
import {
|
|
15
15
|
KnowledgeIdAllocator,
|
|
@@ -25,6 +25,30 @@ var SCAN_STATE_FILE = ".scan-state.json";
|
|
|
25
25
|
var FORENSIC_FILE = ".fabric/forensic.json";
|
|
26
26
|
var AGENTS_META_FILE = ".fabric/agents.meta.json";
|
|
27
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;
|
|
28
52
|
async function runInitScan(targetInput, options = {}) {
|
|
29
53
|
const startTs = Date.now();
|
|
30
54
|
const target = normalizeTarget(targetInput);
|
|
@@ -32,20 +56,20 @@ async function runInitScan(targetInput, options = {}) {
|
|
|
32
56
|
if (!existsSync(forensicPath)) {
|
|
33
57
|
throw new Error(t("cli.scan.error.missing-forensic", { path: forensicPath }));
|
|
34
58
|
}
|
|
59
|
+
await migrateLegacyBaselineFilenames(target);
|
|
35
60
|
const forensic = await readForensic(forensicPath);
|
|
36
61
|
const nowIso = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
37
|
-
const tags = deriveTagsFromForensic(forensic);
|
|
38
62
|
const fabricConfig = readFabricConfig(target);
|
|
39
63
|
const fabricLanguage = fabricConfig.fabric_language ?? "match-existing";
|
|
40
64
|
const resolvedLanguage = resolveFabricLanguage(fabricLanguage, target);
|
|
41
65
|
const candidates = [
|
|
42
|
-
buildTechStackEntry(forensic, nowIso,
|
|
43
|
-
buildModuleStructureEntry(forensic, nowIso,
|
|
44
|
-
buildBuildConfigEntry(forensic, nowIso,
|
|
45
|
-
buildCodeStyleEntry(forensic, nowIso,
|
|
46
|
-
buildCIConfigEntry(forensic, nowIso
|
|
47
|
-
buildReadmeFirstParaEntry(target, forensic, nowIso,
|
|
48
|
-
buildProjectBriefEntry(target, forensic, nowIso
|
|
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)
|
|
49
73
|
];
|
|
50
74
|
const entries = candidates.filter((e) => e !== null);
|
|
51
75
|
const sidecarPath = join(target, KNOWLEDGE_DIR, SCAN_STATE_FILE);
|
|
@@ -55,11 +79,12 @@ async function runInitScan(targetInput, options = {}) {
|
|
|
55
79
|
const skipped = [];
|
|
56
80
|
const placedEntries = [];
|
|
57
81
|
for (const entry of entries) {
|
|
58
|
-
const
|
|
59
|
-
const existingId =
|
|
82
|
+
const subdirAbs = join(target, KNOWLEDGE_DIR, entry.target_subdir);
|
|
83
|
+
const existingId = findExistingIdBySlug(sidecar, subdirAbs, entry.slug);
|
|
60
84
|
const id = existingId ?? await allocator.allocate(entry.layer, entry.type);
|
|
61
85
|
const built = { ...entry, id };
|
|
62
86
|
placedEntries.push(built);
|
|
87
|
+
const targetPath = join(subdirAbs, `${id}--${entry.slug}.md`);
|
|
63
88
|
const fullContent = renderMarkdown(built);
|
|
64
89
|
const bodyHash = sha256(stripFrontmatter(fullContent));
|
|
65
90
|
const sidecarKey = id;
|
|
@@ -303,7 +328,7 @@ function detectExistingLanguage(target) {
|
|
|
303
328
|
const ratio = cjkCount / denominator;
|
|
304
329
|
return ratio > ZH_CN_RATIO_THRESHOLD ? "zh-CN-hybrid" : "en";
|
|
305
330
|
}
|
|
306
|
-
function buildTechStackEntry(forensic, nowIso,
|
|
331
|
+
function buildTechStackEntry(forensic, nowIso, language = "en") {
|
|
307
332
|
const framework = forensic.framework;
|
|
308
333
|
const byExt = forensic.topology.by_ext ?? {};
|
|
309
334
|
const topExtensions = Object.entries(byExt).sort(([, a], [, b]) => b - a).slice(0, 5).map(([ext, count]) => `${ext} (${count})`);
|
|
@@ -330,12 +355,12 @@ function buildTechStackEntry(forensic, nowIso, tags, language = "en") {
|
|
|
330
355
|
body,
|
|
331
356
|
target_subdir: "models",
|
|
332
357
|
slug: "tech-stack",
|
|
333
|
-
tags,
|
|
358
|
+
tags: [],
|
|
334
359
|
relevance_scope: "narrow",
|
|
335
360
|
relevance_paths: relevancePaths
|
|
336
361
|
};
|
|
337
362
|
}
|
|
338
|
-
function buildModuleStructureEntry(forensic, nowIso,
|
|
363
|
+
function buildModuleStructureEntry(forensic, nowIso, language = "en") {
|
|
339
364
|
const keyDirs = forensic.topology.key_dirs ?? [];
|
|
340
365
|
const entryPoints = forensic.entry_points ?? [];
|
|
341
366
|
const totalFiles = forensic.topology.total_files ?? 0;
|
|
@@ -361,12 +386,12 @@ function buildModuleStructureEntry(forensic, nowIso, tags, language = "en") {
|
|
|
361
386
|
body,
|
|
362
387
|
target_subdir: "models",
|
|
363
388
|
slug: "module-structure",
|
|
364
|
-
tags,
|
|
389
|
+
tags: [],
|
|
365
390
|
relevance_scope: "narrow",
|
|
366
391
|
relevance_paths: relevancePaths
|
|
367
392
|
};
|
|
368
393
|
}
|
|
369
|
-
function buildBuildConfigEntry(forensic, nowIso,
|
|
394
|
+
function buildBuildConfigEntry(forensic, nowIso, language = "en") {
|
|
370
395
|
const configFiles = (forensic.candidate_files ?? []).filter((entry) => entry.family === "config").map((entry) => entry.path);
|
|
371
396
|
const framework = forensic.framework.kind;
|
|
372
397
|
const configBlock = configFiles.length > 0 ? configFiles.map((file) => `- ${file}`).join("\n") : "- (no config files detected)";
|
|
@@ -389,12 +414,12 @@ function buildBuildConfigEntry(forensic, nowIso, tags, language = "en") {
|
|
|
389
414
|
body,
|
|
390
415
|
target_subdir: "processes",
|
|
391
416
|
slug: "build-config",
|
|
392
|
-
tags,
|
|
417
|
+
tags: [],
|
|
393
418
|
relevance_scope: "narrow",
|
|
394
419
|
relevance_paths: relevancePaths
|
|
395
420
|
};
|
|
396
421
|
}
|
|
397
|
-
function buildCodeStyleEntry(forensic, nowIso,
|
|
422
|
+
function buildCodeStyleEntry(forensic, nowIso, language = "en") {
|
|
398
423
|
const dominantPatterns = (forensic.assertions ?? []).filter((a) => a.type === "pattern" || a.type === "domain").slice(0, 4).map((a) => `- ${a.statement}`);
|
|
399
424
|
const proposedRules = (forensic.assertions ?? []).map((a) => a.proposed_rule).filter((rule) => typeof rule === "string" && rule.length > 0).slice(0, 4);
|
|
400
425
|
const patternsBlock = dominantPatterns.length > 0 ? dominantPatterns.join("\n") : "- (no dominant patterns detected)";
|
|
@@ -424,12 +449,12 @@ function buildCodeStyleEntry(forensic, nowIso, tags, language = "en") {
|
|
|
424
449
|
body,
|
|
425
450
|
target_subdir: "guidelines",
|
|
426
451
|
slug: "code-style",
|
|
427
|
-
tags,
|
|
452
|
+
tags: [],
|
|
428
453
|
relevance_scope: "narrow",
|
|
429
454
|
relevance_paths: relevancePaths
|
|
430
455
|
};
|
|
431
456
|
}
|
|
432
|
-
function buildCIConfigEntry(forensic, nowIso
|
|
457
|
+
function buildCIConfigEntry(forensic, nowIso) {
|
|
433
458
|
const ciFiles = (forensic.candidate_files ?? []).map((entry) => entry.path).filter((path) => isCIConfigPath(path));
|
|
434
459
|
const ciExtensions = forensic.topology.by_ext ?? {};
|
|
435
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));
|
|
@@ -466,12 +491,12 @@ function buildCIConfigEntry(forensic, nowIso, tags) {
|
|
|
466
491
|
body,
|
|
467
492
|
target_subdir: "processes",
|
|
468
493
|
slug: "ci-config",
|
|
469
|
-
tags,
|
|
494
|
+
tags: [],
|
|
470
495
|
relevance_scope: "narrow",
|
|
471
496
|
relevance_paths: relevancePaths
|
|
472
497
|
};
|
|
473
498
|
}
|
|
474
|
-
function buildReadmeFirstParaEntry(target, forensic, nowIso,
|
|
499
|
+
function buildReadmeFirstParaEntry(target, forensic, nowIso, language = "en") {
|
|
475
500
|
if (forensic.readme.quality === "missing") {
|
|
476
501
|
return null;
|
|
477
502
|
}
|
|
@@ -501,7 +526,7 @@ function buildReadmeFirstParaEntry(target, forensic, nowIso, tags, language = "e
|
|
|
501
526
|
body,
|
|
502
527
|
target_subdir: "models",
|
|
503
528
|
slug: "readme-first-paragraph",
|
|
504
|
-
tags,
|
|
529
|
+
tags: [],
|
|
505
530
|
// v2.0-rc.7 T2: broad by design — single repo-root file, the Phase 1.5
|
|
506
531
|
// PreToolUse blacklist already covers README. Anchoring this entry to
|
|
507
532
|
// README.md would surface it on every README edit, which is noise.
|
|
@@ -509,7 +534,7 @@ function buildReadmeFirstParaEntry(target, forensic, nowIso, tags, language = "e
|
|
|
509
534
|
relevance_paths: []
|
|
510
535
|
};
|
|
511
536
|
}
|
|
512
|
-
function buildProjectBriefEntry(target, forensic, nowIso
|
|
537
|
+
function buildProjectBriefEntry(target, forensic, nowIso) {
|
|
513
538
|
if (forensic.readme.quality === "missing") {
|
|
514
539
|
return null;
|
|
515
540
|
}
|
|
@@ -541,7 +566,7 @@ function buildProjectBriefEntry(target, forensic, nowIso, tags) {
|
|
|
541
566
|
body,
|
|
542
567
|
target_subdir: "models",
|
|
543
568
|
slug: "project-brief",
|
|
544
|
-
tags,
|
|
569
|
+
tags: [],
|
|
545
570
|
// v2.0-rc.7 T2: broad — project brief is a cross-cutting description
|
|
546
571
|
// with no path anchor. Narrowing it to README.md would duplicate the
|
|
547
572
|
// readme-first-paragraph surface; keeping it broad lets the
|
|
@@ -603,47 +628,6 @@ function quoteIfNeeded(value) {
|
|
|
603
628
|
function stripFrontmatter(content) {
|
|
604
629
|
return content.replace(/^---[\s\S]*?\r?\n---\s*\r?\n?/u, "");
|
|
605
630
|
}
|
|
606
|
-
function deriveTagsFromForensic(forensic) {
|
|
607
|
-
const MAX_TAGS = 5;
|
|
608
|
-
const seen = /* @__PURE__ */ new Set();
|
|
609
|
-
const tags = [];
|
|
610
|
-
function add(raw) {
|
|
611
|
-
const normalized = raw.toLowerCase().trim().replace(/\s+/gu, "-");
|
|
612
|
-
if (normalized.length > 0 && !seen.has(normalized)) {
|
|
613
|
-
seen.add(normalized);
|
|
614
|
-
tags.push(normalized);
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
if (forensic.framework.kind) {
|
|
618
|
-
add(forensic.framework.kind);
|
|
619
|
-
}
|
|
620
|
-
const SKIP_EXTS = /* @__PURE__ */ new Set([".json", ".md", ".lock", ".yaml", ".yml", ".txt", ".env"]);
|
|
621
|
-
const EXT_MAP = {
|
|
622
|
-
".ts": "typescript",
|
|
623
|
-
".tsx": "typescript",
|
|
624
|
-
".js": "javascript",
|
|
625
|
-
".jsx": "javascript",
|
|
626
|
-
".mjs": "javascript",
|
|
627
|
-
".cjs": "javascript",
|
|
628
|
-
".py": "python",
|
|
629
|
-
".go": "go",
|
|
630
|
-
".rs": "rust",
|
|
631
|
-
".java": "java",
|
|
632
|
-
".cs": "csharp",
|
|
633
|
-
".rb": "ruby",
|
|
634
|
-
".php": "php",
|
|
635
|
-
".swift": "swift",
|
|
636
|
-
".kt": "kotlin"
|
|
637
|
-
};
|
|
638
|
-
const byExt = forensic.topology.by_ext ?? {};
|
|
639
|
-
const sorted = Object.entries(byExt).filter(([ext]) => !SKIP_EXTS.has(ext)).sort(([, a], [, b]) => b - a);
|
|
640
|
-
for (const [ext] of sorted) {
|
|
641
|
-
if (tags.length >= MAX_TAGS) break;
|
|
642
|
-
const mapped = EXT_MAP[ext] ?? ext.replace(/^\./u, "");
|
|
643
|
-
add(mapped);
|
|
644
|
-
}
|
|
645
|
-
return tags.slice(0, MAX_TAGS);
|
|
646
|
-
}
|
|
647
631
|
async function readForensic(forensicPath) {
|
|
648
632
|
const raw = await readFile(forensicPath, "utf8");
|
|
649
633
|
return JSON.parse(raw);
|
|
@@ -669,29 +653,143 @@ async function readScanState(sidecarPath) {
|
|
|
669
653
|
return {};
|
|
670
654
|
}
|
|
671
655
|
}
|
|
672
|
-
function
|
|
673
|
-
|
|
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)) {
|
|
674
757
|
return null;
|
|
675
758
|
}
|
|
759
|
+
let entries;
|
|
676
760
|
try {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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) {
|
|
684
781
|
return null;
|
|
685
782
|
}
|
|
686
|
-
const candidate = idLine[1].replace(/^["'](.*)["']$/u, "$1").trim();
|
|
687
|
-
if (/^K[PT]-(MOD|DEC|GLD|PIT|PRO)-\d{4,}$/.test(candidate) && sidecar[candidate] !== void 0) {
|
|
688
|
-
return candidate;
|
|
689
|
-
}
|
|
690
|
-
return null;
|
|
691
783
|
} catch {
|
|
692
|
-
void target;
|
|
693
784
|
return null;
|
|
694
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;
|
|
695
793
|
}
|
|
696
794
|
function isCIConfigPath(path) {
|
|
697
795
|
return path.startsWith(".github/workflows/") || path.startsWith(".gitlab-ci") || path === "azure-pipelines.yml" || path === ".circleci/config.yml" || path === "Jenkinsfile" || path === ".travis.yml";
|
|
@@ -759,7 +857,7 @@ async function registerKnowledgeNodesInMeta(target, entries) {
|
|
|
759
857
|
}
|
|
760
858
|
const nodes = typeof meta.nodes === "object" && meta.nodes !== null ? meta.nodes : {};
|
|
761
859
|
for (const entry of entries) {
|
|
762
|
-
const contentRef = `${KNOWLEDGE_DIR}/${entry.target_subdir}/${entry.slug}.md`;
|
|
860
|
+
const contentRef = `${KNOWLEDGE_DIR}/${entry.target_subdir}/${entry.id}--${entry.slug}.md`;
|
|
763
861
|
const absPath = join(target, contentRef);
|
|
764
862
|
let hash = "";
|
|
765
863
|
try {
|
package/dist/index.js
CHANGED
|
@@ -11,19 +11,19 @@ import { defineCommand, runMain } from "citty";
|
|
|
11
11
|
|
|
12
12
|
// src/commands/index.ts
|
|
13
13
|
var allCommands = {
|
|
14
|
-
install: () => import("./install-
|
|
15
|
-
doctor: () => import("./doctor-
|
|
14
|
+
install: () => import("./install-WJZQZM7D.js").then((module) => module.default),
|
|
15
|
+
doctor: () => import("./doctor-HIX2FFEP.js").then((module) => module.default),
|
|
16
16
|
serve: () => import("./serve-6PPQX7AW.js").then((module) => module.default),
|
|
17
17
|
uninstall: () => import("./uninstall-L2HEEOU3.js").then((module) => module.default),
|
|
18
18
|
config: () => import("./config-AYP5F72E.js").then((module) => module.default),
|
|
19
|
-
"plan-context-hint": () => import("./plan-context-hint-
|
|
19
|
+
"plan-context-hint": () => import("./plan-context-hint-RYVSMULL.js").then((module) => module.default)
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
// src/index.ts
|
|
23
23
|
var main = defineCommand({
|
|
24
24
|
meta: {
|
|
25
25
|
name: "fabric",
|
|
26
|
-
version: "2.0.0-rc.
|
|
26
|
+
version: "2.0.0-rc.22",
|
|
27
27
|
description: t("cli.main.description")
|
|
28
28
|
},
|
|
29
29
|
subCommands: allCommands
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
import {
|
|
6
6
|
detectExistingLanguage,
|
|
7
7
|
runInitScan
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-PSVKSMRO.js";
|
|
9
9
|
import {
|
|
10
10
|
installArchiveHintHook,
|
|
11
11
|
installFabricArchiveSkill,
|
|
@@ -1299,7 +1299,7 @@ function readProjectName(target) {
|
|
|
1299
1299
|
return basename(target);
|
|
1300
1300
|
}
|
|
1301
1301
|
function getCliVersion() {
|
|
1302
|
-
return true ? "2.0.0-rc.
|
|
1302
|
+
return true ? "2.0.0-rc.22" : "unknown";
|
|
1303
1303
|
}
|
|
1304
1304
|
function sortRecord(record) {
|
|
1305
1305
|
return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
|
|
@@ -72,13 +72,20 @@ async function runPlanContextHint(opts) {
|
|
|
72
72
|
maturity: item.maturity ?? item.description.maturity ?? "",
|
|
73
73
|
summary: item.description.summary
|
|
74
74
|
}));
|
|
75
|
-
|
|
75
|
+
const output = {
|
|
76
76
|
version: 2,
|
|
77
77
|
revision_hash: result.revision_hash,
|
|
78
78
|
target_paths: targetPaths,
|
|
79
79
|
entries,
|
|
80
80
|
broad_count: sharedIndex.length
|
|
81
81
|
};
|
|
82
|
+
if (result.auto_healed === true) {
|
|
83
|
+
output.auto_healed = true;
|
|
84
|
+
if (typeof result.previous_revision_hash === "string") {
|
|
85
|
+
output.previous_revision_hash = result.previous_revision_hash;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return output;
|
|
82
89
|
}
|
|
83
90
|
function parsePathsArg(raw) {
|
|
84
91
|
if (raw === void 0 || raw.length === 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fenglimg/fabric-cli",
|
|
3
|
-
"version": "2.0.0-rc.
|
|
3
|
+
"version": "2.0.0-rc.22",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"fab": "dist/index.js",
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"tree-sitter-javascript": "^0.25.0",
|
|
21
21
|
"tree-sitter-typescript": "^0.23.2",
|
|
22
22
|
"web-tree-sitter": "^0.26.8",
|
|
23
|
-
"@fenglimg/fabric-server": "2.0.0-rc.
|
|
24
|
-
"@fenglimg/fabric-shared": "2.0.0-rc.
|
|
23
|
+
"@fenglimg/fabric-server": "2.0.0-rc.22",
|
|
24
|
+
"@fenglimg/fabric-shared": "2.0.0-rc.22"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/node": "^22.15.0",
|
|
@@ -476,6 +476,46 @@ function renderSummary(payload) {
|
|
|
476
476
|
if (revHash !== null && revHash.length > 0) {
|
|
477
477
|
lines.push(` revision_hash: ${revHash}`);
|
|
478
478
|
}
|
|
479
|
+
|
|
480
|
+
// rc.22 Scope D T-D4 (TASK-011): meta auto-refresh breadcrumb. Emitted ONLY
|
|
481
|
+
// when the server's planContext() detected meta drift and rebuilt the meta
|
|
482
|
+
// in-place (auto_healed === true). One informational line — operators need
|
|
483
|
+
// a paper trail when revision_hash flips mid-session.
|
|
484
|
+
//
|
|
485
|
+
// Variant resolution:
|
|
486
|
+
// - Both hashes present → full transition line (`sha PREV → CUR`) with
|
|
487
|
+
// 8-char hex prefixes stripped of the `sha256:` scheme prefix.
|
|
488
|
+
// - auto_healed:true but previous_revision_hash missing → generic line
|
|
489
|
+
// (T10 noted the server may emit `auto_healed:true` alone if it lost
|
|
490
|
+
// the prior hash for any reason). Stays informational.
|
|
491
|
+
//
|
|
492
|
+
// i18n: routed through renderBanner so zh-CN / en / zh-CN-hybrid variants
|
|
493
|
+
// share one call site. fabric_language is resolved via readFabricLanguage()
|
|
494
|
+
// ONLY when the line is actually emitted (keeps the no-banner path free of
|
|
495
|
+
// the extra config read, matching the broadImportBanner site below).
|
|
496
|
+
if (payload.auto_healed === true) {
|
|
497
|
+
const variant = readFabricLanguage(process.cwd());
|
|
498
|
+
const prevRaw =
|
|
499
|
+
typeof payload.previous_revision_hash === "string"
|
|
500
|
+
? payload.previous_revision_hash
|
|
501
|
+
: null;
|
|
502
|
+
const curRaw =
|
|
503
|
+
typeof payload.revision_hash === "string" ? payload.revision_hash : null;
|
|
504
|
+
if (prevRaw && curRaw) {
|
|
505
|
+
// Strip optional `sha256:` scheme prefix, then take first 8 hex chars.
|
|
506
|
+
const stripScheme = (h) =>
|
|
507
|
+
h.startsWith("sha256:") ? h.slice("sha256:".length) : h;
|
|
508
|
+
const prev = stripScheme(prevRaw).slice(0, 8);
|
|
509
|
+
const cur = stripScheme(curRaw).slice(0, 8);
|
|
510
|
+
lines.push(
|
|
511
|
+
renderBanner("metaAutoRefreshedBanner", variant, { prev, cur }),
|
|
512
|
+
);
|
|
513
|
+
} else {
|
|
514
|
+
// Defensive: auto_healed:true but no usable previous hash → generic line.
|
|
515
|
+
lines.push(renderBanner("metaAutoRefreshedBannerGeneric", variant, {}));
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
479
519
|
lines.push(" Use `fab_get_knowledge_sections` to fetch full content.");
|
|
480
520
|
return lines;
|
|
481
521
|
}
|
|
@@ -223,6 +223,36 @@ const STRINGS = {
|
|
|
223
223
|
"zh-CN-hybrid": () =>
|
|
224
224
|
" 📋 Fabric: 知识库稀疏,是否调 /fabric-import 从 git 历史与现有文档回灌知识?",
|
|
225
225
|
},
|
|
226
|
+
|
|
227
|
+
// ---- Broad hook: meta auto-refresh breadcrumb (rc.22 Scope D T-D4) -------
|
|
228
|
+
// Surfaced ONLY when planContext() detected meta drift and rebuilt the meta
|
|
229
|
+
// in-place (server emits `auto_healed: true` in plan-context-hint payload).
|
|
230
|
+
// Single informational line — operators need a breadcrumb when meta auto-
|
|
231
|
+
// heals so a "why did revision change?" question has a paper trail.
|
|
232
|
+
//
|
|
233
|
+
// Two render shapes:
|
|
234
|
+
// - metaAutoRefreshedBanner: full transition with prev → cur 8-char hash
|
|
235
|
+
// prefixes. Used when both previous_revision_hash + revision_hash present.
|
|
236
|
+
// - metaAutoRefreshedBannerGeneric: defensive fallback when the server
|
|
237
|
+
// emitted `auto_healed: true` but did not include previous_revision_hash
|
|
238
|
+
// (T10 noted this edge case). No hash transition shown.
|
|
239
|
+
//
|
|
240
|
+
// Note: 🔄 emoji prefix is intentional (matches the project's general "no
|
|
241
|
+
// emoji" rule's exception for explicit user request — see TASK-011 description).
|
|
242
|
+
// params: { prev, cur } — both already 8-char hex strings, caller-supplied.
|
|
243
|
+
metaAutoRefreshedBanner: {
|
|
244
|
+
"zh-CN": (p) => ` 🔄 Fabric: 元数据已自动刷新(sha ${p.prev} → ${p.cur})`,
|
|
245
|
+
en: (p) => ` 🔄 Fabric: meta auto-refreshed (sha ${p.prev} → ${p.cur})`,
|
|
246
|
+
"zh-CN-hybrid": (p) => ` 🔄 Fabric: 元数据已自动刷新(sha ${p.prev} → ${p.cur})`,
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
// Generic variant — no hash transition. Used when auto_healed:true but
|
|
250
|
+
// previous_revision_hash is missing from the payload.
|
|
251
|
+
metaAutoRefreshedBannerGeneric: {
|
|
252
|
+
"zh-CN": () => " 🔄 Fabric: 元数据已自动刷新",
|
|
253
|
+
en: () => " 🔄 Fabric: meta auto-refreshed",
|
|
254
|
+
"zh-CN-hybrid": () => " 🔄 Fabric: 元数据已自动刷新",
|
|
255
|
+
},
|
|
226
256
|
};
|
|
227
257
|
|
|
228
258
|
/**
|