@fenglimg/fabric-server 1.8.0-rc.2 → 2.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,3 @@
1
- // src/constants.ts
2
- var AGENTS_MD_RESOURCE_URI = "fabric://bootstrap-readme";
3
-
4
1
  // src/cache.ts
5
2
  var ContextCache = class {
6
3
  constructor(defaultTtlMs = 5e3) {
@@ -280,16 +277,24 @@ function flushAndSyncEventLedger(projectRoot) {
280
277
  }
281
278
 
282
279
  // src/services/rule-meta-builder.ts
283
- import { readdir, readFile as readFile3 } from "fs/promises";
280
+ import { mkdir as mkdir2, readdir, readFile as readFile3 } from "fs/promises";
284
281
  import { existsSync as existsSync2, statSync } from "fs";
282
+ import { homedir } from "os";
285
283
  import { isAbsolute, join as join3, relative, resolve as resolve2, sep as sep2 } from "path";
286
284
  import {
287
285
  RULE_TEST_INDEX_SCHEMA_VERSION,
288
286
  agentsMetaSchema as agentsMetaSchema3,
287
+ defaultAgentsMetaCounters,
289
288
  deriveAgentsMetaLayer,
290
289
  deriveAgentsMetaStableId,
291
290
  deriveAgentsMetaTopologyType,
292
- ruleTestIndexSchema
291
+ isKnowledgeStableId,
292
+ ruleTestIndexSchema,
293
+ KnowledgeTypeSchema,
294
+ MaturitySchema,
295
+ LayerSchema,
296
+ StableIdSchema,
297
+ parseKnowledgeId
293
298
  } from "@fenglimg/fabric-shared";
294
299
  import { atomicWriteText as atomicWriteText2 } from "@fenglimg/fabric-shared/node/atomic-write";
295
300
  async function buildRuleMeta(projectRootInput) {
@@ -338,19 +343,15 @@ async function computeRulesBasedAgentsMeta(projectRootInput, existingMeta) {
338
343
  assertExistingDirectory(projectRoot);
339
344
  const previousMeta = existingMeta ?? await readExistingMeta(join3(projectRoot, ".fabric", "agents.meta.json"));
340
345
  const existingByContentRef = indexExistingNodesByContentRef(previousMeta);
341
- const ruleFiles = await findFabricRuleFiles(projectRoot);
346
+ const ruleFiles = await findKnowledgeFiles(projectRoot);
342
347
  const nodes = {};
343
- const bootstrapNode = await createBootstrapNode(projectRoot, existingByContentRef.get(".fabric/bootstrap/README.md")?.node);
344
- if (bootstrapNode !== void 0) {
345
- nodes.L0 = bootstrapNode;
346
- }
347
348
  for (const contentRef of ruleFiles) {
348
- const source = await readFile3(join3(projectRoot, contentRef), "utf8");
349
+ const source = await readFile3(resolveContentRefPath(projectRoot, contentRef), "utf8");
349
350
  const existing = existingByContentRef.get(contentRef);
350
- const id = deriveNodeId(contentRef);
351
351
  const hash = sha256(source);
352
352
  const defaults = createDefaultNodeMeta(contentRef);
353
353
  const identity = deriveRuleIdentity(contentRef, source, existing?.node);
354
+ const id = isKnowledgeStableId(identity.stableId) ? identity.stableId : deriveNodeId(contentRef);
354
355
  nodes[id] = {
355
356
  ...defaults,
356
357
  ...existing?.node,
@@ -363,10 +364,12 @@ async function computeRulesBasedAgentsMeta(projectRootInput, existingMeta) {
363
364
  sections: extractRuleSections(source)
364
365
  };
365
366
  }
367
+ const counters = previousMeta?.counters ?? defaultAgentsMetaCounters();
366
368
  return {
367
369
  ...previousMeta ?? {},
368
370
  revision: computeRevision(nodes),
369
- nodes: sortNodes(nodes)
371
+ nodes: sortNodes(nodes),
372
+ counters
370
373
  };
371
374
  }
372
375
  async function computeRuleTestIndex(projectRootInput, computedMeta, previousIndex) {
@@ -465,25 +468,51 @@ async function readExistingRuleTestIndex(indexPath) {
465
468
  return void 0;
466
469
  }
467
470
  }
468
- async function findFabricRuleFiles(projectRoot) {
469
- const rulesRoot = join3(projectRoot, ".fabric", "rules");
470
- if (!existsSync2(rulesRoot) || !statSync(rulesRoot).isDirectory()) {
471
- return [];
471
+ var KNOWLEDGE_SUBDIRS = ["decisions", "pitfalls", "guidelines", "models", "processes", "pending"];
472
+ var PERSONAL_CONTENT_REF_PREFIX = "~/.fabric/knowledge/";
473
+ var TEAM_CONTENT_REF_PREFIX = ".fabric/knowledge/";
474
+ function resolvePersonalRoot() {
475
+ return process.env.FABRIC_HOME ?? homedir();
476
+ }
477
+ function resolveContentRefPath(projectRoot, contentRef) {
478
+ if (contentRef.startsWith(PERSONAL_CONTENT_REF_PREFIX)) {
479
+ return join3(resolvePersonalRoot(), ".fabric", "knowledge", contentRef.slice(PERSONAL_CONTENT_REF_PREFIX.length));
480
+ }
481
+ return join3(projectRoot, contentRef);
482
+ }
483
+ async function findKnowledgeFiles(projectRoot) {
484
+ const teamRoot = join3(projectRoot, ".fabric", "knowledge");
485
+ const personalRoot = join3(resolvePersonalRoot(), ".fabric", "knowledge");
486
+ try {
487
+ await mkdir2(personalRoot, { recursive: true });
488
+ for (const sub of KNOWLEDGE_SUBDIRS) {
489
+ await mkdir2(join3(personalRoot, sub), { recursive: true });
490
+ }
491
+ } catch {
472
492
  }
473
493
  const files = [];
474
- const stack = [rulesRoot];
475
- while (stack.length > 0) {
476
- const current = stack.pop();
477
- if (current === void 0) {
494
+ for (const [root, prefix] of [
495
+ [teamRoot, TEAM_CONTENT_REF_PREFIX],
496
+ [personalRoot, PERSONAL_CONTENT_REF_PREFIX]
497
+ ]) {
498
+ if (!existsSync2(root) || !statSync(root).isDirectory()) {
478
499
  continue;
479
500
  }
480
- for (const entry of await readdir(current, { withFileTypes: true })) {
481
- const absolutePath = join3(current, entry.name);
482
- const relativePath = toPosixPath(relative(projectRoot, absolutePath));
483
- if (entry.isDirectory()) {
484
- stack.push(absolutePath);
485
- } else if (entry.isFile() && entry.name.endsWith(".md")) {
486
- files.push(relativePath);
501
+ for (const subdir of KNOWLEDGE_SUBDIRS) {
502
+ const dir = join3(root, subdir);
503
+ let entries;
504
+ try {
505
+ entries = await readdir(dir, { withFileTypes: true });
506
+ } catch (error) {
507
+ if (isNodeError3(error) && error.code === "ENOENT") {
508
+ continue;
509
+ }
510
+ throw error;
511
+ }
512
+ for (const entry of entries) {
513
+ if (entry.isFile() && entry.name.endsWith(".md")) {
514
+ files.push(`${prefix}${subdir}/${entry.name}`);
515
+ }
487
516
  }
488
517
  }
489
518
  }
@@ -598,11 +627,14 @@ function indexExistingNodesByContentRef(existingMeta) {
598
627
  return byContentRef;
599
628
  }
600
629
  function deriveNodeId(file) {
601
- if (file === ".fabric/bootstrap/README.md") {
602
- return "L0";
603
- }
604
630
  const layer = deriveRuleMetaLayer(file);
605
631
  const relativeStem = getRuleRelativeStem(file);
632
+ if (file.startsWith(PERSONAL_CONTENT_REF_PREFIX)) {
633
+ return `${layer}/personal/${relativeStem}`;
634
+ }
635
+ if (file.startsWith(TEAM_CONTENT_REF_PREFIX)) {
636
+ return `${layer}/team/${relativeStem}`;
637
+ }
606
638
  return `${layer}/${relativeStem}`;
607
639
  }
608
640
  function createDefaultNodeMeta(contentRef) {
@@ -620,31 +652,7 @@ function createDefaultNodeMeta(contentRef) {
620
652
  hash: ""
621
653
  };
622
654
  }
623
- async function createBootstrapNode(projectRoot, existing) {
624
- const contentRef = ".fabric/bootstrap/README.md";
625
- const bootstrapPath = join3(projectRoot, contentRef);
626
- if (!existsSync2(bootstrapPath)) {
627
- return void 0;
628
- }
629
- const hash = sha256(await readFile3(bootstrapPath, "utf8"));
630
- const identity = {
631
- stableId: existing?.stable_id ?? deriveAgentsMetaStableId(contentRef),
632
- identitySource: existing?.identity_source ?? "derived"
633
- };
634
- return {
635
- ...createDefaultNodeMeta(contentRef),
636
- ...existing,
637
- file: contentRef,
638
- content_ref: contentRef,
639
- hash,
640
- stable_id: identity.stableId,
641
- identity_source: identity.identitySource
642
- };
643
- }
644
655
  function deriveScopeGlob(contentRef) {
645
- if (contentRef === ".fabric/bootstrap/README.md") {
646
- return "**";
647
- }
648
656
  const stem = getRuleRelativeStem(contentRef);
649
657
  const segments = stem.split("/").filter(Boolean);
650
658
  if (segments.length === 0 || stem === "root") {
@@ -660,10 +668,10 @@ function deriveScopeGlob(contentRef) {
660
668
  return scopePath === "" ? "**" : `${scopePath}/**`;
661
669
  }
662
670
  function getRuleRelativeStem(contentRef) {
663
- return contentRef.replace(/^\.fabric\/rules\//u, "").replace(/\.md$/u, "");
671
+ return contentRef.replace(/^~\/\.fabric\/knowledge\//u, "").replace(/^\.fabric\/knowledge\//u, "").replace(/\.md$/u, "");
664
672
  }
665
673
  function toAgentsCompatiblePath(contentRef) {
666
- return contentRef.replace(/^\.fabric\/rules\//u, ".fabric/agents/");
674
+ return contentRef.replace(/^~\/\.fabric\/knowledge\//u, ".fabric/agents/").replace(/^\.fabric\/knowledge\//u, ".fabric/agents/");
667
675
  }
668
676
  function sortNodes(nodes) {
669
677
  return Object.fromEntries(Object.entries(nodes).sort(([left], [right]) => left.localeCompare(right)));
@@ -708,7 +716,7 @@ function collectDriftDetails(existingMeta, computedMeta) {
708
716
  async function recordBaselineSynced(projectRoot, input) {
709
717
  if (input.driftDetails.length > 0) {
710
718
  await appendEventLedgerEvent(projectRoot, {
711
- event_type: "rule_drift_detected",
719
+ event_type: "knowledge_drift_detected",
712
720
  revision: input.previousRevision ?? input.revision,
713
721
  drifted_stable_ids: input.driftDetails.map((detail) => detail.stable_id),
714
722
  missing_files: input.driftDetails.filter((detail) => detail.actual_hash === null).map((detail) => detail.file),
@@ -716,21 +724,6 @@ async function recordBaselineSynced(projectRoot, input) {
716
724
  details: input.driftDetails
717
725
  });
718
726
  }
719
- await appendEventLedgerEvent(projectRoot, {
720
- event_type: "rule_baseline_accepted",
721
- revision: input.revision,
722
- previous_revision: input.previousRevision,
723
- accepted_stable_ids: input.acceptedStableIds,
724
- source: input.source
725
- });
726
- await appendEventLedgerEvent(projectRoot, {
727
- event_type: "baseline_synced",
728
- revision: input.revision,
729
- previous_revision: input.previousRevision,
730
- synced_files: input.syncedFiles,
731
- accepted_stable_ids: input.acceptedStableIds,
732
- source: input.source
733
- });
734
727
  }
735
728
  function flattenKeys(value, keys = {}) {
736
729
  if (value && typeof value === "object") {
@@ -745,6 +738,19 @@ function toPosixPath(path) {
745
738
  return path.split(sep2).join("/");
746
739
  }
747
740
  function deriveRuleIdentity(file, source, existing) {
741
+ const declaredKnowledgeId = extractDeclaredKnowledgeId(source);
742
+ if (declaredKnowledgeId !== void 0) {
743
+ return {
744
+ stableId: declaredKnowledgeId,
745
+ identitySource: "declared"
746
+ };
747
+ }
748
+ if (existing?.stable_id !== void 0 && isKnowledgeStableId(existing.stable_id)) {
749
+ return {
750
+ stableId: existing.stable_id,
751
+ identitySource: "declared"
752
+ };
753
+ }
748
754
  const declaredStableId = extractDeclaredStableId(source);
749
755
  const derivedStableId = deriveAgentsMetaStableId(toAgentsCompatiblePath(file));
750
756
  if (declaredStableId !== void 0) {
@@ -768,6 +774,18 @@ function extractDeclaredStableId(source) {
768
774
  const match = /^(?:\uFEFF)?(?:---\r?\n[\s\S]*?\r?\n---\s*(?:\r?\n|$))?<!--\s*fab:rule-id\s+([A-Za-z0-9][A-Za-z0-9/_-]*)\s*-->\s*(?:\r?\n|$)/u.exec(source);
769
775
  return match?.[1];
770
776
  }
777
+ function extractDeclaredKnowledgeId(source) {
778
+ const frontmatter = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/u.exec(source);
779
+ if (frontmatter === null) {
780
+ return void 0;
781
+ }
782
+ const idMatch = /^id:\s*(.+?)\s*$/mu.exec(frontmatter[1]);
783
+ if (idMatch === null) {
784
+ return void 0;
785
+ }
786
+ const candidate = idMatch[1].replace(/^["'](.*)["']$/u, "$1").trim();
787
+ return isKnowledgeStableId(candidate) ? candidate : void 0;
788
+ }
771
789
  function extractRuleDescription(source) {
772
790
  const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/u.exec(source);
773
791
  const description = frontmatter === null ? void 0 : extractDescriptionFromFrontmatter(frontmatter[1]);
@@ -784,7 +802,15 @@ function extractRuleDescription(source) {
784
802
  intent_clues: [],
785
803
  tech_stack: [],
786
804
  impact: [],
787
- must_read_if: summary
805
+ must_read_if: summary,
806
+ // v2.0 knowledge fields are absent in heading-only fallback.
807
+ id: void 0,
808
+ knowledge_type: void 0,
809
+ maturity: void 0,
810
+ knowledge_layer: void 0,
811
+ layer_reason: void 0,
812
+ created_at: void 0,
813
+ tags: void 0
788
814
  };
789
815
  }
790
816
  function extractRuleSections(source) {
@@ -796,13 +822,99 @@ function extractDescriptionFromFrontmatter(frontmatter) {
796
822
  if (summary === void 0) {
797
823
  return void 0;
798
824
  }
825
+ const knowledge = extractKnowledgeFieldsFromFrontmatter(frontmatter);
799
826
  return {
800
827
  summary,
801
828
  intent_clues: extractInlineArray(frontmatter, "intent_clues"),
802
829
  tech_stack: extractInlineArray(frontmatter, "tech_stack"),
803
830
  impact: extractInlineArray(frontmatter, "impact"),
804
831
  must_read_if: extractScalar(frontmatter, "must_read_if") ?? summary,
805
- entities: extractInlineArray(frontmatter, "entities")
832
+ entities: extractInlineArray(frontmatter, "entities"),
833
+ id: knowledge.id,
834
+ knowledge_type: knowledge.knowledge_type,
835
+ maturity: knowledge.maturity,
836
+ knowledge_layer: knowledge.knowledge_layer,
837
+ layer_reason: knowledge.layer_reason,
838
+ created_at: knowledge.created_at,
839
+ tags: knowledge.tags
840
+ };
841
+ }
842
+ function extractKnowledgeFieldsFromFrontmatter(frontmatter) {
843
+ const rawId = extractScalar(frontmatter, "id");
844
+ const rawType = extractScalar(frontmatter, "type");
845
+ const rawMaturity = extractScalar(frontmatter, "maturity");
846
+ const rawLayer = extractScalar(frontmatter, "layer");
847
+ const rawLayerReason = extractScalar(frontmatter, "layer_reason");
848
+ const rawCreatedAt = extractScalar(frontmatter, "created_at");
849
+ let id;
850
+ if (rawId !== void 0) {
851
+ const parsed = StableIdSchema.safeParse(rawId);
852
+ if (parsed.success) {
853
+ id = parsed.data;
854
+ } else {
855
+ process.stderr.write(`[fabric] frontmatter: invalid knowledge id format ${JSON.stringify(rawId)}; skipping
856
+ `);
857
+ }
858
+ }
859
+ let knowledge_type;
860
+ if (rawType !== void 0) {
861
+ const parsed = KnowledgeTypeSchema.safeParse(rawType);
862
+ if (parsed.success) {
863
+ knowledge_type = parsed.data;
864
+ } else {
865
+ process.stderr.write(`[fabric] frontmatter: unknown knowledge type ${JSON.stringify(rawType)}; skipping
866
+ `);
867
+ }
868
+ }
869
+ let maturity;
870
+ if (rawMaturity !== void 0) {
871
+ const parsed = MaturitySchema.safeParse(rawMaturity);
872
+ if (parsed.success) {
873
+ maturity = parsed.data;
874
+ } else {
875
+ process.stderr.write(`[fabric] frontmatter: unknown maturity ${JSON.stringify(rawMaturity)}; skipping
876
+ `);
877
+ }
878
+ }
879
+ let knowledge_layer;
880
+ if (rawLayer !== void 0) {
881
+ const parsed = LayerSchema.safeParse(rawLayer);
882
+ if (parsed.success) {
883
+ knowledge_layer = parsed.data;
884
+ } else {
885
+ process.stderr.write(`[fabric] frontmatter: unknown layer ${JSON.stringify(rawLayer)}; skipping
886
+ `);
887
+ }
888
+ }
889
+ let created_at;
890
+ if (rawCreatedAt !== void 0) {
891
+ if (!Number.isNaN(Date.parse(rawCreatedAt))) {
892
+ created_at = rawCreatedAt;
893
+ } else {
894
+ process.stderr.write(`[fabric] frontmatter: malformed created_at ${JSON.stringify(rawCreatedAt)}; skipping
895
+ `);
896
+ }
897
+ }
898
+ if (id !== void 0 && knowledge_layer !== void 0) {
899
+ const decoded = parseKnowledgeId(id);
900
+ if (decoded !== null && decoded.layer !== knowledge_layer) {
901
+ process.stderr.write(
902
+ `[fabric] frontmatter: id ${id} encodes layer ${decoded.layer} but layer field says ${knowledge_layer}; dropping both
903
+ `
904
+ );
905
+ id = void 0;
906
+ knowledge_layer = void 0;
907
+ }
908
+ }
909
+ const tags = extractInlineArray(frontmatter, "tags");
910
+ return {
911
+ id,
912
+ knowledge_type,
913
+ maturity,
914
+ knowledge_layer,
915
+ layer_reason: rawLayerReason,
916
+ created_at,
917
+ tags: tags.length > 0 ? tags : void 0
806
918
  };
807
919
  }
808
920
  function extractScalar(frontmatter, key) {
@@ -868,12 +980,12 @@ async function readMetaEntries(projectRoot) {
868
980
  return map;
869
981
  }
870
982
  async function findRuleFiles(projectRoot) {
871
- const rulesRoot = join4(projectRoot, ".fabric", "rules");
872
- if (!existsSync3(rulesRoot) || !statSync2(rulesRoot).isDirectory()) {
983
+ const knowledgeRoot = join4(projectRoot, ".fabric", "knowledge");
984
+ if (!existsSync3(knowledgeRoot) || !statSync2(knowledgeRoot).isDirectory()) {
873
985
  return [];
874
986
  }
875
987
  const files = [];
876
- const stack = [rulesRoot];
988
+ const stack = [knowledgeRoot];
877
989
  while (stack.length > 0) {
878
990
  const current = stack.pop();
879
991
  if (current === void 0) {
@@ -1006,7 +1118,7 @@ async function appendRuleSyncEvents(projectRoot, events) {
1006
1118
  const staleFiles = events.filter((e) => e.type !== "rule_removed").map((e) => e.path);
1007
1119
  if (missingFiles.length > 0 || staleFiles.length > 0) {
1008
1120
  await appendEventLedgerEvent(projectRoot, {
1009
- event_type: "rule_drift_detected",
1121
+ event_type: "knowledge_drift_detected",
1010
1122
  drifted_stable_ids: driftedIds,
1011
1123
  missing_files: missingFiles,
1012
1124
  stale_files: staleFiles
@@ -1142,844 +1254,148 @@ async function reconcileRules(projectRoot, opts) {
1142
1254
  }
1143
1255
 
1144
1256
  // src/services/doctor.ts
1145
- import { existsSync as existsSync4, mkdirSync, readdirSync, readFileSync, rmdirSync, renameSync, statSync as statSync3, unlinkSync } from "fs";
1146
- import { access, readFile as readFile7, writeFile as writeFile2 } from "fs/promises";
1257
+ import { existsSync as existsSync4, readdirSync, readFileSync, statSync as statSync3 } from "fs";
1258
+ import { access, mkdir as mkdir3, readFile as readFile5, writeFile as writeFile2 } from "fs/promises";
1147
1259
  import { constants } from "fs";
1148
- import { isAbsolute as isAbsolute3, join as join8, posix as posix2, resolve as resolve4 } from "path";
1260
+ import { isAbsolute as isAbsolute2, join as join5, posix, resolve as resolve3 } from "path";
1149
1261
  import {
1150
1262
  agentsMetaSchema as agentsMetaSchema4,
1263
+ AgentsMetaCountersSchema,
1151
1264
  forensicReportSchema,
1265
+ parseKnowledgeId as parseKnowledgeId2,
1152
1266
  ruleTestIndexSchema as ruleTestIndexSchema2
1153
1267
  } from "@fenglimg/fabric-shared";
1154
1268
  import { detectFramework } from "@fenglimg/fabric-shared/node";
1155
-
1156
- // src/services/rule-sections.ts
1157
- import { readFile as readFile6 } from "fs/promises";
1158
- import { join as join7 } from "path";
1159
-
1160
- // src/services/audit-log.ts
1161
- import { open, stat as stat2 } from "fs/promises";
1162
- import { isAbsolute as isAbsolute2, join as join5, posix, relative as relative3, resolve as resolve3 } from "path";
1163
- var AUDIT_LOG_FILE = `${FABRIC_DIR}/audit.jsonl`;
1164
- var DEFAULT_AUDIT_WINDOW_MS = 5 * 60 * 1e3;
1165
- async function appendGetRulesAuditEvent(projectRoot, input) {
1166
- const entry = {
1167
- kind: "audit-event",
1168
- event: "get_rules",
1169
- ts: input.ts ?? Date.now(),
1170
- path: normalizeAuditPath(projectRoot, input.path),
1171
- client_hash: input.client_hash
1172
- };
1173
- await appendAuditLogEventLedgerEvents(projectRoot, [entry], {
1174
- rule_context: {
1175
- required_stable_ids: input.required_stable_ids,
1176
- ai_selectable_stable_ids: input.ai_selectable_stable_ids,
1177
- final_stable_ids: input.final_stable_ids
1178
- },
1179
- correlation_id: input.correlation_id,
1180
- session_id: input.session_id
1181
- });
1182
- return entry;
1183
- }
1184
- async function appendRuleSelectionAuditEvent(projectRoot, input) {
1185
- const entry = {
1186
- kind: "audit-event",
1187
- event: "rule_selection",
1188
- ts: input.ts ?? Date.now(),
1189
- path: normalizeAuditPath(projectRoot, input.path),
1190
- selection_token: input.selection_token,
1191
- target_paths: input.target_paths.map((path) => normalizeAuditPath(projectRoot, path)),
1192
- required_stable_ids: input.required_stable_ids,
1193
- ai_selectable_stable_ids: input.ai_selectable_stable_ids,
1194
- ai_selected_stable_ids: input.ai_selected_stable_ids,
1195
- final_stable_ids: input.final_stable_ids,
1196
- ai_selection_reasons: input.ai_selection_reasons,
1197
- rejected_stable_ids: input.rejected_stable_ids,
1198
- ignored_stable_ids: input.ignored_stable_ids
1269
+ import { atomicWriteJson as atomicWriteJson2 } from "@fenglimg/fabric-shared/node/atomic-write";
1270
+ var KNOWLEDGE_SUBDIRS2 = ["decisions", "pitfalls", "guidelines", "models", "processes", "pending"];
1271
+ var COUNTER_TYPE_CODES = ["MOD", "DEC", "GLD", "PIT", "PRO"];
1272
+ var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
1273
+ var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
1274
+ ".fabric",
1275
+ ".git",
1276
+ ".next",
1277
+ ".turbo",
1278
+ "Library",
1279
+ "Temp",
1280
+ "build",
1281
+ "coverage",
1282
+ "dist",
1283
+ "node_modules"
1284
+ ]);
1285
+ var TARGET_FILE_PATHS = [
1286
+ ".fabric/forensic.json",
1287
+ ".fabric/agents.meta.json",
1288
+ ".fabric/rule-test.index.json",
1289
+ ".fabric/events.jsonl",
1290
+ ".fabric/knowledge"
1291
+ ];
1292
+ async function runDoctorReport(target) {
1293
+ const projectRoot = normalizeTarget(target);
1294
+ const framework = detectFramework(projectRoot);
1295
+ const entryPoints = collectEntryPoints(projectRoot);
1296
+ const [
1297
+ forensic,
1298
+ meta,
1299
+ eventLedger,
1300
+ ruleTestIndex
1301
+ ] = await Promise.all([
1302
+ inspectForensic(projectRoot),
1303
+ inspectMeta(projectRoot),
1304
+ inspectEventLedger(projectRoot),
1305
+ inspectRuleTestIndex(projectRoot)
1306
+ ]);
1307
+ const mcpConfigInWrongFile = inspectMcpConfigInWrongFile(projectRoot);
1308
+ const metaManuallyDiverged = await inspectMetaManuallyDiverged(projectRoot);
1309
+ const knowledgeDirUnindexed = inspectKnowledgeDirUnindexed(projectRoot, meta);
1310
+ const knowledgeDirMissing = inspectKnowledgeDirMissing(projectRoot);
1311
+ const stableIdCollision = await inspectStableIdCollisions(projectRoot);
1312
+ const counterDesync = inspectCounterDesync(meta);
1313
+ const preexistingRootFiles = inspectPreexistingRootFiles(projectRoot);
1314
+ const bootstrapAnchor = inspectBootstrapAnchor(projectRoot);
1315
+ const checks = [
1316
+ createBootstrapAnchorCheck(bootstrapAnchor),
1317
+ createKnowledgeDirMissingCheck(knowledgeDirMissing),
1318
+ createForensicCheck(forensic, framework.kind, entryPoints.length),
1319
+ // v2.0: removed `createInitContextCheck` — `.fabric/init-context.json`
1320
+ // is owned by the AI-side client init skill, not by `fabric init` CLI.
1321
+ // The file's absence is a legitimate post-init state when the skill has
1322
+ // not yet run, so flagging it as a doctor manual_error misrepresents
1323
+ // ownership.
1324
+ createMetaCheck(meta),
1325
+ createRuleContentRefCheck(meta),
1326
+ // v2.0 / rc.2: `createRuleSectionsCheck` removed — it parsed v1.x
1327
+ // [MANDATORY_INJECTION] sections out of legacy rule files, a structural
1328
+ // concept that has no v2 equivalent. rc.4 will introduce a dedicated v2
1329
+ // lint suite for the new knowledge frontmatter contract.
1330
+ createRuleTestIndexCheck(ruleTestIndex),
1331
+ createEventLedgerCheck(eventLedger),
1332
+ createEventLedgerPartialWriteCheck(eventLedger),
1333
+ createMcpConfigInWrongFileCheck(mcpConfigInWrongFile),
1334
+ createMetaManuallyDivergedCheck(metaManuallyDiverged),
1335
+ createKnowledgeDirUnindexedCheck(knowledgeDirUnindexed),
1336
+ createStableIdCollisionCheck(stableIdCollision),
1337
+ createCounterDesyncCheck(counterDesync),
1338
+ createPreexistingRootFilesCheck(preexistingRootFiles)
1339
+ // v2.0 / rc.2: `createLegacyClientPathCheck` removed. The schema now
1340
+ // rejects retired clientPaths keys (windsurf/rooCode/geminiCLI) at Zod
1341
+ // parse time, so the soft-deprecation warn-and-fix path no longer has a
1342
+ // reachable input — fabric.config.json with a retired key fails before
1343
+ // doctor ever inspects it.
1344
+ // v2.0 / rc.2: `createLegacyV1ArtifactsCheck` removed alongside its
1345
+ // path-list constant. The visibility-only warning referenced v1.x
1346
+ // artifacts that are now archaeology. rc.4 owns v2 lint coverage; on a
1347
+ // clean v2 install nothing is lost since the check fired only when v1
1348
+ // artifacts remained.
1349
+ ];
1350
+ const fixableErrors = collectIssues(checks, "fixable_error");
1351
+ const manualErrors = collectIssues(checks, "manual_error");
1352
+ const warnings = collectIssues(checks, "warning");
1353
+ const infos = collectIssues(checks, "info");
1354
+ return {
1355
+ status: reduceStatus(checks.map((check) => check.status)),
1356
+ checks,
1357
+ fixable_errors: fixableErrors,
1358
+ manual_errors: manualErrors,
1359
+ warnings,
1360
+ infos,
1361
+ summary: {
1362
+ target: projectRoot,
1363
+ framework: {
1364
+ kind: framework.kind,
1365
+ version: framework.version,
1366
+ subkind: framework.subkind
1367
+ },
1368
+ entryPoints,
1369
+ metaRevision: meta.revision,
1370
+ computedMetaRevision: meta.computedRevision,
1371
+ ruleCount: meta.ruleCount,
1372
+ eventLedgerPath: eventLedger.path,
1373
+ fixableErrorCount: fixableErrors.length,
1374
+ manualErrorCount: manualErrors.length,
1375
+ warningCount: warnings.length,
1376
+ infoCount: infos.length,
1377
+ targetFiles: Object.fromEntries(
1378
+ TARGET_FILE_PATHS.map((path) => [path, existsSync4(join5(projectRoot, path))])
1379
+ )
1380
+ }
1199
1381
  };
1200
- await appendAuditLogEventLedgerEvents(projectRoot, [entry], {
1201
- correlation_id: input.correlation_id,
1202
- session_id: input.session_id
1203
- });
1204
- return entry;
1205
1382
  }
1206
- function normalizeAuditPath(projectRoot, value) {
1207
- const normalizedProjectRoot = resolve3(projectRoot);
1208
- const candidate = isAbsolute2(value) ? resolve3(value) : resolve3(normalizedProjectRoot, value);
1209
- const relativePath = relative3(normalizedProjectRoot, candidate);
1210
- if (relativePath.length > 0 && relativePath !== "." && !relativePath.startsWith("..") && !isAbsolute2(relativePath)) {
1211
- return posix.normalize(relativePath.split("\\").join("/"));
1383
+ async function runDoctorFix(target) {
1384
+ const projectRoot = normalizeTarget(target);
1385
+ const before = await runDoctorReport(projectRoot);
1386
+ const fixed = [];
1387
+ if (before.fixable_errors.some((issue) => issue.code === "knowledge_dir_missing")) {
1388
+ await ensureKnowledgeSubdirs(projectRoot);
1389
+ fixed.push(findIssue(before.fixable_errors, "knowledge_dir_missing"));
1212
1390
  }
1213
- return posix.normalize(value.replaceAll("\\", "/"));
1214
- }
1215
- async function appendAuditLogEventLedgerEvents(projectRoot, entries, metadata = {}) {
1216
- for (const entry of entries) {
1217
- if (entry.event === "get_rules") {
1218
- await appendEventLedgerEvent(projectRoot, {
1219
- event_type: "rule_context_planned",
1220
- ts: entry.ts,
1221
- target_paths: [entry.path],
1222
- required_stable_ids: metadata.rule_context?.required_stable_ids ?? [],
1223
- ai_selectable_stable_ids: metadata.rule_context?.ai_selectable_stable_ids ?? [],
1224
- final_stable_ids: metadata.rule_context?.final_stable_ids ?? [],
1225
- client_hash: entry.client_hash,
1226
- correlation_id: metadata.correlation_id,
1227
- session_id: metadata.session_id
1228
- });
1229
- continue;
1230
- }
1231
- if (entry.event === "rule_selection") {
1232
- await appendEventLedgerEvent(projectRoot, {
1233
- event_type: "rule_selection",
1234
- ts: entry.ts,
1235
- selection_token: entry.selection_token,
1236
- target_paths: entry.target_paths,
1237
- required_stable_ids: entry.required_stable_ids,
1238
- ai_selectable_stable_ids: entry.ai_selectable_stable_ids,
1239
- ai_selected_stable_ids: entry.ai_selected_stable_ids,
1240
- final_stable_ids: entry.final_stable_ids,
1241
- ai_selection_reasons: entry.ai_selection_reasons,
1242
- rejected_stable_ids: entry.rejected_stable_ids,
1243
- ignored_stable_ids: entry.ignored_stable_ids,
1244
- correlation_id: metadata.correlation_id,
1245
- session_id: metadata.session_id
1246
- });
1247
- continue;
1248
- }
1249
- await appendEventLedgerEvent(projectRoot, {
1250
- event_type: "edit_intent_checked",
1251
- ts: entry.ts,
1252
- path: entry.path,
1253
- compliant: entry.compliant,
1254
- intent: entry.intent,
1255
- ledger_entry_id: entry.ledger_entry_id,
1256
- ledger_source: "ai",
1257
- matched_rule_context_ts: entry.matched_get_rules_ts,
1258
- window_ms: entry.window_ms,
1259
- correlation_id: metadata.correlation_id,
1260
- session_id: metadata.session_id
1261
- });
1391
+ if (before.fixable_errors.some((issue) => issue.code === "event_ledger_missing")) {
1392
+ await ensureEventLedger(projectRoot);
1393
+ fixed.push(findIssue(before.fixable_errors, "event_ledger_missing"));
1262
1394
  }
1263
- }
1264
-
1265
- // src/services/get-rules.ts
1266
- import { readFile as readFile5 } from "fs/promises";
1267
- import { join as join6 } from "path";
1268
- import { minimatch } from "minimatch";
1269
- var PRIORITY_ORDER = {
1270
- high: 0,
1271
- medium: 1,
1272
- low: 2
1273
- };
1274
- async function getRules(projectRoot, input) {
1275
- const context = await loadGetRulesContext(projectRoot);
1276
- const stale = input.client_hash !== void 0 && input.client_hash !== context.meta.revision;
1277
- const matchedNodes = matchRuleNodes(context.meta, input.path);
1278
- const requiredStableIds = matchedNodes.filter((node) => node.level === "L2").map((node) => node.stable_id);
1279
- const aiSelectableStableIds = matchedNodes.filter((node) => node.level === "L1").map((node) => node.stable_id);
1280
- const rules = await resolveRulesForPath(projectRoot, context, input.path);
1281
- const result = {
1282
- revision_hash: context.meta.revision,
1283
- stale,
1284
- rules
1285
- };
1286
- try {
1287
- await appendGetRulesAuditEvent(projectRoot, {
1288
- path: input.path,
1289
- client_hash: input.client_hash,
1290
- required_stable_ids: requiredStableIds,
1291
- ai_selectable_stable_ids: aiSelectableStableIds,
1292
- final_stable_ids: [...requiredStableIds, ...aiSelectableStableIds],
1293
- correlation_id: input.correlation_id,
1294
- session_id: input.session_id
1295
- });
1296
- } catch {
1297
- }
1298
- return result;
1299
- }
1300
- async function loadGetRulesContext(projectRoot) {
1301
- const cached = contextCache.get("context", projectRoot);
1302
- if (cached !== void 0) {
1303
- return cached;
1304
- }
1305
- const meta = await readAgentsMeta(projectRoot);
1306
- const l0Content = await readFile5(join6(projectRoot, ".fabric", "bootstrap", "README.md"), "utf8");
1307
- const context = {
1308
- meta,
1309
- l0Content,
1310
- humanLockedNearby: []
1311
- };
1312
- contextCache.set("context", projectRoot, context);
1313
- return context;
1314
- }
1315
- async function resolveRulesForPath(projectRoot, context, path, options = {}) {
1316
- const matchedNodes = matchRuleNodes(context.meta, path);
1317
- const loaded = await loadMatchedRules(projectRoot, matchedNodes);
1318
- return buildRulesPayload(context, loaded, options);
1319
- }
1320
- function normalizeRulesPath(value) {
1321
- return value.replaceAll("\\", "/");
1322
- }
1323
- function matchRuleNodes(meta, path) {
1324
- const requestedPath = normalizeRulesPath(path);
1325
- return Object.entries(meta.nodes).filter(([, node]) => shouldLoadNodeForPath(requestedPath, node)).sort((left, right) => {
1326
- const [leftId, leftNode] = left;
1327
- const [rightId, rightNode] = right;
1328
- const priorityDelta = PRIORITY_ORDER[leftNode.priority] - PRIORITY_ORDER[rightNode.priority];
1329
- return priorityDelta !== 0 ? priorityDelta : leftId.localeCompare(rightId);
1330
- }).map(([nodeId, node]) => ({
1331
- node_id: nodeId,
1332
- level: classifyNode(nodeId, node),
1333
- stable_id: node.stable_id ?? nodeId,
1334
- identity_source: node.identity_source ?? "derived",
1335
- node
1336
- }));
1337
- }
1338
- async function loadMatchedRules(projectRoot, matchedNodes, fileContentCache = /* @__PURE__ */ new Map()) {
1339
- const rules = [];
1340
- const stubs = [];
1341
- for (const matchedNode of matchedNodes) {
1342
- if (matchedNode.level === null) {
1343
- continue;
1344
- }
1345
- if (matchedNode.node.activation?.tier === "description") {
1346
- stubs.push({
1347
- stable_id: matchedNode.stable_id,
1348
- identity_source: matchedNode.identity_source,
1349
- level: matchedNode.level,
1350
- path: matchedNode.node.file,
1351
- description: matchedNode.node.activation.description ?? ""
1352
- });
1353
- continue;
1354
- }
1355
- rules.push({
1356
- level: matchedNode.level,
1357
- stable_id: matchedNode.stable_id,
1358
- identity_source: matchedNode.identity_source,
1359
- entry: {
1360
- path: matchedNode.node.file,
1361
- content: await readRuleContent(projectRoot, matchedNode.node.file, fileContentCache)
1362
- }
1363
- });
1364
- }
1365
- return { rules, stubs };
1366
- }
1367
- function buildRulesPayload(context, loaded, options = {}) {
1368
- const { L1, L2 } = partitionRulesByLevel(loaded.rules, options.dedupeByPath ?? false);
1369
- return {
1370
- L0: context.l0Content,
1371
- L1,
1372
- L2,
1373
- human_locked_nearby: context.humanLockedNearby,
1374
- description_stubs: loaded.stubs.length > 0 ? dedupeDescriptionStubsByPath(loaded.stubs).map(toDescriptionStub) : void 0
1375
- };
1376
- }
1377
- function classifyNode(nodeId, node) {
1378
- if (nodeId.startsWith("L1/")) {
1379
- return "L1";
1380
- }
1381
- if (nodeId.startsWith("L2/")) {
1382
- return "L2";
1383
- }
1384
- return node.layer === "L0" ? null : node.layer;
1385
- }
1386
- function partitionRulesByLevel(loadedRules, dedupeByPath) {
1387
- const l1 = [];
1388
- const l2 = [];
1389
- for (const rule of loadedRules) {
1390
- if (rule.level === "L1") {
1391
- l1.push(rule.entry);
1392
- continue;
1393
- }
1394
- if (rule.level === "L2") {
1395
- l2.push(rule.entry);
1396
- }
1397
- }
1398
- return {
1399
- L1: dedupeByPath ? dedupeEntriesByPath(l1) : l1,
1400
- L2: dedupeByPath ? dedupeEntriesByPath(l2) : l2
1401
- };
1402
- }
1403
- function dedupeEntriesByPath(entries) {
1404
- const seenPaths = /* @__PURE__ */ new Set();
1405
- return entries.filter((entry) => {
1406
- if (seenPaths.has(entry.path)) {
1407
- return false;
1408
- }
1409
- seenPaths.add(entry.path);
1410
- return true;
1411
- });
1412
- }
1413
- function shouldLoadNodeForPath(requestedPath, node) {
1414
- switch (node.activation?.tier) {
1415
- case "always":
1416
- return true;
1417
- case "description":
1418
- return true;
1419
- case "path":
1420
- case void 0:
1421
- return minimatch(requestedPath, normalizeRulesPath(node.scope_glob), { dot: true });
1422
- }
1423
- }
1424
- function dedupeDescriptionStubsByPath(stubs) {
1425
- const seenPaths = /* @__PURE__ */ new Set();
1426
- return stubs.filter((stub) => {
1427
- if (seenPaths.has(stub.path)) {
1428
- return false;
1429
- }
1430
- seenPaths.add(stub.path);
1431
- return true;
1432
- });
1433
- }
1434
- function toDescriptionStub(stub) {
1435
- return {
1436
- path: stub.path,
1437
- description: stub.description
1438
- };
1439
- }
1440
- async function readRuleContent(projectRoot, file, fileContentCache) {
1441
- const cached = fileContentCache.get(file);
1442
- if (cached !== void 0) {
1443
- return await cached;
1444
- }
1445
- const pending = readFile5(join6(projectRoot, file), "utf8");
1446
- fileContentCache.set(file, pending);
1447
- return await pending;
1448
- }
1449
-
1450
- // src/services/plan-context.ts
1451
- var SELECTION_TOKEN_TTL_MS = 5 * 60 * 1e3;
1452
- var selectionTokenCache = /* @__PURE__ */ new Map();
1453
- async function planContext(projectRoot, input) {
1454
- const meta = await readAgentsMeta(projectRoot);
1455
- const stale = input.client_hash !== void 0 && input.client_hash !== meta.revision;
1456
- const uniquePaths = dedupePaths(input.paths);
1457
- const allDescriptions = buildDescriptionIndex(meta);
1458
- const entries = uniquePaths.map((path) => {
1459
- const profile = buildRequirementProfile(path, input);
1460
- const descriptionIndex = allDescriptions.filter((item) => shouldIncludeIndexItemForPath(item, meta, path));
1461
- const requiredStableIds2 = descriptionIndex.filter((item) => item.required).map((item) => item.stable_id);
1462
- const aiSelectableStableIds2 = descriptionIndex.filter((item) => item.selectable).map((item) => item.stable_id);
1463
- return {
1464
- path,
1465
- requirement_profile: profile,
1466
- description_index: descriptionIndex,
1467
- required_stable_ids: requiredStableIds2,
1468
- ai_selectable_stable_ids: aiSelectableStableIds2,
1469
- initial_selected_stable_ids: requiredStableIds2,
1470
- selection_policy: {
1471
- required_levels: ["L0", "L2"],
1472
- ai_selectable_levels: ["L1"],
1473
- final_fetch_rule: "required_stable_ids + ai_selected_l1_stable_ids"
1474
- }
1475
- };
1476
- });
1477
- const requiredStableIds = dedupeStableIds(entries.flatMap((entry) => entry.required_stable_ids));
1478
- const aiSelectableStableIds = dedupeStableIds(entries.flatMap((entry) => entry.ai_selectable_stable_ids));
1479
- const sharedDescriptionIndex = dedupeDescriptionIndex(entries.flatMap((entry) => entry.description_index));
1480
- const selectionToken = createSelectionToken(meta.revision, uniquePaths, requiredStableIds, aiSelectableStableIds);
1481
- const result = {
1482
- revision_hash: meta.revision,
1483
- stale,
1484
- selection_token: selectionToken,
1485
- entries,
1486
- shared: {
1487
- required_stable_ids: requiredStableIds,
1488
- ai_selectable_stable_ids: aiSelectableStableIds,
1489
- description_index: sharedDescriptionIndex,
1490
- preflight_diagnostics: buildPreflightDiagnostics(meta)
1491
- }
1492
- };
1493
- try {
1494
- await appendEventLedgerEvent(projectRoot, {
1495
- event_type: "rule_context_planned",
1496
- target_paths: uniquePaths,
1497
- required_stable_ids: requiredStableIds,
1498
- ai_selectable_stable_ids: aiSelectableStableIds,
1499
- final_stable_ids: requiredStableIds,
1500
- selection_token: selectionToken,
1501
- client_hash: input.client_hash,
1502
- intent: input.intent,
1503
- known_tech: input.known_tech,
1504
- diagnostics: result.shared.preflight_diagnostics,
1505
- correlation_id: input.correlation_id,
1506
- session_id: input.session_id
1507
- });
1508
- } catch {
1509
- }
1510
- return result;
1511
- }
1512
- function readSelectionToken(token, now = Date.now()) {
1513
- const state = selectionTokenCache.get(token);
1514
- if (state === void 0) {
1515
- return void 0;
1516
- }
1517
- if (state.expires_at <= now) {
1518
- selectionTokenCache.delete(token);
1519
- return void 0;
1520
- }
1521
- return state;
1522
- }
1523
- function createSelectionToken(revisionHash, targetPaths, requiredStableIds, aiSelectableStableIds, now = Date.now()) {
1524
- const token = `selection:${revisionHash}:${now.toString(36)}:${Math.random().toString(36).slice(2)}`;
1525
- selectionTokenCache.set(token, {
1526
- token,
1527
- revision_hash: revisionHash,
1528
- target_paths: targetPaths,
1529
- required_stable_ids: requiredStableIds,
1530
- ai_selectable_stable_ids: aiSelectableStableIds,
1531
- created_at: now,
1532
- expires_at: now + SELECTION_TOKEN_TTL_MS
1533
- });
1534
- return token;
1535
- }
1536
- function dedupePaths(paths) {
1537
- const seenPaths = /* @__PURE__ */ new Set();
1538
- return paths.flatMap((path) => {
1539
- const normalizedPath = normalizeRulesPath(path);
1540
- if (seenPaths.has(normalizedPath)) {
1541
- return [];
1542
- }
1543
- seenPaths.add(normalizedPath);
1544
- return [normalizedPath];
1545
- });
1546
- }
1547
- function buildRequirementProfile(path, input) {
1548
- const normalizedPath = normalizeRulesPath(path);
1549
- const extensionMatch = /(\.[^./\\]+)$/u.exec(normalizedPath);
1550
- const knownTech = dedupeStableIds([
1551
- ...input.known_tech ?? [],
1552
- ...extensionMatch?.[1] === ".ts" ? ["TypeScript"] : []
1553
- ]);
1554
- return {
1555
- target_path: normalizedPath,
1556
- path_segments: normalizedPath.split("/").filter(Boolean),
1557
- extension: extensionMatch?.[1] ?? "",
1558
- inferred_domain: inferDomains(normalizedPath),
1559
- known_tech: knownTech,
1560
- user_intent: input.intent ?? "",
1561
- intent_tokens: tokenizeIntent(input.intent ?? ""),
1562
- impact_hints: inferImpactHints(input.intent ?? ""),
1563
- detected_entities: input.detected_entities?.[normalizedPath] ?? input.detected_entities?.[path] ?? []
1564
- };
1565
- }
1566
- function buildDescriptionIndex(meta) {
1567
- return Object.entries(meta.nodes).flatMap(([nodeId, node]) => {
1568
- const level = node.level ?? node.layer;
1569
- const description = node.description ?? descriptionFromLegacyActivation(node.activation?.description);
1570
- if (description === void 0) {
1571
- return [];
1572
- }
1573
- return [{
1574
- stable_id: node.stable_id ?? nodeId,
1575
- level,
1576
- required: level === "L0" || level === "L2",
1577
- selectable: level === "L1",
1578
- description
1579
- }];
1580
- }).sort(compareDescriptionIndexItems);
1581
- }
1582
- function descriptionFromLegacyActivation(summary) {
1583
- if (summary === void 0) {
1584
- return void 0;
1585
- }
1586
- return {
1587
- summary,
1588
- intent_clues: [],
1589
- tech_stack: [],
1590
- impact: [],
1591
- must_read_if: summary
1592
- };
1593
- }
1594
- function shouldIncludeIndexItemForPath(item, meta, path) {
1595
- if (item.level === "L0" || item.level === "L1") {
1596
- return true;
1597
- }
1598
- const node = Object.values(meta.nodes).find((candidate) => candidate.stable_id === item.stable_id);
1599
- if (node === void 0) {
1600
- return false;
1601
- }
1602
- return node.scope_glob === path || minimatchSimple(path, node.scope_glob);
1603
- }
1604
- function minimatchSimple(path, glob) {
1605
- if (glob === "**") {
1606
- return true;
1607
- }
1608
- if (glob.endsWith("/**")) {
1609
- return path.startsWith(glob.slice(0, -3));
1610
- }
1611
- return path === glob;
1612
- }
1613
- function buildPreflightDiagnostics(meta) {
1614
- const missingDescriptionStableIds = Object.entries(meta.nodes).filter(([, node]) => node.description === void 0 && node.activation?.description === void 0).map(([nodeId, node]) => node.stable_id ?? nodeId).sort();
1615
- if (missingDescriptionStableIds.length === 0) {
1616
- return [];
1617
- }
1618
- return [{
1619
- code: "missing_description",
1620
- severity: "warn",
1621
- stable_ids: missingDescriptionStableIds,
1622
- message: `Resolved registry includes ${missingDescriptionStableIds.length} node(s) without structured descriptions.`
1623
- }];
1624
- }
1625
- function inferDomains(path) {
1626
- const domains = [];
1627
- if (path.includes("/ui/") || path.toLowerCase().includes("ui")) {
1628
- domains.push("UI");
1629
- }
1630
- if (path.includes("assets/scripts")) {
1631
- domains.push("Gameplay");
1632
- }
1633
- if (path.includes("resources") || path.includes("assets/resources")) {
1634
- domains.push("Asset");
1635
- }
1636
- return domains;
1637
- }
1638
- function tokenizeIntent(intent) {
1639
- const tokens = ["\u6027\u80FD", "\u4F18\u5316", "drawcall", "\u6E32\u67D3", "\u5361\u987F", "\u95EA\u70C1", "\u754C\u9762", "UI", "\u8D44\u6E90", "\u56FE\u96C6"].filter((token) => intent.toLowerCase().includes(token.toLowerCase()));
1640
- return dedupeStableIds(tokens);
1641
- }
1642
- function inferImpactHints(intent) {
1643
- return /性能|优化|drawcall|渲染|卡顿|闪烁/iu.test(intent) ? ["Performance"] : [];
1644
- }
1645
- function dedupeStableIds(stableIds) {
1646
- return Array.from(new Set(stableIds));
1647
- }
1648
- function dedupeDescriptionIndex(items) {
1649
- const seenStableIds = /* @__PURE__ */ new Set();
1650
- return items.filter((item) => {
1651
- if (seenStableIds.has(item.stable_id)) {
1652
- return false;
1653
- }
1654
- seenStableIds.add(item.stable_id);
1655
- return true;
1656
- });
1657
- }
1658
- function compareDescriptionIndexItems(left, right) {
1659
- const levelDelta = levelOrder(left.level) - levelOrder(right.level);
1660
- return levelDelta !== 0 ? levelDelta : left.stable_id.localeCompare(right.stable_id);
1661
- }
1662
- function levelOrder(level) {
1663
- switch (level) {
1664
- case "L0":
1665
- return 0;
1666
- case "L1":
1667
- return 1;
1668
- case "L2":
1669
- return 2;
1670
- }
1671
- }
1672
-
1673
- // src/services/rule-sections.ts
1674
- var RULE_SECTION_NAMES = [
1675
- "MISSION_STATEMENT",
1676
- "MANDATORY_INJECTION",
1677
- "BUSINESS_LOGIC_CHUNKS",
1678
- "CONTEXT_INFO"
1679
- ];
1680
- var PRIORITY_ORDER2 = {
1681
- high: 0,
1682
- medium: 1,
1683
- low: 2
1684
- };
1685
- function parseRuleSections(content) {
1686
- const sections = /* @__PURE__ */ new Map();
1687
- const lines = content.split(/\r?\n/u);
1688
- let activeSection;
1689
- let activeSectionDepth = 0;
1690
- let buffer = [];
1691
- const flush = () => {
1692
- if (activeSection === void 0) {
1693
- return;
1694
- }
1695
- const text = buffer.join("\n").trim();
1696
- if (text.length === 0) {
1697
- buffer = [];
1698
- return;
1699
- }
1700
- sections.set(activeSection, [...sections.get(activeSection) ?? [], text]);
1701
- buffer = [];
1702
- };
1703
- for (const line of lines) {
1704
- const heading = /^(#{2,6})\s+\[([A-Z_]+)\]\s*$/u.exec(line.trim());
1705
- if (heading !== null) {
1706
- flush();
1707
- activeSection = isRuleSectionName(heading[2]) ? heading[2] : void 0;
1708
- activeSectionDepth = activeSection === void 0 ? 0 : heading[1].length;
1709
- continue;
1710
- }
1711
- const ordinaryHeading = /^(#{1,6})\s+/u.exec(line.trim());
1712
- if (ordinaryHeading !== null) {
1713
- if (activeSection !== void 0 && ordinaryHeading[1].length > activeSectionDepth) {
1714
- buffer.push(line);
1715
- continue;
1716
- }
1717
- flush();
1718
- activeSection = void 0;
1719
- activeSectionDepth = 0;
1720
- continue;
1721
- }
1722
- if (activeSection !== void 0) {
1723
- buffer.push(line);
1724
- }
1725
- }
1726
- flush();
1727
- return new Map(
1728
- Array.from(sections.entries()).map(([section, values]) => [section, values.join("\n\n")])
1729
- );
1730
- }
1731
- async function getRuleSections(projectRoot, input) {
1732
- const token = readSelectionToken(input.selection_token);
1733
- if (token === void 0) {
1734
- throw new Error("selection_token is missing or expired");
1735
- }
1736
- validateAiSelections(token.ai_selectable_stable_ids, input.ai_selected_stable_ids, input.ai_selection_reasons);
1737
- const meta = await readAgentsMeta(projectRoot);
1738
- const selectedStableIds = [...token.required_stable_ids, ...input.ai_selected_stable_ids];
1739
- const selectedRules = sortRuleNodes(selectedStableIds.map((stableId) => findRuleNode(meta, stableId)));
1740
- const diagnostics = [];
1741
- const rules = [];
1742
- for (const rule of selectedRules) {
1743
- const content = await readFile6(join7(projectRoot, rule.path), "utf8");
1744
- const parsedSections = parseRuleSections(content);
1745
- const sections = {};
1746
- for (const section of input.sections) {
1747
- const sectionContent = parsedSections.get(section);
1748
- sections[section] = sectionContent ?? "";
1749
- if (sectionContent === void 0) {
1750
- diagnostics.push({
1751
- code: "missing_section",
1752
- severity: "warn",
1753
- stable_id: rule.stable_id,
1754
- section,
1755
- message: `Rule ${rule.stable_id} does not define section ${section}.`
1756
- });
1757
- }
1758
- }
1759
- rules.push({
1760
- stable_id: rule.stable_id,
1761
- level: rule.level,
1762
- path: rule.path,
1763
- sections
1764
- });
1765
- }
1766
- const result = {
1767
- revision_hash: meta.revision,
1768
- precedence: ["L2", "L1", "L0"],
1769
- selected_stable_ids: rules.map((rule) => rule.stable_id),
1770
- rules,
1771
- diagnostics
1772
- };
1773
- await appendRuleSelectionAuditEvent(projectRoot, {
1774
- path: token.target_paths[0] ?? "",
1775
- selection_token: input.selection_token,
1776
- target_paths: token.target_paths,
1777
- required_stable_ids: token.required_stable_ids,
1778
- ai_selectable_stable_ids: token.ai_selectable_stable_ids,
1779
- ai_selected_stable_ids: input.ai_selected_stable_ids,
1780
- final_stable_ids: result.selected_stable_ids,
1781
- ai_selection_reasons: pickSelectionReasons(input.ai_selected_stable_ids, input.ai_selection_reasons),
1782
- rejected_stable_ids: [],
1783
- ignored_stable_ids: [],
1784
- correlation_id: input.correlation_id,
1785
- session_id: input.session_id
1786
- });
1787
- try {
1788
- await appendEventLedgerEvent(projectRoot, {
1789
- event_type: "rule_sections_fetched",
1790
- selection_token: input.selection_token,
1791
- target_paths: token.target_paths,
1792
- requested_sections: input.sections,
1793
- final_stable_ids: result.selected_stable_ids,
1794
- ai_selected_stable_ids: input.ai_selected_stable_ids,
1795
- diagnostics,
1796
- correlation_id: input.correlation_id,
1797
- session_id: input.session_id
1798
- });
1799
- } catch {
1800
- }
1801
- return result;
1802
- }
1803
- function validateAiSelections(aiSelectableStableIds, aiSelectedStableIds, aiSelectionReasons) {
1804
- const selectable = new Set(aiSelectableStableIds);
1805
- for (const stableId of aiSelectedStableIds) {
1806
- if (!selectable.has(stableId)) {
1807
- throw new Error(`Invalid L1 rule selection: ${stableId}`);
1808
- }
1809
- if (aiSelectionReasons[stableId]?.trim() === "") {
1810
- throw new Error(`Missing AI selection reason for ${stableId}`);
1811
- }
1812
- if (aiSelectionReasons[stableId] === void 0) {
1813
- throw new Error(`Missing AI selection reason for ${stableId}`);
1814
- }
1815
- }
1816
- }
1817
- function findRuleNode(meta, stableId) {
1818
- for (const [nodeId, node] of Object.entries(meta.nodes)) {
1819
- const nodeStableId = node.stable_id ?? nodeId;
1820
- if (nodeStableId !== stableId) {
1821
- continue;
1822
- }
1823
- const level = node.level ?? node.layer;
1824
- return {
1825
- stable_id: nodeStableId,
1826
- level,
1827
- path: normalizeRulesPath(node.content_ref ?? node.file),
1828
- priority: node.priority,
1829
- node
1830
- };
1831
- }
1832
- throw new Error(`Selected rule is not present in agents.meta.json: ${stableId}`);
1833
- }
1834
- function sortRuleNodes(rules) {
1835
- return [...rules].sort((left, right) => {
1836
- const levelDelta = outputLevelOrder(left.level) - outputLevelOrder(right.level);
1837
- if (levelDelta !== 0) {
1838
- return levelDelta;
1839
- }
1840
- const priorityDelta = PRIORITY_ORDER2[left.priority] - PRIORITY_ORDER2[right.priority];
1841
- if (priorityDelta !== 0) {
1842
- return priorityDelta;
1843
- }
1844
- return left.stable_id.localeCompare(right.stable_id);
1845
- });
1846
- }
1847
- function outputLevelOrder(level) {
1848
- switch (level) {
1849
- case "L0":
1850
- return 0;
1851
- case "L1":
1852
- return 1;
1853
- case "L2":
1854
- return 2;
1855
- }
1856
- }
1857
- function isRuleSectionName(value) {
1858
- return RULE_SECTION_NAMES.includes(value);
1859
- }
1860
- function pickSelectionReasons(selectedStableIds, reasons) {
1861
- return Object.fromEntries(selectedStableIds.map((stableId) => [stableId, reasons[stableId] ?? ""]));
1862
- }
1863
-
1864
- // src/services/doctor.ts
1865
- import { atomicWriteJson as atomicWriteJson2, atomicWriteText as atomicWriteText3 } from "@fenglimg/fabric-shared/node/atomic-write";
1866
- import { buildBootstrapContent, FABRIC_BOOTSTRAP_PATH } from "@fenglimg/fabric-shared/node/bootstrap-guide";
1867
- var LEGACY_CLIENT_PATH_KEYS = ["windsurf", "rooCode", "geminiCLI"];
1868
- var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
1869
- var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
1870
- ".fabric",
1871
- ".git",
1872
- ".next",
1873
- ".turbo",
1874
- "Library",
1875
- "Temp",
1876
- "build",
1877
- "coverage",
1878
- "dist",
1879
- "node_modules"
1880
- ]);
1881
- var TARGET_FILE_PATHS = [
1882
- ".fabric/bootstrap/README.md",
1883
- ".fabric/INITIAL_TAXONOMY.md",
1884
- ".fabric/forensic.json",
1885
- ".fabric/init-context.json",
1886
- ".fabric/agents.meta.json",
1887
- ".fabric/rule-test.index.json",
1888
- ".fabric/events.jsonl"
1889
- ];
1890
- async function runDoctorReport(target) {
1891
- const projectRoot = normalizeTarget(target);
1892
- const framework = detectFramework(projectRoot);
1893
- const entryPoints = collectEntryPoints(projectRoot);
1894
- const [
1895
- forensic,
1896
- initContext,
1897
- meta,
1898
- eventLedger,
1899
- ruleSections,
1900
- ruleTestIndex
1901
- ] = await Promise.all([
1902
- inspectForensic(projectRoot),
1903
- inspectInitContext(projectRoot),
1904
- inspectMeta(projectRoot),
1905
- inspectEventLedger(projectRoot),
1906
- inspectRuleSections(projectRoot),
1907
- inspectRuleTestIndex(projectRoot)
1908
- ]);
1909
- const mcpConfigInWrongFile = inspectMcpConfigInWrongFile(projectRoot);
1910
- const metaManuallyDiverged = await inspectMetaManuallyDiverged(projectRoot);
1911
- const rulesDirUnindexed = inspectRulesDirUnindexed(projectRoot, meta);
1912
- const stableIdCollision = await inspectStableIdCollisions(projectRoot);
1913
- const claudeSkillLegacyPath = inspectClaudeSkillLegacyPath(projectRoot);
1914
- const claudeHookLegacyPath = inspectClaudeHookLegacyPath(projectRoot);
1915
- const preexistingRootFiles = inspectPreexistingRootFiles(projectRoot);
1916
- const legacyClientPaths = inspectLegacyClientPaths(projectRoot);
1917
- const taxonomyExists = existsSync4(join8(projectRoot, ".fabric", "INITIAL_TAXONOMY.md"));
1918
- const bootstrapExists = existsSync4(join8(projectRoot, ".fabric", "bootstrap", "README.md"));
1919
- const checks = [
1920
- createBootstrapCheck(bootstrapExists),
1921
- createTaxonomyCheck(taxonomyExists),
1922
- createForensicCheck(forensic, framework.kind, entryPoints.length),
1923
- createInitContextCheck(initContext),
1924
- createMetaCheck(meta),
1925
- createRuleContentRefCheck(meta),
1926
- createRuleSectionsCheck(ruleSections),
1927
- createRuleTestIndexCheck(ruleTestIndex),
1928
- createEventLedgerCheck(eventLedger),
1929
- createEventLedgerPartialWriteCheck(eventLedger),
1930
- createMcpConfigInWrongFileCheck(mcpConfigInWrongFile),
1931
- createMetaManuallyDivergedCheck(metaManuallyDiverged),
1932
- createRulesDirUnindexedCheck(rulesDirUnindexed),
1933
- createStableIdCollisionCheck(stableIdCollision),
1934
- createClaudeSkillLegacyPathCheck(claudeSkillLegacyPath),
1935
- createClaudeHookLegacyPathCheck(claudeHookLegacyPath),
1936
- createPreexistingRootFilesCheck(preexistingRootFiles),
1937
- createLegacyClientPathCheck(legacyClientPaths)
1938
- ];
1939
- const fixableErrors = collectIssues(checks, "fixable_error");
1940
- const manualErrors = collectIssues(checks, "manual_error");
1941
- const warnings = collectIssues(checks, "warning");
1942
- const infos = collectIssues(checks, "info");
1943
- return {
1944
- status: reduceStatus(checks.map((check) => check.status)),
1945
- checks,
1946
- fixable_errors: fixableErrors,
1947
- manual_errors: manualErrors,
1948
- warnings,
1949
- infos,
1950
- summary: {
1951
- target: projectRoot,
1952
- framework: {
1953
- kind: framework.kind,
1954
- version: framework.version,
1955
- subkind: framework.subkind
1956
- },
1957
- entryPoints,
1958
- metaRevision: meta.revision,
1959
- computedMetaRevision: meta.computedRevision,
1960
- ruleCount: meta.ruleCount,
1961
- eventLedgerPath: eventLedger.path,
1962
- fixableErrorCount: fixableErrors.length,
1963
- manualErrorCount: manualErrors.length,
1964
- warningCount: warnings.length,
1965
- infoCount: infos.length,
1966
- targetFiles: Object.fromEntries(
1967
- TARGET_FILE_PATHS.map((path) => [path, existsSync4(join8(projectRoot, path))])
1968
- )
1969
- }
1970
- };
1971
- }
1972
- async function runDoctorFix(target) {
1973
- const projectRoot = normalizeTarget(target);
1974
- const before = await runDoctorReport(projectRoot);
1975
- const fixed = [];
1976
- if (before.fixable_errors.some((issue) => issue.code === "bootstrap_missing")) {
1977
- await writeDefaultBootstrap(projectRoot);
1978
- fixed.push(findIssue(before.fixable_errors, "bootstrap_missing"));
1979
- }
1980
- if (before.fixable_errors.some((issue) => issue.code === "event_ledger_missing")) {
1981
- await ensureEventLedger(projectRoot);
1982
- fixed.push(findIssue(before.fixable_errors, "event_ledger_missing"));
1395
+ if (before.fixable_errors.some((issue) => issue.code === "counter_desync")) {
1396
+ await fixCounterDesync(projectRoot);
1397
+ fixed.push(findIssue(before.fixable_errors, "counter_desync"));
1398
+ contextCache.invalidate("meta_write", projectRoot);
1983
1399
  }
1984
1400
  if (before.fixable_errors.some(
1985
1401
  (issue) => [
@@ -1988,7 +1404,7 @@ async function runDoctorFix(target) {
1988
1404
  "rule_test_index_missing",
1989
1405
  "rule_test_index_stale",
1990
1406
  "content_ref_missing",
1991
- "rules_dir_unindexed"
1407
+ "knowledge_dir_unindexed"
1992
1408
  ].includes(issue.code)
1993
1409
  )) {
1994
1410
  await reconcileRules(projectRoot, { trigger: "doctor" });
@@ -1999,12 +1415,14 @@ async function runDoctorFix(target) {
1999
1415
  "rule_test_index_missing",
2000
1416
  "rule_test_index_stale",
2001
1417
  "content_ref_missing",
2002
- "rules_dir_unindexed"
1418
+ "knowledge_dir_unindexed"
2003
1419
  ].includes(candidate.code)
2004
1420
  )) {
2005
1421
  fixed.push(issue);
2006
1422
  }
2007
1423
  contextCache.invalidate("meta_write", projectRoot);
1424
+ await fixCounterDesync(projectRoot);
1425
+ contextCache.invalidate("meta_write", projectRoot);
2008
1426
  }
2009
1427
  if (before.fixable_errors.some((issue) => issue.code === "event_ledger_partial_write")) {
2010
1428
  const ledgerPath = getEventLedgerPath(projectRoot);
@@ -2021,18 +1439,6 @@ async function runDoctorFix(target) {
2021
1439
  await fixMcpConfigInWrongFile(projectRoot);
2022
1440
  fixed.push(findIssue(before.fixable_errors, "mcp_config_in_wrong_file"));
2023
1441
  }
2024
- if (before.fixable_errors.some((issue) => issue.code === "claude_skill_legacy_path")) {
2025
- await fixClaudeSkillLegacyPath(projectRoot);
2026
- fixed.push(findIssue(before.fixable_errors, "claude_skill_legacy_path"));
2027
- }
2028
- if (before.fixable_errors.some((issue) => issue.code === "claude_hook_legacy_path")) {
2029
- await fixClaudeHookLegacyPath(projectRoot);
2030
- fixed.push(findIssue(before.fixable_errors, "claude_hook_legacy_path"));
2031
- }
2032
- if (before.warnings.some((issue) => issue.code === "legacy_client_path_present")) {
2033
- await fixLegacyClientPaths(projectRoot);
2034
- fixed.push(findIssue(before.warnings, "legacy_client_path_present"));
2035
- }
2036
1442
  const report = await runDoctorReport(projectRoot);
2037
1443
  return {
2038
1444
  changed: fixed.length > 0,
@@ -2044,9 +1450,9 @@ async function runDoctorFix(target) {
2044
1450
  };
2045
1451
  }
2046
1452
  async function inspectForensic(projectRoot) {
2047
- const path = join8(projectRoot, ".fabric", "forensic.json");
1453
+ const path = join5(projectRoot, ".fabric", "forensic.json");
2048
1454
  try {
2049
- const parsed = forensicReportSchema.parse(JSON.parse(await readFile7(path, "utf8")));
1455
+ const parsed = forensicReportSchema.parse(JSON.parse(await readFile5(path, "utf8")));
2050
1456
  return { present: true, valid: true, report: parsed };
2051
1457
  } catch (error) {
2052
1458
  if (isMissingFileError(error)) {
@@ -2055,20 +1461,8 @@ async function inspectForensic(projectRoot) {
2055
1461
  return { present: true, valid: false, report: null, error: error instanceof Error ? error.message : String(error) };
2056
1462
  }
2057
1463
  }
2058
- async function inspectInitContext(projectRoot) {
2059
- const path = join8(projectRoot, ".fabric", "init-context.json");
2060
- try {
2061
- JSON.parse(await readFile7(path, "utf8"));
2062
- return { exists: true, validJson: true };
2063
- } catch (error) {
2064
- if (isMissingFileError(error)) {
2065
- return { exists: false, validJson: false, error: ".fabric/init-context.json is missing." };
2066
- }
2067
- return { exists: true, validJson: false, error: error instanceof Error ? error.message : String(error) };
2068
- }
2069
- }
2070
1464
  function inspectMcpConfigInWrongFile(projectRoot) {
2071
- const settingsPath = join8(projectRoot, ".claude", "settings.json");
1465
+ const settingsPath = join5(projectRoot, ".claude", "settings.json");
2072
1466
  if (!existsSync4(settingsPath)) {
2073
1467
  return { hasWrongEntry: false, settingsPath };
2074
1468
  }
@@ -2089,10 +1483,10 @@ function inspectMcpConfigInWrongFile(projectRoot) {
2089
1483
  }
2090
1484
  }
2091
1485
  async function inspectMeta(projectRoot) {
2092
- const metaPath = join8(projectRoot, ".fabric", "agents.meta.json");
1486
+ const metaPath = join5(projectRoot, ".fabric", "agents.meta.json");
2093
1487
  const built = await tryBuildRuleMeta(projectRoot);
2094
1488
  try {
2095
- const raw = await readFile7(metaPath, "utf8");
1489
+ const raw = await readFile5(metaPath, "utf8");
2096
1490
  const meta = agentsMetaSchema4.parse(JSON.parse(raw));
2097
1491
  const contentRefIssues = inspectContentRefs(projectRoot, meta);
2098
1492
  const changed = built === null ? false : built.changed;
@@ -2102,7 +1496,10 @@ async function inspectMeta(projectRoot) {
2102
1496
  meta,
2103
1497
  revision: meta.revision,
2104
1498
  computedRevision: built?.meta.revision ?? null,
2105
- ruleCount: Object.values(meta.nodes).filter((node) => (node.content_ref ?? node.file).startsWith(".fabric/rules/")).length,
1499
+ ruleCount: Object.values(meta.nodes).filter((node) => {
1500
+ const ref = node.content_ref ?? node.file;
1501
+ return ref.startsWith(".fabric/knowledge/") || ref.startsWith("~/.fabric/knowledge/");
1502
+ }).length,
2106
1503
  missingContentRefs: contentRefIssues.missing,
2107
1504
  invalidContentRefs: contentRefIssues.invalid,
2108
1505
  stale: changed || built !== null && meta.revision !== built.meta.revision,
@@ -2150,17 +1547,16 @@ function inspectContentRefs(projectRoot, meta) {
2150
1547
  const invalid = [];
2151
1548
  for (const node of Object.values(meta.nodes)) {
2152
1549
  const contentRef = normalizePath(node.content_ref ?? node.file);
2153
- if (contentRef === ".fabric/bootstrap/README.md") {
2154
- if (!existsSync4(join8(projectRoot, contentRef))) {
2155
- missing.push(contentRef);
2156
- }
1550
+ const isPersonalKnowledge = contentRef.startsWith("~/.fabric/knowledge/");
1551
+ const isTeamKnowledge = contentRef.startsWith(".fabric/knowledge/");
1552
+ if (!isPersonalKnowledge && !isTeamKnowledge) {
1553
+ invalid.push(contentRef);
2157
1554
  continue;
2158
1555
  }
2159
- if (!contentRef.startsWith(".fabric/rules/")) {
2160
- invalid.push(contentRef);
1556
+ if (isPersonalKnowledge) {
2161
1557
  continue;
2162
1558
  }
2163
- if (!existsSync4(join8(projectRoot, contentRef))) {
1559
+ if (!existsSync4(join5(projectRoot, contentRef))) {
2164
1560
  missing.push(contentRef);
2165
1561
  }
2166
1562
  }
@@ -2175,7 +1571,7 @@ async function inspectEventLedger(projectRoot) {
2175
1571
  try {
2176
1572
  await access(path, constants.W_OK);
2177
1573
  const { warnings } = await readEventLedger(projectRoot);
2178
- const raw = await readFile7(path, "utf8");
1574
+ const raw = await readFile5(path, "utf8");
2179
1575
  const invalidLine = raw.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).find((line) => !isValidJsonLine(line));
2180
1576
  const partialWarning = warnings.find((w) => w.kind === "partial_write_at_tail");
2181
1577
  return {
@@ -2201,29 +1597,11 @@ async function inspectEventLedger(projectRoot) {
2201
1597
  };
2202
1598
  }
2203
1599
  }
2204
- async function inspectRuleSections(projectRoot) {
2205
- const invalidFiles = [];
2206
- const files = findRuleFiles2(projectRoot);
2207
- for (const file of files) {
2208
- try {
2209
- parseRuleSections(await readFile7(join8(projectRoot, file), "utf8"));
2210
- } catch (error) {
2211
- invalidFiles.push({
2212
- file,
2213
- reason: error instanceof Error ? error.message : String(error)
2214
- });
2215
- }
2216
- }
2217
- return {
2218
- checkedCount: files.length,
2219
- invalidFiles
2220
- };
2221
- }
2222
1600
  async function inspectRuleTestIndex(projectRoot) {
2223
- const path = join8(projectRoot, ".fabric", "rule-test.index.json");
1601
+ const path = join5(projectRoot, ".fabric", "rule-test.index.json");
2224
1602
  const built = await tryBuildRuleMeta(projectRoot);
2225
1603
  try {
2226
- const index = ruleTestIndexSchema2.parse(JSON.parse(await readFile7(path, "utf8")));
1604
+ const index = ruleTestIndexSchema2.parse(JSON.parse(await readFile5(path, "utf8")));
2227
1605
  return {
2228
1606
  present: true,
2229
1607
  valid: true,
@@ -2242,17 +1620,56 @@ async function inspectRuleTestIndex(projectRoot) {
2242
1620
  };
2243
1621
  }
2244
1622
  }
2245
- function createBootstrapCheck(exists) {
2246
- if (!exists) {
2247
- return issueCheck("Bootstrap README", "error", "fixable_error", "bootstrap_missing", ".fabric/bootstrap/README.md is missing.", "Run `fab doctor --fix` to generate the bootstrap guide.");
1623
+ function inspectBootstrapAnchor(projectRoot) {
1624
+ return {
1625
+ hasAgentsMd: existsSync4(join5(projectRoot, "AGENTS.md")),
1626
+ hasClaudeMd: existsSync4(join5(projectRoot, "CLAUDE.md"))
1627
+ };
1628
+ }
1629
+ function createBootstrapAnchorCheck(inspection) {
1630
+ if (!inspection.hasAgentsMd && !inspection.hasClaudeMd) {
1631
+ return issueCheck(
1632
+ "Bootstrap anchor",
1633
+ "error",
1634
+ "fixable_error",
1635
+ "bootstrap_anchor_missing",
1636
+ "Neither AGENTS.md nor CLAUDE.md exists at the repo root. Fabric requires a bootstrap anchor file at the project root.",
1637
+ "Run `fabric init` to generate the AGENTS.md / CLAUDE.md bootstrap anchor at the repo root."
1638
+ );
2248
1639
  }
2249
- return okCheck("Bootstrap README", ".fabric/bootstrap/README.md exists.");
1640
+ const present = [
1641
+ inspection.hasAgentsMd ? "AGENTS.md" : null,
1642
+ inspection.hasClaudeMd ? "CLAUDE.md" : null
1643
+ ].filter((entry) => entry !== null).join(", ");
1644
+ return okCheck("Bootstrap anchor", `Bootstrap anchor present at repo root: ${present}.`);
2250
1645
  }
2251
- function createTaxonomyCheck(exists) {
2252
- if (!exists) {
2253
- return issueCheck("Initial taxonomy", "error", "manual_error", "taxonomy_missing", ".fabric/INITIAL_TAXONOMY.md is missing.", "Run `fab init` to regenerate project scaffolding including INITIAL_TAXONOMY.md.");
1646
+ function inspectKnowledgeDirMissing(projectRoot) {
1647
+ const knowledgeRoot = join5(projectRoot, ".fabric", "knowledge");
1648
+ const missingSubdirs = [];
1649
+ for (const sub of KNOWLEDGE_SUBDIRS2) {
1650
+ const path = join5(knowledgeRoot, sub);
1651
+ if (!existsSync4(path)) {
1652
+ missingSubdirs.push(`.fabric/knowledge/${sub}`);
1653
+ }
1654
+ }
1655
+ return { missingSubdirs };
1656
+ }
1657
+ function createKnowledgeDirMissingCheck(inspection) {
1658
+ if (inspection.missingSubdirs.length > 0) {
1659
+ const list = inspection.missingSubdirs.join(", ");
1660
+ return issueCheck(
1661
+ "Knowledge layout",
1662
+ "error",
1663
+ "fixable_error",
1664
+ "knowledge_dir_missing",
1665
+ `${inspection.missingSubdirs.length} required knowledge subdir${inspection.missingSubdirs.length === 1 ? " is" : "s are"} missing: ${list}.`,
1666
+ "Run `fab doctor --fix` to create the missing .fabric/knowledge/* subdirectories."
1667
+ );
2254
1668
  }
2255
- return okCheck("Initial taxonomy", ".fabric/INITIAL_TAXONOMY.md exists.");
1669
+ return okCheck(
1670
+ "Knowledge layout",
1671
+ `All ${KNOWLEDGE_SUBDIRS2.length} required .fabric/knowledge/* subdirectories exist.`
1672
+ );
2256
1673
  }
2257
1674
  function createForensicCheck(forensic, frameworkKind, entryPointCount) {
2258
1675
  if (!forensic.present) {
@@ -2270,18 +1687,9 @@ function createForensicCheck(forensic, frameworkKind, entryPointCount) {
2270
1687
  }
2271
1688
  return okCheck("Scan evidence", `.fabric/forensic.json is valid for ${forensic.report?.framework.kind ?? "unknown"}.`);
2272
1689
  }
2273
- function createInitContextCheck(initContext) {
2274
- if (!initContext.exists) {
2275
- return issueCheck("Init context", "error", "manual_error", "init_context_missing", initContext.error ?? ".fabric/init-context.json is missing.", "Run the fabric-init skill in Claude Code or Codex CLI to complete initialization. See docs/migration-1.8.md FAQ.");
2276
- }
2277
- if (!initContext.validJson) {
2278
- return issueCheck("Init context", "error", "manual_error", "init_context_invalid", initContext.error ?? ".fabric/init-context.json is invalid.", "Delete .fabric/init-context.json and run `fab init` to regenerate it.");
2279
- }
2280
- return okCheck("Init context", ".fabric/init-context.json is valid JSON.");
2281
- }
2282
1690
  function createMetaCheck(meta) {
2283
1691
  if (!meta.present) {
2284
- return issueCheck("Agents metadata", "error", "fixable_error", "agents_meta_missing", ".fabric/agents.meta.json is missing.", "Run `fab doctor --fix` to rebuild agents.meta.json from .fabric/rules/.");
1692
+ return issueCheck("Agents metadata", "error", "fixable_error", "agents_meta_missing", ".fabric/agents.meta.json is missing.", "Run `fab doctor --fix` to rebuild agents.meta.json from .fabric/knowledge/.");
2285
1693
  }
2286
1694
  if (!meta.valid) {
2287
1695
  return issueCheck("Agents metadata", "error", "manual_error", "agents_meta_invalid", meta.readError ?? ".fabric/agents.meta.json is invalid.", "Delete .fabric/agents.meta.json and run `fab doctor --fix` to regenerate it.");
@@ -2292,11 +1700,11 @@ function createMetaCheck(meta) {
2292
1700
  "error",
2293
1701
  "fixable_error",
2294
1702
  "agents_meta_stale",
2295
- `.fabric/agents.meta.json revision ${meta.revision} does not match .fabric/rules derived revision ${meta.computedRevision ?? "<unknown>"}.`,
2296
- "Run `fab doctor --fix` to reconcile agents.meta.json with the current rule files."
1703
+ `.fabric/agents.meta.json revision ${meta.revision} does not match .fabric/knowledge derived revision ${meta.computedRevision ?? "<unknown>"}.`,
1704
+ "Run `fab doctor --fix` to reconcile agents.meta.json with the current knowledge files."
2297
1705
  );
2298
1706
  }
2299
- return okCheck("Agents metadata", `.fabric/agents.meta.json revision ${meta.revision} is aligned with .fabric/rules.`);
1707
+ return okCheck("Agents metadata", `.fabric/agents.meta.json revision ${meta.revision} is aligned with .fabric/knowledge.`);
2300
1708
  }
2301
1709
  function createRuleContentRefCheck(meta) {
2302
1710
  if (!meta.valid) {
@@ -2308,8 +1716,8 @@ function createRuleContentRefCheck(meta) {
2308
1716
  "error",
2309
1717
  "manual_error",
2310
1718
  "content_ref_outside_rules",
2311
- `${meta.invalidContentRefs.length} content_ref entr${meta.invalidContentRefs.length === 1 ? "y is" : "ies are"} outside .fabric/rules.`,
2312
- "Edit agents.meta.json to ensure all content_ref values point inside .fabric/rules/."
1719
+ `${meta.invalidContentRefs.length} content_ref entr${meta.invalidContentRefs.length === 1 ? "y is" : "ies are"} outside .fabric/knowledge.`,
1720
+ "Edit agents.meta.json to ensure all content_ref values point inside .fabric/knowledge/{type}/ (team) or ~/.fabric/knowledge/{type}/ (personal)."
2313
1721
  );
2314
1722
  }
2315
1723
  if (meta.missingContentRefs.length > 0) {
@@ -2319,23 +1727,10 @@ function createRuleContentRefCheck(meta) {
2319
1727
  "fixable_error",
2320
1728
  "content_ref_missing",
2321
1729
  `${meta.missingContentRefs.length} content_ref target${meta.missingContentRefs.length === 1 ? "" : "s"} are missing. Run \`fab doctor --fix\` to reconcile.`,
2322
- "Run `fab doctor --fix` to reconcile agents.meta.json with the files present in .fabric/rules/."
2323
- );
2324
- }
2325
- return okCheck("Rule content refs", "All content_ref entries resolve to .fabric/rules files or bootstrap README.");
2326
- }
2327
- function createRuleSectionsCheck(snapshot) {
2328
- if (snapshot.invalidFiles.length > 0) {
2329
- return issueCheck(
2330
- "Rule sections",
2331
- "error",
2332
- "manual_error",
2333
- "rule_sections_invalid",
2334
- `${snapshot.invalidFiles.length} rule file${snapshot.invalidFiles.length === 1 ? "" : "s"} could not be parsed.`,
2335
- "Edit the rule file(s) to fix the section structure, then re-run `fab doctor`."
1730
+ "Run `fab doctor --fix` to reconcile agents.meta.json with the files present in .fabric/knowledge/."
2336
1731
  );
2337
1732
  }
2338
- return okCheck("Rule sections", `${snapshot.checkedCount} .fabric/rules file${snapshot.checkedCount === 1 ? "" : "s"} parsed.`);
1733
+ return okCheck("Rule content refs", "All content_ref entries resolve to .fabric/knowledge files.");
2339
1734
  }
2340
1735
  function createRuleTestIndexCheck(index) {
2341
1736
  if (!index.present) {
@@ -2419,13 +1814,13 @@ function findIssue(issues, code) {
2419
1814
  };
2420
1815
  }
2421
1816
  async function inspectMetaManuallyDiverged(projectRoot) {
2422
- const metaPath = join8(projectRoot, ".fabric", "agents.meta.json");
1817
+ const metaPath = join5(projectRoot, ".fabric", "agents.meta.json");
2423
1818
  if (!existsSync4(metaPath)) {
2424
1819
  return { extraMetaEntries: [], hashMismatchEntries: [], readable: false };
2425
1820
  }
2426
1821
  let meta;
2427
1822
  try {
2428
- const raw = await readFile7(metaPath, "utf8");
1823
+ const raw = await readFile5(metaPath, "utf8");
2429
1824
  meta = agentsMetaSchema4.parse(JSON.parse(raw));
2430
1825
  } catch (error) {
2431
1826
  return {
@@ -2439,7 +1834,7 @@ async function inspectMetaManuallyDiverged(projectRoot) {
2439
1834
  const hashMismatchEntries = [];
2440
1835
  for (const node of Object.values(meta.nodes)) {
2441
1836
  const contentRef = node.content_ref ?? node.file;
2442
- const absPath = join8(projectRoot, contentRef);
1837
+ const absPath = join5(projectRoot, contentRef);
2443
1838
  if (!existsSync4(absPath)) {
2444
1839
  extraMetaEntries.push(contentRef);
2445
1840
  continue;
@@ -2456,87 +1851,89 @@ async function inspectMetaManuallyDiverged(projectRoot) {
2456
1851
  }
2457
1852
  return { extraMetaEntries, hashMismatchEntries, readable: true };
2458
1853
  }
2459
- function inspectRulesDirUnindexed(projectRoot, meta) {
2460
- const rulesDir = join8(projectRoot, ".fabric", "rules");
2461
- if (!existsSync4(rulesDir)) {
1854
+ function inspectKnowledgeDirUnindexed(projectRoot, meta) {
1855
+ const physicalMdFiles = /* @__PURE__ */ new Set();
1856
+ collectMdFilesUnder(physicalMdFiles, projectRoot, join5(projectRoot, ".fabric", "knowledge"), ".fabric/knowledge");
1857
+ if (physicalMdFiles.size === 0) {
2462
1858
  return { unindexedFiles: [] };
2463
1859
  }
2464
- const physicalMdFiles = /* @__PURE__ */ new Set();
2465
- const stack = [rulesDir];
1860
+ const indexedRefs = /* @__PURE__ */ new Set();
1861
+ if (meta.valid && meta.meta !== null) {
1862
+ for (const node of Object.values(meta.meta.nodes)) {
1863
+ const ref = normalizePath(node.content_ref ?? node.file);
1864
+ indexedRefs.add(ref);
1865
+ }
1866
+ }
1867
+ const unindexedFiles = [...physicalMdFiles].filter((f) => !indexedRefs.has(f)).sort();
1868
+ return { unindexedFiles };
1869
+ }
1870
+ function collectMdFilesUnder(out, projectRoot, rootDir, relPrefix) {
1871
+ if (!existsSync4(rootDir)) {
1872
+ return;
1873
+ }
1874
+ const stack = [rootDir];
2466
1875
  while (stack.length > 0) {
2467
1876
  const dir = stack.pop();
2468
1877
  if (dir === void 0) {
2469
1878
  continue;
2470
1879
  }
2471
1880
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
2472
- const abs = join8(dir, entry.name);
1881
+ const abs = join5(dir, entry.name);
2473
1882
  if (entry.isDirectory()) {
2474
1883
  stack.push(abs);
2475
1884
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
2476
- const rel = posix2.join(".fabric/rules", abs.slice(rulesDir.length + 1).replace(/\\/gu, "/"));
2477
- physicalMdFiles.add(rel);
1885
+ const rel = posix.join(relPrefix, abs.slice(rootDir.length + 1).replace(/\\/gu, "/"));
1886
+ out.add(rel);
2478
1887
  }
2479
1888
  }
2480
1889
  }
2481
- const indexedRefs = /* @__PURE__ */ new Set();
2482
- if (meta.valid && meta.meta !== null) {
2483
- for (const node of Object.values(meta.meta.nodes)) {
2484
- const ref = normalizePath(node.content_ref ?? node.file);
2485
- indexedRefs.add(ref);
2486
- }
2487
- }
2488
- const unindexedFiles = [...physicalMdFiles].filter((f) => !indexedRefs.has(f)).sort();
2489
- return { unindexedFiles };
2490
1890
  }
2491
- function createRulesDirUnindexedCheck(inspection) {
1891
+ function createKnowledgeDirUnindexedCheck(inspection) {
2492
1892
  if (inspection.unindexedFiles.length > 0) {
2493
1893
  return issueCheck(
2494
- "Rules dir unindexed",
1894
+ "Knowledge dir unindexed",
2495
1895
  "error",
2496
1896
  "fixable_error",
2497
- "rules_dir_unindexed",
2498
- `${inspection.unindexedFiles.length} .md file${inspection.unindexedFiles.length === 1 ? "" : "s"} in .fabric/rules/ not indexed in agents.meta.json. Run \`fab doctor --fix\` to index the missing rule files.`,
2499
- "Run `fab doctor --fix` to index the missing rule files."
1897
+ "knowledge_dir_unindexed",
1898
+ `${inspection.unindexedFiles.length} .md file${inspection.unindexedFiles.length === 1 ? "" : "s"} in .fabric/knowledge/ not indexed in agents.meta.json. Run \`fab doctor --fix\` to index the missing knowledge files.`,
1899
+ "Run `fab doctor --fix` to index the missing knowledge files."
2500
1900
  );
2501
1901
  }
2502
- return okCheck("Rules dir unindexed", "All .fabric/rules/ .md files are indexed in agents.meta.json.");
1902
+ return okCheck("Knowledge dir unindexed", "All .fabric/knowledge/ .md files are indexed in agents.meta.json.");
2503
1903
  }
2504
1904
  async function inspectStableIdCollisions(projectRoot) {
2505
- const rulesDir = join8(projectRoot, ".fabric", "rules");
2506
- if (!existsSync4(rulesDir)) {
2507
- return { collisions: [] };
2508
- }
2509
- const mdFiles = [];
2510
- const stack = [rulesDir];
2511
- while (stack.length > 0) {
2512
- const dir = stack.pop();
2513
- if (dir === void 0) {
2514
- continue;
2515
- }
2516
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
2517
- const abs = join8(dir, entry.name);
2518
- if (entry.isDirectory()) {
2519
- stack.push(abs);
2520
- } else if (entry.isFile() && entry.name.endsWith(".md")) {
2521
- mdFiles.push(abs);
1905
+ const found = [];
1906
+ const knowledgeDir = join5(projectRoot, ".fabric", "knowledge");
1907
+ if (existsSync4(knowledgeDir)) {
1908
+ const stack = [knowledgeDir];
1909
+ while (stack.length > 0) {
1910
+ const dir = stack.pop();
1911
+ if (dir === void 0) {
1912
+ continue;
1913
+ }
1914
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
1915
+ const abs = join5(dir, entry.name);
1916
+ if (entry.isDirectory()) {
1917
+ stack.push(abs);
1918
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
1919
+ let source;
1920
+ try {
1921
+ source = await readFile5(abs, "utf8");
1922
+ } catch {
1923
+ continue;
1924
+ }
1925
+ const id = extractKnowledgeFrontmatterId(source);
1926
+ if (id === null) {
1927
+ continue;
1928
+ }
1929
+ const relPath = posix.join(".fabric/knowledge", abs.slice(knowledgeDir.length + 1).replace(/\\/gu, "/"));
1930
+ found.push({ stableId: id, relPath });
1931
+ }
2522
1932
  }
2523
1933
  }
2524
1934
  }
2525
1935
  const stableIdToFiles = /* @__PURE__ */ new Map();
2526
- const DECLARED_ID_PATTERN = /^(?:\uFEFF)?(?:---\r?\n[\s\S]*?\r?\n---\s*(?:\r?\n|$))?<!--\s*fab:rule-id\s+([A-Za-z0-9][A-Za-z0-9/_-]*)\s*-->\s*(?:\r?\n|$)/u;
2527
- for (const absPath of mdFiles) {
2528
- let source;
2529
- try {
2530
- source = await readFile7(absPath, "utf8");
2531
- } catch {
2532
- continue;
2533
- }
2534
- const match = DECLARED_ID_PATTERN.exec(source);
2535
- if (match === null) {
2536
- continue;
2537
- }
2538
- const stableId = match[1];
2539
- const relPath = posix2.join(".fabric/rules", absPath.slice(rulesDir.length + 1).replace(/\\/gu, "/"));
1936
+ for (const { stableId, relPath } of found) {
2540
1937
  const existing = stableIdToFiles.get(stableId) ?? [];
2541
1938
  existing.push(relPath);
2542
1939
  stableIdToFiles.set(stableId, existing);
@@ -2549,6 +1946,85 @@ async function inspectStableIdCollisions(projectRoot) {
2549
1946
  }
2550
1947
  return { collisions: collisions.sort((a, b) => a.stable_id.localeCompare(b.stable_id)) };
2551
1948
  }
1949
+ function extractKnowledgeFrontmatterId(source) {
1950
+ const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
1951
+ const fm = FM_PATTERN.exec(source);
1952
+ if (fm === null) {
1953
+ return null;
1954
+ }
1955
+ const block = fm[1];
1956
+ const ID_LINE = /^id:\s*("?)(K[PT]-(?:MOD|DEC|GLD|PIT|PRO)-\d{4,})\1\s*$/mu;
1957
+ const idMatch = ID_LINE.exec(block);
1958
+ return idMatch === null ? null : idMatch[2];
1959
+ }
1960
+ function inspectCounterDesync(meta) {
1961
+ if (!meta.valid || meta.meta === null) {
1962
+ return { desyncs: [], correctedCounters: null };
1963
+ }
1964
+ const current = AgentsMetaCountersSchema.parse(meta.meta.counters ?? void 0);
1965
+ const observed = {
1966
+ KP: { MOD: 0, DEC: 0, GLD: 0, PIT: 0, PRO: 0 },
1967
+ KT: { MOD: 0, DEC: 0, GLD: 0, PIT: 0, PRO: 0 }
1968
+ };
1969
+ for (const node of Object.values(meta.meta.nodes)) {
1970
+ const id = node.stable_id;
1971
+ if (id === void 0) {
1972
+ continue;
1973
+ }
1974
+ const parsed = parseKnowledgeId2(id);
1975
+ if (parsed === null) {
1976
+ continue;
1977
+ }
1978
+ const layer = parsed.layer === "personal" ? "KP" : "KT";
1979
+ const typeCode = [
1980
+ ["model", "MOD"],
1981
+ ["decision", "DEC"],
1982
+ ["guideline", "GLD"],
1983
+ ["pitfall", "PIT"],
1984
+ ["process", "PRO"]
1985
+ ].find(([t]) => t === parsed.type)?.[1];
1986
+ if (typeCode === void 0) {
1987
+ continue;
1988
+ }
1989
+ if (parsed.counter > observed[layer][typeCode]) {
1990
+ observed[layer][typeCode] = parsed.counter;
1991
+ }
1992
+ }
1993
+ const desyncs = [];
1994
+ const corrected = {
1995
+ KP: { ...current.KP },
1996
+ KT: { ...current.KT }
1997
+ };
1998
+ for (const layer of ["KP", "KT"]) {
1999
+ for (const code of COUNTER_TYPE_CODES) {
2000
+ const obs = observed[layer][code];
2001
+ const cur = current[layer][code];
2002
+ if (obs > cur) {
2003
+ desyncs.push({ layer, type: code, observed: obs, current: cur });
2004
+ corrected[layer][code] = obs;
2005
+ }
2006
+ }
2007
+ }
2008
+ return {
2009
+ desyncs,
2010
+ correctedCounters: desyncs.length === 0 ? null : corrected
2011
+ };
2012
+ }
2013
+ function createCounterDesyncCheck(inspection) {
2014
+ if (inspection.desyncs.length > 0) {
2015
+ const first = inspection.desyncs[0];
2016
+ const detail = `counters.${first.layer}.${first.type} = ${first.current} but observed K${first.layer === "KP" ? "P" : "T"}-${first.type}-${String(first.observed).padStart(4, "0")}`;
2017
+ return issueCheck(
2018
+ "Knowledge counter desync",
2019
+ "error",
2020
+ "fixable_error",
2021
+ "counter_desync",
2022
+ `${inspection.desyncs.length} knowledge counter${inspection.desyncs.length === 1 ? "" : "s"} desynced from observed stable_ids. ${detail}. Run \`fab doctor --fix\` to bump counters.`,
2023
+ "Run `fab doctor --fix` to bump agents.meta.json counters to the maximum observed counter value."
2024
+ );
2025
+ }
2026
+ return okCheck("Knowledge counter desync", "agents.meta.json counters envelope is consistent with observed stable_ids.");
2027
+ }
2552
2028
  function createStableIdCollisionCheck(inspection) {
2553
2029
  if (inspection.collisions.length > 0) {
2554
2030
  const first = inspection.collisions[0];
@@ -2558,11 +2034,11 @@ function createStableIdCollisionCheck(inspection) {
2558
2034
  "warn",
2559
2035
  "warning",
2560
2036
  "stable_id_collision",
2561
- `${detail} Edit one of the rule files to use a unique stable_id.`,
2562
- "Edit one of the colliding rule files to declare a different `<!-- fab:rule-id X -->` value."
2037
+ `${detail} Edit one of the knowledge files to use a unique stable_id.`,
2038
+ "Edit one of the colliding knowledge files to declare a different `id: K[PT]-XXX-NNNN` frontmatter value."
2563
2039
  );
2564
2040
  }
2565
- return okCheck("Stable ID collision", "No declared stable_id collisions found in .fabric/rules/.");
2041
+ return okCheck("Stable ID collision", "No declared stable_id collisions found in .fabric/knowledge/.");
2566
2042
  }
2567
2043
  function createMetaManuallyDivergedCheck(inspection) {
2568
2044
  if (!inspection.readable) {
@@ -2592,7 +2068,7 @@ function createMetaManuallyDivergedCheck(inspection) {
2592
2068
  }
2593
2069
  function inspectPreexistingRootFiles(projectRoot) {
2594
2070
  const candidates = ["CLAUDE.md", "AGENTS.md"];
2595
- const detected = candidates.filter((name) => existsSync4(join8(projectRoot, name)));
2071
+ const detected = candidates.filter((name) => existsSync4(join5(projectRoot, name)));
2596
2072
  return { detected };
2597
2073
  }
2598
2074
  function createPreexistingRootFilesCheck(inspection) {
@@ -2605,336 +2081,464 @@ function createPreexistingRootFilesCheck(inspection) {
2605
2081
  kind: "info",
2606
2082
  code: "preexisting_root_claude_md",
2607
2083
  fixable: false,
2608
- message: `${inspection.detected.join(", ")} detected at project root. These root files are not auto-loaded by Fabric MCP.`,
2609
- actionHint: "Move rule content to `.fabric/rules/` if you want it available in MCP responses."
2610
- };
2611
- }
2612
- function inspectClaudeSkillLegacyPath(projectRoot) {
2613
- const legacyPath = join8(projectRoot, ".claude", "skills", "agents-md-init", "SKILL.md");
2614
- const newPath = join8(projectRoot, ".claude", "skills", "fabric-init", "SKILL.md");
2615
- const hasLegacy = existsSync4(legacyPath) && !existsSync4(newPath);
2616
- return { hasLegacy, legacyPath, newPath };
2617
- }
2618
- function createClaudeSkillLegacyPathCheck(inspection) {
2619
- if (inspection.hasLegacy) {
2620
- return issueCheck(
2621
- "Claude skill path",
2622
- "error",
2623
- "fixable_error",
2624
- "claude_skill_legacy_path",
2625
- `.claude/skills/agents-md-init/SKILL.md exists at the legacy path. Run --fix to migrate it to .claude/skills/fabric-init/SKILL.md (user edits preserved).`,
2626
- "Run `fab doctor --fix` to rename agents-md-init/ to fabric-init/, preserving any user edits to SKILL.md."
2627
- );
2628
- }
2629
- return okCheck("Claude skill path", ".claude/skills/fabric-init/SKILL.md is at the canonical path (or not present).");
2084
+ message: `${inspection.detected.join(", ")} detected at project root. These root files are not auto-loaded by Fabric MCP.`,
2085
+ actionHint: "Move knowledge content to `.fabric/knowledge/{type}/` if you want it available in MCP responses."
2086
+ };
2630
2087
  }
2631
- async function fixClaudeSkillLegacyPath(projectRoot) {
2632
- const legacyPath = join8(projectRoot, ".claude", "skills", "agents-md-init", "SKILL.md");
2633
- const newPath = join8(projectRoot, ".claude", "skills", "fabric-init", "SKILL.md");
2634
- if (!existsSync4(legacyPath)) {
2088
+ async function fixMcpConfigInWrongFile(projectRoot) {
2089
+ const settingsPath = join5(projectRoot, ".claude", "settings.json");
2090
+ if (!existsSync4(settingsPath)) {
2635
2091
  return;
2636
2092
  }
2637
- mkdirSync(join8(newPath, ".."), { recursive: true });
2638
- renameSync(legacyPath, newPath);
2639
- const legacyDir = join8(legacyPath, "..");
2093
+ let settings;
2640
2094
  try {
2641
- rmdirSync(legacyDir);
2095
+ const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
2096
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
2097
+ return;
2098
+ }
2099
+ settings = parsed;
2642
2100
  } catch {
2101
+ return;
2102
+ }
2103
+ const mcpServers = settings.mcpServers;
2104
+ if (mcpServers === null || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
2105
+ return;
2106
+ }
2107
+ const { fabric: _removed, ...remainingServers } = mcpServers;
2108
+ const cleaned = { ...settings };
2109
+ if (Object.keys(remainingServers).length === 0) {
2110
+ delete cleaned.mcpServers;
2111
+ } else {
2112
+ cleaned.mcpServers = remainingServers;
2643
2113
  }
2114
+ await atomicWriteJson2(settingsPath, cleaned, { indent: 2 });
2644
2115
  await appendEventLedgerEvent(projectRoot, {
2645
- event_type: "claude_skill_path_migrated",
2646
- from: legacyPath,
2647
- to: newPath
2116
+ event_type: "mcp_config_migrated",
2117
+ source: "doctor_fix",
2118
+ removed_from: ".claude/settings.json"
2648
2119
  });
2649
2120
  }
2650
- var LEGACY_HOOK_FILENAME = "agents-md-init-reminder.cjs";
2651
- var NEW_HOOK_FILENAME = "fabric-init-reminder.cjs";
2652
- function inspectClaudeHookLegacyPath(projectRoot) {
2653
- const legacyHookPath = join8(projectRoot, ".claude", "hooks", LEGACY_HOOK_FILENAME);
2654
- const newHookPath = join8(projectRoot, ".claude", "hooks", NEW_HOOK_FILENAME);
2655
- const settingsPath = join8(projectRoot, ".claude", "settings.json");
2656
- const hasLegacyFile = existsSync4(legacyHookPath);
2657
- let hasLegacySettingsCommand = false;
2658
- if (existsSync4(settingsPath)) {
2659
- try {
2660
- const raw = readFileSync(settingsPath, "utf8");
2661
- hasLegacySettingsCommand = raw.includes(LEGACY_HOOK_FILENAME);
2662
- } catch {
2663
- }
2121
+ async function ensureKnowledgeSubdirs(projectRoot) {
2122
+ for (const sub of KNOWLEDGE_SUBDIRS2) {
2123
+ await mkdir3(join5(projectRoot, ".fabric", "knowledge", sub), { recursive: true });
2664
2124
  }
2665
- return { hasLegacyFile, hasLegacySettingsCommand, legacyHookPath, newHookPath, settingsPath };
2666
2125
  }
2667
- function createClaudeHookLegacyPathCheck(inspection) {
2668
- if (inspection.hasLegacyFile || inspection.hasLegacySettingsCommand) {
2669
- return issueCheck(
2670
- "Claude hook path",
2671
- "error",
2672
- "fixable_error",
2673
- "claude_hook_legacy_path",
2674
- `.claude/hooks/${LEGACY_HOOK_FILENAME} (or its reference in .claude/settings.json) exists at the legacy path. Run --fix to migrate to ${NEW_HOOK_FILENAME}.`,
2675
- `Run \`fab doctor --fix\` to rename ${LEGACY_HOOK_FILENAME} to ${NEW_HOOK_FILENAME} and update .claude/settings.json hook commands.`
2676
- );
2126
+ async function fixCounterDesync(projectRoot) {
2127
+ const metaPath = join5(projectRoot, ".fabric", "agents.meta.json");
2128
+ if (!existsSync4(metaPath)) {
2129
+ return;
2677
2130
  }
2678
- return okCheck("Claude hook path", `.claude/hooks/${NEW_HOOK_FILENAME} is at the canonical path (or not present).`);
2679
- }
2680
- async function fixClaudeHookLegacyPath(projectRoot) {
2681
- const { hasLegacyFile, hasLegacySettingsCommand, legacyHookPath, newHookPath, settingsPath } = inspectClaudeHookLegacyPath(projectRoot);
2682
- if (hasLegacyFile) {
2683
- if (existsSync4(newHookPath)) {
2684
- unlinkSync(legacyHookPath);
2685
- } else {
2686
- mkdirSync(join8(newHookPath, ".."), { recursive: true });
2687
- renameSync(legacyHookPath, newHookPath);
2688
- }
2131
+ let meta;
2132
+ try {
2133
+ meta = agentsMetaSchema4.parse(JSON.parse(await readFile5(metaPath, "utf8")));
2134
+ } catch {
2135
+ return;
2689
2136
  }
2690
- if (hasLegacySettingsCommand) {
2691
- try {
2692
- const raw = readFileSync(settingsPath, "utf8");
2693
- const updated = raw.split(LEGACY_HOOK_FILENAME).join(NEW_HOOK_FILENAME);
2694
- if (updated !== raw) {
2695
- const parsed = JSON.parse(updated);
2696
- await atomicWriteJson2(settingsPath, parsed);
2697
- }
2698
- } catch {
2699
- }
2137
+ const synthetic = {
2138
+ present: true,
2139
+ valid: true,
2140
+ meta,
2141
+ revision: meta.revision,
2142
+ computedRevision: null,
2143
+ ruleCount: 0,
2144
+ missingContentRefs: [],
2145
+ invalidContentRefs: [],
2146
+ stale: false,
2147
+ changed: false
2148
+ };
2149
+ const desync = inspectCounterDesync(synthetic);
2150
+ if (desync.desyncs.length === 0 || desync.correctedCounters === null) {
2151
+ return;
2700
2152
  }
2701
- if (hasLegacyFile || hasLegacySettingsCommand) {
2702
- await appendEventLedgerEvent(projectRoot, {
2703
- event_type: "claude_hook_path_migrated",
2704
- from: legacyHookPath,
2705
- to: newHookPath
2706
- });
2153
+ const updated = { ...meta, counters: desync.correctedCounters };
2154
+ await atomicWriteJson2(metaPath, updated, { indent: 2 });
2155
+ }
2156
+ async function ensureEventLedger(projectRoot) {
2157
+ const path = getEventLedgerPath(projectRoot);
2158
+ await ensureParentDirectory(path);
2159
+ await writeFile2(path, "", { encoding: "utf8", flag: "a" });
2160
+ }
2161
+ function createFixMessage(fixed, report) {
2162
+ const fixedText = fixed.length === 0 ? "No deterministic doctor fixes were needed." : `Applied ${fixed.length} deterministic doctor fix${fixed.length === 1 ? "" : "es"}.`;
2163
+ const manualText = report.manual_errors.length === 0 ? "No manual errors remain." : `${report.manual_errors.length} manual error${report.manual_errors.length === 1 ? "" : "s"} remain.`;
2164
+ return `${fixedText} ${manualText}`;
2165
+ }
2166
+ function isValidJsonLine(line) {
2167
+ try {
2168
+ JSON.parse(line);
2169
+ return true;
2170
+ } catch {
2171
+ return false;
2707
2172
  }
2708
2173
  }
2709
- function inspectLegacyClientPaths(projectRoot) {
2710
- const configPath = join8(projectRoot, "fabric.config.json");
2711
- if (!existsSync4(configPath)) {
2712
- return { presentKeys: [] };
2174
+ function normalizeTarget(targetInput) {
2175
+ return isAbsolute2(targetInput) ? targetInput : resolve3(process.cwd(), targetInput);
2176
+ }
2177
+ function normalizePath(path) {
2178
+ return posix.normalize(path.split("\\").join("/"));
2179
+ }
2180
+ function collectEntryPoints(root) {
2181
+ if (!existsSync4(root) || !statSync3(root).isDirectory()) {
2182
+ return [];
2713
2183
  }
2714
- try {
2715
- const parsed = JSON.parse(readFileSync(configPath, "utf8"));
2716
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
2717
- return { presentKeys: [] };
2184
+ const entries = [];
2185
+ const stack = [root];
2186
+ while (stack.length > 0) {
2187
+ const current = stack.pop();
2188
+ if (current === void 0) {
2189
+ continue;
2718
2190
  }
2719
- const config = parsed;
2720
- const clientPaths = config.clientPaths;
2721
- if (clientPaths === null || typeof clientPaths !== "object" || Array.isArray(clientPaths)) {
2722
- return { presentKeys: [] };
2191
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
2192
+ const absolutePath = join5(current, entry.name);
2193
+ const relativePath = normalizePath(absolutePath.slice(root.length + 1));
2194
+ if (relativePath.length === 0) {
2195
+ continue;
2196
+ }
2197
+ if (entry.isDirectory()) {
2198
+ if (!IGNORED_DIRECTORIES.has(entry.name)) {
2199
+ stack.push(absolutePath);
2200
+ }
2201
+ continue;
2202
+ }
2203
+ if (!entry.isFile()) {
2204
+ continue;
2205
+ }
2206
+ const reason = getEntryPointReason(relativePath);
2207
+ if (reason !== null) {
2208
+ entries.push({ path: relativePath, reason });
2209
+ }
2723
2210
  }
2724
- const cp = clientPaths;
2725
- const presentKeys = LEGACY_CLIENT_PATH_KEYS.filter((key) => key in cp);
2726
- return { presentKeys };
2727
- } catch {
2728
- return { presentKeys: [] };
2729
2211
  }
2212
+ return entries.sort((left, right) => left.path.localeCompare(right.path));
2730
2213
  }
2731
- function createLegacyClientPathCheck(inspection) {
2732
- if (inspection.presentKeys.length > 0) {
2733
- return issueCheck(
2734
- "Legacy client paths",
2735
- "warn",
2736
- "warning",
2737
- "legacy_client_path_present",
2738
- `fabric.config.json contains deprecated clientPaths keys: ${inspection.presentKeys.join(", ")}. These clients are removed in 1.8.0; run --fix to clean now or accept the upcoming removal.`,
2739
- "Run `fab doctor --fix` to remove deprecated clientPaths keys (windsurf, rooCode, geminiCLI) from fabric.config.json."
2740
- );
2214
+ function getEntryPointReason(relativePath) {
2215
+ const extension = relativePath.slice(relativePath.lastIndexOf("."));
2216
+ if (!SCRIPT_EXTENSIONS.has(extension)) {
2217
+ return null;
2741
2218
  }
2742
- return okCheck("Legacy client paths", "No deprecated clientPaths keys found in fabric.config.json.");
2743
- }
2744
- async function fixLegacyClientPaths(projectRoot) {
2745
- const configPath = join8(projectRoot, "fabric.config.json");
2746
- if (!existsSync4(configPath)) {
2747
- return;
2219
+ const directory = posix.dirname(relativePath);
2220
+ const fileName = posix.basename(relativePath);
2221
+ const fileBase = fileName.slice(0, Math.max(fileName.lastIndexOf("."), 0));
2222
+ if (directory === "assets/scripts" || directory === "scripts") {
2223
+ return "top-level script";
2748
2224
  }
2749
- let config;
2750
- try {
2751
- const parsed = JSON.parse(readFileSync(configPath, "utf8"));
2752
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
2753
- return;
2754
- }
2755
- config = parsed;
2756
- } catch {
2757
- return;
2225
+ if (directory === "src" && /^(App|app|index|main)$/.test(fileBase)) {
2226
+ return "application entry";
2758
2227
  }
2759
- const clientPaths = config.clientPaths;
2760
- if (clientPaths === null || typeof clientPaths !== "object" || Array.isArray(clientPaths)) {
2761
- return;
2228
+ if ((directory === "app" || directory.startsWith("app/")) && /^(layout|page|route)$/.test(fileBase)) {
2229
+ return "next app route";
2762
2230
  }
2763
- const cp = clientPaths;
2764
- const removed = [];
2765
- for (const key of LEGACY_CLIENT_PATH_KEYS) {
2766
- if (key in cp) {
2767
- delete cp[key];
2768
- removed.push(key);
2769
- }
2231
+ if ((directory === "pages" || directory.startsWith("pages/")) && fileName !== "_app.d.ts") {
2232
+ return "next page route";
2770
2233
  }
2771
- if (removed.length === 0) {
2772
- return;
2234
+ return null;
2235
+ }
2236
+ function reduceStatus(statuses) {
2237
+ if (statuses.includes("error")) {
2238
+ return "error";
2773
2239
  }
2774
- const updatedConfig = { ...config, clientPaths: cp };
2775
- await atomicWriteJson2(configPath, updatedConfig, { indent: 2 });
2776
- await appendEventLedgerEvent(projectRoot, {
2777
- event_type: "legacy_client_path_present",
2778
- removed
2240
+ if (statuses.includes("warn")) {
2241
+ return "warn";
2242
+ }
2243
+ return "ok";
2244
+ }
2245
+ function isMissingFileError(error) {
2246
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
2247
+ }
2248
+
2249
+ // src/services/get-rules.ts
2250
+ import { readFile as readFile6 } from "fs/promises";
2251
+ import { join as join7 } from "path";
2252
+ import { minimatch } from "minimatch";
2253
+
2254
+ // src/services/audit-log.ts
2255
+ import { open, stat as stat2 } from "fs/promises";
2256
+ import { isAbsolute as isAbsolute3, join as join6, posix as posix2, relative as relative3, resolve as resolve4 } from "path";
2257
+ var AUDIT_LOG_FILE = `${FABRIC_DIR}/audit.jsonl`;
2258
+ var DEFAULT_AUDIT_WINDOW_MS = 5 * 60 * 1e3;
2259
+ async function appendGetRulesAuditEvent(projectRoot, input) {
2260
+ const entry = {
2261
+ kind: "audit-event",
2262
+ event: "get_rules",
2263
+ ts: input.ts ?? Date.now(),
2264
+ path: normalizeAuditPath(projectRoot, input.path),
2265
+ client_hash: input.client_hash
2266
+ };
2267
+ await appendAuditLogEventLedgerEvents(projectRoot, [entry], {
2268
+ rule_context: {
2269
+ required_stable_ids: input.required_stable_ids,
2270
+ ai_selectable_stable_ids: input.ai_selectable_stable_ids,
2271
+ final_stable_ids: input.final_stable_ids
2272
+ },
2273
+ correlation_id: input.correlation_id,
2274
+ session_id: input.session_id
2779
2275
  });
2276
+ return entry;
2780
2277
  }
2781
- async function fixMcpConfigInWrongFile(projectRoot) {
2782
- const settingsPath = join8(projectRoot, ".claude", "settings.json");
2783
- if (!existsSync4(settingsPath)) {
2784
- return;
2278
+ async function appendRuleSelectionAuditEvent(projectRoot, input) {
2279
+ const entry = {
2280
+ kind: "audit-event",
2281
+ event: "knowledge_selection",
2282
+ ts: input.ts ?? Date.now(),
2283
+ path: normalizeAuditPath(projectRoot, input.path),
2284
+ selection_token: input.selection_token,
2285
+ target_paths: input.target_paths.map((path) => normalizeAuditPath(projectRoot, path)),
2286
+ required_stable_ids: input.required_stable_ids,
2287
+ ai_selectable_stable_ids: input.ai_selectable_stable_ids,
2288
+ ai_selected_stable_ids: input.ai_selected_stable_ids,
2289
+ final_stable_ids: input.final_stable_ids,
2290
+ ai_selection_reasons: input.ai_selection_reasons,
2291
+ rejected_stable_ids: input.rejected_stable_ids,
2292
+ ignored_stable_ids: input.ignored_stable_ids
2293
+ };
2294
+ await appendAuditLogEventLedgerEvents(projectRoot, [entry], {
2295
+ correlation_id: input.correlation_id,
2296
+ session_id: input.session_id
2297
+ });
2298
+ return entry;
2299
+ }
2300
+ function normalizeAuditPath(projectRoot, value) {
2301
+ const normalizedProjectRoot = resolve4(projectRoot);
2302
+ const candidate = isAbsolute3(value) ? resolve4(value) : resolve4(normalizedProjectRoot, value);
2303
+ const relativePath = relative3(normalizedProjectRoot, candidate);
2304
+ if (relativePath.length > 0 && relativePath !== "." && !relativePath.startsWith("..") && !isAbsolute3(relativePath)) {
2305
+ return posix2.normalize(relativePath.split("\\").join("/"));
2785
2306
  }
2786
- let settings;
2307
+ return posix2.normalize(value.replaceAll("\\", "/"));
2308
+ }
2309
+ async function appendAuditLogEventLedgerEvents(projectRoot, entries, metadata = {}) {
2310
+ for (const entry of entries) {
2311
+ if (entry.event === "get_rules") {
2312
+ await appendEventLedgerEvent(projectRoot, {
2313
+ event_type: "knowledge_context_planned",
2314
+ ts: entry.ts,
2315
+ target_paths: [entry.path],
2316
+ required_stable_ids: metadata.rule_context?.required_stable_ids ?? [],
2317
+ ai_selectable_stable_ids: metadata.rule_context?.ai_selectable_stable_ids ?? [],
2318
+ final_stable_ids: metadata.rule_context?.final_stable_ids ?? [],
2319
+ client_hash: entry.client_hash,
2320
+ correlation_id: metadata.correlation_id,
2321
+ session_id: metadata.session_id
2322
+ });
2323
+ continue;
2324
+ }
2325
+ if (entry.event === "knowledge_selection") {
2326
+ await appendEventLedgerEvent(projectRoot, {
2327
+ event_type: "knowledge_selection",
2328
+ ts: entry.ts,
2329
+ selection_token: entry.selection_token,
2330
+ target_paths: entry.target_paths,
2331
+ required_stable_ids: entry.required_stable_ids,
2332
+ ai_selectable_stable_ids: entry.ai_selectable_stable_ids,
2333
+ ai_selected_stable_ids: entry.ai_selected_stable_ids,
2334
+ final_stable_ids: entry.final_stable_ids,
2335
+ ai_selection_reasons: entry.ai_selection_reasons,
2336
+ rejected_stable_ids: entry.rejected_stable_ids,
2337
+ ignored_stable_ids: entry.ignored_stable_ids,
2338
+ correlation_id: metadata.correlation_id,
2339
+ session_id: metadata.session_id
2340
+ });
2341
+ continue;
2342
+ }
2343
+ await appendEventLedgerEvent(projectRoot, {
2344
+ event_type: "edit_intent_checked",
2345
+ ts: entry.ts,
2346
+ path: entry.path,
2347
+ compliant: entry.compliant,
2348
+ intent: entry.intent,
2349
+ ledger_entry_id: entry.ledger_entry_id,
2350
+ ledger_source: "ai",
2351
+ matched_rule_context_ts: entry.matched_get_rules_ts,
2352
+ window_ms: entry.window_ms,
2353
+ correlation_id: metadata.correlation_id,
2354
+ session_id: metadata.session_id
2355
+ });
2356
+ }
2357
+ }
2358
+
2359
+ // src/services/get-rules.ts
2360
+ var PRIORITY_ORDER = {
2361
+ high: 0,
2362
+ medium: 1,
2363
+ low: 2
2364
+ };
2365
+ async function getRules(projectRoot, input) {
2366
+ const context = await loadGetRulesContext(projectRoot);
2367
+ const stale = input.client_hash !== void 0 && input.client_hash !== context.meta.revision;
2368
+ const matchedNodes = matchRuleNodes(context.meta, input.path);
2369
+ const requiredStableIds = matchedNodes.filter((node) => node.level === "L2").map((node) => node.stable_id);
2370
+ const aiSelectableStableIds = matchedNodes.filter((node) => node.level === "L1").map((node) => node.stable_id);
2371
+ const rules = await resolveRulesForPath(projectRoot, context, input.path);
2372
+ const result = {
2373
+ revision_hash: context.meta.revision,
2374
+ stale,
2375
+ rules
2376
+ };
2787
2377
  try {
2788
- const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
2789
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
2790
- return;
2791
- }
2792
- settings = parsed;
2378
+ await appendGetRulesAuditEvent(projectRoot, {
2379
+ path: input.path,
2380
+ client_hash: input.client_hash,
2381
+ required_stable_ids: requiredStableIds,
2382
+ ai_selectable_stable_ids: aiSelectableStableIds,
2383
+ final_stable_ids: [...requiredStableIds, ...aiSelectableStableIds],
2384
+ correlation_id: input.correlation_id,
2385
+ session_id: input.session_id
2386
+ });
2793
2387
  } catch {
2794
- return;
2795
2388
  }
2796
- const mcpServers = settings.mcpServers;
2797
- if (mcpServers === null || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
2798
- return;
2799
- }
2800
- const { fabric: _removed, ...remainingServers } = mcpServers;
2801
- const cleaned = { ...settings };
2802
- if (Object.keys(remainingServers).length === 0) {
2803
- delete cleaned.mcpServers;
2804
- } else {
2805
- cleaned.mcpServers = remainingServers;
2389
+ return result;
2390
+ }
2391
+ async function loadGetRulesContext(projectRoot) {
2392
+ const cached = contextCache.get("context", projectRoot);
2393
+ if (cached !== void 0) {
2394
+ return cached;
2806
2395
  }
2807
- await atomicWriteJson2(settingsPath, cleaned, { indent: 2 });
2808
- await appendEventLedgerEvent(projectRoot, {
2809
- event_type: "mcp_config_migrated",
2810
- source: "doctor_fix",
2811
- removed_from: ".claude/settings.json"
2812
- });
2396
+ const meta = await readAgentsMeta(projectRoot);
2397
+ const l0Content = await readFile6(join7(projectRoot, ".fabric", "bootstrap", "README.md"), "utf8");
2398
+ const context = {
2399
+ meta,
2400
+ l0Content,
2401
+ humanLockedNearby: []
2402
+ };
2403
+ contextCache.set("context", projectRoot, context);
2404
+ return context;
2813
2405
  }
2814
- async function writeDefaultBootstrap(projectRoot) {
2815
- const path = join8(projectRoot, FABRIC_BOOTSTRAP_PATH);
2816
- await ensureParentDirectory(path);
2817
- await atomicWriteText3(path, buildBootstrapContent(projectRoot));
2406
+ async function resolveRulesForPath(projectRoot, context, path, options = {}) {
2407
+ const matchedNodes = matchRuleNodes(context.meta, path);
2408
+ const loaded = await loadMatchedRules(projectRoot, matchedNodes);
2409
+ return buildRulesPayload(context, loaded, options);
2818
2410
  }
2819
- async function ensureEventLedger(projectRoot) {
2820
- const path = getEventLedgerPath(projectRoot);
2821
- await ensureParentDirectory(path);
2822
- await writeFile2(path, "", { encoding: "utf8", flag: "a" });
2411
+ function normalizeRulesPath(value) {
2412
+ return value.replaceAll("\\", "/");
2823
2413
  }
2824
- function createFixMessage(fixed, report) {
2825
- const fixedText = fixed.length === 0 ? "No deterministic doctor fixes were needed." : `Applied ${fixed.length} deterministic doctor fix${fixed.length === 1 ? "" : "es"}.`;
2826
- const manualText = report.manual_errors.length === 0 ? "No manual errors remain." : `${report.manual_errors.length} manual error${report.manual_errors.length === 1 ? "" : "s"} remain.`;
2827
- return `${fixedText} ${manualText}`;
2414
+ function matchRuleNodes(meta, path) {
2415
+ const requestedPath = normalizeRulesPath(path);
2416
+ return Object.entries(meta.nodes).filter(([, node]) => shouldLoadNodeForPath(requestedPath, node)).sort((left, right) => {
2417
+ const [leftId, leftNode] = left;
2418
+ const [rightId, rightNode] = right;
2419
+ const priorityDelta = PRIORITY_ORDER[leftNode.priority] - PRIORITY_ORDER[rightNode.priority];
2420
+ return priorityDelta !== 0 ? priorityDelta : leftId.localeCompare(rightId);
2421
+ }).map(([nodeId, node]) => ({
2422
+ node_id: nodeId,
2423
+ level: classifyNode(nodeId, node),
2424
+ stable_id: node.stable_id ?? nodeId,
2425
+ identity_source: node.identity_source ?? "derived",
2426
+ node
2427
+ }));
2828
2428
  }
2829
- function findRuleFiles2(projectRoot) {
2830
- const rulesRoot = join8(projectRoot, ".fabric", "rules");
2831
- if (!existsSync4(rulesRoot) || !statSync3(rulesRoot).isDirectory()) {
2832
- return [];
2833
- }
2834
- const files = [];
2835
- const stack = [rulesRoot];
2836
- while (stack.length > 0) {
2837
- const current = stack.pop();
2838
- if (current === void 0) {
2429
+ async function loadMatchedRules(projectRoot, matchedNodes, fileContentCache = /* @__PURE__ */ new Map()) {
2430
+ const rules = [];
2431
+ const stubs = [];
2432
+ for (const matchedNode of matchedNodes) {
2433
+ if (matchedNode.level === null) {
2839
2434
  continue;
2840
2435
  }
2841
- for (const entry of readdirSync(current, { withFileTypes: true })) {
2842
- const absolutePath = join8(current, entry.name);
2843
- const relativePath = normalizePath(absolutePath.slice(projectRoot.length + 1));
2844
- if (entry.isDirectory()) {
2845
- stack.push(absolutePath);
2846
- } else if (entry.isFile() && entry.name.endsWith(".md")) {
2847
- files.push(relativePath);
2848
- }
2436
+ if (matchedNode.node.activation?.tier === "description") {
2437
+ stubs.push({
2438
+ stable_id: matchedNode.stable_id,
2439
+ identity_source: matchedNode.identity_source,
2440
+ level: matchedNode.level,
2441
+ path: matchedNode.node.file,
2442
+ description: matchedNode.node.activation.description ?? ""
2443
+ });
2444
+ continue;
2849
2445
  }
2446
+ rules.push({
2447
+ level: matchedNode.level,
2448
+ stable_id: matchedNode.stable_id,
2449
+ identity_source: matchedNode.identity_source,
2450
+ entry: {
2451
+ path: matchedNode.node.file,
2452
+ content: await readRuleContent(projectRoot, matchedNode.node.file, fileContentCache)
2453
+ }
2454
+ });
2850
2455
  }
2851
- return files.sort();
2852
- }
2853
- function isValidJsonLine(line) {
2854
- try {
2855
- JSON.parse(line);
2856
- return true;
2857
- } catch {
2858
- return false;
2859
- }
2860
- }
2861
- function normalizeTarget(targetInput) {
2862
- return isAbsolute3(targetInput) ? targetInput : resolve4(process.cwd(), targetInput);
2456
+ return { rules, stubs };
2863
2457
  }
2864
- function normalizePath(path) {
2865
- return posix2.normalize(path.split("\\").join("/"));
2458
+ function buildRulesPayload(context, loaded, options = {}) {
2459
+ const { L1, L2 } = partitionRulesByLevel(loaded.rules, options.dedupeByPath ?? false);
2460
+ return {
2461
+ L0: context.l0Content,
2462
+ L1,
2463
+ L2,
2464
+ human_locked_nearby: context.humanLockedNearby,
2465
+ description_stubs: loaded.stubs.length > 0 ? dedupeDescriptionStubsByPath(loaded.stubs).map(toDescriptionStub) : void 0
2466
+ };
2866
2467
  }
2867
- function collectEntryPoints(root) {
2868
- if (!existsSync4(root) || !statSync3(root).isDirectory()) {
2869
- return [];
2468
+ function classifyNode(nodeId, node) {
2469
+ if (nodeId.startsWith("L1/")) {
2470
+ return "L1";
2870
2471
  }
2871
- const entries = [];
2872
- const stack = [root];
2873
- while (stack.length > 0) {
2874
- const current = stack.pop();
2875
- if (current === void 0) {
2472
+ if (nodeId.startsWith("L2/")) {
2473
+ return "L2";
2474
+ }
2475
+ return node.layer === "L0" ? null : node.layer;
2476
+ }
2477
+ function partitionRulesByLevel(loadedRules, dedupeByPath) {
2478
+ const l1 = [];
2479
+ const l2 = [];
2480
+ for (const rule of loadedRules) {
2481
+ if (rule.level === "L1") {
2482
+ l1.push(rule.entry);
2876
2483
  continue;
2877
2484
  }
2878
- for (const entry of readdirSync(current, { withFileTypes: true })) {
2879
- const absolutePath = join8(current, entry.name);
2880
- const relativePath = normalizePath(absolutePath.slice(root.length + 1));
2881
- if (relativePath.length === 0) {
2882
- continue;
2883
- }
2884
- if (entry.isDirectory()) {
2885
- if (!IGNORED_DIRECTORIES.has(entry.name)) {
2886
- stack.push(absolutePath);
2887
- }
2888
- continue;
2889
- }
2890
- if (!entry.isFile()) {
2891
- continue;
2892
- }
2893
- const reason = getEntryPointReason(relativePath);
2894
- if (reason !== null) {
2895
- entries.push({ path: relativePath, reason });
2896
- }
2485
+ if (rule.level === "L2") {
2486
+ l2.push(rule.entry);
2897
2487
  }
2898
2488
  }
2899
- return entries.sort((left, right) => left.path.localeCompare(right.path));
2489
+ return {
2490
+ L1: dedupeByPath ? dedupeEntriesByPath(l1) : l1,
2491
+ L2: dedupeByPath ? dedupeEntriesByPath(l2) : l2
2492
+ };
2900
2493
  }
2901
- function getEntryPointReason(relativePath) {
2902
- const extension = relativePath.slice(relativePath.lastIndexOf("."));
2903
- if (!SCRIPT_EXTENSIONS.has(extension)) {
2904
- return null;
2905
- }
2906
- const directory = posix2.dirname(relativePath);
2907
- const fileName = posix2.basename(relativePath);
2908
- const fileBase = fileName.slice(0, Math.max(fileName.lastIndexOf("."), 0));
2909
- if (directory === "assets/scripts" || directory === "scripts") {
2910
- return "top-level script";
2911
- }
2912
- if (directory === "src" && /^(App|app|index|main)$/.test(fileBase)) {
2913
- return "application entry";
2914
- }
2915
- if ((directory === "app" || directory.startsWith("app/")) && /^(layout|page|route)$/.test(fileBase)) {
2916
- return "next app route";
2917
- }
2918
- if ((directory === "pages" || directory.startsWith("pages/")) && fileName !== "_app.d.ts") {
2919
- return "next page route";
2920
- }
2921
- return null;
2494
+ function dedupeEntriesByPath(entries) {
2495
+ const seenPaths = /* @__PURE__ */ new Set();
2496
+ return entries.filter((entry) => {
2497
+ if (seenPaths.has(entry.path)) {
2498
+ return false;
2499
+ }
2500
+ seenPaths.add(entry.path);
2501
+ return true;
2502
+ });
2922
2503
  }
2923
- function reduceStatus(statuses) {
2924
- if (statuses.includes("error")) {
2925
- return "error";
2926
- }
2927
- if (statuses.includes("warn")) {
2928
- return "warn";
2504
+ function shouldLoadNodeForPath(requestedPath, node) {
2505
+ switch (node.activation?.tier) {
2506
+ case "always":
2507
+ return true;
2508
+ case "description":
2509
+ return true;
2510
+ case "path":
2511
+ case void 0:
2512
+ return minimatch(requestedPath, normalizeRulesPath(node.scope_glob), { dot: true });
2929
2513
  }
2930
- return "ok";
2931
2514
  }
2932
- function isMissingFileError(error) {
2933
- return error instanceof Error && "code" in error && error.code === "ENOENT";
2515
+ function dedupeDescriptionStubsByPath(stubs) {
2516
+ const seenPaths = /* @__PURE__ */ new Set();
2517
+ return stubs.filter((stub) => {
2518
+ if (seenPaths.has(stub.path)) {
2519
+ return false;
2520
+ }
2521
+ seenPaths.add(stub.path);
2522
+ return true;
2523
+ });
2524
+ }
2525
+ function toDescriptionStub(stub) {
2526
+ return {
2527
+ path: stub.path,
2528
+ description: stub.description
2529
+ };
2530
+ }
2531
+ async function readRuleContent(projectRoot, file, fileContentCache) {
2532
+ const cached = fileContentCache.get(file);
2533
+ if (cached !== void 0) {
2534
+ return await cached;
2535
+ }
2536
+ const pending = readFile6(join7(projectRoot, file), "utf8");
2537
+ fileContentCache.set(file, pending);
2538
+ return await pending;
2934
2539
  }
2935
2540
 
2936
2541
  export {
2937
- AGENTS_MD_RESOURCE_URI,
2938
2542
  contextCache,
2939
2543
  resolveProjectRoot,
2940
2544
  readAgentsMeta,
@@ -2960,9 +2564,9 @@ export {
2960
2564
  invalidateRuleSyncCooldown,
2961
2565
  ensureRulesFresh,
2962
2566
  reconcileRules,
2567
+ appendRuleSelectionAuditEvent,
2963
2568
  getRules,
2964
- planContext,
2965
- getRuleSections,
2569
+ normalizeRulesPath,
2966
2570
  runDoctorReport,
2967
2571
  runDoctorFix
2968
2572
  };