@fenglimg/fabric-cli 2.0.0-rc.15 → 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-4HC5ZK7H.js +598 -0
- package/dist/chunk-KZ2YITOS.js +225 -0
- package/dist/{chunk-SKSYUHKK.js → chunk-MF3OTILQ.js} +0 -4
- package/dist/{chunk-AXIFEVAS.js → chunk-PSVKSMRO.js} +181 -83
- package/dist/{chunk-OBQU6NHO.js → chunk-ZSESMG6L.js} +0 -6
- package/dist/{config-7YD365I3.js → config-AYP5F72E.js} +2 -2
- package/dist/{doctor-6XHLQJXB.js → doctor-HIX2FFEP.js} +129 -3
- package/dist/index.js +11 -8
- package/dist/{install-JLDCHAXV.js → install-WJZQZM7D.js} +23 -25
- package/dist/{plan-context-hint-73U4FGKO.js → plan-context-hint-RYVSMULL.js} +12 -5
- package/dist/{serve-L3X5UHG2.js → serve-6PPQX7AW.js} +1 -1
- package/dist/{uninstall-DD6FIFCI.js → uninstall-L2HEEOU3.js} +147 -55
- package/package.json +3 -3
- package/templates/hooks/fabric-hint.cjs +350 -21
- package/templates/hooks/knowledge-hint-broad.cjs +79 -14
- package/templates/hooks/knowledge-hint-narrow.cjs +31 -7
- package/templates/hooks/lib/banner-i18n.cjs +282 -0
- package/dist/chunk-AIB54QRT.js +0 -82
- package/dist/chunk-UTF4YBDN.js +0 -366
- package/templates/agents-md/AGENTS.md.template +0 -59
- package/templates/bootstrap/CLAUDE.md +0 -8
- package/templates/bootstrap/codex-AGENTS-header.md +0 -6
- package/templates/bootstrap/cursor-fabric-bootstrap.mdc +0 -10
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
resolveClients
|
|
4
|
+
} from "./chunk-MF3OTILQ.js";
|
|
5
|
+
import {
|
|
6
|
+
t
|
|
7
|
+
} from "./chunk-6ICJICVU.js";
|
|
8
|
+
|
|
9
|
+
// src/commands/config.ts
|
|
10
|
+
import { existsSync, statSync } from "fs";
|
|
11
|
+
import { readFile } from "fs/promises";
|
|
12
|
+
import { join, resolve } from "path";
|
|
13
|
+
import { fileURLToPath } from "url";
|
|
14
|
+
import { cancel, intro, isCancel, log, outro, select, text } from "@clack/prompts";
|
|
15
|
+
import { getPanelFields } from "@fenglimg/fabric-shared";
|
|
16
|
+
import { atomicWriteJson } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
17
|
+
import { defineCommand } from "citty";
|
|
18
|
+
async function loadFabricConfig(workspaceRoot) {
|
|
19
|
+
const configPath = resolve(workspaceRoot, "fabric.config.json");
|
|
20
|
+
if (!existsSync(configPath)) {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
const parsed = JSON.parse(await readFile(configPath, "utf8"));
|
|
24
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
25
|
+
throw new Error(t("cli.config.errors.expected-object", { path: configPath }));
|
|
26
|
+
}
|
|
27
|
+
return parsed;
|
|
28
|
+
}
|
|
29
|
+
function resolveServerPath(override) {
|
|
30
|
+
if (override) return override;
|
|
31
|
+
if (process.env.FAB_SERVER_PATH) return resolve(process.env.FAB_SERVER_PATH);
|
|
32
|
+
return fileURLToPath(import.meta.resolve("@fenglimg/fabric-server"));
|
|
33
|
+
}
|
|
34
|
+
var PANEL_CONFIG_RELATIVE_PATH = [".fabric", "fabric-config.json"];
|
|
35
|
+
var EXIT_CHOICE = "__exit__";
|
|
36
|
+
var configCmd = defineCommand({
|
|
37
|
+
meta: {
|
|
38
|
+
name: "config",
|
|
39
|
+
description: t("cli.config.description")
|
|
40
|
+
},
|
|
41
|
+
args: {
|
|
42
|
+
target: {
|
|
43
|
+
type: "string",
|
|
44
|
+
description: t("cli.config.args.target.description"),
|
|
45
|
+
valueHint: "path"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
async run({ args }) {
|
|
49
|
+
const workspaceRoot = resolve(args.target ?? process.cwd());
|
|
50
|
+
const configPath = join(workspaceRoot, ...PANEL_CONFIG_RELATIVE_PATH);
|
|
51
|
+
const fabricDir = join(workspaceRoot, ".fabric");
|
|
52
|
+
const fabricDirOk = existsSync(fabricDir) && statSync(fabricDir).isDirectory();
|
|
53
|
+
const configOk = fabricDirOk && existsSync(configPath);
|
|
54
|
+
if (!configOk) {
|
|
55
|
+
console.error(t("cli.config.errors.uninit-workspace.message"));
|
|
56
|
+
process.exitCode = 1;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (!isInteractiveConfig()) {
|
|
60
|
+
console.log(t("cli.config.intro"));
|
|
61
|
+
console.log(t("cli.config.non-tty-notice"));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
intro(t("cli.config.intro"));
|
|
65
|
+
let edited = false;
|
|
66
|
+
while (true) {
|
|
67
|
+
const current = await readPanelConfig(configPath);
|
|
68
|
+
const fields = getPanelFields();
|
|
69
|
+
const fieldChoice = await select({
|
|
70
|
+
message: t("cli.config.menu.field-select"),
|
|
71
|
+
options: [
|
|
72
|
+
...fields.map((field2) => ({
|
|
73
|
+
value: field2.key,
|
|
74
|
+
label: formatFieldMenuLabel(field2, current)
|
|
75
|
+
})),
|
|
76
|
+
{ value: EXIT_CHOICE, label: t("cli.config.menu.exit") }
|
|
77
|
+
]
|
|
78
|
+
});
|
|
79
|
+
if (isCancel(fieldChoice)) {
|
|
80
|
+
cancel(t("cli.config.cancel"));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (fieldChoice === EXIT_CHOICE) {
|
|
84
|
+
outro(edited ? t("cli.config.outro") : t("cli.config.outro-no-changes"));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const field = fields.find((f) => f.key === fieldChoice);
|
|
88
|
+
if (!field) {
|
|
89
|
+
log.warn(t("cli.config.errors.unknown-field"));
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const newValue = await promptFieldValue(field, current);
|
|
93
|
+
if (newValue === CANCELLED) {
|
|
94
|
+
cancel(t("cli.config.cancel"));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (newValue === SKIPPED) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const refreshed = await readPanelConfig(configPath);
|
|
102
|
+
const merged = { ...refreshed, [field.key]: newValue };
|
|
103
|
+
await atomicWriteJson(configPath, merged);
|
|
104
|
+
edited = true;
|
|
105
|
+
log.success(
|
|
106
|
+
t("cli.config.write.success", {
|
|
107
|
+
key: field.key,
|
|
108
|
+
value: field.format_for_display(newValue)
|
|
109
|
+
})
|
|
110
|
+
);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
113
|
+
log.error(t("cli.config.write.failure", { message }));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
var config_default = configCmd;
|
|
119
|
+
var CANCELLED = /* @__PURE__ */ Symbol("config-cancelled");
|
|
120
|
+
var SKIPPED = /* @__PURE__ */ Symbol("config-skipped");
|
|
121
|
+
async function promptFieldValue(field, current) {
|
|
122
|
+
const currentValue = current[field.key];
|
|
123
|
+
const currentDisplay = field.format_for_display(currentValue);
|
|
124
|
+
if (field.widget === "select") {
|
|
125
|
+
const enumValues = field.enum_values ?? [];
|
|
126
|
+
if (enumValues.length === 0) {
|
|
127
|
+
log.warn(t("cli.config.errors.no-enum-options"));
|
|
128
|
+
return SKIPPED;
|
|
129
|
+
}
|
|
130
|
+
const initialValue = enumValues.includes(String(currentValue)) ? String(currentValue) : enumValues.includes(String(field.default)) ? String(field.default) : enumValues[0];
|
|
131
|
+
const picked = await select({
|
|
132
|
+
message: t("cli.config.prompt.select", {
|
|
133
|
+
key: field.key,
|
|
134
|
+
current: currentDisplay
|
|
135
|
+
}),
|
|
136
|
+
options: enumValues.map((value) => ({ value, label: value })),
|
|
137
|
+
initialValue
|
|
138
|
+
});
|
|
139
|
+
if (isCancel(picked)) {
|
|
140
|
+
return CANCELLED;
|
|
141
|
+
}
|
|
142
|
+
const result = field.validate(String(picked));
|
|
143
|
+
if (!result.ok) {
|
|
144
|
+
log.error(result.error);
|
|
145
|
+
return SKIPPED;
|
|
146
|
+
}
|
|
147
|
+
return result.value;
|
|
148
|
+
}
|
|
149
|
+
const entered = await text({
|
|
150
|
+
message: t("cli.config.prompt.text", {
|
|
151
|
+
key: field.key,
|
|
152
|
+
current: currentDisplay
|
|
153
|
+
}),
|
|
154
|
+
placeholder: currentDisplay,
|
|
155
|
+
initialValue: currentDisplay,
|
|
156
|
+
validate(raw) {
|
|
157
|
+
const result = field.validate(raw ?? "");
|
|
158
|
+
return result.ok ? void 0 : result.error;
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
if (isCancel(entered)) {
|
|
162
|
+
return CANCELLED;
|
|
163
|
+
}
|
|
164
|
+
const finalResult = field.validate(String(entered));
|
|
165
|
+
if (!finalResult.ok) {
|
|
166
|
+
log.error(finalResult.error);
|
|
167
|
+
return SKIPPED;
|
|
168
|
+
}
|
|
169
|
+
return finalResult.value;
|
|
170
|
+
}
|
|
171
|
+
function formatFieldMenuLabel(field, current) {
|
|
172
|
+
const key = field.key;
|
|
173
|
+
const rawValue = current[key];
|
|
174
|
+
const display = field.format_for_display(rawValue);
|
|
175
|
+
const isDefault = rawValue === void 0 || rawValue === null;
|
|
176
|
+
const labelText = t(field.label_i18n_key);
|
|
177
|
+
const valueLabel = isDefault ? `${display} ${t("cli.config.value.default-marker")}` : display;
|
|
178
|
+
return `[${field.group}] ${key} (${labelText}) \u2014 ${t("cli.config.value.current", { value: valueLabel })}`;
|
|
179
|
+
}
|
|
180
|
+
async function readPanelConfig(configPath) {
|
|
181
|
+
const raw = await readFile(configPath, "utf8");
|
|
182
|
+
const parsed = JSON.parse(raw);
|
|
183
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
184
|
+
throw new Error(t("cli.config.errors.expected-object", { path: configPath }));
|
|
185
|
+
}
|
|
186
|
+
return parsed;
|
|
187
|
+
}
|
|
188
|
+
function isInteractiveConfig() {
|
|
189
|
+
return Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY) && Boolean(process.stderr.isTTY);
|
|
190
|
+
}
|
|
191
|
+
async function installMcpClients(target, options = {}) {
|
|
192
|
+
const workspaceRoot = resolve(target);
|
|
193
|
+
const fabricConfig = await loadFabricConfig(workspaceRoot);
|
|
194
|
+
const selectedClients = options.clients === void 0 ? null : new Set(options.clients);
|
|
195
|
+
const serverPath = resolveServerPath(options.localServerPath);
|
|
196
|
+
const writers = resolveClients(workspaceRoot, fabricConfig, { claudeMcpScope: options.claudeMcpScope }).filter(
|
|
197
|
+
(writer) => selectedClients === null ? true : selectedClients.has(writer.clientKind)
|
|
198
|
+
);
|
|
199
|
+
const installed = [];
|
|
200
|
+
const skipped = [];
|
|
201
|
+
const details = [];
|
|
202
|
+
for (const writer of writers) {
|
|
203
|
+
const configPath = await writer.detect(workspaceRoot);
|
|
204
|
+
if (configPath === null) {
|
|
205
|
+
skipped.push(writer.clientKind);
|
|
206
|
+
details.push({ client: writer.clientKind, path: null, action: "skipped" });
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (options.dryRun) {
|
|
210
|
+
skipped.push(writer.clientKind);
|
|
211
|
+
details.push({ client: writer.clientKind, path: configPath, action: "dry-run" });
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
await writer.write(serverPath, workspaceRoot);
|
|
215
|
+
installed.push(writer.clientKind);
|
|
216
|
+
details.push({ client: writer.clientKind, path: configPath, action: "wrote" });
|
|
217
|
+
}
|
|
218
|
+
return { installed, skipped, details };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export {
|
|
222
|
+
configCmd,
|
|
223
|
+
config_default,
|
|
224
|
+
installMcpClients
|
|
225
|
+
};
|
|
@@ -479,7 +479,6 @@ function detectClientSupports(workspaceRoot, fabricConfig = {}) {
|
|
|
479
479
|
clientKind: "ClaudeCodeCLI",
|
|
480
480
|
label: "Claude Code CLI",
|
|
481
481
|
detected: claudeDetected || hasExplicitPath(clientPaths, "claudeCodeCLI"),
|
|
482
|
-
bootstrapTargetPath: ".fabric/bootstrap/README.md",
|
|
483
482
|
configPath: "project .claude/settings.json",
|
|
484
483
|
capabilities: {
|
|
485
484
|
bootstrap: true,
|
|
@@ -496,7 +495,6 @@ function detectClientSupports(workspaceRoot, fabricConfig = {}) {
|
|
|
496
495
|
clientKind: "ClaudeCodeDesktop",
|
|
497
496
|
label: "Claude Code Desktop",
|
|
498
497
|
detected: claudeDesktopDetected || hasExplicitPath(clientPaths, "claudeCodeDesktop"),
|
|
499
|
-
bootstrapTargetPath: ".fabric/bootstrap/README.md",
|
|
500
498
|
configPath: "desktop Claude config",
|
|
501
499
|
capabilities: {
|
|
502
500
|
bootstrap: true,
|
|
@@ -509,7 +507,6 @@ function detectClientSupports(workspaceRoot, fabricConfig = {}) {
|
|
|
509
507
|
clientKind: "Cursor",
|
|
510
508
|
label: "Cursor",
|
|
511
509
|
detected: cursorDetected || hasExplicitPath(clientPaths, "cursor"),
|
|
512
|
-
bootstrapTargetPath: ".fabric/bootstrap/README.md",
|
|
513
510
|
configPath: ".cursor/mcp.json",
|
|
514
511
|
capabilities: {
|
|
515
512
|
bootstrap: true,
|
|
@@ -522,7 +519,6 @@ function detectClientSupports(workspaceRoot, fabricConfig = {}) {
|
|
|
522
519
|
clientKind: "CodexCLI",
|
|
523
520
|
label: "Codex CLI",
|
|
524
521
|
detected: codexDetected || hasExplicitPath(clientPaths, "codexCLI"),
|
|
525
|
-
bootstrapTargetPath: ".fabric/bootstrap/README.md",
|
|
526
522
|
configPath: "~/.codex/config.toml",
|
|
527
523
|
capabilities: {
|
|
528
524
|
bootstrap: true,
|
|
@@ -4,12 +4,12 @@ import {
|
|
|
4
4
|
} from "./chunk-6ICJICVU.js";
|
|
5
5
|
import {
|
|
6
6
|
readFabricConfig
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-ZSESMG6L.js";
|
|
8
8
|
|
|
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 {
|
|
@@ -16,13 +16,10 @@ function readFabricConfig(workspaceRoot = process.cwd()) {
|
|
|
16
16
|
}
|
|
17
17
|
function resolveDevMode(cliTarget, workspaceRoot = process.cwd()) {
|
|
18
18
|
const envTarget = normalizeTarget(process.env.EXTERNAL_FIXTURE_PATH, workspaceRoot);
|
|
19
|
-
const fabricConfig = readFabricConfig(workspaceRoot);
|
|
20
|
-
const configTarget = normalizeTarget(fabricConfig.externalFixturePath, workspaceRoot);
|
|
21
19
|
const directTarget = normalizeTarget(cliTarget, workspaceRoot);
|
|
22
20
|
const chain = [
|
|
23
21
|
formatResolutionStep("cliTarget", directTarget),
|
|
24
22
|
formatResolutionStep("EXTERNAL_FIXTURE_PATH", envTarget),
|
|
25
|
-
formatResolutionStep("fabric.config.json.externalFixturePath", configTarget),
|
|
26
23
|
formatResolutionStep("process.cwd()", workspaceRoot)
|
|
27
24
|
];
|
|
28
25
|
if (directTarget !== void 0) {
|
|
@@ -31,9 +28,6 @@ function resolveDevMode(cliTarget, workspaceRoot = process.cwd()) {
|
|
|
31
28
|
if (envTarget !== void 0) {
|
|
32
29
|
return { target: envTarget, source: "env", chain };
|
|
33
30
|
}
|
|
34
|
-
if (configTarget !== void 0) {
|
|
35
|
-
return { target: configTarget, source: "config", chain };
|
|
36
|
-
}
|
|
37
31
|
return { target: workspaceRoot, source: "cwd", chain };
|
|
38
32
|
}
|
|
39
33
|
function createDebugLogger(debug) {
|