@fenglimg/fabric-server 1.8.0-rc.3 → 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,846 +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 codexSkillLegacyPath = inspectCodexSkillLegacyPath(projectRoot);
1916
- const preexistingRootFiles = inspectPreexistingRootFiles(projectRoot);
1917
- const legacyClientPaths = inspectLegacyClientPaths(projectRoot);
1918
- const taxonomyExists = existsSync4(join8(projectRoot, ".fabric", "INITIAL_TAXONOMY.md"));
1919
- const bootstrapExists = existsSync4(join8(projectRoot, ".fabric", "bootstrap", "README.md"));
1920
- const checks = [
1921
- createBootstrapCheck(bootstrapExists),
1922
- createTaxonomyCheck(taxonomyExists),
1923
- createForensicCheck(forensic, framework.kind, entryPoints.length),
1924
- createInitContextCheck(initContext),
1925
- createMetaCheck(meta),
1926
- createRuleContentRefCheck(meta),
1927
- createRuleSectionsCheck(ruleSections),
1928
- createRuleTestIndexCheck(ruleTestIndex),
1929
- createEventLedgerCheck(eventLedger),
1930
- createEventLedgerPartialWriteCheck(eventLedger),
1931
- createMcpConfigInWrongFileCheck(mcpConfigInWrongFile),
1932
- createMetaManuallyDivergedCheck(metaManuallyDiverged),
1933
- createRulesDirUnindexedCheck(rulesDirUnindexed),
1934
- createStableIdCollisionCheck(stableIdCollision),
1935
- createClaudeSkillLegacyPathCheck(claudeSkillLegacyPath),
1936
- createClaudeHookLegacyPathCheck(claudeHookLegacyPath),
1937
- createCodexSkillLegacyPathCheck(codexSkillLegacyPath),
1938
- createPreexistingRootFilesCheck(preexistingRootFiles),
1939
- createLegacyClientPathCheck(legacyClientPaths)
1940
- ];
1941
- const fixableErrors = collectIssues(checks, "fixable_error");
1942
- const manualErrors = collectIssues(checks, "manual_error");
1943
- const warnings = collectIssues(checks, "warning");
1944
- const infos = collectIssues(checks, "info");
1945
- return {
1946
- status: reduceStatus(checks.map((check) => check.status)),
1947
- checks,
1948
- fixable_errors: fixableErrors,
1949
- manual_errors: manualErrors,
1950
- warnings,
1951
- infos,
1952
- summary: {
1953
- target: projectRoot,
1954
- framework: {
1955
- kind: framework.kind,
1956
- version: framework.version,
1957
- subkind: framework.subkind
1958
- },
1959
- entryPoints,
1960
- metaRevision: meta.revision,
1961
- computedMetaRevision: meta.computedRevision,
1962
- ruleCount: meta.ruleCount,
1963
- eventLedgerPath: eventLedger.path,
1964
- fixableErrorCount: fixableErrors.length,
1965
- manualErrorCount: manualErrors.length,
1966
- warningCount: warnings.length,
1967
- infoCount: infos.length,
1968
- targetFiles: Object.fromEntries(
1969
- TARGET_FILE_PATHS.map((path) => [path, existsSync4(join8(projectRoot, path))])
1970
- )
1971
- }
1972
- };
1973
- }
1974
- async function runDoctorFix(target) {
1975
- const projectRoot = normalizeTarget(target);
1976
- const before = await runDoctorReport(projectRoot);
1977
- const fixed = [];
1978
- if (before.fixable_errors.some((issue) => issue.code === "bootstrap_missing")) {
1979
- await writeDefaultBootstrap(projectRoot);
1980
- fixed.push(findIssue(before.fixable_errors, "bootstrap_missing"));
1981
- }
1982
- if (before.fixable_errors.some((issue) => issue.code === "event_ledger_missing")) {
1983
- await ensureEventLedger(projectRoot);
1984
- 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);
1985
1399
  }
1986
1400
  if (before.fixable_errors.some(
1987
1401
  (issue) => [
@@ -1990,7 +1404,7 @@ async function runDoctorFix(target) {
1990
1404
  "rule_test_index_missing",
1991
1405
  "rule_test_index_stale",
1992
1406
  "content_ref_missing",
1993
- "rules_dir_unindexed"
1407
+ "knowledge_dir_unindexed"
1994
1408
  ].includes(issue.code)
1995
1409
  )) {
1996
1410
  await reconcileRules(projectRoot, { trigger: "doctor" });
@@ -2001,12 +1415,14 @@ async function runDoctorFix(target) {
2001
1415
  "rule_test_index_missing",
2002
1416
  "rule_test_index_stale",
2003
1417
  "content_ref_missing",
2004
- "rules_dir_unindexed"
1418
+ "knowledge_dir_unindexed"
2005
1419
  ].includes(candidate.code)
2006
1420
  )) {
2007
1421
  fixed.push(issue);
2008
1422
  }
2009
1423
  contextCache.invalidate("meta_write", projectRoot);
1424
+ await fixCounterDesync(projectRoot);
1425
+ contextCache.invalidate("meta_write", projectRoot);
2010
1426
  }
2011
1427
  if (before.fixable_errors.some((issue) => issue.code === "event_ledger_partial_write")) {
2012
1428
  const ledgerPath = getEventLedgerPath(projectRoot);
@@ -2023,22 +1439,6 @@ async function runDoctorFix(target) {
2023
1439
  await fixMcpConfigInWrongFile(projectRoot);
2024
1440
  fixed.push(findIssue(before.fixable_errors, "mcp_config_in_wrong_file"));
2025
1441
  }
2026
- if (before.fixable_errors.some((issue) => issue.code === "claude_skill_legacy_path")) {
2027
- await fixClaudeSkillLegacyPath(projectRoot);
2028
- fixed.push(findIssue(before.fixable_errors, "claude_skill_legacy_path"));
2029
- }
2030
- if (before.fixable_errors.some((issue) => issue.code === "claude_hook_legacy_path")) {
2031
- await fixClaudeHookLegacyPath(projectRoot);
2032
- fixed.push(findIssue(before.fixable_errors, "claude_hook_legacy_path"));
2033
- }
2034
- if (before.fixable_errors.some((issue) => issue.code === "codex_skill_legacy_path")) {
2035
- await fixCodexSkillLegacyPath(projectRoot);
2036
- fixed.push(findIssue(before.fixable_errors, "codex_skill_legacy_path"));
2037
- }
2038
- if (before.warnings.some((issue) => issue.code === "legacy_client_path_present")) {
2039
- await fixLegacyClientPaths(projectRoot);
2040
- fixed.push(findIssue(before.warnings, "legacy_client_path_present"));
2041
- }
2042
1442
  const report = await runDoctorReport(projectRoot);
2043
1443
  return {
2044
1444
  changed: fixed.length > 0,
@@ -2050,9 +1450,9 @@ async function runDoctorFix(target) {
2050
1450
  };
2051
1451
  }
2052
1452
  async function inspectForensic(projectRoot) {
2053
- const path = join8(projectRoot, ".fabric", "forensic.json");
1453
+ const path = join5(projectRoot, ".fabric", "forensic.json");
2054
1454
  try {
2055
- const parsed = forensicReportSchema.parse(JSON.parse(await readFile7(path, "utf8")));
1455
+ const parsed = forensicReportSchema.parse(JSON.parse(await readFile5(path, "utf8")));
2056
1456
  return { present: true, valid: true, report: parsed };
2057
1457
  } catch (error) {
2058
1458
  if (isMissingFileError(error)) {
@@ -2061,20 +1461,8 @@ async function inspectForensic(projectRoot) {
2061
1461
  return { present: true, valid: false, report: null, error: error instanceof Error ? error.message : String(error) };
2062
1462
  }
2063
1463
  }
2064
- async function inspectInitContext(projectRoot) {
2065
- const path = join8(projectRoot, ".fabric", "init-context.json");
2066
- try {
2067
- JSON.parse(await readFile7(path, "utf8"));
2068
- return { exists: true, validJson: true };
2069
- } catch (error) {
2070
- if (isMissingFileError(error)) {
2071
- return { exists: false, validJson: false, error: ".fabric/init-context.json is missing." };
2072
- }
2073
- return { exists: true, validJson: false, error: error instanceof Error ? error.message : String(error) };
2074
- }
2075
- }
2076
1464
  function inspectMcpConfigInWrongFile(projectRoot) {
2077
- const settingsPath = join8(projectRoot, ".claude", "settings.json");
1465
+ const settingsPath = join5(projectRoot, ".claude", "settings.json");
2078
1466
  if (!existsSync4(settingsPath)) {
2079
1467
  return { hasWrongEntry: false, settingsPath };
2080
1468
  }
@@ -2095,10 +1483,10 @@ function inspectMcpConfigInWrongFile(projectRoot) {
2095
1483
  }
2096
1484
  }
2097
1485
  async function inspectMeta(projectRoot) {
2098
- const metaPath = join8(projectRoot, ".fabric", "agents.meta.json");
1486
+ const metaPath = join5(projectRoot, ".fabric", "agents.meta.json");
2099
1487
  const built = await tryBuildRuleMeta(projectRoot);
2100
1488
  try {
2101
- const raw = await readFile7(metaPath, "utf8");
1489
+ const raw = await readFile5(metaPath, "utf8");
2102
1490
  const meta = agentsMetaSchema4.parse(JSON.parse(raw));
2103
1491
  const contentRefIssues = inspectContentRefs(projectRoot, meta);
2104
1492
  const changed = built === null ? false : built.changed;
@@ -2108,7 +1496,10 @@ async function inspectMeta(projectRoot) {
2108
1496
  meta,
2109
1497
  revision: meta.revision,
2110
1498
  computedRevision: built?.meta.revision ?? null,
2111
- 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,
2112
1503
  missingContentRefs: contentRefIssues.missing,
2113
1504
  invalidContentRefs: contentRefIssues.invalid,
2114
1505
  stale: changed || built !== null && meta.revision !== built.meta.revision,
@@ -2156,17 +1547,16 @@ function inspectContentRefs(projectRoot, meta) {
2156
1547
  const invalid = [];
2157
1548
  for (const node of Object.values(meta.nodes)) {
2158
1549
  const contentRef = normalizePath(node.content_ref ?? node.file);
2159
- if (contentRef === ".fabric/bootstrap/README.md") {
2160
- if (!existsSync4(join8(projectRoot, contentRef))) {
2161
- missing.push(contentRef);
2162
- }
1550
+ const isPersonalKnowledge = contentRef.startsWith("~/.fabric/knowledge/");
1551
+ const isTeamKnowledge = contentRef.startsWith(".fabric/knowledge/");
1552
+ if (!isPersonalKnowledge && !isTeamKnowledge) {
1553
+ invalid.push(contentRef);
2163
1554
  continue;
2164
1555
  }
2165
- if (!contentRef.startsWith(".fabric/rules/")) {
2166
- invalid.push(contentRef);
1556
+ if (isPersonalKnowledge) {
2167
1557
  continue;
2168
1558
  }
2169
- if (!existsSync4(join8(projectRoot, contentRef))) {
1559
+ if (!existsSync4(join5(projectRoot, contentRef))) {
2170
1560
  missing.push(contentRef);
2171
1561
  }
2172
1562
  }
@@ -2181,7 +1571,7 @@ async function inspectEventLedger(projectRoot) {
2181
1571
  try {
2182
1572
  await access(path, constants.W_OK);
2183
1573
  const { warnings } = await readEventLedger(projectRoot);
2184
- const raw = await readFile7(path, "utf8");
1574
+ const raw = await readFile5(path, "utf8");
2185
1575
  const invalidLine = raw.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).find((line) => !isValidJsonLine(line));
2186
1576
  const partialWarning = warnings.find((w) => w.kind === "partial_write_at_tail");
2187
1577
  return {
@@ -2207,29 +1597,11 @@ async function inspectEventLedger(projectRoot) {
2207
1597
  };
2208
1598
  }
2209
1599
  }
2210
- async function inspectRuleSections(projectRoot) {
2211
- const invalidFiles = [];
2212
- const files = findRuleFiles2(projectRoot);
2213
- for (const file of files) {
2214
- try {
2215
- parseRuleSections(await readFile7(join8(projectRoot, file), "utf8"));
2216
- } catch (error) {
2217
- invalidFiles.push({
2218
- file,
2219
- reason: error instanceof Error ? error.message : String(error)
2220
- });
2221
- }
2222
- }
2223
- return {
2224
- checkedCount: files.length,
2225
- invalidFiles
2226
- };
2227
- }
2228
1600
  async function inspectRuleTestIndex(projectRoot) {
2229
- const path = join8(projectRoot, ".fabric", "rule-test.index.json");
1601
+ const path = join5(projectRoot, ".fabric", "rule-test.index.json");
2230
1602
  const built = await tryBuildRuleMeta(projectRoot);
2231
1603
  try {
2232
- const index = ruleTestIndexSchema2.parse(JSON.parse(await readFile7(path, "utf8")));
1604
+ const index = ruleTestIndexSchema2.parse(JSON.parse(await readFile5(path, "utf8")));
2233
1605
  return {
2234
1606
  present: true,
2235
1607
  valid: true,
@@ -2248,17 +1620,56 @@ async function inspectRuleTestIndex(projectRoot) {
2248
1620
  };
2249
1621
  }
2250
1622
  }
2251
- function createBootstrapCheck(exists) {
2252
- if (!exists) {
2253
- 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
+ );
2254
1639
  }
2255
- 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}.`);
2256
1645
  }
2257
- function createTaxonomyCheck(exists) {
2258
- if (!exists) {
2259
- 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
+ }
2260
1654
  }
2261
- return okCheck("Initial taxonomy", ".fabric/INITIAL_TAXONOMY.md exists.");
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
+ );
1668
+ }
1669
+ return okCheck(
1670
+ "Knowledge layout",
1671
+ `All ${KNOWLEDGE_SUBDIRS2.length} required .fabric/knowledge/* subdirectories exist.`
1672
+ );
2262
1673
  }
2263
1674
  function createForensicCheck(forensic, frameworkKind, entryPointCount) {
2264
1675
  if (!forensic.present) {
@@ -2276,18 +1687,9 @@ function createForensicCheck(forensic, frameworkKind, entryPointCount) {
2276
1687
  }
2277
1688
  return okCheck("Scan evidence", `.fabric/forensic.json is valid for ${forensic.report?.framework.kind ?? "unknown"}.`);
2278
1689
  }
2279
- function createInitContextCheck(initContext) {
2280
- if (!initContext.exists) {
2281
- 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.");
2282
- }
2283
- if (!initContext.validJson) {
2284
- 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.");
2285
- }
2286
- return okCheck("Init context", ".fabric/init-context.json is valid JSON.");
2287
- }
2288
1690
  function createMetaCheck(meta) {
2289
1691
  if (!meta.present) {
2290
- 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/.");
2291
1693
  }
2292
1694
  if (!meta.valid) {
2293
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.");
@@ -2298,11 +1700,11 @@ function createMetaCheck(meta) {
2298
1700
  "error",
2299
1701
  "fixable_error",
2300
1702
  "agents_meta_stale",
2301
- `.fabric/agents.meta.json revision ${meta.revision} does not match .fabric/rules derived revision ${meta.computedRevision ?? "<unknown>"}.`,
2302
- "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."
2303
1705
  );
2304
1706
  }
2305
- 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.`);
2306
1708
  }
2307
1709
  function createRuleContentRefCheck(meta) {
2308
1710
  if (!meta.valid) {
@@ -2314,8 +1716,8 @@ function createRuleContentRefCheck(meta) {
2314
1716
  "error",
2315
1717
  "manual_error",
2316
1718
  "content_ref_outside_rules",
2317
- `${meta.invalidContentRefs.length} content_ref entr${meta.invalidContentRefs.length === 1 ? "y is" : "ies are"} outside .fabric/rules.`,
2318
- "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)."
2319
1721
  );
2320
1722
  }
2321
1723
  if (meta.missingContentRefs.length > 0) {
@@ -2325,23 +1727,10 @@ function createRuleContentRefCheck(meta) {
2325
1727
  "fixable_error",
2326
1728
  "content_ref_missing",
2327
1729
  `${meta.missingContentRefs.length} content_ref target${meta.missingContentRefs.length === 1 ? "" : "s"} are missing. Run \`fab doctor --fix\` to reconcile.`,
2328
- "Run `fab doctor --fix` to reconcile agents.meta.json with the files present in .fabric/rules/."
2329
- );
2330
- }
2331
- return okCheck("Rule content refs", "All content_ref entries resolve to .fabric/rules files or bootstrap README.");
2332
- }
2333
- function createRuleSectionsCheck(snapshot) {
2334
- if (snapshot.invalidFiles.length > 0) {
2335
- return issueCheck(
2336
- "Rule sections",
2337
- "error",
2338
- "manual_error",
2339
- "rule_sections_invalid",
2340
- `${snapshot.invalidFiles.length} rule file${snapshot.invalidFiles.length === 1 ? "" : "s"} could not be parsed.`,
2341
- "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/."
2342
1731
  );
2343
1732
  }
2344
- 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.");
2345
1734
  }
2346
1735
  function createRuleTestIndexCheck(index) {
2347
1736
  if (!index.present) {
@@ -2425,13 +1814,13 @@ function findIssue(issues, code) {
2425
1814
  };
2426
1815
  }
2427
1816
  async function inspectMetaManuallyDiverged(projectRoot) {
2428
- const metaPath = join8(projectRoot, ".fabric", "agents.meta.json");
1817
+ const metaPath = join5(projectRoot, ".fabric", "agents.meta.json");
2429
1818
  if (!existsSync4(metaPath)) {
2430
1819
  return { extraMetaEntries: [], hashMismatchEntries: [], readable: false };
2431
1820
  }
2432
1821
  let meta;
2433
1822
  try {
2434
- const raw = await readFile7(metaPath, "utf8");
1823
+ const raw = await readFile5(metaPath, "utf8");
2435
1824
  meta = agentsMetaSchema4.parse(JSON.parse(raw));
2436
1825
  } catch (error) {
2437
1826
  return {
@@ -2445,7 +1834,7 @@ async function inspectMetaManuallyDiverged(projectRoot) {
2445
1834
  const hashMismatchEntries = [];
2446
1835
  for (const node of Object.values(meta.nodes)) {
2447
1836
  const contentRef = node.content_ref ?? node.file;
2448
- const absPath = join8(projectRoot, contentRef);
1837
+ const absPath = join5(projectRoot, contentRef);
2449
1838
  if (!existsSync4(absPath)) {
2450
1839
  extraMetaEntries.push(contentRef);
2451
1840
  continue;
@@ -2462,87 +1851,89 @@ async function inspectMetaManuallyDiverged(projectRoot) {
2462
1851
  }
2463
1852
  return { extraMetaEntries, hashMismatchEntries, readable: true };
2464
1853
  }
2465
- function inspectRulesDirUnindexed(projectRoot, meta) {
2466
- const rulesDir = join8(projectRoot, ".fabric", "rules");
2467
- 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) {
2468
1858
  return { unindexedFiles: [] };
2469
1859
  }
2470
- const physicalMdFiles = /* @__PURE__ */ new Set();
2471
- 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];
2472
1875
  while (stack.length > 0) {
2473
1876
  const dir = stack.pop();
2474
1877
  if (dir === void 0) {
2475
1878
  continue;
2476
1879
  }
2477
1880
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
2478
- const abs = join8(dir, entry.name);
1881
+ const abs = join5(dir, entry.name);
2479
1882
  if (entry.isDirectory()) {
2480
1883
  stack.push(abs);
2481
1884
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
2482
- const rel = posix2.join(".fabric/rules", abs.slice(rulesDir.length + 1).replace(/\\/gu, "/"));
2483
- physicalMdFiles.add(rel);
1885
+ const rel = posix.join(relPrefix, abs.slice(rootDir.length + 1).replace(/\\/gu, "/"));
1886
+ out.add(rel);
2484
1887
  }
2485
1888
  }
2486
1889
  }
2487
- const indexedRefs = /* @__PURE__ */ new Set();
2488
- if (meta.valid && meta.meta !== null) {
2489
- for (const node of Object.values(meta.meta.nodes)) {
2490
- const ref = normalizePath(node.content_ref ?? node.file);
2491
- indexedRefs.add(ref);
2492
- }
2493
- }
2494
- const unindexedFiles = [...physicalMdFiles].filter((f) => !indexedRefs.has(f)).sort();
2495
- return { unindexedFiles };
2496
1890
  }
2497
- function createRulesDirUnindexedCheck(inspection) {
1891
+ function createKnowledgeDirUnindexedCheck(inspection) {
2498
1892
  if (inspection.unindexedFiles.length > 0) {
2499
1893
  return issueCheck(
2500
- "Rules dir unindexed",
1894
+ "Knowledge dir unindexed",
2501
1895
  "error",
2502
1896
  "fixable_error",
2503
- "rules_dir_unindexed",
2504
- `${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.`,
2505
- "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."
2506
1900
  );
2507
1901
  }
2508
- 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.");
2509
1903
  }
2510
1904
  async function inspectStableIdCollisions(projectRoot) {
2511
- const rulesDir = join8(projectRoot, ".fabric", "rules");
2512
- if (!existsSync4(rulesDir)) {
2513
- return { collisions: [] };
2514
- }
2515
- const mdFiles = [];
2516
- const stack = [rulesDir];
2517
- while (stack.length > 0) {
2518
- const dir = stack.pop();
2519
- if (dir === void 0) {
2520
- continue;
2521
- }
2522
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
2523
- const abs = join8(dir, entry.name);
2524
- if (entry.isDirectory()) {
2525
- stack.push(abs);
2526
- } else if (entry.isFile() && entry.name.endsWith(".md")) {
2527
- 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
+ }
2528
1932
  }
2529
1933
  }
2530
1934
  }
2531
1935
  const stableIdToFiles = /* @__PURE__ */ new Map();
2532
- 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;
2533
- for (const absPath of mdFiles) {
2534
- let source;
2535
- try {
2536
- source = await readFile7(absPath, "utf8");
2537
- } catch {
2538
- continue;
2539
- }
2540
- const match = DECLARED_ID_PATTERN.exec(source);
2541
- if (match === null) {
2542
- continue;
2543
- }
2544
- const stableId = match[1];
2545
- const relPath = posix2.join(".fabric/rules", absPath.slice(rulesDir.length + 1).replace(/\\/gu, "/"));
1936
+ for (const { stableId, relPath } of found) {
2546
1937
  const existing = stableIdToFiles.get(stableId) ?? [];
2547
1938
  existing.push(relPath);
2548
1939
  stableIdToFiles.set(stableId, existing);
@@ -2555,6 +1946,85 @@ async function inspectStableIdCollisions(projectRoot) {
2555
1946
  }
2556
1947
  return { collisions: collisions.sort((a, b) => a.stable_id.localeCompare(b.stable_id)) };
2557
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
+ }
2558
2028
  function createStableIdCollisionCheck(inspection) {
2559
2029
  if (inspection.collisions.length > 0) {
2560
2030
  const first = inspection.collisions[0];
@@ -2564,11 +2034,11 @@ function createStableIdCollisionCheck(inspection) {
2564
2034
  "warn",
2565
2035
  "warning",
2566
2036
  "stable_id_collision",
2567
- `${detail} Edit one of the rule files to use a unique stable_id.`,
2568
- "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."
2569
2039
  );
2570
2040
  }
2571
- 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/.");
2572
2042
  }
2573
2043
  function createMetaManuallyDivergedCheck(inspection) {
2574
2044
  if (!inspection.readable) {
@@ -2598,7 +2068,7 @@ function createMetaManuallyDivergedCheck(inspection) {
2598
2068
  }
2599
2069
  function inspectPreexistingRootFiles(projectRoot) {
2600
2070
  const candidates = ["CLAUDE.md", "AGENTS.md"];
2601
- const detected = candidates.filter((name) => existsSync4(join8(projectRoot, name)));
2071
+ const detected = candidates.filter((name) => existsSync4(join5(projectRoot, name)));
2602
2072
  return { detected };
2603
2073
  }
2604
2074
  function createPreexistingRootFilesCheck(inspection) {
@@ -2612,380 +2082,463 @@ function createPreexistingRootFilesCheck(inspection) {
2612
2082
  code: "preexisting_root_claude_md",
2613
2083
  fixable: false,
2614
2084
  message: `${inspection.detected.join(", ")} detected at project root. These root files are not auto-loaded by Fabric MCP.`,
2615
- actionHint: "Move rule content to `.fabric/rules/` if you want it available in MCP responses."
2085
+ actionHint: "Move knowledge content to `.fabric/knowledge/{type}/` if you want it available in MCP responses."
2616
2086
  };
2617
2087
  }
2618
- function inspectClaudeSkillLegacyPath(projectRoot) {
2619
- const legacyPath = join8(projectRoot, ".claude", "skills", "agents-md-init", "SKILL.md");
2620
- const newPath = join8(projectRoot, ".claude", "skills", "fabric-init", "SKILL.md");
2621
- const hasLegacy = existsSync4(legacyPath) && !existsSync4(newPath);
2622
- return { hasLegacy, legacyPath, newPath };
2623
- }
2624
- function createClaudeSkillLegacyPathCheck(inspection) {
2625
- if (inspection.hasLegacy) {
2626
- return issueCheck(
2627
- "Claude skill path",
2628
- "error",
2629
- "fixable_error",
2630
- "claude_skill_legacy_path",
2631
- `.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).`,
2632
- "Run `fab doctor --fix` to rename agents-md-init/ to fabric-init/, preserving any user edits to SKILL.md."
2633
- );
2634
- }
2635
- return okCheck("Claude skill path", ".claude/skills/fabric-init/SKILL.md is at the canonical path (or not present).");
2636
- }
2637
- async function fixClaudeSkillLegacyPath(projectRoot) {
2638
- const legacyPath = join8(projectRoot, ".claude", "skills", "agents-md-init", "SKILL.md");
2639
- const newPath = join8(projectRoot, ".claude", "skills", "fabric-init", "SKILL.md");
2640
- if (!existsSync4(legacyPath)) {
2641
- return;
2642
- }
2643
- mkdirSync(join8(newPath, ".."), { recursive: true });
2644
- renameSync(legacyPath, newPath);
2645
- const legacyDir = join8(legacyPath, "..");
2646
- try {
2647
- rmdirSync(legacyDir);
2648
- } catch {
2649
- }
2650
- await appendEventLedgerEvent(projectRoot, {
2651
- event_type: "claude_skill_path_migrated",
2652
- from: legacyPath,
2653
- to: newPath
2654
- });
2655
- }
2656
- var LEGACY_HOOK_FILENAME = "agents-md-init-reminder.cjs";
2657
- var NEW_HOOK_FILENAME = "fabric-init-reminder.cjs";
2658
- function inspectClaudeHookLegacyPath(projectRoot) {
2659
- const legacyHookPath = join8(projectRoot, ".claude", "hooks", LEGACY_HOOK_FILENAME);
2660
- const newHookPath = join8(projectRoot, ".claude", "hooks", NEW_HOOK_FILENAME);
2661
- const settingsPath = join8(projectRoot, ".claude", "settings.json");
2662
- const hasLegacyFile = existsSync4(legacyHookPath);
2663
- let hasLegacySettingsCommand = false;
2664
- if (existsSync4(settingsPath)) {
2665
- try {
2666
- const raw = readFileSync(settingsPath, "utf8");
2667
- hasLegacySettingsCommand = raw.includes(LEGACY_HOOK_FILENAME);
2668
- } catch {
2669
- }
2670
- }
2671
- return { hasLegacyFile, hasLegacySettingsCommand, legacyHookPath, newHookPath, settingsPath };
2672
- }
2673
- function createClaudeHookLegacyPathCheck(inspection) {
2674
- if (inspection.hasLegacyFile || inspection.hasLegacySettingsCommand) {
2675
- return issueCheck(
2676
- "Claude hook path",
2677
- "error",
2678
- "fixable_error",
2679
- "claude_hook_legacy_path",
2680
- `.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}.`,
2681
- `Run \`fab doctor --fix\` to rename ${LEGACY_HOOK_FILENAME} to ${NEW_HOOK_FILENAME} and update .claude/settings.json hook commands.`
2682
- );
2088
+ async function fixMcpConfigInWrongFile(projectRoot) {
2089
+ const settingsPath = join5(projectRoot, ".claude", "settings.json");
2090
+ if (!existsSync4(settingsPath)) {
2091
+ return;
2683
2092
  }
2684
- return okCheck("Claude hook path", `.claude/hooks/${NEW_HOOK_FILENAME} is at the canonical path (or not present).`);
2685
- }
2686
- async function fixClaudeHookLegacyPath(projectRoot) {
2687
- const { hasLegacyFile, hasLegacySettingsCommand, legacyHookPath, newHookPath, settingsPath } = inspectClaudeHookLegacyPath(projectRoot);
2688
- if (hasLegacyFile) {
2689
- if (existsSync4(newHookPath)) {
2690
- unlinkSync(legacyHookPath);
2691
- } else {
2692
- mkdirSync(join8(newHookPath, ".."), { recursive: true });
2693
- renameSync(legacyHookPath, newHookPath);
2093
+ let settings;
2094
+ try {
2095
+ const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
2096
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
2097
+ return;
2694
2098
  }
2099
+ settings = parsed;
2100
+ } catch {
2101
+ return;
2695
2102
  }
2696
- if (hasLegacySettingsCommand) {
2697
- try {
2698
- const raw = readFileSync(settingsPath, "utf8");
2699
- const updated = raw.split(LEGACY_HOOK_FILENAME).join(NEW_HOOK_FILENAME);
2700
- if (updated !== raw) {
2701
- const parsed = JSON.parse(updated);
2702
- await atomicWriteJson2(settingsPath, parsed);
2703
- }
2704
- } catch {
2705
- }
2103
+ const mcpServers = settings.mcpServers;
2104
+ if (mcpServers === null || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
2105
+ return;
2706
2106
  }
2707
- if (hasLegacyFile || hasLegacySettingsCommand) {
2708
- await appendEventLedgerEvent(projectRoot, {
2709
- event_type: "claude_hook_path_migrated",
2710
- from: legacyHookPath,
2711
- to: newHookPath
2712
- });
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;
2713
2113
  }
2114
+ await atomicWriteJson2(settingsPath, cleaned, { indent: 2 });
2115
+ await appendEventLedgerEvent(projectRoot, {
2116
+ event_type: "mcp_config_migrated",
2117
+ source: "doctor_fix",
2118
+ removed_from: ".claude/settings.json"
2119
+ });
2714
2120
  }
2715
- function inspectCodexSkillLegacyPath(projectRoot) {
2716
- const legacyPath = join8(projectRoot, ".agents", "skills", "fabric-init", "SKILL.md");
2717
- const newPath = join8(projectRoot, ".codex", "skills", "fabric-init", "SKILL.md");
2718
- const hasLegacy = existsSync4(legacyPath) && !existsSync4(newPath);
2719
- return { hasLegacy, legacyPath, newPath };
2720
- }
2721
- function createCodexSkillLegacyPathCheck(inspection) {
2722
- if (inspection.hasLegacy) {
2723
- return issueCheck(
2724
- "Codex skill path",
2725
- "error",
2726
- "fixable_error",
2727
- "codex_skill_legacy_path",
2728
- `.agents/skills/fabric-init/SKILL.md exists at the legacy path. Codex CLI reads repo skills from .codex/skills/, not .agents/skills/. Run --fix to migrate it to .codex/skills/fabric-init/SKILL.md (user edits preserved).`,
2729
- "Run `fab doctor --fix` to move .agents/skills/fabric-init/ to .codex/skills/fabric-init/, preserving any user edits to SKILL.md."
2730
- );
2121
+ async function ensureKnowledgeSubdirs(projectRoot) {
2122
+ for (const sub of KNOWLEDGE_SUBDIRS2) {
2123
+ await mkdir3(join5(projectRoot, ".fabric", "knowledge", sub), { recursive: true });
2731
2124
  }
2732
- return okCheck("Codex skill path", ".codex/skills/fabric-init/SKILL.md is at the canonical path (or not present).");
2733
2125
  }
2734
- async function fixCodexSkillLegacyPath(projectRoot) {
2735
- const { hasLegacy, legacyPath, newPath } = inspectCodexSkillLegacyPath(projectRoot);
2736
- if (!hasLegacy) {
2126
+ async function fixCounterDesync(projectRoot) {
2127
+ const metaPath = join5(projectRoot, ".fabric", "agents.meta.json");
2128
+ if (!existsSync4(metaPath)) {
2737
2129
  return;
2738
2130
  }
2739
- mkdirSync(join8(newPath, ".."), { recursive: true });
2740
- renameSync(legacyPath, newPath);
2741
- const legacyDir = join8(legacyPath, "..");
2131
+ let meta;
2742
2132
  try {
2743
- rmdirSync(legacyDir);
2133
+ meta = agentsMetaSchema4.parse(JSON.parse(await readFile5(metaPath, "utf8")));
2744
2134
  } catch {
2135
+ return;
2745
2136
  }
2746
- try {
2747
- rmdirSync(join8(legacyDir, ".."));
2748
- } catch {
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;
2749
2152
  }
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) {
2750
2167
  try {
2751
- rmdirSync(join8(legacyDir, "..", ".."));
2168
+ JSON.parse(line);
2169
+ return true;
2752
2170
  } catch {
2171
+ return false;
2753
2172
  }
2754
- await appendEventLedgerEvent(projectRoot, {
2755
- event_type: "codex_skill_path_migrated",
2756
- from: legacyPath,
2757
- to: newPath
2758
- });
2759
2173
  }
2760
- function inspectLegacyClientPaths(projectRoot) {
2761
- const configPath = join8(projectRoot, "fabric.config.json");
2762
- if (!existsSync4(configPath)) {
2763
- 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 [];
2764
2183
  }
2765
- try {
2766
- const parsed = JSON.parse(readFileSync(configPath, "utf8"));
2767
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
2768
- 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;
2769
2190
  }
2770
- const config = parsed;
2771
- const clientPaths = config.clientPaths;
2772
- if (clientPaths === null || typeof clientPaths !== "object" || Array.isArray(clientPaths)) {
2773
- 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
+ }
2774
2210
  }
2775
- const cp = clientPaths;
2776
- const presentKeys = LEGACY_CLIENT_PATH_KEYS.filter((key) => key in cp);
2777
- return { presentKeys };
2778
- } catch {
2779
- return { presentKeys: [] };
2780
2211
  }
2212
+ return entries.sort((left, right) => left.path.localeCompare(right.path));
2781
2213
  }
2782
- function createLegacyClientPathCheck(inspection) {
2783
- if (inspection.presentKeys.length > 0) {
2784
- return issueCheck(
2785
- "Legacy client paths",
2786
- "warn",
2787
- "warning",
2788
- "legacy_client_path_present",
2789
- `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.`,
2790
- "Run `fab doctor --fix` to remove deprecated clientPaths keys (windsurf, rooCode, geminiCLI) from fabric.config.json."
2791
- );
2214
+ function getEntryPointReason(relativePath) {
2215
+ const extension = relativePath.slice(relativePath.lastIndexOf("."));
2216
+ if (!SCRIPT_EXTENSIONS.has(extension)) {
2217
+ return null;
2792
2218
  }
2793
- return okCheck("Legacy client paths", "No deprecated clientPaths keys found in fabric.config.json.");
2794
- }
2795
- async function fixLegacyClientPaths(projectRoot) {
2796
- const configPath = join8(projectRoot, "fabric.config.json");
2797
- if (!existsSync4(configPath)) {
2798
- 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";
2799
2224
  }
2800
- let config;
2801
- try {
2802
- const parsed = JSON.parse(readFileSync(configPath, "utf8"));
2803
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
2804
- return;
2805
- }
2806
- config = parsed;
2807
- } catch {
2808
- return;
2225
+ if (directory === "src" && /^(App|app|index|main)$/.test(fileBase)) {
2226
+ return "application entry";
2809
2227
  }
2810
- const clientPaths = config.clientPaths;
2811
- if (clientPaths === null || typeof clientPaths !== "object" || Array.isArray(clientPaths)) {
2812
- return;
2228
+ if ((directory === "app" || directory.startsWith("app/")) && /^(layout|page|route)$/.test(fileBase)) {
2229
+ return "next app route";
2813
2230
  }
2814
- const cp = clientPaths;
2815
- const removed = [];
2816
- for (const key of LEGACY_CLIENT_PATH_KEYS) {
2817
- if (key in cp) {
2818
- delete cp[key];
2819
- removed.push(key);
2820
- }
2231
+ if ((directory === "pages" || directory.startsWith("pages/")) && fileName !== "_app.d.ts") {
2232
+ return "next page route";
2821
2233
  }
2822
- if (removed.length === 0) {
2823
- return;
2234
+ return null;
2235
+ }
2236
+ function reduceStatus(statuses) {
2237
+ if (statuses.includes("error")) {
2238
+ return "error";
2824
2239
  }
2825
- const updatedConfig = { ...config, clientPaths: cp };
2826
- await atomicWriteJson2(configPath, updatedConfig, { indent: 2 });
2827
- await appendEventLedgerEvent(projectRoot, {
2828
- event_type: "legacy_client_path_present",
2829
- 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
2830
2275
  });
2276
+ return entry;
2831
2277
  }
2832
- async function fixMcpConfigInWrongFile(projectRoot) {
2833
- const settingsPath = join8(projectRoot, ".claude", "settings.json");
2834
- if (!existsSync4(settingsPath)) {
2835
- 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("/"));
2836
2306
  }
2837
- 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
+ };
2838
2377
  try {
2839
- const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
2840
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
2841
- return;
2842
- }
2843
- 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
+ });
2844
2387
  } catch {
2845
- return;
2846
2388
  }
2847
- const mcpServers = settings.mcpServers;
2848
- if (mcpServers === null || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
2849
- return;
2850
- }
2851
- const { fabric: _removed, ...remainingServers } = mcpServers;
2852
- const cleaned = { ...settings };
2853
- if (Object.keys(remainingServers).length === 0) {
2854
- delete cleaned.mcpServers;
2855
- } else {
2856
- 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;
2857
2395
  }
2858
- await atomicWriteJson2(settingsPath, cleaned, { indent: 2 });
2859
- await appendEventLedgerEvent(projectRoot, {
2860
- event_type: "mcp_config_migrated",
2861
- source: "doctor_fix",
2862
- removed_from: ".claude/settings.json"
2863
- });
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;
2864
2405
  }
2865
- async function writeDefaultBootstrap(projectRoot) {
2866
- const path = join8(projectRoot, FABRIC_BOOTSTRAP_PATH);
2867
- await ensureParentDirectory(path);
2868
- 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);
2869
2410
  }
2870
- async function ensureEventLedger(projectRoot) {
2871
- const path = getEventLedgerPath(projectRoot);
2872
- await ensureParentDirectory(path);
2873
- await writeFile2(path, "", { encoding: "utf8", flag: "a" });
2411
+ function normalizeRulesPath(value) {
2412
+ return value.replaceAll("\\", "/");
2874
2413
  }
2875
- function createFixMessage(fixed, report) {
2876
- const fixedText = fixed.length === 0 ? "No deterministic doctor fixes were needed." : `Applied ${fixed.length} deterministic doctor fix${fixed.length === 1 ? "" : "es"}.`;
2877
- const manualText = report.manual_errors.length === 0 ? "No manual errors remain." : `${report.manual_errors.length} manual error${report.manual_errors.length === 1 ? "" : "s"} remain.`;
2878
- 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
+ }));
2879
2428
  }
2880
- function findRuleFiles2(projectRoot) {
2881
- const rulesRoot = join8(projectRoot, ".fabric", "rules");
2882
- if (!existsSync4(rulesRoot) || !statSync3(rulesRoot).isDirectory()) {
2883
- return [];
2884
- }
2885
- const files = [];
2886
- const stack = [rulesRoot];
2887
- while (stack.length > 0) {
2888
- const current = stack.pop();
2889
- 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) {
2890
2434
  continue;
2891
2435
  }
2892
- for (const entry of readdirSync(current, { withFileTypes: true })) {
2893
- const absolutePath = join8(current, entry.name);
2894
- const relativePath = normalizePath(absolutePath.slice(projectRoot.length + 1));
2895
- if (entry.isDirectory()) {
2896
- stack.push(absolutePath);
2897
- } else if (entry.isFile() && entry.name.endsWith(".md")) {
2898
- files.push(relativePath);
2899
- }
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;
2900
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
+ });
2901
2455
  }
2902
- return files.sort();
2903
- }
2904
- function isValidJsonLine(line) {
2905
- try {
2906
- JSON.parse(line);
2907
- return true;
2908
- } catch {
2909
- return false;
2910
- }
2911
- }
2912
- function normalizeTarget(targetInput) {
2913
- return isAbsolute3(targetInput) ? targetInput : resolve4(process.cwd(), targetInput);
2456
+ return { rules, stubs };
2914
2457
  }
2915
- function normalizePath(path) {
2916
- 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
+ };
2917
2467
  }
2918
- function collectEntryPoints(root) {
2919
- if (!existsSync4(root) || !statSync3(root).isDirectory()) {
2920
- return [];
2468
+ function classifyNode(nodeId, node) {
2469
+ if (nodeId.startsWith("L1/")) {
2470
+ return "L1";
2921
2471
  }
2922
- const entries = [];
2923
- const stack = [root];
2924
- while (stack.length > 0) {
2925
- const current = stack.pop();
2926
- 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);
2927
2483
  continue;
2928
2484
  }
2929
- for (const entry of readdirSync(current, { withFileTypes: true })) {
2930
- const absolutePath = join8(current, entry.name);
2931
- const relativePath = normalizePath(absolutePath.slice(root.length + 1));
2932
- if (relativePath.length === 0) {
2933
- continue;
2934
- }
2935
- if (entry.isDirectory()) {
2936
- if (!IGNORED_DIRECTORIES.has(entry.name)) {
2937
- stack.push(absolutePath);
2938
- }
2939
- continue;
2940
- }
2941
- if (!entry.isFile()) {
2942
- continue;
2943
- }
2944
- const reason = getEntryPointReason(relativePath);
2945
- if (reason !== null) {
2946
- entries.push({ path: relativePath, reason });
2947
- }
2485
+ if (rule.level === "L2") {
2486
+ l2.push(rule.entry);
2948
2487
  }
2949
2488
  }
2950
- 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
+ };
2951
2493
  }
2952
- function getEntryPointReason(relativePath) {
2953
- const extension = relativePath.slice(relativePath.lastIndexOf("."));
2954
- if (!SCRIPT_EXTENSIONS.has(extension)) {
2955
- return null;
2956
- }
2957
- const directory = posix2.dirname(relativePath);
2958
- const fileName = posix2.basename(relativePath);
2959
- const fileBase = fileName.slice(0, Math.max(fileName.lastIndexOf("."), 0));
2960
- if (directory === "assets/scripts" || directory === "scripts") {
2961
- return "top-level script";
2962
- }
2963
- if (directory === "src" && /^(App|app|index|main)$/.test(fileBase)) {
2964
- return "application entry";
2965
- }
2966
- if ((directory === "app" || directory.startsWith("app/")) && /^(layout|page|route)$/.test(fileBase)) {
2967
- return "next app route";
2968
- }
2969
- if ((directory === "pages" || directory.startsWith("pages/")) && fileName !== "_app.d.ts") {
2970
- return "next page route";
2971
- }
2972
- 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
+ });
2973
2503
  }
2974
- function reduceStatus(statuses) {
2975
- if (statuses.includes("error")) {
2976
- return "error";
2977
- }
2978
- if (statuses.includes("warn")) {
2979
- 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 });
2980
2513
  }
2981
- return "ok";
2982
2514
  }
2983
- function isMissingFileError(error) {
2984
- 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;
2985
2539
  }
2986
2540
 
2987
2541
  export {
2988
- AGENTS_MD_RESOURCE_URI,
2989
2542
  contextCache,
2990
2543
  resolveProjectRoot,
2991
2544
  readAgentsMeta,
@@ -3011,9 +2564,9 @@ export {
3011
2564
  invalidateRuleSyncCooldown,
3012
2565
  ensureRulesFresh,
3013
2566
  reconcileRules,
2567
+ appendRuleSelectionAuditEvent,
3014
2568
  getRules,
3015
- planContext,
3016
- getRuleSections,
2569
+ normalizeRulesPath,
3017
2570
  runDoctorReport,
3018
2571
  runDoctorFix
3019
2572
  };