@fenglimg/fabric-cli 2.0.0-rc.1 → 2.0.0-rc.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +6 -6
  2. package/dist/{chunk-UHNP7T7W.js → chunk-5MQ52F42.js} +347 -86
  3. package/dist/chunk-6ICJICVU.js +10 -0
  4. package/dist/chunk-AW3G7ZH5.js +576 -0
  5. package/dist/chunk-HQLEHH4O.js +321 -0
  6. package/dist/{chunk-5LOYBXWD.js → chunk-OBQU6NHO.js} +2 -52
  7. package/dist/chunk-WPTA74BY.js +184 -0
  8. package/dist/chunk-WWNXR34K.js +49 -0
  9. package/dist/doctor-RILCO5OG.js +282 -0
  10. package/dist/hooks-NX32PPEN.js +13 -0
  11. package/dist/index.js +8 -5
  12. package/dist/{init-DRHUYHYA.js → init-C56PWHID.js} +225 -491
  13. package/dist/plan-context-hint-QMUPAXIB.js +98 -0
  14. package/dist/{scan-HU2EGITF.js → scan-66EKMNAY.js} +6 -2
  15. package/dist/{serve-3LXXSBFR.js → serve-NGLXHDYC.js} +8 -4
  16. package/dist/uninstall-DBAR2JBS.js +1082 -0
  17. package/package.json +3 -3
  18. package/templates/bootstrap/CLAUDE.md +1 -1
  19. package/templates/bootstrap/codex-AGENTS-header.md +1 -1
  20. package/templates/bootstrap/cursor-fabric-bootstrap.mdc +1 -1
  21. package/templates/hooks/configs/README.md +73 -0
  22. package/templates/hooks/configs/claude-code.json +37 -0
  23. package/templates/hooks/configs/codex-hooks.json +20 -0
  24. package/templates/hooks/configs/cursor-hooks.json +20 -0
  25. package/templates/hooks/fabric-hint.cjs +1337 -0
  26. package/templates/hooks/knowledge-hint-broad.cjs +612 -0
  27. package/templates/hooks/knowledge-hint-narrow.cjs +826 -0
  28. package/templates/hooks/lib/session-digest-writer.cjs +172 -0
  29. package/templates/skills/fabric-archive/SKILL.md +640 -0
  30. package/templates/skills/fabric-import/SKILL.md +850 -0
  31. package/templates/skills/fabric-review/SKILL.md +717 -0
  32. package/dist/doctor-DUHWLAYD.js +0 -98
package/README.md CHANGED
@@ -1,16 +1,16 @@
1
- # @fenglimg/fabric-cli
2
-
1
+ # @fenglimg/fabric-cli
2
+
3
3
  `fabric` 是 Fabric 的主命令,`fab` 是永久别名,两者等价。
4
-
4
+
5
5
  ## 快速开始
6
6
 
7
7
  1. 在 monorepo 根目录运行 `pnpm install`。
8
8
  2. 用 `pnpm --filter @fenglimg/fabric-cli build` 构建 CLI。
9
9
  3. 在目标项目运行 `fabric init`,完成一站式初始化。
10
- 4. 启动 `fabric serve`,再去客户端里验证 `fab_plan_context` 和 `fab_get_rule_sections`。
10
+ 4. 启动 `fabric serve`,再去客户端里验证 `fab_plan_context` 和 `fab_get_knowledge_sections`。
11
11
 
12
12
  `fabric init` 会自动准备 bootstrap、MCP 配置和 git hooks。公共命令面只保留 `init`、`scan`、`doctor`、`serve`。
13
-
13
+
14
14
  ## 常用命令
15
15
 
16
16
  - `fabric init`
@@ -21,4 +21,4 @@
21
21
  - `fabric doctor --fix`
22
22
  - `fabric serve`
23
23
 
24
- `fabric doctor --fix` 只修复确定性的派生状态,例如 `.fabric/agents.meta.json`、`.fabric/rule-test.index.json`、缺失的 `.fabric/events.jsonl` 和 stale hashes;语义冲突、缺失 rule section、未完成的初始化确认仍需要人工处理。
24
+ `fabric doctor --fix` 只修复确定性的派生状态,例如 `.fabric/agents.meta.json`、`.fabric/.cache/knowledge-test.index.json`、缺失的 `.fabric/events.jsonl` 和 stale hashes;语义冲突、缺失 rule section、未完成的初始化确认仍需要人工处理。
@@ -1,11 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- createDebugLogger,
4
3
  paint,
5
- resolveDevMode,
6
- symbol,
4
+ symbol
5
+ } from "./chunk-WWNXR34K.js";
6
+ import {
7
+ createDebugLogger,
8
+ readFabricConfig,
9
+ resolveDevMode
10
+ } from "./chunk-OBQU6NHO.js";
11
+ import {
7
12
  t
8
- } from "./chunk-5LOYBXWD.js";
13
+ } from "./chunk-6ICJICVU.js";
9
14
 
10
15
  // src/commands/scan.ts
11
16
  import { createHash } from "crypto";
@@ -16,7 +21,7 @@ import { defineCommand } from "citty";
16
21
  import {
17
22
  KnowledgeIdAllocator,
18
23
  appendEventLedgerEvent,
19
- writeRuleMeta
24
+ writeKnowledgeMeta
20
25
  } from "@fenglimg/fabric-server";
21
26
  import {
22
27
  formatKnowledgeId
@@ -82,13 +87,16 @@ async function runInitScan(targetInput, options = {}) {
82
87
  const forensic = await readForensic(forensicPath);
83
88
  const nowIso = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
84
89
  const tags = deriveTagsFromForensic(forensic);
90
+ const fabricConfig = readFabricConfig(target);
91
+ const knowledgeLanguage = fabricConfig.knowledge_language ?? "match-existing";
92
+ const resolvedLanguage = resolveKnowledgeLanguage(knowledgeLanguage, target);
85
93
  const candidates = [
86
- buildTechStackEntry(forensic, nowIso, tags),
87
- buildModuleStructureEntry(forensic, nowIso, tags),
88
- buildBuildConfigEntry(forensic, nowIso, tags),
89
- buildCodeStyleEntry(forensic, nowIso, tags),
94
+ buildTechStackEntry(forensic, nowIso, tags, resolvedLanguage),
95
+ buildModuleStructureEntry(forensic, nowIso, tags, resolvedLanguage),
96
+ buildBuildConfigEntry(forensic, nowIso, tags, resolvedLanguage),
97
+ buildCodeStyleEntry(forensic, nowIso, tags, resolvedLanguage),
90
98
  buildCIConfigEntry(forensic, nowIso, tags),
91
- buildReadmeFirstParaEntry(target, forensic, nowIso, tags),
99
+ buildReadmeFirstParaEntry(target, forensic, nowIso, tags, resolvedLanguage),
92
100
  buildProjectBriefEntry(target, forensic, nowIso, tags)
93
101
  ];
94
102
  const entries = candidates.filter((e) => e !== null);
@@ -119,7 +127,7 @@ async function runInitScan(targetInput, options = {}) {
119
127
  await ensureParentDirectory(sidecarPath);
120
128
  await atomicWriteJson(sidecarPath, sidecar);
121
129
  await registerKnowledgeNodesInMeta(target, placedEntries);
122
- await writeRuleMeta(target, { source: "doctor_fix" });
130
+ await writeKnowledgeMeta(target, { source: "doctor_fix" });
123
131
  const durationMs = Date.now() - startTs;
124
132
  await appendEventLedgerEvent(target, {
125
133
  event_type: "init_scan_completed",
@@ -178,124 +186,338 @@ var scanCommand = defineCommand({
178
186
  }
179
187
  });
180
188
  var scan_default = scanCommand;
181
- function buildTechStackEntry(forensic, nowIso, tags) {
189
+ var BASELINE_TEMPLATES = {
190
+ en: {
191
+ "tech-stack": {
192
+ title: ({ frameworkSummary }) => `Tech stack: ${frameworkSummary}`,
193
+ build: ({ projectName, frameworkSummary, topExtensionsLine, evidenceLines }) => ({
194
+ mission: `Track the primary tech stack and runtime surface used by ${projectName}.`,
195
+ context: [
196
+ `Framework: ${frameworkSummary}`,
197
+ `Top file extensions: ${topExtensionsLine}`,
198
+ `Evidence:`,
199
+ ...evidenceLines.map((line) => `- ${line}`)
200
+ ].join("\n")
201
+ })
202
+ },
203
+ "module-structure": {
204
+ title: "Module structure",
205
+ build: ({ projectName, totalFiles, maxDepth, dirsBlock, entriesBlock }) => ({
206
+ mission: `Map the high-level module layout and primary entry points of ${projectName}.`,
207
+ context: [
208
+ `Total files: ${totalFiles}`,
209
+ `Max directory depth: ${maxDepth}`,
210
+ "",
211
+ "Key directories:",
212
+ dirsBlock,
213
+ "",
214
+ "Entry points:",
215
+ entriesBlock
216
+ ].join("\n")
217
+ })
218
+ },
219
+ "build-config": {
220
+ title: "Build configuration",
221
+ build: ({ projectName, framework, configBlock }) => ({
222
+ mission: `Document the deterministic build/bootstrap configuration anchoring ${projectName}.`,
223
+ businessLogic: [
224
+ `1. Detect framework: \`${framework}\`.`,
225
+ `2. Read configuration files in declared order.`,
226
+ `3. Honor compiler/bundler boundaries before generating new code.`,
227
+ `4. Treat config drift as a fact-check signal \u2014 re-run \`fab scan\` after edits.`
228
+ ].join("\n"),
229
+ context: [
230
+ `Framework: ${framework}`,
231
+ "",
232
+ "Configuration files:",
233
+ configBlock
234
+ ].join("\n")
235
+ })
236
+ },
237
+ "code-style": {
238
+ title: "Code style guidelines",
239
+ build: ({ projectName, rulesBlock, patternsBlock }) => ({
240
+ mission: `Codify the recurring authoring conventions observed in ${projectName}.`,
241
+ mandatoryInjection: [
242
+ "When generating or modifying source files in this repo, AI agents MUST:",
243
+ rulesBlock
244
+ ].join("\n"),
245
+ context: [
246
+ "Detected patterns:",
247
+ patternsBlock
248
+ ].join("\n")
249
+ })
250
+ },
251
+ "readme-first-paragraph": {
252
+ title: "README first paragraph",
253
+ build: ({ lineCount, quality, excerpt }) => ({
254
+ mission: `Preserve the README headline and first paragraph as the canonical project elevator pitch.`,
255
+ context: [
256
+ `Source: README.md (${lineCount} lines, quality=${quality})`,
257
+ "",
258
+ "Excerpt:",
259
+ "> " + excerpt.split("\n").join("\n> ")
260
+ ].join("\n")
261
+ })
262
+ }
263
+ },
264
+ "zh-CN": {
265
+ "tech-stack": {
266
+ title: ({ frameworkSummary }) => `Tech stack: ${frameworkSummary}`,
267
+ build: ({ projectName, frameworkSummary, topExtensionsLine, evidenceLines }) => ({
268
+ mission: `\u8BB0\u5F55 ${projectName} \u6240\u4F7F\u7528\u7684\u4E3B\u8981 tech stack \u4E0E\u8FD0\u884C\u65F6\u9762\u3002`,
269
+ context: [
270
+ `Framework\uFF1A${frameworkSummary}`,
271
+ `\u4E3B\u8981\u6587\u4EF6\u540E\u7F00\uFF1A${topExtensionsLine}`,
272
+ `\u8BC1\u636E\uFF1A`,
273
+ ...evidenceLines.map((line) => `- ${line}`)
274
+ ].join("\n")
275
+ })
276
+ },
277
+ "module-structure": {
278
+ title: "Module structure",
279
+ build: ({ projectName, totalFiles, maxDepth, dirsBlock, entriesBlock }) => ({
280
+ mission: `\u68B3\u7406 ${projectName} \u7684\u9AD8\u5C42 module \u5E03\u5C40\u4E0E\u4E3B\u8981 entry point\u3002`,
281
+ context: [
282
+ `\u6587\u4EF6\u603B\u6570\uFF1A${totalFiles}`,
283
+ `\u6700\u5927\u76EE\u5F55\u6DF1\u5EA6\uFF1A${maxDepth}`,
284
+ "",
285
+ "\u5173\u952E\u76EE\u5F55\uFF1A",
286
+ dirsBlock,
287
+ "",
288
+ "Entry points\uFF1A",
289
+ entriesBlock
290
+ ].join("\n")
291
+ })
292
+ },
293
+ "build-config": {
294
+ title: "Build configuration",
295
+ build: ({ projectName, framework, configBlock }) => ({
296
+ mission: `\u8BB0\u5F55 ${projectName} \u6240\u4F9D\u8D56\u7684\u3001\u786E\u5B9A\u6027\u7684 build / bootstrap \u914D\u7F6E\u3002`,
297
+ businessLogic: [
298
+ `1. \u63A2\u6D4B framework\uFF1A\`${framework}\`\u3002`,
299
+ `2. \u6309\u58F0\u660E\u987A\u5E8F\u8BFB\u53D6 configuration files\u3002`,
300
+ `3. \u5728\u751F\u6210\u65B0\u4EE3\u7801\u4E4B\u524D\uFF0C\u5C0A\u91CD compiler / bundler \u7684\u8FB9\u754C\u3002`,
301
+ `4. \u628A config \u6F02\u79FB\u89C6\u4E3A fact-check \u4FE1\u53F7 \u2014\u2014 \u4FEE\u6539\u540E\u91CD\u65B0\u8FD0\u884C \`fab scan\`\u3002`
302
+ ].join("\n"),
303
+ context: [
304
+ `Framework\uFF1A${framework}`,
305
+ "",
306
+ "Configuration files\uFF1A",
307
+ configBlock
308
+ ].join("\n")
309
+ })
310
+ },
311
+ "code-style": {
312
+ title: "Code style guidelines",
313
+ build: ({ projectName, rulesBlock, patternsBlock }) => ({
314
+ mission: `\u56FA\u5316 ${projectName} \u4E2D\u53CD\u590D\u51FA\u73B0\u7684\u5199\u7801\u7EA6\u5B9A\u3002`,
315
+ mandatoryInjection: [
316
+ "\u5728\u672C\u4ED3\u5E93\u5185\u751F\u6210\u6216\u4FEE\u6539\u6E90\u7801\u6587\u4EF6\u65F6\uFF0CAI agent \u5FC5\u987B\uFF1A",
317
+ rulesBlock
318
+ ].join("\n"),
319
+ context: [
320
+ "\u89C2\u5BDF\u5230\u7684\u6A21\u5F0F\uFF1A",
321
+ patternsBlock
322
+ ].join("\n")
323
+ })
324
+ },
325
+ "readme-first-paragraph": {
326
+ title: "README first paragraph",
327
+ build: ({ lineCount, quality, excerpt }) => ({
328
+ mission: `\u628A README \u7684\u6807\u9898\u4E0E\u9996\u6BB5\u4FDD\u7559\u4E3A\u9879\u76EE\u5BF9\u5916\u7684 canonical elevator pitch\u3002`,
329
+ context: [
330
+ `\u6765\u6E90\uFF1AREADME.md\uFF08${lineCount} \u884C\uFF0Cquality=${quality}\uFF09`,
331
+ "",
332
+ "\u6458\u5F55\uFF1A",
333
+ "> " + excerpt.split("\n").join("\n> ")
334
+ ].join("\n")
335
+ })
336
+ }
337
+ }
338
+ };
339
+ function resolveTemplateTitle(template, inputs) {
340
+ return typeof template.title === "function" ? template.title(inputs) : template.title;
341
+ }
342
+ function resolveKnowledgeLanguage(configured, target) {
343
+ if (configured === "en" || configured === "zh-CN") {
344
+ return configured;
345
+ }
346
+ return detectExistingLanguage(target);
347
+ }
348
+ function detectExistingLanguage(target) {
349
+ const ZH_CN_RATIO_THRESHOLD = 0.3;
350
+ const samples = [];
351
+ const readmePath = join(target, "README.md");
352
+ if (existsSync(readmePath)) {
353
+ try {
354
+ samples.push(readFileSync(readmePath, "utf8"));
355
+ } catch {
356
+ }
357
+ }
358
+ const docsDir = join(target, "docs");
359
+ if (existsSync(docsDir)) {
360
+ try {
361
+ const stat = statSync(docsDir);
362
+ if (stat.isDirectory()) {
363
+ for (const entry of readdirSync(docsDir, { withFileTypes: true })) {
364
+ if (!entry.isFile()) continue;
365
+ if (!/\.(md|mdx|txt)$/iu.test(entry.name)) continue;
366
+ try {
367
+ samples.push(readFileSync(join(docsDir, entry.name), "utf8"));
368
+ } catch {
369
+ }
370
+ }
371
+ }
372
+ } catch {
373
+ }
374
+ }
375
+ if (samples.length === 0) {
376
+ return "en";
377
+ }
378
+ let cjkCount = 0;
379
+ let asciiLetterCount = 0;
380
+ for (const sample of samples) {
381
+ for (const ch of sample) {
382
+ const code = ch.codePointAt(0) ?? 0;
383
+ if (code >= 19968 && code <= 40959) {
384
+ cjkCount += 1;
385
+ } else if (code >= 65 && code <= 90 || code >= 97 && code <= 122) {
386
+ asciiLetterCount += 1;
387
+ }
388
+ }
389
+ }
390
+ const denominator = cjkCount + asciiLetterCount;
391
+ if (denominator === 0) {
392
+ return "en";
393
+ }
394
+ const ratio = cjkCount / denominator;
395
+ return ratio > ZH_CN_RATIO_THRESHOLD ? "zh-CN" : "en";
396
+ }
397
+ function buildTechStackEntry(forensic, nowIso, tags, language = "en") {
182
398
  const framework = forensic.framework;
183
399
  const byExt = forensic.topology.by_ext ?? {};
184
400
  const topExtensions = Object.entries(byExt).sort(([, a], [, b]) => b - a).slice(0, 5).map(([ext, count]) => `${ext} (${count})`);
401
+ const frameworkSummary = `${framework.kind}${framework.version ? ` ${framework.version}` : ""}${framework.subkind ? ` / ${framework.subkind}` : ""}`;
402
+ const topExtensionsLine = topExtensions.length > 0 ? topExtensions.join(", ") : "(none)";
185
403
  const evidenceLines = framework.evidence.length > 0 ? framework.evidence.slice(0, 6) : ["(no explicit framework evidence)"];
186
- const body = renderSections({
187
- mission: `Track the primary tech stack and runtime surface used by ${forensic.project_name}.`,
188
- context: [
189
- `Framework: ${framework.kind}${framework.version ? ` ${framework.version}` : ""}${framework.subkind ? ` / ${framework.subkind}` : ""}`,
190
- `Top file extensions: ${topExtensions.length > 0 ? topExtensions.join(", ") : "(none)"}`,
191
- `Evidence:`,
192
- ...evidenceLines.map((line) => `- ${line}`)
193
- ].join("\n")
194
- });
404
+ const inputs = {
405
+ projectName: forensic.project_name,
406
+ frameworkSummary,
407
+ topExtensionsLine,
408
+ evidenceLines
409
+ };
410
+ const template = BASELINE_TEMPLATES[language]["tech-stack"];
411
+ const sections = template.build(inputs);
412
+ const body = renderSections(sections);
413
+ const relevancePaths = ["package.json", "pnpm-workspace.yaml"];
195
414
  return {
196
415
  type: "model",
197
416
  layer: "team",
198
417
  maturity: "verified",
199
418
  layer_reason: LAYER_REASON,
200
419
  created_at: nowIso,
201
- title: `Tech stack: ${framework.kind}`,
420
+ title: resolveTemplateTitle(template, inputs),
202
421
  body,
203
422
  target_subdir: "models",
204
423
  slug: "tech-stack",
205
- tags
424
+ tags,
425
+ relevance_scope: "narrow",
426
+ relevance_paths: relevancePaths
206
427
  };
207
428
  }
208
- function buildModuleStructureEntry(forensic, nowIso, tags) {
429
+ function buildModuleStructureEntry(forensic, nowIso, tags, language = "en") {
209
430
  const keyDirs = forensic.topology.key_dirs ?? [];
210
431
  const entryPoints = forensic.entry_points ?? [];
211
432
  const totalFiles = forensic.topology.total_files ?? 0;
212
433
  const dirsBlock = keyDirs.length > 0 ? keyDirs.slice(0, 12).map((dir) => `- ${dir}`).join("\n") : "- (no key directories detected)";
213
434
  const entriesBlock = entryPoints.length > 0 ? entryPoints.slice(0, 8).map((ep) => `- ${ep.path} \u2014 ${ep.reason}`).join("\n") : "- (no entry points detected)";
214
- const body = renderSections({
215
- mission: `Map the high-level module layout and primary entry points of ${forensic.project_name}.`,
216
- context: [
217
- `Total files: ${totalFiles}`,
218
- `Max directory depth: ${forensic.topology.max_depth ?? 0}`,
219
- "",
220
- "Key directories:",
221
- dirsBlock,
222
- "",
223
- "Entry points:",
224
- entriesBlock
225
- ].join("\n")
226
- });
435
+ const inputs = {
436
+ projectName: forensic.project_name,
437
+ totalFiles,
438
+ maxDepth: forensic.topology.max_depth ?? 0,
439
+ dirsBlock,
440
+ entriesBlock
441
+ };
442
+ const template = BASELINE_TEMPLATES[language]["module-structure"];
443
+ const body = renderSections(template.build(inputs));
444
+ const relevancePaths = ["packages/**/package.json"];
227
445
  return {
228
446
  type: "model",
229
447
  layer: "team",
230
448
  maturity: "verified",
231
449
  layer_reason: LAYER_REASON,
232
450
  created_at: nowIso,
233
- title: "Module structure",
451
+ title: resolveTemplateTitle(template, inputs),
234
452
  body,
235
453
  target_subdir: "models",
236
454
  slug: "module-structure",
237
- tags
455
+ tags,
456
+ relevance_scope: "narrow",
457
+ relevance_paths: relevancePaths
238
458
  };
239
459
  }
240
- function buildBuildConfigEntry(forensic, nowIso, tags) {
460
+ function buildBuildConfigEntry(forensic, nowIso, tags, language = "en") {
241
461
  const configFiles = (forensic.candidate_files ?? []).filter((entry) => entry.family === "config").map((entry) => entry.path);
242
462
  const framework = forensic.framework.kind;
243
463
  const configBlock = configFiles.length > 0 ? configFiles.map((file) => `- ${file}`).join("\n") : "- (no config files detected)";
244
- const body = renderSections({
245
- mission: `Document the deterministic build/bootstrap configuration anchoring ${forensic.project_name}.`,
246
- businessLogic: [
247
- `1. Detect framework: \`${framework}\`.`,
248
- `2. Read configuration files in declared order.`,
249
- `3. Honor compiler/bundler boundaries before generating new code.`,
250
- `4. Treat config drift as a fact-check signal \u2014 re-run \`fab scan\` after edits.`
251
- ].join("\n"),
252
- context: [
253
- `Framework: ${framework}`,
254
- "",
255
- "Configuration files:",
256
- configBlock
257
- ].join("\n")
258
- });
464
+ const inputs = {
465
+ projectName: forensic.project_name,
466
+ framework,
467
+ configBlock
468
+ };
469
+ const template = BASELINE_TEMPLATES[language]["build-config"];
470
+ const body = renderSections(template.build(inputs));
471
+ const discovered = configFiles.filter((path) => isBuildConfigPath(path));
472
+ const relevancePaths = discovered.length > 0 ? Array.from(new Set(discovered)) : ["tsconfig.json", "tsconfig.*.json", "vite.config.*", "rollup.config.*", "webpack.config.*"];
259
473
  return {
260
474
  type: "process",
261
475
  layer: "team",
262
476
  maturity: "verified",
263
477
  layer_reason: LAYER_REASON,
264
478
  created_at: nowIso,
265
- title: "Build configuration",
479
+ title: resolveTemplateTitle(template, inputs),
266
480
  body,
267
481
  target_subdir: "processes",
268
482
  slug: "build-config",
269
- tags
483
+ tags,
484
+ relevance_scope: "narrow",
485
+ relevance_paths: relevancePaths
270
486
  };
271
487
  }
272
- function buildCodeStyleEntry(forensic, nowIso, tags) {
488
+ function buildCodeStyleEntry(forensic, nowIso, tags, language = "en") {
273
489
  const dominantPatterns = (forensic.assertions ?? []).filter((a) => a.type === "pattern" || a.type === "domain").slice(0, 4).map((a) => `- ${a.statement}`);
274
490
  const proposedRules = (forensic.assertions ?? []).map((a) => a.proposed_rule).filter((rule) => typeof rule === "string" && rule.length > 0).slice(0, 4);
275
491
  const patternsBlock = dominantPatterns.length > 0 ? dominantPatterns.join("\n") : "- (no dominant patterns detected)";
276
492
  const rulesBlock = proposedRules.length > 0 ? proposedRules.map((rule) => `- ${rule}`).join("\n") : "- Follow existing module/file patterns; do not introduce new conventions without team agreement.";
277
- const body = renderSections({
278
- mission: `Codify the recurring authoring conventions observed in ${forensic.project_name}.`,
279
- mandatoryInjection: [
280
- "When generating or modifying source files in this repo, AI agents MUST:",
281
- rulesBlock
282
- ].join("\n"),
283
- context: [
284
- "Detected patterns:",
285
- patternsBlock
286
- ].join("\n")
287
- });
493
+ const inputs = {
494
+ projectName: forensic.project_name,
495
+ rulesBlock,
496
+ patternsBlock
497
+ };
498
+ const template = BASELINE_TEMPLATES[language]["code-style"];
499
+ const body = renderSections(template.build(inputs));
500
+ const relevancePaths = [
501
+ ".prettierrc",
502
+ ".prettierrc.*",
503
+ ".editorconfig",
504
+ "eslint.config.*",
505
+ ".eslintrc",
506
+ ".eslintrc.*"
507
+ ];
288
508
  return {
289
509
  type: "guideline",
290
510
  layer: "team",
291
511
  maturity: "verified",
292
512
  layer_reason: LAYER_REASON,
293
513
  created_at: nowIso,
294
- title: "Code style guidelines",
514
+ title: resolveTemplateTitle(template, inputs),
295
515
  body,
296
516
  target_subdir: "guidelines",
297
517
  slug: "code-style",
298
- tags
518
+ tags,
519
+ relevance_scope: "narrow",
520
+ relevance_paths: relevancePaths
299
521
  };
300
522
  }
301
523
  function buildCIConfigEntry(forensic, nowIso, tags) {
@@ -319,6 +541,12 @@ function buildCIConfigEntry(forensic, nowIso, tags) {
319
541
  filesBlock
320
542
  ].join("\n")
321
543
  });
544
+ const relevancePaths = [
545
+ ".github/workflows/**",
546
+ ".gitlab-ci.yml",
547
+ ".circleci/**",
548
+ "Jenkinsfile"
549
+ ];
322
550
  return {
323
551
  type: "process",
324
552
  layer: "team",
@@ -329,10 +557,12 @@ function buildCIConfigEntry(forensic, nowIso, tags) {
329
557
  body,
330
558
  target_subdir: "processes",
331
559
  slug: "ci-config",
332
- tags
560
+ tags,
561
+ relevance_scope: "narrow",
562
+ relevance_paths: relevancePaths
333
563
  };
334
564
  }
335
- function buildReadmeFirstParaEntry(target, forensic, nowIso, tags) {
565
+ function buildReadmeFirstParaEntry(target, forensic, nowIso, tags, language = "en") {
336
566
  if (forensic.readme.quality === "missing") {
337
567
  return null;
338
568
  }
@@ -345,26 +575,29 @@ function buildReadmeFirstParaEntry(target, forensic, nowIso, tags) {
345
575
  if (firstPara === null) {
346
576
  return null;
347
577
  }
348
- const body = renderSections({
349
- mission: `Preserve the README headline and first paragraph as the canonical project elevator pitch.`,
350
- context: [
351
- `Source: README.md (${forensic.readme.line_count} lines, quality=${forensic.readme.quality})`,
352
- "",
353
- "Excerpt:",
354
- "> " + firstPara.split("\n").join("\n> ")
355
- ].join("\n")
356
- });
578
+ const inputs = {
579
+ lineCount: forensic.readme.line_count,
580
+ quality: forensic.readme.quality,
581
+ excerpt: firstPara
582
+ };
583
+ const template = BASELINE_TEMPLATES[language]["readme-first-paragraph"];
584
+ const body = renderSections(template.build(inputs));
357
585
  return {
358
586
  type: "model",
359
587
  layer: "team",
360
588
  maturity: "verified",
361
589
  layer_reason: LAYER_REASON,
362
590
  created_at: nowIso,
363
- title: "README first paragraph",
591
+ title: resolveTemplateTitle(template, inputs),
364
592
  body,
365
593
  target_subdir: "models",
366
594
  slug: "readme-first-paragraph",
367
- tags
595
+ tags,
596
+ // v2.0-rc.7 T2: broad by design — single repo-root file, the Phase 1.5
597
+ // PreToolUse blacklist already covers README. Anchoring this entry to
598
+ // README.md would surface it on every README edit, which is noise.
599
+ relevance_scope: "broad",
600
+ relevance_paths: []
368
601
  };
369
602
  }
370
603
  function buildProjectBriefEntry(target, forensic, nowIso, tags) {
@@ -399,7 +632,13 @@ function buildProjectBriefEntry(target, forensic, nowIso, tags) {
399
632
  body,
400
633
  target_subdir: "models",
401
634
  slug: "project-brief",
402
- tags
635
+ tags,
636
+ // v2.0-rc.7 T2: broad — project brief is a cross-cutting description
637
+ // with no path anchor. Narrowing it to README.md would duplicate the
638
+ // readme-first-paragraph surface; keeping it broad lets the
639
+ // SessionStart broad hint do the right thing.
640
+ relevance_scope: "broad",
641
+ relevance_paths: []
403
642
  };
404
643
  }
405
644
  function renderMarkdown(entry) {
@@ -413,6 +652,7 @@ ${entry.body}
413
652
  }
414
653
  function renderFrontmatter(entry) {
415
654
  const tagsLine = entry.tags.length > 0 ? `tags: [${entry.tags.join(", ")}]` : "tags: []";
655
+ const relevancePathsLine = entry.relevance_paths.length > 0 ? `relevance_paths: [${entry.relevance_paths.map((p) => quoteIfNeeded(p)).join(", ")}]` : "relevance_paths: []";
416
656
  const lines = [
417
657
  "---",
418
658
  `id: ${entry.id}`,
@@ -422,6 +662,8 @@ function renderFrontmatter(entry) {
422
662
  `layer_reason: ${quoteIfNeeded(entry.layer_reason)}`,
423
663
  `created_at: ${entry.created_at}`,
424
664
  tagsLine,
665
+ `relevance_scope: ${entry.relevance_scope}`,
666
+ relevancePathsLine,
425
667
  "---"
426
668
  ];
427
669
  return lines.join("\n");
@@ -545,6 +787,19 @@ function findExistingIdForFile(sidecar, targetPath, target) {
545
787
  function isCIConfigPath(path) {
546
788
  return path.startsWith(".github/workflows/") || path.startsWith(".gitlab-ci") || path === "azure-pipelines.yml" || path === ".circleci/config.yml" || path === "Jenkinsfile" || path === ".travis.yml";
547
789
  }
790
+ function isBuildConfigPath(path) {
791
+ const lower = path.toLowerCase();
792
+ const basename = lower.split("/").pop() ?? lower;
793
+ if (basename.startsWith("tsconfig") && basename.endsWith(".json")) return true;
794
+ if (basename === "package.json") return true;
795
+ if (basename === "pnpm-workspace.yaml" || basename === "pnpm-workspace.yml") return true;
796
+ if (basename.startsWith("vite.config.")) return true;
797
+ if (basename.startsWith("rollup.config.")) return true;
798
+ if (basename.startsWith("webpack.config.")) return true;
799
+ if (basename.startsWith("vitest.config.")) return true;
800
+ if (basename === "turbo.json" || basename === "nx.json") return true;
801
+ return false;
802
+ }
548
803
  function extractFirstParagraph(readme) {
549
804
  const lines = readme.split(/\r?\n/);
550
805
  let i = 0;
@@ -724,8 +979,13 @@ var __testing__ = {
724
979
  renderMarkdown,
725
980
  stripFrontmatter,
726
981
  isCIConfigPath,
982
+ isBuildConfigPath,
727
983
  extractFirstParagraph,
728
- extractExplicitDescription
984
+ extractExplicitDescription,
985
+ // TASK-008: bilingual template registry + language detection
986
+ detectExistingLanguage,
987
+ resolveKnowledgeLanguage,
988
+ BASELINE_TEMPLATES
729
989
  };
730
990
 
731
991
  export {
@@ -735,6 +995,7 @@ export {
735
995
  runInitScan,
736
996
  scanCommand,
737
997
  scan_default,
998
+ detectExistingLanguage,
738
999
  deriveTagsFromForensic,
739
1000
  __testing__
740
1001
  };
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/i18n.ts
4
+ import { createTranslator, detectNodeLocale } from "@fenglimg/fabric-shared";
5
+ var locale = detectNodeLocale();
6
+ var t = createTranslator(locale);
7
+
8
+ export {
9
+ t
10
+ };