@fenglimg/fabric-server 1.8.0-rc.2 → 2.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-EGGZFXMO.js → chunk-NRWDWAVO.js} +945 -1341
- package/dist/{http-Q7GIL23Y.js → http-CHCOF6DJ.js} +8 -19
- package/dist/index.d.ts +45 -7
- package/dist/index.js +564 -16
- package/dist/static/assets/index-DNSKn-El.js +10 -0
- package/dist/static/index.html +1 -1
- package/package.json +3 -3
- package/dist/static/assets/index-DeTFBeTM.js +0 -10
|
@@ -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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
}
|
|
486
|
-
|
|
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\/
|
|
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\/
|
|
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: "
|
|
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
|
|
872
|
-
if (!existsSync3(
|
|
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 = [
|
|
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: "
|
|
1121
|
+
event_type: "knowledge_drift_detected",
|
|
1010
1122
|
drifted_stable_ids: driftedIds,
|
|
1011
1123
|
missing_files: missingFiles,
|
|
1012
1124
|
stale_files: staleFiles
|
|
@@ -1142,844 +1254,148 @@ async function reconcileRules(projectRoot, opts) {
|
|
|
1142
1254
|
}
|
|
1143
1255
|
|
|
1144
1256
|
// src/services/doctor.ts
|
|
1145
|
-
import { existsSync as existsSync4,
|
|
1146
|
-
import { access, readFile as
|
|
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
|
|
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
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
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
|
|
1207
|
-
const
|
|
1208
|
-
const
|
|
1209
|
-
const
|
|
1210
|
-
if (
|
|
1211
|
-
|
|
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
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
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
|
-
|
|
1266
|
-
|
|
1267
|
-
import { join as join6 } from "path";
|
|
1268
|
-
import { minimatch } from "minimatch";
|
|
1269
|
-
var PRIORITY_ORDER = {
|
|
1270
|
-
high: 0,
|
|
1271
|
-
medium: 1,
|
|
1272
|
-
low: 2
|
|
1273
|
-
};
|
|
1274
|
-
async function getRules(projectRoot, input) {
|
|
1275
|
-
const context = await loadGetRulesContext(projectRoot);
|
|
1276
|
-
const stale = input.client_hash !== void 0 && input.client_hash !== context.meta.revision;
|
|
1277
|
-
const matchedNodes = matchRuleNodes(context.meta, input.path);
|
|
1278
|
-
const requiredStableIds = matchedNodes.filter((node) => node.level === "L2").map((node) => node.stable_id);
|
|
1279
|
-
const aiSelectableStableIds = matchedNodes.filter((node) => node.level === "L1").map((node) => node.stable_id);
|
|
1280
|
-
const rules = await resolveRulesForPath(projectRoot, context, input.path);
|
|
1281
|
-
const result = {
|
|
1282
|
-
revision_hash: context.meta.revision,
|
|
1283
|
-
stale,
|
|
1284
|
-
rules
|
|
1285
|
-
};
|
|
1286
|
-
try {
|
|
1287
|
-
await appendGetRulesAuditEvent(projectRoot, {
|
|
1288
|
-
path: input.path,
|
|
1289
|
-
client_hash: input.client_hash,
|
|
1290
|
-
required_stable_ids: requiredStableIds,
|
|
1291
|
-
ai_selectable_stable_ids: aiSelectableStableIds,
|
|
1292
|
-
final_stable_ids: [...requiredStableIds, ...aiSelectableStableIds],
|
|
1293
|
-
correlation_id: input.correlation_id,
|
|
1294
|
-
session_id: input.session_id
|
|
1295
|
-
});
|
|
1296
|
-
} catch {
|
|
1297
|
-
}
|
|
1298
|
-
return result;
|
|
1299
|
-
}
|
|
1300
|
-
async function loadGetRulesContext(projectRoot) {
|
|
1301
|
-
const cached = contextCache.get("context", projectRoot);
|
|
1302
|
-
if (cached !== void 0) {
|
|
1303
|
-
return cached;
|
|
1304
|
-
}
|
|
1305
|
-
const meta = await readAgentsMeta(projectRoot);
|
|
1306
|
-
const l0Content = await readFile5(join6(projectRoot, ".fabric", "bootstrap", "README.md"), "utf8");
|
|
1307
|
-
const context = {
|
|
1308
|
-
meta,
|
|
1309
|
-
l0Content,
|
|
1310
|
-
humanLockedNearby: []
|
|
1311
|
-
};
|
|
1312
|
-
contextCache.set("context", projectRoot, context);
|
|
1313
|
-
return context;
|
|
1314
|
-
}
|
|
1315
|
-
async function resolveRulesForPath(projectRoot, context, path, options = {}) {
|
|
1316
|
-
const matchedNodes = matchRuleNodes(context.meta, path);
|
|
1317
|
-
const loaded = await loadMatchedRules(projectRoot, matchedNodes);
|
|
1318
|
-
return buildRulesPayload(context, loaded, options);
|
|
1319
|
-
}
|
|
1320
|
-
function normalizeRulesPath(value) {
|
|
1321
|
-
return value.replaceAll("\\", "/");
|
|
1322
|
-
}
|
|
1323
|
-
function matchRuleNodes(meta, path) {
|
|
1324
|
-
const requestedPath = normalizeRulesPath(path);
|
|
1325
|
-
return Object.entries(meta.nodes).filter(([, node]) => shouldLoadNodeForPath(requestedPath, node)).sort((left, right) => {
|
|
1326
|
-
const [leftId, leftNode] = left;
|
|
1327
|
-
const [rightId, rightNode] = right;
|
|
1328
|
-
const priorityDelta = PRIORITY_ORDER[leftNode.priority] - PRIORITY_ORDER[rightNode.priority];
|
|
1329
|
-
return priorityDelta !== 0 ? priorityDelta : leftId.localeCompare(rightId);
|
|
1330
|
-
}).map(([nodeId, node]) => ({
|
|
1331
|
-
node_id: nodeId,
|
|
1332
|
-
level: classifyNode(nodeId, node),
|
|
1333
|
-
stable_id: node.stable_id ?? nodeId,
|
|
1334
|
-
identity_source: node.identity_source ?? "derived",
|
|
1335
|
-
node
|
|
1336
|
-
}));
|
|
1337
|
-
}
|
|
1338
|
-
async function loadMatchedRules(projectRoot, matchedNodes, fileContentCache = /* @__PURE__ */ new Map()) {
|
|
1339
|
-
const rules = [];
|
|
1340
|
-
const stubs = [];
|
|
1341
|
-
for (const matchedNode of matchedNodes) {
|
|
1342
|
-
if (matchedNode.level === null) {
|
|
1343
|
-
continue;
|
|
1344
|
-
}
|
|
1345
|
-
if (matchedNode.node.activation?.tier === "description") {
|
|
1346
|
-
stubs.push({
|
|
1347
|
-
stable_id: matchedNode.stable_id,
|
|
1348
|
-
identity_source: matchedNode.identity_source,
|
|
1349
|
-
level: matchedNode.level,
|
|
1350
|
-
path: matchedNode.node.file,
|
|
1351
|
-
description: matchedNode.node.activation.description ?? ""
|
|
1352
|
-
});
|
|
1353
|
-
continue;
|
|
1354
|
-
}
|
|
1355
|
-
rules.push({
|
|
1356
|
-
level: matchedNode.level,
|
|
1357
|
-
stable_id: matchedNode.stable_id,
|
|
1358
|
-
identity_source: matchedNode.identity_source,
|
|
1359
|
-
entry: {
|
|
1360
|
-
path: matchedNode.node.file,
|
|
1361
|
-
content: await readRuleContent(projectRoot, matchedNode.node.file, fileContentCache)
|
|
1362
|
-
}
|
|
1363
|
-
});
|
|
1364
|
-
}
|
|
1365
|
-
return { rules, stubs };
|
|
1366
|
-
}
|
|
1367
|
-
function buildRulesPayload(context, loaded, options = {}) {
|
|
1368
|
-
const { L1, L2 } = partitionRulesByLevel(loaded.rules, options.dedupeByPath ?? false);
|
|
1369
|
-
return {
|
|
1370
|
-
L0: context.l0Content,
|
|
1371
|
-
L1,
|
|
1372
|
-
L2,
|
|
1373
|
-
human_locked_nearby: context.humanLockedNearby,
|
|
1374
|
-
description_stubs: loaded.stubs.length > 0 ? dedupeDescriptionStubsByPath(loaded.stubs).map(toDescriptionStub) : void 0
|
|
1375
|
-
};
|
|
1376
|
-
}
|
|
1377
|
-
function classifyNode(nodeId, node) {
|
|
1378
|
-
if (nodeId.startsWith("L1/")) {
|
|
1379
|
-
return "L1";
|
|
1380
|
-
}
|
|
1381
|
-
if (nodeId.startsWith("L2/")) {
|
|
1382
|
-
return "L2";
|
|
1383
|
-
}
|
|
1384
|
-
return node.layer === "L0" ? null : node.layer;
|
|
1385
|
-
}
|
|
1386
|
-
function partitionRulesByLevel(loadedRules, dedupeByPath) {
|
|
1387
|
-
const l1 = [];
|
|
1388
|
-
const l2 = [];
|
|
1389
|
-
for (const rule of loadedRules) {
|
|
1390
|
-
if (rule.level === "L1") {
|
|
1391
|
-
l1.push(rule.entry);
|
|
1392
|
-
continue;
|
|
1393
|
-
}
|
|
1394
|
-
if (rule.level === "L2") {
|
|
1395
|
-
l2.push(rule.entry);
|
|
1396
|
-
}
|
|
1397
|
-
}
|
|
1398
|
-
return {
|
|
1399
|
-
L1: dedupeByPath ? dedupeEntriesByPath(l1) : l1,
|
|
1400
|
-
L2: dedupeByPath ? dedupeEntriesByPath(l2) : l2
|
|
1401
|
-
};
|
|
1402
|
-
}
|
|
1403
|
-
function dedupeEntriesByPath(entries) {
|
|
1404
|
-
const seenPaths = /* @__PURE__ */ new Set();
|
|
1405
|
-
return entries.filter((entry) => {
|
|
1406
|
-
if (seenPaths.has(entry.path)) {
|
|
1407
|
-
return false;
|
|
1408
|
-
}
|
|
1409
|
-
seenPaths.add(entry.path);
|
|
1410
|
-
return true;
|
|
1411
|
-
});
|
|
1412
|
-
}
|
|
1413
|
-
function shouldLoadNodeForPath(requestedPath, node) {
|
|
1414
|
-
switch (node.activation?.tier) {
|
|
1415
|
-
case "always":
|
|
1416
|
-
return true;
|
|
1417
|
-
case "description":
|
|
1418
|
-
return true;
|
|
1419
|
-
case "path":
|
|
1420
|
-
case void 0:
|
|
1421
|
-
return minimatch(requestedPath, normalizeRulesPath(node.scope_glob), { dot: true });
|
|
1422
|
-
}
|
|
1423
|
-
}
|
|
1424
|
-
function dedupeDescriptionStubsByPath(stubs) {
|
|
1425
|
-
const seenPaths = /* @__PURE__ */ new Set();
|
|
1426
|
-
return stubs.filter((stub) => {
|
|
1427
|
-
if (seenPaths.has(stub.path)) {
|
|
1428
|
-
return false;
|
|
1429
|
-
}
|
|
1430
|
-
seenPaths.add(stub.path);
|
|
1431
|
-
return true;
|
|
1432
|
-
});
|
|
1433
|
-
}
|
|
1434
|
-
function toDescriptionStub(stub) {
|
|
1435
|
-
return {
|
|
1436
|
-
path: stub.path,
|
|
1437
|
-
description: stub.description
|
|
1438
|
-
};
|
|
1439
|
-
}
|
|
1440
|
-
async function readRuleContent(projectRoot, file, fileContentCache) {
|
|
1441
|
-
const cached = fileContentCache.get(file);
|
|
1442
|
-
if (cached !== void 0) {
|
|
1443
|
-
return await cached;
|
|
1444
|
-
}
|
|
1445
|
-
const pending = readFile5(join6(projectRoot, file), "utf8");
|
|
1446
|
-
fileContentCache.set(file, pending);
|
|
1447
|
-
return await pending;
|
|
1448
|
-
}
|
|
1449
|
-
|
|
1450
|
-
// src/services/plan-context.ts
|
|
1451
|
-
var SELECTION_TOKEN_TTL_MS = 5 * 60 * 1e3;
|
|
1452
|
-
var selectionTokenCache = /* @__PURE__ */ new Map();
|
|
1453
|
-
async function planContext(projectRoot, input) {
|
|
1454
|
-
const meta = await readAgentsMeta(projectRoot);
|
|
1455
|
-
const stale = input.client_hash !== void 0 && input.client_hash !== meta.revision;
|
|
1456
|
-
const uniquePaths = dedupePaths(input.paths);
|
|
1457
|
-
const allDescriptions = buildDescriptionIndex(meta);
|
|
1458
|
-
const entries = uniquePaths.map((path) => {
|
|
1459
|
-
const profile = buildRequirementProfile(path, input);
|
|
1460
|
-
const descriptionIndex = allDescriptions.filter((item) => shouldIncludeIndexItemForPath(item, meta, path));
|
|
1461
|
-
const requiredStableIds2 = descriptionIndex.filter((item) => item.required).map((item) => item.stable_id);
|
|
1462
|
-
const aiSelectableStableIds2 = descriptionIndex.filter((item) => item.selectable).map((item) => item.stable_id);
|
|
1463
|
-
return {
|
|
1464
|
-
path,
|
|
1465
|
-
requirement_profile: profile,
|
|
1466
|
-
description_index: descriptionIndex,
|
|
1467
|
-
required_stable_ids: requiredStableIds2,
|
|
1468
|
-
ai_selectable_stable_ids: aiSelectableStableIds2,
|
|
1469
|
-
initial_selected_stable_ids: requiredStableIds2,
|
|
1470
|
-
selection_policy: {
|
|
1471
|
-
required_levels: ["L0", "L2"],
|
|
1472
|
-
ai_selectable_levels: ["L1"],
|
|
1473
|
-
final_fetch_rule: "required_stable_ids + ai_selected_l1_stable_ids"
|
|
1474
|
-
}
|
|
1475
|
-
};
|
|
1476
|
-
});
|
|
1477
|
-
const requiredStableIds = dedupeStableIds(entries.flatMap((entry) => entry.required_stable_ids));
|
|
1478
|
-
const aiSelectableStableIds = dedupeStableIds(entries.flatMap((entry) => entry.ai_selectable_stable_ids));
|
|
1479
|
-
const sharedDescriptionIndex = dedupeDescriptionIndex(entries.flatMap((entry) => entry.description_index));
|
|
1480
|
-
const selectionToken = createSelectionToken(meta.revision, uniquePaths, requiredStableIds, aiSelectableStableIds);
|
|
1481
|
-
const result = {
|
|
1482
|
-
revision_hash: meta.revision,
|
|
1483
|
-
stale,
|
|
1484
|
-
selection_token: selectionToken,
|
|
1485
|
-
entries,
|
|
1486
|
-
shared: {
|
|
1487
|
-
required_stable_ids: requiredStableIds,
|
|
1488
|
-
ai_selectable_stable_ids: aiSelectableStableIds,
|
|
1489
|
-
description_index: sharedDescriptionIndex,
|
|
1490
|
-
preflight_diagnostics: buildPreflightDiagnostics(meta)
|
|
1491
|
-
}
|
|
1492
|
-
};
|
|
1493
|
-
try {
|
|
1494
|
-
await appendEventLedgerEvent(projectRoot, {
|
|
1495
|
-
event_type: "rule_context_planned",
|
|
1496
|
-
target_paths: uniquePaths,
|
|
1497
|
-
required_stable_ids: requiredStableIds,
|
|
1498
|
-
ai_selectable_stable_ids: aiSelectableStableIds,
|
|
1499
|
-
final_stable_ids: requiredStableIds,
|
|
1500
|
-
selection_token: selectionToken,
|
|
1501
|
-
client_hash: input.client_hash,
|
|
1502
|
-
intent: input.intent,
|
|
1503
|
-
known_tech: input.known_tech,
|
|
1504
|
-
diagnostics: result.shared.preflight_diagnostics,
|
|
1505
|
-
correlation_id: input.correlation_id,
|
|
1506
|
-
session_id: input.session_id
|
|
1507
|
-
});
|
|
1508
|
-
} catch {
|
|
1509
|
-
}
|
|
1510
|
-
return result;
|
|
1511
|
-
}
|
|
1512
|
-
function readSelectionToken(token, now = Date.now()) {
|
|
1513
|
-
const state = selectionTokenCache.get(token);
|
|
1514
|
-
if (state === void 0) {
|
|
1515
|
-
return void 0;
|
|
1516
|
-
}
|
|
1517
|
-
if (state.expires_at <= now) {
|
|
1518
|
-
selectionTokenCache.delete(token);
|
|
1519
|
-
return void 0;
|
|
1520
|
-
}
|
|
1521
|
-
return state;
|
|
1522
|
-
}
|
|
1523
|
-
function createSelectionToken(revisionHash, targetPaths, requiredStableIds, aiSelectableStableIds, now = Date.now()) {
|
|
1524
|
-
const token = `selection:${revisionHash}:${now.toString(36)}:${Math.random().toString(36).slice(2)}`;
|
|
1525
|
-
selectionTokenCache.set(token, {
|
|
1526
|
-
token,
|
|
1527
|
-
revision_hash: revisionHash,
|
|
1528
|
-
target_paths: targetPaths,
|
|
1529
|
-
required_stable_ids: requiredStableIds,
|
|
1530
|
-
ai_selectable_stable_ids: aiSelectableStableIds,
|
|
1531
|
-
created_at: now,
|
|
1532
|
-
expires_at: now + SELECTION_TOKEN_TTL_MS
|
|
1533
|
-
});
|
|
1534
|
-
return token;
|
|
1535
|
-
}
|
|
1536
|
-
function dedupePaths(paths) {
|
|
1537
|
-
const seenPaths = /* @__PURE__ */ new Set();
|
|
1538
|
-
return paths.flatMap((path) => {
|
|
1539
|
-
const normalizedPath = normalizeRulesPath(path);
|
|
1540
|
-
if (seenPaths.has(normalizedPath)) {
|
|
1541
|
-
return [];
|
|
1542
|
-
}
|
|
1543
|
-
seenPaths.add(normalizedPath);
|
|
1544
|
-
return [normalizedPath];
|
|
1545
|
-
});
|
|
1546
|
-
}
|
|
1547
|
-
function buildRequirementProfile(path, input) {
|
|
1548
|
-
const normalizedPath = normalizeRulesPath(path);
|
|
1549
|
-
const extensionMatch = /(\.[^./\\]+)$/u.exec(normalizedPath);
|
|
1550
|
-
const knownTech = dedupeStableIds([
|
|
1551
|
-
...input.known_tech ?? [],
|
|
1552
|
-
...extensionMatch?.[1] === ".ts" ? ["TypeScript"] : []
|
|
1553
|
-
]);
|
|
1554
|
-
return {
|
|
1555
|
-
target_path: normalizedPath,
|
|
1556
|
-
path_segments: normalizedPath.split("/").filter(Boolean),
|
|
1557
|
-
extension: extensionMatch?.[1] ?? "",
|
|
1558
|
-
inferred_domain: inferDomains(normalizedPath),
|
|
1559
|
-
known_tech: knownTech,
|
|
1560
|
-
user_intent: input.intent ?? "",
|
|
1561
|
-
intent_tokens: tokenizeIntent(input.intent ?? ""),
|
|
1562
|
-
impact_hints: inferImpactHints(input.intent ?? ""),
|
|
1563
|
-
detected_entities: input.detected_entities?.[normalizedPath] ?? input.detected_entities?.[path] ?? []
|
|
1564
|
-
};
|
|
1565
|
-
}
|
|
1566
|
-
function buildDescriptionIndex(meta) {
|
|
1567
|
-
return Object.entries(meta.nodes).flatMap(([nodeId, node]) => {
|
|
1568
|
-
const level = node.level ?? node.layer;
|
|
1569
|
-
const description = node.description ?? descriptionFromLegacyActivation(node.activation?.description);
|
|
1570
|
-
if (description === void 0) {
|
|
1571
|
-
return [];
|
|
1572
|
-
}
|
|
1573
|
-
return [{
|
|
1574
|
-
stable_id: node.stable_id ?? nodeId,
|
|
1575
|
-
level,
|
|
1576
|
-
required: level === "L0" || level === "L2",
|
|
1577
|
-
selectable: level === "L1",
|
|
1578
|
-
description
|
|
1579
|
-
}];
|
|
1580
|
-
}).sort(compareDescriptionIndexItems);
|
|
1581
|
-
}
|
|
1582
|
-
function descriptionFromLegacyActivation(summary) {
|
|
1583
|
-
if (summary === void 0) {
|
|
1584
|
-
return void 0;
|
|
1585
|
-
}
|
|
1586
|
-
return {
|
|
1587
|
-
summary,
|
|
1588
|
-
intent_clues: [],
|
|
1589
|
-
tech_stack: [],
|
|
1590
|
-
impact: [],
|
|
1591
|
-
must_read_if: summary
|
|
1592
|
-
};
|
|
1593
|
-
}
|
|
1594
|
-
function shouldIncludeIndexItemForPath(item, meta, path) {
|
|
1595
|
-
if (item.level === "L0" || item.level === "L1") {
|
|
1596
|
-
return true;
|
|
1597
|
-
}
|
|
1598
|
-
const node = Object.values(meta.nodes).find((candidate) => candidate.stable_id === item.stable_id);
|
|
1599
|
-
if (node === void 0) {
|
|
1600
|
-
return false;
|
|
1601
|
-
}
|
|
1602
|
-
return node.scope_glob === path || minimatchSimple(path, node.scope_glob);
|
|
1603
|
-
}
|
|
1604
|
-
function minimatchSimple(path, glob) {
|
|
1605
|
-
if (glob === "**") {
|
|
1606
|
-
return true;
|
|
1607
|
-
}
|
|
1608
|
-
if (glob.endsWith("/**")) {
|
|
1609
|
-
return path.startsWith(glob.slice(0, -3));
|
|
1610
|
-
}
|
|
1611
|
-
return path === glob;
|
|
1612
|
-
}
|
|
1613
|
-
function buildPreflightDiagnostics(meta) {
|
|
1614
|
-
const missingDescriptionStableIds = Object.entries(meta.nodes).filter(([, node]) => node.description === void 0 && node.activation?.description === void 0).map(([nodeId, node]) => node.stable_id ?? nodeId).sort();
|
|
1615
|
-
if (missingDescriptionStableIds.length === 0) {
|
|
1616
|
-
return [];
|
|
1617
|
-
}
|
|
1618
|
-
return [{
|
|
1619
|
-
code: "missing_description",
|
|
1620
|
-
severity: "warn",
|
|
1621
|
-
stable_ids: missingDescriptionStableIds,
|
|
1622
|
-
message: `Resolved registry includes ${missingDescriptionStableIds.length} node(s) without structured descriptions.`
|
|
1623
|
-
}];
|
|
1624
|
-
}
|
|
1625
|
-
function inferDomains(path) {
|
|
1626
|
-
const domains = [];
|
|
1627
|
-
if (path.includes("/ui/") || path.toLowerCase().includes("ui")) {
|
|
1628
|
-
domains.push("UI");
|
|
1629
|
-
}
|
|
1630
|
-
if (path.includes("assets/scripts")) {
|
|
1631
|
-
domains.push("Gameplay");
|
|
1632
|
-
}
|
|
1633
|
-
if (path.includes("resources") || path.includes("assets/resources")) {
|
|
1634
|
-
domains.push("Asset");
|
|
1635
|
-
}
|
|
1636
|
-
return domains;
|
|
1637
|
-
}
|
|
1638
|
-
function tokenizeIntent(intent) {
|
|
1639
|
-
const tokens = ["\u6027\u80FD", "\u4F18\u5316", "drawcall", "\u6E32\u67D3", "\u5361\u987F", "\u95EA\u70C1", "\u754C\u9762", "UI", "\u8D44\u6E90", "\u56FE\u96C6"].filter((token) => intent.toLowerCase().includes(token.toLowerCase()));
|
|
1640
|
-
return dedupeStableIds(tokens);
|
|
1641
|
-
}
|
|
1642
|
-
function inferImpactHints(intent) {
|
|
1643
|
-
return /性能|优化|drawcall|渲染|卡顿|闪烁/iu.test(intent) ? ["Performance"] : [];
|
|
1644
|
-
}
|
|
1645
|
-
function dedupeStableIds(stableIds) {
|
|
1646
|
-
return Array.from(new Set(stableIds));
|
|
1647
|
-
}
|
|
1648
|
-
function dedupeDescriptionIndex(items) {
|
|
1649
|
-
const seenStableIds = /* @__PURE__ */ new Set();
|
|
1650
|
-
return items.filter((item) => {
|
|
1651
|
-
if (seenStableIds.has(item.stable_id)) {
|
|
1652
|
-
return false;
|
|
1653
|
-
}
|
|
1654
|
-
seenStableIds.add(item.stable_id);
|
|
1655
|
-
return true;
|
|
1656
|
-
});
|
|
1657
|
-
}
|
|
1658
|
-
function compareDescriptionIndexItems(left, right) {
|
|
1659
|
-
const levelDelta = levelOrder(left.level) - levelOrder(right.level);
|
|
1660
|
-
return levelDelta !== 0 ? levelDelta : left.stable_id.localeCompare(right.stable_id);
|
|
1661
|
-
}
|
|
1662
|
-
function levelOrder(level) {
|
|
1663
|
-
switch (level) {
|
|
1664
|
-
case "L0":
|
|
1665
|
-
return 0;
|
|
1666
|
-
case "L1":
|
|
1667
|
-
return 1;
|
|
1668
|
-
case "L2":
|
|
1669
|
-
return 2;
|
|
1670
|
-
}
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
|
-
// src/services/rule-sections.ts
|
|
1674
|
-
var RULE_SECTION_NAMES = [
|
|
1675
|
-
"MISSION_STATEMENT",
|
|
1676
|
-
"MANDATORY_INJECTION",
|
|
1677
|
-
"BUSINESS_LOGIC_CHUNKS",
|
|
1678
|
-
"CONTEXT_INFO"
|
|
1679
|
-
];
|
|
1680
|
-
var PRIORITY_ORDER2 = {
|
|
1681
|
-
high: 0,
|
|
1682
|
-
medium: 1,
|
|
1683
|
-
low: 2
|
|
1684
|
-
};
|
|
1685
|
-
function parseRuleSections(content) {
|
|
1686
|
-
const sections = /* @__PURE__ */ new Map();
|
|
1687
|
-
const lines = content.split(/\r?\n/u);
|
|
1688
|
-
let activeSection;
|
|
1689
|
-
let activeSectionDepth = 0;
|
|
1690
|
-
let buffer = [];
|
|
1691
|
-
const flush = () => {
|
|
1692
|
-
if (activeSection === void 0) {
|
|
1693
|
-
return;
|
|
1694
|
-
}
|
|
1695
|
-
const text = buffer.join("\n").trim();
|
|
1696
|
-
if (text.length === 0) {
|
|
1697
|
-
buffer = [];
|
|
1698
|
-
return;
|
|
1699
|
-
}
|
|
1700
|
-
sections.set(activeSection, [...sections.get(activeSection) ?? [], text]);
|
|
1701
|
-
buffer = [];
|
|
1702
|
-
};
|
|
1703
|
-
for (const line of lines) {
|
|
1704
|
-
const heading = /^(#{2,6})\s+\[([A-Z_]+)\]\s*$/u.exec(line.trim());
|
|
1705
|
-
if (heading !== null) {
|
|
1706
|
-
flush();
|
|
1707
|
-
activeSection = isRuleSectionName(heading[2]) ? heading[2] : void 0;
|
|
1708
|
-
activeSectionDepth = activeSection === void 0 ? 0 : heading[1].length;
|
|
1709
|
-
continue;
|
|
1710
|
-
}
|
|
1711
|
-
const ordinaryHeading = /^(#{1,6})\s+/u.exec(line.trim());
|
|
1712
|
-
if (ordinaryHeading !== null) {
|
|
1713
|
-
if (activeSection !== void 0 && ordinaryHeading[1].length > activeSectionDepth) {
|
|
1714
|
-
buffer.push(line);
|
|
1715
|
-
continue;
|
|
1716
|
-
}
|
|
1717
|
-
flush();
|
|
1718
|
-
activeSection = void 0;
|
|
1719
|
-
activeSectionDepth = 0;
|
|
1720
|
-
continue;
|
|
1721
|
-
}
|
|
1722
|
-
if (activeSection !== void 0) {
|
|
1723
|
-
buffer.push(line);
|
|
1724
|
-
}
|
|
1725
|
-
}
|
|
1726
|
-
flush();
|
|
1727
|
-
return new Map(
|
|
1728
|
-
Array.from(sections.entries()).map(([section, values]) => [section, values.join("\n\n")])
|
|
1729
|
-
);
|
|
1730
|
-
}
|
|
1731
|
-
async function getRuleSections(projectRoot, input) {
|
|
1732
|
-
const token = readSelectionToken(input.selection_token);
|
|
1733
|
-
if (token === void 0) {
|
|
1734
|
-
throw new Error("selection_token is missing or expired");
|
|
1735
|
-
}
|
|
1736
|
-
validateAiSelections(token.ai_selectable_stable_ids, input.ai_selected_stable_ids, input.ai_selection_reasons);
|
|
1737
|
-
const meta = await readAgentsMeta(projectRoot);
|
|
1738
|
-
const selectedStableIds = [...token.required_stable_ids, ...input.ai_selected_stable_ids];
|
|
1739
|
-
const selectedRules = sortRuleNodes(selectedStableIds.map((stableId) => findRuleNode(meta, stableId)));
|
|
1740
|
-
const diagnostics = [];
|
|
1741
|
-
const rules = [];
|
|
1742
|
-
for (const rule of selectedRules) {
|
|
1743
|
-
const content = await readFile6(join7(projectRoot, rule.path), "utf8");
|
|
1744
|
-
const parsedSections = parseRuleSections(content);
|
|
1745
|
-
const sections = {};
|
|
1746
|
-
for (const section of input.sections) {
|
|
1747
|
-
const sectionContent = parsedSections.get(section);
|
|
1748
|
-
sections[section] = sectionContent ?? "";
|
|
1749
|
-
if (sectionContent === void 0) {
|
|
1750
|
-
diagnostics.push({
|
|
1751
|
-
code: "missing_section",
|
|
1752
|
-
severity: "warn",
|
|
1753
|
-
stable_id: rule.stable_id,
|
|
1754
|
-
section,
|
|
1755
|
-
message: `Rule ${rule.stable_id} does not define section ${section}.`
|
|
1756
|
-
});
|
|
1757
|
-
}
|
|
1758
|
-
}
|
|
1759
|
-
rules.push({
|
|
1760
|
-
stable_id: rule.stable_id,
|
|
1761
|
-
level: rule.level,
|
|
1762
|
-
path: rule.path,
|
|
1763
|
-
sections
|
|
1764
|
-
});
|
|
1765
|
-
}
|
|
1766
|
-
const result = {
|
|
1767
|
-
revision_hash: meta.revision,
|
|
1768
|
-
precedence: ["L2", "L1", "L0"],
|
|
1769
|
-
selected_stable_ids: rules.map((rule) => rule.stable_id),
|
|
1770
|
-
rules,
|
|
1771
|
-
diagnostics
|
|
1772
|
-
};
|
|
1773
|
-
await appendRuleSelectionAuditEvent(projectRoot, {
|
|
1774
|
-
path: token.target_paths[0] ?? "",
|
|
1775
|
-
selection_token: input.selection_token,
|
|
1776
|
-
target_paths: token.target_paths,
|
|
1777
|
-
required_stable_ids: token.required_stable_ids,
|
|
1778
|
-
ai_selectable_stable_ids: token.ai_selectable_stable_ids,
|
|
1779
|
-
ai_selected_stable_ids: input.ai_selected_stable_ids,
|
|
1780
|
-
final_stable_ids: result.selected_stable_ids,
|
|
1781
|
-
ai_selection_reasons: pickSelectionReasons(input.ai_selected_stable_ids, input.ai_selection_reasons),
|
|
1782
|
-
rejected_stable_ids: [],
|
|
1783
|
-
ignored_stable_ids: [],
|
|
1784
|
-
correlation_id: input.correlation_id,
|
|
1785
|
-
session_id: input.session_id
|
|
1786
|
-
});
|
|
1787
|
-
try {
|
|
1788
|
-
await appendEventLedgerEvent(projectRoot, {
|
|
1789
|
-
event_type: "rule_sections_fetched",
|
|
1790
|
-
selection_token: input.selection_token,
|
|
1791
|
-
target_paths: token.target_paths,
|
|
1792
|
-
requested_sections: input.sections,
|
|
1793
|
-
final_stable_ids: result.selected_stable_ids,
|
|
1794
|
-
ai_selected_stable_ids: input.ai_selected_stable_ids,
|
|
1795
|
-
diagnostics,
|
|
1796
|
-
correlation_id: input.correlation_id,
|
|
1797
|
-
session_id: input.session_id
|
|
1798
|
-
});
|
|
1799
|
-
} catch {
|
|
1800
|
-
}
|
|
1801
|
-
return result;
|
|
1802
|
-
}
|
|
1803
|
-
function validateAiSelections(aiSelectableStableIds, aiSelectedStableIds, aiSelectionReasons) {
|
|
1804
|
-
const selectable = new Set(aiSelectableStableIds);
|
|
1805
|
-
for (const stableId of aiSelectedStableIds) {
|
|
1806
|
-
if (!selectable.has(stableId)) {
|
|
1807
|
-
throw new Error(`Invalid L1 rule selection: ${stableId}`);
|
|
1808
|
-
}
|
|
1809
|
-
if (aiSelectionReasons[stableId]?.trim() === "") {
|
|
1810
|
-
throw new Error(`Missing AI selection reason for ${stableId}`);
|
|
1811
|
-
}
|
|
1812
|
-
if (aiSelectionReasons[stableId] === void 0) {
|
|
1813
|
-
throw new Error(`Missing AI selection reason for ${stableId}`);
|
|
1814
|
-
}
|
|
1815
|
-
}
|
|
1816
|
-
}
|
|
1817
|
-
function findRuleNode(meta, stableId) {
|
|
1818
|
-
for (const [nodeId, node] of Object.entries(meta.nodes)) {
|
|
1819
|
-
const nodeStableId = node.stable_id ?? nodeId;
|
|
1820
|
-
if (nodeStableId !== stableId) {
|
|
1821
|
-
continue;
|
|
1822
|
-
}
|
|
1823
|
-
const level = node.level ?? node.layer;
|
|
1824
|
-
return {
|
|
1825
|
-
stable_id: nodeStableId,
|
|
1826
|
-
level,
|
|
1827
|
-
path: normalizeRulesPath(node.content_ref ?? node.file),
|
|
1828
|
-
priority: node.priority,
|
|
1829
|
-
node
|
|
1830
|
-
};
|
|
1831
|
-
}
|
|
1832
|
-
throw new Error(`Selected rule is not present in agents.meta.json: ${stableId}`);
|
|
1833
|
-
}
|
|
1834
|
-
function sortRuleNodes(rules) {
|
|
1835
|
-
return [...rules].sort((left, right) => {
|
|
1836
|
-
const levelDelta = outputLevelOrder(left.level) - outputLevelOrder(right.level);
|
|
1837
|
-
if (levelDelta !== 0) {
|
|
1838
|
-
return levelDelta;
|
|
1839
|
-
}
|
|
1840
|
-
const priorityDelta = PRIORITY_ORDER2[left.priority] - PRIORITY_ORDER2[right.priority];
|
|
1841
|
-
if (priorityDelta !== 0) {
|
|
1842
|
-
return priorityDelta;
|
|
1843
|
-
}
|
|
1844
|
-
return left.stable_id.localeCompare(right.stable_id);
|
|
1845
|
-
});
|
|
1846
|
-
}
|
|
1847
|
-
function outputLevelOrder(level) {
|
|
1848
|
-
switch (level) {
|
|
1849
|
-
case "L0":
|
|
1850
|
-
return 0;
|
|
1851
|
-
case "L1":
|
|
1852
|
-
return 1;
|
|
1853
|
-
case "L2":
|
|
1854
|
-
return 2;
|
|
1855
|
-
}
|
|
1856
|
-
}
|
|
1857
|
-
function isRuleSectionName(value) {
|
|
1858
|
-
return RULE_SECTION_NAMES.includes(value);
|
|
1859
|
-
}
|
|
1860
|
-
function pickSelectionReasons(selectedStableIds, reasons) {
|
|
1861
|
-
return Object.fromEntries(selectedStableIds.map((stableId) => [stableId, reasons[stableId] ?? ""]));
|
|
1862
|
-
}
|
|
1863
|
-
|
|
1864
|
-
// src/services/doctor.ts
|
|
1865
|
-
import { atomicWriteJson as atomicWriteJson2, atomicWriteText as atomicWriteText3 } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
1866
|
-
import { buildBootstrapContent, FABRIC_BOOTSTRAP_PATH } from "@fenglimg/fabric-shared/node/bootstrap-guide";
|
|
1867
|
-
var LEGACY_CLIENT_PATH_KEYS = ["windsurf", "rooCode", "geminiCLI"];
|
|
1868
|
-
var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
|
|
1869
|
-
var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
1870
|
-
".fabric",
|
|
1871
|
-
".git",
|
|
1872
|
-
".next",
|
|
1873
|
-
".turbo",
|
|
1874
|
-
"Library",
|
|
1875
|
-
"Temp",
|
|
1876
|
-
"build",
|
|
1877
|
-
"coverage",
|
|
1878
|
-
"dist",
|
|
1879
|
-
"node_modules"
|
|
1880
|
-
]);
|
|
1881
|
-
var TARGET_FILE_PATHS = [
|
|
1882
|
-
".fabric/bootstrap/README.md",
|
|
1883
|
-
".fabric/INITIAL_TAXONOMY.md",
|
|
1884
|
-
".fabric/forensic.json",
|
|
1885
|
-
".fabric/init-context.json",
|
|
1886
|
-
".fabric/agents.meta.json",
|
|
1887
|
-
".fabric/rule-test.index.json",
|
|
1888
|
-
".fabric/events.jsonl"
|
|
1889
|
-
];
|
|
1890
|
-
async function runDoctorReport(target) {
|
|
1891
|
-
const projectRoot = normalizeTarget(target);
|
|
1892
|
-
const framework = detectFramework(projectRoot);
|
|
1893
|
-
const entryPoints = collectEntryPoints(projectRoot);
|
|
1894
|
-
const [
|
|
1895
|
-
forensic,
|
|
1896
|
-
initContext,
|
|
1897
|
-
meta,
|
|
1898
|
-
eventLedger,
|
|
1899
|
-
ruleSections,
|
|
1900
|
-
ruleTestIndex
|
|
1901
|
-
] = await Promise.all([
|
|
1902
|
-
inspectForensic(projectRoot),
|
|
1903
|
-
inspectInitContext(projectRoot),
|
|
1904
|
-
inspectMeta(projectRoot),
|
|
1905
|
-
inspectEventLedger(projectRoot),
|
|
1906
|
-
inspectRuleSections(projectRoot),
|
|
1907
|
-
inspectRuleTestIndex(projectRoot)
|
|
1908
|
-
]);
|
|
1909
|
-
const mcpConfigInWrongFile = inspectMcpConfigInWrongFile(projectRoot);
|
|
1910
|
-
const metaManuallyDiverged = await inspectMetaManuallyDiverged(projectRoot);
|
|
1911
|
-
const rulesDirUnindexed = inspectRulesDirUnindexed(projectRoot, meta);
|
|
1912
|
-
const stableIdCollision = await inspectStableIdCollisions(projectRoot);
|
|
1913
|
-
const claudeSkillLegacyPath = inspectClaudeSkillLegacyPath(projectRoot);
|
|
1914
|
-
const claudeHookLegacyPath = inspectClaudeHookLegacyPath(projectRoot);
|
|
1915
|
-
const preexistingRootFiles = inspectPreexistingRootFiles(projectRoot);
|
|
1916
|
-
const legacyClientPaths = inspectLegacyClientPaths(projectRoot);
|
|
1917
|
-
const taxonomyExists = existsSync4(join8(projectRoot, ".fabric", "INITIAL_TAXONOMY.md"));
|
|
1918
|
-
const bootstrapExists = existsSync4(join8(projectRoot, ".fabric", "bootstrap", "README.md"));
|
|
1919
|
-
const checks = [
|
|
1920
|
-
createBootstrapCheck(bootstrapExists),
|
|
1921
|
-
createTaxonomyCheck(taxonomyExists),
|
|
1922
|
-
createForensicCheck(forensic, framework.kind, entryPoints.length),
|
|
1923
|
-
createInitContextCheck(initContext),
|
|
1924
|
-
createMetaCheck(meta),
|
|
1925
|
-
createRuleContentRefCheck(meta),
|
|
1926
|
-
createRuleSectionsCheck(ruleSections),
|
|
1927
|
-
createRuleTestIndexCheck(ruleTestIndex),
|
|
1928
|
-
createEventLedgerCheck(eventLedger),
|
|
1929
|
-
createEventLedgerPartialWriteCheck(eventLedger),
|
|
1930
|
-
createMcpConfigInWrongFileCheck(mcpConfigInWrongFile),
|
|
1931
|
-
createMetaManuallyDivergedCheck(metaManuallyDiverged),
|
|
1932
|
-
createRulesDirUnindexedCheck(rulesDirUnindexed),
|
|
1933
|
-
createStableIdCollisionCheck(stableIdCollision),
|
|
1934
|
-
createClaudeSkillLegacyPathCheck(claudeSkillLegacyPath),
|
|
1935
|
-
createClaudeHookLegacyPathCheck(claudeHookLegacyPath),
|
|
1936
|
-
createPreexistingRootFilesCheck(preexistingRootFiles),
|
|
1937
|
-
createLegacyClientPathCheck(legacyClientPaths)
|
|
1938
|
-
];
|
|
1939
|
-
const fixableErrors = collectIssues(checks, "fixable_error");
|
|
1940
|
-
const manualErrors = collectIssues(checks, "manual_error");
|
|
1941
|
-
const warnings = collectIssues(checks, "warning");
|
|
1942
|
-
const infos = collectIssues(checks, "info");
|
|
1943
|
-
return {
|
|
1944
|
-
status: reduceStatus(checks.map((check) => check.status)),
|
|
1945
|
-
checks,
|
|
1946
|
-
fixable_errors: fixableErrors,
|
|
1947
|
-
manual_errors: manualErrors,
|
|
1948
|
-
warnings,
|
|
1949
|
-
infos,
|
|
1950
|
-
summary: {
|
|
1951
|
-
target: projectRoot,
|
|
1952
|
-
framework: {
|
|
1953
|
-
kind: framework.kind,
|
|
1954
|
-
version: framework.version,
|
|
1955
|
-
subkind: framework.subkind
|
|
1956
|
-
},
|
|
1957
|
-
entryPoints,
|
|
1958
|
-
metaRevision: meta.revision,
|
|
1959
|
-
computedMetaRevision: meta.computedRevision,
|
|
1960
|
-
ruleCount: meta.ruleCount,
|
|
1961
|
-
eventLedgerPath: eventLedger.path,
|
|
1962
|
-
fixableErrorCount: fixableErrors.length,
|
|
1963
|
-
manualErrorCount: manualErrors.length,
|
|
1964
|
-
warningCount: warnings.length,
|
|
1965
|
-
infoCount: infos.length,
|
|
1966
|
-
targetFiles: Object.fromEntries(
|
|
1967
|
-
TARGET_FILE_PATHS.map((path) => [path, existsSync4(join8(projectRoot, path))])
|
|
1968
|
-
)
|
|
1969
|
-
}
|
|
1970
|
-
};
|
|
1971
|
-
}
|
|
1972
|
-
async function runDoctorFix(target) {
|
|
1973
|
-
const projectRoot = normalizeTarget(target);
|
|
1974
|
-
const before = await runDoctorReport(projectRoot);
|
|
1975
|
-
const fixed = [];
|
|
1976
|
-
if (before.fixable_errors.some((issue) => issue.code === "bootstrap_missing")) {
|
|
1977
|
-
await writeDefaultBootstrap(projectRoot);
|
|
1978
|
-
fixed.push(findIssue(before.fixable_errors, "bootstrap_missing"));
|
|
1979
|
-
}
|
|
1980
|
-
if (before.fixable_errors.some((issue) => issue.code === "event_ledger_missing")) {
|
|
1981
|
-
await ensureEventLedger(projectRoot);
|
|
1982
|
-
fixed.push(findIssue(before.fixable_errors, "event_ledger_missing"));
|
|
1395
|
+
if (before.fixable_errors.some((issue) => issue.code === "counter_desync")) {
|
|
1396
|
+
await fixCounterDesync(projectRoot);
|
|
1397
|
+
fixed.push(findIssue(before.fixable_errors, "counter_desync"));
|
|
1398
|
+
contextCache.invalidate("meta_write", projectRoot);
|
|
1983
1399
|
}
|
|
1984
1400
|
if (before.fixable_errors.some(
|
|
1985
1401
|
(issue) => [
|
|
@@ -1988,7 +1404,7 @@ async function runDoctorFix(target) {
|
|
|
1988
1404
|
"rule_test_index_missing",
|
|
1989
1405
|
"rule_test_index_stale",
|
|
1990
1406
|
"content_ref_missing",
|
|
1991
|
-
"
|
|
1407
|
+
"knowledge_dir_unindexed"
|
|
1992
1408
|
].includes(issue.code)
|
|
1993
1409
|
)) {
|
|
1994
1410
|
await reconcileRules(projectRoot, { trigger: "doctor" });
|
|
@@ -1999,12 +1415,14 @@ async function runDoctorFix(target) {
|
|
|
1999
1415
|
"rule_test_index_missing",
|
|
2000
1416
|
"rule_test_index_stale",
|
|
2001
1417
|
"content_ref_missing",
|
|
2002
|
-
"
|
|
1418
|
+
"knowledge_dir_unindexed"
|
|
2003
1419
|
].includes(candidate.code)
|
|
2004
1420
|
)) {
|
|
2005
1421
|
fixed.push(issue);
|
|
2006
1422
|
}
|
|
2007
1423
|
contextCache.invalidate("meta_write", projectRoot);
|
|
1424
|
+
await fixCounterDesync(projectRoot);
|
|
1425
|
+
contextCache.invalidate("meta_write", projectRoot);
|
|
2008
1426
|
}
|
|
2009
1427
|
if (before.fixable_errors.some((issue) => issue.code === "event_ledger_partial_write")) {
|
|
2010
1428
|
const ledgerPath = getEventLedgerPath(projectRoot);
|
|
@@ -2021,18 +1439,6 @@ async function runDoctorFix(target) {
|
|
|
2021
1439
|
await fixMcpConfigInWrongFile(projectRoot);
|
|
2022
1440
|
fixed.push(findIssue(before.fixable_errors, "mcp_config_in_wrong_file"));
|
|
2023
1441
|
}
|
|
2024
|
-
if (before.fixable_errors.some((issue) => issue.code === "claude_skill_legacy_path")) {
|
|
2025
|
-
await fixClaudeSkillLegacyPath(projectRoot);
|
|
2026
|
-
fixed.push(findIssue(before.fixable_errors, "claude_skill_legacy_path"));
|
|
2027
|
-
}
|
|
2028
|
-
if (before.fixable_errors.some((issue) => issue.code === "claude_hook_legacy_path")) {
|
|
2029
|
-
await fixClaudeHookLegacyPath(projectRoot);
|
|
2030
|
-
fixed.push(findIssue(before.fixable_errors, "claude_hook_legacy_path"));
|
|
2031
|
-
}
|
|
2032
|
-
if (before.warnings.some((issue) => issue.code === "legacy_client_path_present")) {
|
|
2033
|
-
await fixLegacyClientPaths(projectRoot);
|
|
2034
|
-
fixed.push(findIssue(before.warnings, "legacy_client_path_present"));
|
|
2035
|
-
}
|
|
2036
1442
|
const report = await runDoctorReport(projectRoot);
|
|
2037
1443
|
return {
|
|
2038
1444
|
changed: fixed.length > 0,
|
|
@@ -2044,9 +1450,9 @@ async function runDoctorFix(target) {
|
|
|
2044
1450
|
};
|
|
2045
1451
|
}
|
|
2046
1452
|
async function inspectForensic(projectRoot) {
|
|
2047
|
-
const path =
|
|
1453
|
+
const path = join5(projectRoot, ".fabric", "forensic.json");
|
|
2048
1454
|
try {
|
|
2049
|
-
const parsed = forensicReportSchema.parse(JSON.parse(await
|
|
1455
|
+
const parsed = forensicReportSchema.parse(JSON.parse(await readFile5(path, "utf8")));
|
|
2050
1456
|
return { present: true, valid: true, report: parsed };
|
|
2051
1457
|
} catch (error) {
|
|
2052
1458
|
if (isMissingFileError(error)) {
|
|
@@ -2055,20 +1461,8 @@ async function inspectForensic(projectRoot) {
|
|
|
2055
1461
|
return { present: true, valid: false, report: null, error: error instanceof Error ? error.message : String(error) };
|
|
2056
1462
|
}
|
|
2057
1463
|
}
|
|
2058
|
-
async function inspectInitContext(projectRoot) {
|
|
2059
|
-
const path = join8(projectRoot, ".fabric", "init-context.json");
|
|
2060
|
-
try {
|
|
2061
|
-
JSON.parse(await readFile7(path, "utf8"));
|
|
2062
|
-
return { exists: true, validJson: true };
|
|
2063
|
-
} catch (error) {
|
|
2064
|
-
if (isMissingFileError(error)) {
|
|
2065
|
-
return { exists: false, validJson: false, error: ".fabric/init-context.json is missing." };
|
|
2066
|
-
}
|
|
2067
|
-
return { exists: true, validJson: false, error: error instanceof Error ? error.message : String(error) };
|
|
2068
|
-
}
|
|
2069
|
-
}
|
|
2070
1464
|
function inspectMcpConfigInWrongFile(projectRoot) {
|
|
2071
|
-
const settingsPath =
|
|
1465
|
+
const settingsPath = join5(projectRoot, ".claude", "settings.json");
|
|
2072
1466
|
if (!existsSync4(settingsPath)) {
|
|
2073
1467
|
return { hasWrongEntry: false, settingsPath };
|
|
2074
1468
|
}
|
|
@@ -2089,10 +1483,10 @@ function inspectMcpConfigInWrongFile(projectRoot) {
|
|
|
2089
1483
|
}
|
|
2090
1484
|
}
|
|
2091
1485
|
async function inspectMeta(projectRoot) {
|
|
2092
|
-
const metaPath =
|
|
1486
|
+
const metaPath = join5(projectRoot, ".fabric", "agents.meta.json");
|
|
2093
1487
|
const built = await tryBuildRuleMeta(projectRoot);
|
|
2094
1488
|
try {
|
|
2095
|
-
const raw = await
|
|
1489
|
+
const raw = await readFile5(metaPath, "utf8");
|
|
2096
1490
|
const meta = agentsMetaSchema4.parse(JSON.parse(raw));
|
|
2097
1491
|
const contentRefIssues = inspectContentRefs(projectRoot, meta);
|
|
2098
1492
|
const changed = built === null ? false : built.changed;
|
|
@@ -2102,7 +1496,10 @@ async function inspectMeta(projectRoot) {
|
|
|
2102
1496
|
meta,
|
|
2103
1497
|
revision: meta.revision,
|
|
2104
1498
|
computedRevision: built?.meta.revision ?? null,
|
|
2105
|
-
ruleCount: Object.values(meta.nodes).filter((node) =>
|
|
1499
|
+
ruleCount: Object.values(meta.nodes).filter((node) => {
|
|
1500
|
+
const ref = node.content_ref ?? node.file;
|
|
1501
|
+
return ref.startsWith(".fabric/knowledge/") || ref.startsWith("~/.fabric/knowledge/");
|
|
1502
|
+
}).length,
|
|
2106
1503
|
missingContentRefs: contentRefIssues.missing,
|
|
2107
1504
|
invalidContentRefs: contentRefIssues.invalid,
|
|
2108
1505
|
stale: changed || built !== null && meta.revision !== built.meta.revision,
|
|
@@ -2150,17 +1547,16 @@ function inspectContentRefs(projectRoot, meta) {
|
|
|
2150
1547
|
const invalid = [];
|
|
2151
1548
|
for (const node of Object.values(meta.nodes)) {
|
|
2152
1549
|
const contentRef = normalizePath(node.content_ref ?? node.file);
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
1550
|
+
const isPersonalKnowledge = contentRef.startsWith("~/.fabric/knowledge/");
|
|
1551
|
+
const isTeamKnowledge = contentRef.startsWith(".fabric/knowledge/");
|
|
1552
|
+
if (!isPersonalKnowledge && !isTeamKnowledge) {
|
|
1553
|
+
invalid.push(contentRef);
|
|
2157
1554
|
continue;
|
|
2158
1555
|
}
|
|
2159
|
-
if (
|
|
2160
|
-
invalid.push(contentRef);
|
|
1556
|
+
if (isPersonalKnowledge) {
|
|
2161
1557
|
continue;
|
|
2162
1558
|
}
|
|
2163
|
-
if (!existsSync4(
|
|
1559
|
+
if (!existsSync4(join5(projectRoot, contentRef))) {
|
|
2164
1560
|
missing.push(contentRef);
|
|
2165
1561
|
}
|
|
2166
1562
|
}
|
|
@@ -2175,7 +1571,7 @@ async function inspectEventLedger(projectRoot) {
|
|
|
2175
1571
|
try {
|
|
2176
1572
|
await access(path, constants.W_OK);
|
|
2177
1573
|
const { warnings } = await readEventLedger(projectRoot);
|
|
2178
|
-
const raw = await
|
|
1574
|
+
const raw = await readFile5(path, "utf8");
|
|
2179
1575
|
const invalidLine = raw.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).find((line) => !isValidJsonLine(line));
|
|
2180
1576
|
const partialWarning = warnings.find((w) => w.kind === "partial_write_at_tail");
|
|
2181
1577
|
return {
|
|
@@ -2201,29 +1597,11 @@ async function inspectEventLedger(projectRoot) {
|
|
|
2201
1597
|
};
|
|
2202
1598
|
}
|
|
2203
1599
|
}
|
|
2204
|
-
async function inspectRuleSections(projectRoot) {
|
|
2205
|
-
const invalidFiles = [];
|
|
2206
|
-
const files = findRuleFiles2(projectRoot);
|
|
2207
|
-
for (const file of files) {
|
|
2208
|
-
try {
|
|
2209
|
-
parseRuleSections(await readFile7(join8(projectRoot, file), "utf8"));
|
|
2210
|
-
} catch (error) {
|
|
2211
|
-
invalidFiles.push({
|
|
2212
|
-
file,
|
|
2213
|
-
reason: error instanceof Error ? error.message : String(error)
|
|
2214
|
-
});
|
|
2215
|
-
}
|
|
2216
|
-
}
|
|
2217
|
-
return {
|
|
2218
|
-
checkedCount: files.length,
|
|
2219
|
-
invalidFiles
|
|
2220
|
-
};
|
|
2221
|
-
}
|
|
2222
1600
|
async function inspectRuleTestIndex(projectRoot) {
|
|
2223
|
-
const path =
|
|
1601
|
+
const path = join5(projectRoot, ".fabric", "rule-test.index.json");
|
|
2224
1602
|
const built = await tryBuildRuleMeta(projectRoot);
|
|
2225
1603
|
try {
|
|
2226
|
-
const index = ruleTestIndexSchema2.parse(JSON.parse(await
|
|
1604
|
+
const index = ruleTestIndexSchema2.parse(JSON.parse(await readFile5(path, "utf8")));
|
|
2227
1605
|
return {
|
|
2228
1606
|
present: true,
|
|
2229
1607
|
valid: true,
|
|
@@ -2242,17 +1620,56 @@ async function inspectRuleTestIndex(projectRoot) {
|
|
|
2242
1620
|
};
|
|
2243
1621
|
}
|
|
2244
1622
|
}
|
|
2245
|
-
function
|
|
2246
|
-
|
|
2247
|
-
|
|
1623
|
+
function inspectBootstrapAnchor(projectRoot) {
|
|
1624
|
+
return {
|
|
1625
|
+
hasAgentsMd: existsSync4(join5(projectRoot, "AGENTS.md")),
|
|
1626
|
+
hasClaudeMd: existsSync4(join5(projectRoot, "CLAUDE.md"))
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
function createBootstrapAnchorCheck(inspection) {
|
|
1630
|
+
if (!inspection.hasAgentsMd && !inspection.hasClaudeMd) {
|
|
1631
|
+
return issueCheck(
|
|
1632
|
+
"Bootstrap anchor",
|
|
1633
|
+
"error",
|
|
1634
|
+
"fixable_error",
|
|
1635
|
+
"bootstrap_anchor_missing",
|
|
1636
|
+
"Neither AGENTS.md nor CLAUDE.md exists at the repo root. Fabric requires a bootstrap anchor file at the project root.",
|
|
1637
|
+
"Run `fabric init` to generate the AGENTS.md / CLAUDE.md bootstrap anchor at the repo root."
|
|
1638
|
+
);
|
|
2248
1639
|
}
|
|
2249
|
-
|
|
1640
|
+
const present = [
|
|
1641
|
+
inspection.hasAgentsMd ? "AGENTS.md" : null,
|
|
1642
|
+
inspection.hasClaudeMd ? "CLAUDE.md" : null
|
|
1643
|
+
].filter((entry) => entry !== null).join(", ");
|
|
1644
|
+
return okCheck("Bootstrap anchor", `Bootstrap anchor present at repo root: ${present}.`);
|
|
2250
1645
|
}
|
|
2251
|
-
function
|
|
2252
|
-
|
|
2253
|
-
|
|
1646
|
+
function inspectKnowledgeDirMissing(projectRoot) {
|
|
1647
|
+
const knowledgeRoot = join5(projectRoot, ".fabric", "knowledge");
|
|
1648
|
+
const missingSubdirs = [];
|
|
1649
|
+
for (const sub of KNOWLEDGE_SUBDIRS2) {
|
|
1650
|
+
const path = join5(knowledgeRoot, sub);
|
|
1651
|
+
if (!existsSync4(path)) {
|
|
1652
|
+
missingSubdirs.push(`.fabric/knowledge/${sub}`);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
return { missingSubdirs };
|
|
1656
|
+
}
|
|
1657
|
+
function createKnowledgeDirMissingCheck(inspection) {
|
|
1658
|
+
if (inspection.missingSubdirs.length > 0) {
|
|
1659
|
+
const list = inspection.missingSubdirs.join(", ");
|
|
1660
|
+
return issueCheck(
|
|
1661
|
+
"Knowledge layout",
|
|
1662
|
+
"error",
|
|
1663
|
+
"fixable_error",
|
|
1664
|
+
"knowledge_dir_missing",
|
|
1665
|
+
`${inspection.missingSubdirs.length} required knowledge subdir${inspection.missingSubdirs.length === 1 ? " is" : "s are"} missing: ${list}.`,
|
|
1666
|
+
"Run `fab doctor --fix` to create the missing .fabric/knowledge/* subdirectories."
|
|
1667
|
+
);
|
|
2254
1668
|
}
|
|
2255
|
-
return okCheck(
|
|
1669
|
+
return okCheck(
|
|
1670
|
+
"Knowledge layout",
|
|
1671
|
+
`All ${KNOWLEDGE_SUBDIRS2.length} required .fabric/knowledge/* subdirectories exist.`
|
|
1672
|
+
);
|
|
2256
1673
|
}
|
|
2257
1674
|
function createForensicCheck(forensic, frameworkKind, entryPointCount) {
|
|
2258
1675
|
if (!forensic.present) {
|
|
@@ -2270,18 +1687,9 @@ function createForensicCheck(forensic, frameworkKind, entryPointCount) {
|
|
|
2270
1687
|
}
|
|
2271
1688
|
return okCheck("Scan evidence", `.fabric/forensic.json is valid for ${forensic.report?.framework.kind ?? "unknown"}.`);
|
|
2272
1689
|
}
|
|
2273
|
-
function createInitContextCheck(initContext) {
|
|
2274
|
-
if (!initContext.exists) {
|
|
2275
|
-
return issueCheck("Init context", "error", "manual_error", "init_context_missing", initContext.error ?? ".fabric/init-context.json is missing.", "Run the fabric-init skill in Claude Code or Codex CLI to complete initialization. See docs/migration-1.8.md FAQ.");
|
|
2276
|
-
}
|
|
2277
|
-
if (!initContext.validJson) {
|
|
2278
|
-
return issueCheck("Init context", "error", "manual_error", "init_context_invalid", initContext.error ?? ".fabric/init-context.json is invalid.", "Delete .fabric/init-context.json and run `fab init` to regenerate it.");
|
|
2279
|
-
}
|
|
2280
|
-
return okCheck("Init context", ".fabric/init-context.json is valid JSON.");
|
|
2281
|
-
}
|
|
2282
1690
|
function createMetaCheck(meta) {
|
|
2283
1691
|
if (!meta.present) {
|
|
2284
|
-
return issueCheck("Agents metadata", "error", "fixable_error", "agents_meta_missing", ".fabric/agents.meta.json is missing.", "Run `fab doctor --fix` to rebuild agents.meta.json from .fabric/
|
|
1692
|
+
return issueCheck("Agents metadata", "error", "fixable_error", "agents_meta_missing", ".fabric/agents.meta.json is missing.", "Run `fab doctor --fix` to rebuild agents.meta.json from .fabric/knowledge/.");
|
|
2285
1693
|
}
|
|
2286
1694
|
if (!meta.valid) {
|
|
2287
1695
|
return issueCheck("Agents metadata", "error", "manual_error", "agents_meta_invalid", meta.readError ?? ".fabric/agents.meta.json is invalid.", "Delete .fabric/agents.meta.json and run `fab doctor --fix` to regenerate it.");
|
|
@@ -2292,11 +1700,11 @@ function createMetaCheck(meta) {
|
|
|
2292
1700
|
"error",
|
|
2293
1701
|
"fixable_error",
|
|
2294
1702
|
"agents_meta_stale",
|
|
2295
|
-
`.fabric/agents.meta.json revision ${meta.revision} does not match .fabric/
|
|
2296
|
-
"Run `fab doctor --fix` to reconcile agents.meta.json with the current
|
|
1703
|
+
`.fabric/agents.meta.json revision ${meta.revision} does not match .fabric/knowledge derived revision ${meta.computedRevision ?? "<unknown>"}.`,
|
|
1704
|
+
"Run `fab doctor --fix` to reconcile agents.meta.json with the current knowledge files."
|
|
2297
1705
|
);
|
|
2298
1706
|
}
|
|
2299
|
-
return okCheck("Agents metadata", `.fabric/agents.meta.json revision ${meta.revision} is aligned with .fabric/
|
|
1707
|
+
return okCheck("Agents metadata", `.fabric/agents.meta.json revision ${meta.revision} is aligned with .fabric/knowledge.`);
|
|
2300
1708
|
}
|
|
2301
1709
|
function createRuleContentRefCheck(meta) {
|
|
2302
1710
|
if (!meta.valid) {
|
|
@@ -2308,8 +1716,8 @@ function createRuleContentRefCheck(meta) {
|
|
|
2308
1716
|
"error",
|
|
2309
1717
|
"manual_error",
|
|
2310
1718
|
"content_ref_outside_rules",
|
|
2311
|
-
`${meta.invalidContentRefs.length} content_ref entr${meta.invalidContentRefs.length === 1 ? "y is" : "ies are"} outside .fabric/
|
|
2312
|
-
"Edit agents.meta.json to ensure all content_ref values point inside .fabric/
|
|
1719
|
+
`${meta.invalidContentRefs.length} content_ref entr${meta.invalidContentRefs.length === 1 ? "y is" : "ies are"} outside .fabric/knowledge.`,
|
|
1720
|
+
"Edit agents.meta.json to ensure all content_ref values point inside .fabric/knowledge/{type}/ (team) or ~/.fabric/knowledge/{type}/ (personal)."
|
|
2313
1721
|
);
|
|
2314
1722
|
}
|
|
2315
1723
|
if (meta.missingContentRefs.length > 0) {
|
|
@@ -2319,23 +1727,10 @@ function createRuleContentRefCheck(meta) {
|
|
|
2319
1727
|
"fixable_error",
|
|
2320
1728
|
"content_ref_missing",
|
|
2321
1729
|
`${meta.missingContentRefs.length} content_ref target${meta.missingContentRefs.length === 1 ? "" : "s"} are missing. Run \`fab doctor --fix\` to reconcile.`,
|
|
2322
|
-
"Run `fab doctor --fix` to reconcile agents.meta.json with the files present in .fabric/
|
|
2323
|
-
);
|
|
2324
|
-
}
|
|
2325
|
-
return okCheck("Rule content refs", "All content_ref entries resolve to .fabric/rules files or bootstrap README.");
|
|
2326
|
-
}
|
|
2327
|
-
function createRuleSectionsCheck(snapshot) {
|
|
2328
|
-
if (snapshot.invalidFiles.length > 0) {
|
|
2329
|
-
return issueCheck(
|
|
2330
|
-
"Rule sections",
|
|
2331
|
-
"error",
|
|
2332
|
-
"manual_error",
|
|
2333
|
-
"rule_sections_invalid",
|
|
2334
|
-
`${snapshot.invalidFiles.length} rule file${snapshot.invalidFiles.length === 1 ? "" : "s"} could not be parsed.`,
|
|
2335
|
-
"Edit the rule file(s) to fix the section structure, then re-run `fab doctor`."
|
|
1730
|
+
"Run `fab doctor --fix` to reconcile agents.meta.json with the files present in .fabric/knowledge/."
|
|
2336
1731
|
);
|
|
2337
1732
|
}
|
|
2338
|
-
return okCheck("Rule
|
|
1733
|
+
return okCheck("Rule content refs", "All content_ref entries resolve to .fabric/knowledge files.");
|
|
2339
1734
|
}
|
|
2340
1735
|
function createRuleTestIndexCheck(index) {
|
|
2341
1736
|
if (!index.present) {
|
|
@@ -2419,13 +1814,13 @@ function findIssue(issues, code) {
|
|
|
2419
1814
|
};
|
|
2420
1815
|
}
|
|
2421
1816
|
async function inspectMetaManuallyDiverged(projectRoot) {
|
|
2422
|
-
const metaPath =
|
|
1817
|
+
const metaPath = join5(projectRoot, ".fabric", "agents.meta.json");
|
|
2423
1818
|
if (!existsSync4(metaPath)) {
|
|
2424
1819
|
return { extraMetaEntries: [], hashMismatchEntries: [], readable: false };
|
|
2425
1820
|
}
|
|
2426
1821
|
let meta;
|
|
2427
1822
|
try {
|
|
2428
|
-
const raw = await
|
|
1823
|
+
const raw = await readFile5(metaPath, "utf8");
|
|
2429
1824
|
meta = agentsMetaSchema4.parse(JSON.parse(raw));
|
|
2430
1825
|
} catch (error) {
|
|
2431
1826
|
return {
|
|
@@ -2439,7 +1834,7 @@ async function inspectMetaManuallyDiverged(projectRoot) {
|
|
|
2439
1834
|
const hashMismatchEntries = [];
|
|
2440
1835
|
for (const node of Object.values(meta.nodes)) {
|
|
2441
1836
|
const contentRef = node.content_ref ?? node.file;
|
|
2442
|
-
const absPath =
|
|
1837
|
+
const absPath = join5(projectRoot, contentRef);
|
|
2443
1838
|
if (!existsSync4(absPath)) {
|
|
2444
1839
|
extraMetaEntries.push(contentRef);
|
|
2445
1840
|
continue;
|
|
@@ -2456,87 +1851,89 @@ async function inspectMetaManuallyDiverged(projectRoot) {
|
|
|
2456
1851
|
}
|
|
2457
1852
|
return { extraMetaEntries, hashMismatchEntries, readable: true };
|
|
2458
1853
|
}
|
|
2459
|
-
function
|
|
2460
|
-
const
|
|
2461
|
-
|
|
1854
|
+
function inspectKnowledgeDirUnindexed(projectRoot, meta) {
|
|
1855
|
+
const physicalMdFiles = /* @__PURE__ */ new Set();
|
|
1856
|
+
collectMdFilesUnder(physicalMdFiles, projectRoot, join5(projectRoot, ".fabric", "knowledge"), ".fabric/knowledge");
|
|
1857
|
+
if (physicalMdFiles.size === 0) {
|
|
2462
1858
|
return { unindexedFiles: [] };
|
|
2463
1859
|
}
|
|
2464
|
-
const
|
|
2465
|
-
|
|
1860
|
+
const indexedRefs = /* @__PURE__ */ new Set();
|
|
1861
|
+
if (meta.valid && meta.meta !== null) {
|
|
1862
|
+
for (const node of Object.values(meta.meta.nodes)) {
|
|
1863
|
+
const ref = normalizePath(node.content_ref ?? node.file);
|
|
1864
|
+
indexedRefs.add(ref);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
const unindexedFiles = [...physicalMdFiles].filter((f) => !indexedRefs.has(f)).sort();
|
|
1868
|
+
return { unindexedFiles };
|
|
1869
|
+
}
|
|
1870
|
+
function collectMdFilesUnder(out, projectRoot, rootDir, relPrefix) {
|
|
1871
|
+
if (!existsSync4(rootDir)) {
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1874
|
+
const stack = [rootDir];
|
|
2466
1875
|
while (stack.length > 0) {
|
|
2467
1876
|
const dir = stack.pop();
|
|
2468
1877
|
if (dir === void 0) {
|
|
2469
1878
|
continue;
|
|
2470
1879
|
}
|
|
2471
1880
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
2472
|
-
const abs =
|
|
1881
|
+
const abs = join5(dir, entry.name);
|
|
2473
1882
|
if (entry.isDirectory()) {
|
|
2474
1883
|
stack.push(abs);
|
|
2475
1884
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
2476
|
-
const rel =
|
|
2477
|
-
|
|
1885
|
+
const rel = posix.join(relPrefix, abs.slice(rootDir.length + 1).replace(/\\/gu, "/"));
|
|
1886
|
+
out.add(rel);
|
|
2478
1887
|
}
|
|
2479
1888
|
}
|
|
2480
1889
|
}
|
|
2481
|
-
const indexedRefs = /* @__PURE__ */ new Set();
|
|
2482
|
-
if (meta.valid && meta.meta !== null) {
|
|
2483
|
-
for (const node of Object.values(meta.meta.nodes)) {
|
|
2484
|
-
const ref = normalizePath(node.content_ref ?? node.file);
|
|
2485
|
-
indexedRefs.add(ref);
|
|
2486
|
-
}
|
|
2487
|
-
}
|
|
2488
|
-
const unindexedFiles = [...physicalMdFiles].filter((f) => !indexedRefs.has(f)).sort();
|
|
2489
|
-
return { unindexedFiles };
|
|
2490
1890
|
}
|
|
2491
|
-
function
|
|
1891
|
+
function createKnowledgeDirUnindexedCheck(inspection) {
|
|
2492
1892
|
if (inspection.unindexedFiles.length > 0) {
|
|
2493
1893
|
return issueCheck(
|
|
2494
|
-
"
|
|
1894
|
+
"Knowledge dir unindexed",
|
|
2495
1895
|
"error",
|
|
2496
1896
|
"fixable_error",
|
|
2497
|
-
"
|
|
2498
|
-
`${inspection.unindexedFiles.length} .md file${inspection.unindexedFiles.length === 1 ? "" : "s"} in .fabric/
|
|
2499
|
-
"Run `fab doctor --fix` to index the missing
|
|
1897
|
+
"knowledge_dir_unindexed",
|
|
1898
|
+
`${inspection.unindexedFiles.length} .md file${inspection.unindexedFiles.length === 1 ? "" : "s"} in .fabric/knowledge/ not indexed in agents.meta.json. Run \`fab doctor --fix\` to index the missing knowledge files.`,
|
|
1899
|
+
"Run `fab doctor --fix` to index the missing knowledge files."
|
|
2500
1900
|
);
|
|
2501
1901
|
}
|
|
2502
|
-
return okCheck("
|
|
1902
|
+
return okCheck("Knowledge dir unindexed", "All .fabric/knowledge/ .md files are indexed in agents.meta.json.");
|
|
2503
1903
|
}
|
|
2504
1904
|
async function inspectStableIdCollisions(projectRoot) {
|
|
2505
|
-
const
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
1905
|
+
const found = [];
|
|
1906
|
+
const knowledgeDir = join5(projectRoot, ".fabric", "knowledge");
|
|
1907
|
+
if (existsSync4(knowledgeDir)) {
|
|
1908
|
+
const stack = [knowledgeDir];
|
|
1909
|
+
while (stack.length > 0) {
|
|
1910
|
+
const dir = stack.pop();
|
|
1911
|
+
if (dir === void 0) {
|
|
1912
|
+
continue;
|
|
1913
|
+
}
|
|
1914
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1915
|
+
const abs = join5(dir, entry.name);
|
|
1916
|
+
if (entry.isDirectory()) {
|
|
1917
|
+
stack.push(abs);
|
|
1918
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1919
|
+
let source;
|
|
1920
|
+
try {
|
|
1921
|
+
source = await readFile5(abs, "utf8");
|
|
1922
|
+
} catch {
|
|
1923
|
+
continue;
|
|
1924
|
+
}
|
|
1925
|
+
const id = extractKnowledgeFrontmatterId(source);
|
|
1926
|
+
if (id === null) {
|
|
1927
|
+
continue;
|
|
1928
|
+
}
|
|
1929
|
+
const relPath = posix.join(".fabric/knowledge", abs.slice(knowledgeDir.length + 1).replace(/\\/gu, "/"));
|
|
1930
|
+
found.push({ stableId: id, relPath });
|
|
1931
|
+
}
|
|
2522
1932
|
}
|
|
2523
1933
|
}
|
|
2524
1934
|
}
|
|
2525
1935
|
const stableIdToFiles = /* @__PURE__ */ new Map();
|
|
2526
|
-
const
|
|
2527
|
-
for (const absPath of mdFiles) {
|
|
2528
|
-
let source;
|
|
2529
|
-
try {
|
|
2530
|
-
source = await readFile7(absPath, "utf8");
|
|
2531
|
-
} catch {
|
|
2532
|
-
continue;
|
|
2533
|
-
}
|
|
2534
|
-
const match = DECLARED_ID_PATTERN.exec(source);
|
|
2535
|
-
if (match === null) {
|
|
2536
|
-
continue;
|
|
2537
|
-
}
|
|
2538
|
-
const stableId = match[1];
|
|
2539
|
-
const relPath = posix2.join(".fabric/rules", absPath.slice(rulesDir.length + 1).replace(/\\/gu, "/"));
|
|
1936
|
+
for (const { stableId, relPath } of found) {
|
|
2540
1937
|
const existing = stableIdToFiles.get(stableId) ?? [];
|
|
2541
1938
|
existing.push(relPath);
|
|
2542
1939
|
stableIdToFiles.set(stableId, existing);
|
|
@@ -2549,6 +1946,85 @@ async function inspectStableIdCollisions(projectRoot) {
|
|
|
2549
1946
|
}
|
|
2550
1947
|
return { collisions: collisions.sort((a, b) => a.stable_id.localeCompare(b.stable_id)) };
|
|
2551
1948
|
}
|
|
1949
|
+
function extractKnowledgeFrontmatterId(source) {
|
|
1950
|
+
const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
|
|
1951
|
+
const fm = FM_PATTERN.exec(source);
|
|
1952
|
+
if (fm === null) {
|
|
1953
|
+
return null;
|
|
1954
|
+
}
|
|
1955
|
+
const block = fm[1];
|
|
1956
|
+
const ID_LINE = /^id:\s*("?)(K[PT]-(?:MOD|DEC|GLD|PIT|PRO)-\d{4,})\1\s*$/mu;
|
|
1957
|
+
const idMatch = ID_LINE.exec(block);
|
|
1958
|
+
return idMatch === null ? null : idMatch[2];
|
|
1959
|
+
}
|
|
1960
|
+
function inspectCounterDesync(meta) {
|
|
1961
|
+
if (!meta.valid || meta.meta === null) {
|
|
1962
|
+
return { desyncs: [], correctedCounters: null };
|
|
1963
|
+
}
|
|
1964
|
+
const current = AgentsMetaCountersSchema.parse(meta.meta.counters ?? void 0);
|
|
1965
|
+
const observed = {
|
|
1966
|
+
KP: { MOD: 0, DEC: 0, GLD: 0, PIT: 0, PRO: 0 },
|
|
1967
|
+
KT: { MOD: 0, DEC: 0, GLD: 0, PIT: 0, PRO: 0 }
|
|
1968
|
+
};
|
|
1969
|
+
for (const node of Object.values(meta.meta.nodes)) {
|
|
1970
|
+
const id = node.stable_id;
|
|
1971
|
+
if (id === void 0) {
|
|
1972
|
+
continue;
|
|
1973
|
+
}
|
|
1974
|
+
const parsed = parseKnowledgeId2(id);
|
|
1975
|
+
if (parsed === null) {
|
|
1976
|
+
continue;
|
|
1977
|
+
}
|
|
1978
|
+
const layer = parsed.layer === "personal" ? "KP" : "KT";
|
|
1979
|
+
const typeCode = [
|
|
1980
|
+
["model", "MOD"],
|
|
1981
|
+
["decision", "DEC"],
|
|
1982
|
+
["guideline", "GLD"],
|
|
1983
|
+
["pitfall", "PIT"],
|
|
1984
|
+
["process", "PRO"]
|
|
1985
|
+
].find(([t]) => t === parsed.type)?.[1];
|
|
1986
|
+
if (typeCode === void 0) {
|
|
1987
|
+
continue;
|
|
1988
|
+
}
|
|
1989
|
+
if (parsed.counter > observed[layer][typeCode]) {
|
|
1990
|
+
observed[layer][typeCode] = parsed.counter;
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
const desyncs = [];
|
|
1994
|
+
const corrected = {
|
|
1995
|
+
KP: { ...current.KP },
|
|
1996
|
+
KT: { ...current.KT }
|
|
1997
|
+
};
|
|
1998
|
+
for (const layer of ["KP", "KT"]) {
|
|
1999
|
+
for (const code of COUNTER_TYPE_CODES) {
|
|
2000
|
+
const obs = observed[layer][code];
|
|
2001
|
+
const cur = current[layer][code];
|
|
2002
|
+
if (obs > cur) {
|
|
2003
|
+
desyncs.push({ layer, type: code, observed: obs, current: cur });
|
|
2004
|
+
corrected[layer][code] = obs;
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
return {
|
|
2009
|
+
desyncs,
|
|
2010
|
+
correctedCounters: desyncs.length === 0 ? null : corrected
|
|
2011
|
+
};
|
|
2012
|
+
}
|
|
2013
|
+
function createCounterDesyncCheck(inspection) {
|
|
2014
|
+
if (inspection.desyncs.length > 0) {
|
|
2015
|
+
const first = inspection.desyncs[0];
|
|
2016
|
+
const detail = `counters.${first.layer}.${first.type} = ${first.current} but observed K${first.layer === "KP" ? "P" : "T"}-${first.type}-${String(first.observed).padStart(4, "0")}`;
|
|
2017
|
+
return issueCheck(
|
|
2018
|
+
"Knowledge counter desync",
|
|
2019
|
+
"error",
|
|
2020
|
+
"fixable_error",
|
|
2021
|
+
"counter_desync",
|
|
2022
|
+
`${inspection.desyncs.length} knowledge counter${inspection.desyncs.length === 1 ? "" : "s"} desynced from observed stable_ids. ${detail}. Run \`fab doctor --fix\` to bump counters.`,
|
|
2023
|
+
"Run `fab doctor --fix` to bump agents.meta.json counters to the maximum observed counter value."
|
|
2024
|
+
);
|
|
2025
|
+
}
|
|
2026
|
+
return okCheck("Knowledge counter desync", "agents.meta.json counters envelope is consistent with observed stable_ids.");
|
|
2027
|
+
}
|
|
2552
2028
|
function createStableIdCollisionCheck(inspection) {
|
|
2553
2029
|
if (inspection.collisions.length > 0) {
|
|
2554
2030
|
const first = inspection.collisions[0];
|
|
@@ -2558,11 +2034,11 @@ function createStableIdCollisionCheck(inspection) {
|
|
|
2558
2034
|
"warn",
|
|
2559
2035
|
"warning",
|
|
2560
2036
|
"stable_id_collision",
|
|
2561
|
-
`${detail} Edit one of the
|
|
2562
|
-
"Edit one of the colliding
|
|
2037
|
+
`${detail} Edit one of the knowledge files to use a unique stable_id.`,
|
|
2038
|
+
"Edit one of the colliding knowledge files to declare a different `id: K[PT]-XXX-NNNN` frontmatter value."
|
|
2563
2039
|
);
|
|
2564
2040
|
}
|
|
2565
|
-
return okCheck("Stable ID collision", "No declared stable_id collisions found in .fabric/
|
|
2041
|
+
return okCheck("Stable ID collision", "No declared stable_id collisions found in .fabric/knowledge/.");
|
|
2566
2042
|
}
|
|
2567
2043
|
function createMetaManuallyDivergedCheck(inspection) {
|
|
2568
2044
|
if (!inspection.readable) {
|
|
@@ -2592,7 +2068,7 @@ function createMetaManuallyDivergedCheck(inspection) {
|
|
|
2592
2068
|
}
|
|
2593
2069
|
function inspectPreexistingRootFiles(projectRoot) {
|
|
2594
2070
|
const candidates = ["CLAUDE.md", "AGENTS.md"];
|
|
2595
|
-
const detected = candidates.filter((name) => existsSync4(
|
|
2071
|
+
const detected = candidates.filter((name) => existsSync4(join5(projectRoot, name)));
|
|
2596
2072
|
return { detected };
|
|
2597
2073
|
}
|
|
2598
2074
|
function createPreexistingRootFilesCheck(inspection) {
|
|
@@ -2605,336 +2081,464 @@ function createPreexistingRootFilesCheck(inspection) {
|
|
|
2605
2081
|
kind: "info",
|
|
2606
2082
|
code: "preexisting_root_claude_md",
|
|
2607
2083
|
fixable: false,
|
|
2608
|
-
message: `${inspection.detected.join(", ")} detected at project root. These root files are not auto-loaded by Fabric MCP.`,
|
|
2609
|
-
actionHint: "Move
|
|
2610
|
-
};
|
|
2611
|
-
}
|
|
2612
|
-
function inspectClaudeSkillLegacyPath(projectRoot) {
|
|
2613
|
-
const legacyPath = join8(projectRoot, ".claude", "skills", "agents-md-init", "SKILL.md");
|
|
2614
|
-
const newPath = join8(projectRoot, ".claude", "skills", "fabric-init", "SKILL.md");
|
|
2615
|
-
const hasLegacy = existsSync4(legacyPath) && !existsSync4(newPath);
|
|
2616
|
-
return { hasLegacy, legacyPath, newPath };
|
|
2617
|
-
}
|
|
2618
|
-
function createClaudeSkillLegacyPathCheck(inspection) {
|
|
2619
|
-
if (inspection.hasLegacy) {
|
|
2620
|
-
return issueCheck(
|
|
2621
|
-
"Claude skill path",
|
|
2622
|
-
"error",
|
|
2623
|
-
"fixable_error",
|
|
2624
|
-
"claude_skill_legacy_path",
|
|
2625
|
-
`.claude/skills/agents-md-init/SKILL.md exists at the legacy path. Run --fix to migrate it to .claude/skills/fabric-init/SKILL.md (user edits preserved).`,
|
|
2626
|
-
"Run `fab doctor --fix` to rename agents-md-init/ to fabric-init/, preserving any user edits to SKILL.md."
|
|
2627
|
-
);
|
|
2628
|
-
}
|
|
2629
|
-
return okCheck("Claude skill path", ".claude/skills/fabric-init/SKILL.md is at the canonical path (or not present).");
|
|
2084
|
+
message: `${inspection.detected.join(", ")} detected at project root. These root files are not auto-loaded by Fabric MCP.`,
|
|
2085
|
+
actionHint: "Move knowledge content to `.fabric/knowledge/{type}/` if you want it available in MCP responses."
|
|
2086
|
+
};
|
|
2630
2087
|
}
|
|
2631
|
-
async function
|
|
2632
|
-
const
|
|
2633
|
-
|
|
2634
|
-
if (!existsSync4(legacyPath)) {
|
|
2088
|
+
async function fixMcpConfigInWrongFile(projectRoot) {
|
|
2089
|
+
const settingsPath = join5(projectRoot, ".claude", "settings.json");
|
|
2090
|
+
if (!existsSync4(settingsPath)) {
|
|
2635
2091
|
return;
|
|
2636
2092
|
}
|
|
2637
|
-
|
|
2638
|
-
renameSync(legacyPath, newPath);
|
|
2639
|
-
const legacyDir = join8(legacyPath, "..");
|
|
2093
|
+
let settings;
|
|
2640
2094
|
try {
|
|
2641
|
-
|
|
2095
|
+
const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
2096
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
settings = parsed;
|
|
2642
2100
|
} catch {
|
|
2101
|
+
return;
|
|
2102
|
+
}
|
|
2103
|
+
const mcpServers = settings.mcpServers;
|
|
2104
|
+
if (mcpServers === null || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
2107
|
+
const { fabric: _removed, ...remainingServers } = mcpServers;
|
|
2108
|
+
const cleaned = { ...settings };
|
|
2109
|
+
if (Object.keys(remainingServers).length === 0) {
|
|
2110
|
+
delete cleaned.mcpServers;
|
|
2111
|
+
} else {
|
|
2112
|
+
cleaned.mcpServers = remainingServers;
|
|
2643
2113
|
}
|
|
2114
|
+
await atomicWriteJson2(settingsPath, cleaned, { indent: 2 });
|
|
2644
2115
|
await appendEventLedgerEvent(projectRoot, {
|
|
2645
|
-
event_type: "
|
|
2646
|
-
|
|
2647
|
-
|
|
2116
|
+
event_type: "mcp_config_migrated",
|
|
2117
|
+
source: "doctor_fix",
|
|
2118
|
+
removed_from: ".claude/settings.json"
|
|
2648
2119
|
});
|
|
2649
2120
|
}
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
const legacyHookPath = join8(projectRoot, ".claude", "hooks", LEGACY_HOOK_FILENAME);
|
|
2654
|
-
const newHookPath = join8(projectRoot, ".claude", "hooks", NEW_HOOK_FILENAME);
|
|
2655
|
-
const settingsPath = join8(projectRoot, ".claude", "settings.json");
|
|
2656
|
-
const hasLegacyFile = existsSync4(legacyHookPath);
|
|
2657
|
-
let hasLegacySettingsCommand = false;
|
|
2658
|
-
if (existsSync4(settingsPath)) {
|
|
2659
|
-
try {
|
|
2660
|
-
const raw = readFileSync(settingsPath, "utf8");
|
|
2661
|
-
hasLegacySettingsCommand = raw.includes(LEGACY_HOOK_FILENAME);
|
|
2662
|
-
} catch {
|
|
2663
|
-
}
|
|
2121
|
+
async function ensureKnowledgeSubdirs(projectRoot) {
|
|
2122
|
+
for (const sub of KNOWLEDGE_SUBDIRS2) {
|
|
2123
|
+
await mkdir3(join5(projectRoot, ".fabric", "knowledge", sub), { recursive: true });
|
|
2664
2124
|
}
|
|
2665
|
-
return { hasLegacyFile, hasLegacySettingsCommand, legacyHookPath, newHookPath, settingsPath };
|
|
2666
2125
|
}
|
|
2667
|
-
function
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
"error",
|
|
2672
|
-
"fixable_error",
|
|
2673
|
-
"claude_hook_legacy_path",
|
|
2674
|
-
`.claude/hooks/${LEGACY_HOOK_FILENAME} (or its reference in .claude/settings.json) exists at the legacy path. Run --fix to migrate to ${NEW_HOOK_FILENAME}.`,
|
|
2675
|
-
`Run \`fab doctor --fix\` to rename ${LEGACY_HOOK_FILENAME} to ${NEW_HOOK_FILENAME} and update .claude/settings.json hook commands.`
|
|
2676
|
-
);
|
|
2126
|
+
async function fixCounterDesync(projectRoot) {
|
|
2127
|
+
const metaPath = join5(projectRoot, ".fabric", "agents.meta.json");
|
|
2128
|
+
if (!existsSync4(metaPath)) {
|
|
2129
|
+
return;
|
|
2677
2130
|
}
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
if (existsSync4(newHookPath)) {
|
|
2684
|
-
unlinkSync(legacyHookPath);
|
|
2685
|
-
} else {
|
|
2686
|
-
mkdirSync(join8(newHookPath, ".."), { recursive: true });
|
|
2687
|
-
renameSync(legacyHookPath, newHookPath);
|
|
2688
|
-
}
|
|
2131
|
+
let meta;
|
|
2132
|
+
try {
|
|
2133
|
+
meta = agentsMetaSchema4.parse(JSON.parse(await readFile5(metaPath, "utf8")));
|
|
2134
|
+
} catch {
|
|
2135
|
+
return;
|
|
2689
2136
|
}
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2137
|
+
const synthetic = {
|
|
2138
|
+
present: true,
|
|
2139
|
+
valid: true,
|
|
2140
|
+
meta,
|
|
2141
|
+
revision: meta.revision,
|
|
2142
|
+
computedRevision: null,
|
|
2143
|
+
ruleCount: 0,
|
|
2144
|
+
missingContentRefs: [],
|
|
2145
|
+
invalidContentRefs: [],
|
|
2146
|
+
stale: false,
|
|
2147
|
+
changed: false
|
|
2148
|
+
};
|
|
2149
|
+
const desync = inspectCounterDesync(synthetic);
|
|
2150
|
+
if (desync.desyncs.length === 0 || desync.correctedCounters === null) {
|
|
2151
|
+
return;
|
|
2700
2152
|
}
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2153
|
+
const updated = { ...meta, counters: desync.correctedCounters };
|
|
2154
|
+
await atomicWriteJson2(metaPath, updated, { indent: 2 });
|
|
2155
|
+
}
|
|
2156
|
+
async function ensureEventLedger(projectRoot) {
|
|
2157
|
+
const path = getEventLedgerPath(projectRoot);
|
|
2158
|
+
await ensureParentDirectory(path);
|
|
2159
|
+
await writeFile2(path, "", { encoding: "utf8", flag: "a" });
|
|
2160
|
+
}
|
|
2161
|
+
function createFixMessage(fixed, report) {
|
|
2162
|
+
const fixedText = fixed.length === 0 ? "No deterministic doctor fixes were needed." : `Applied ${fixed.length} deterministic doctor fix${fixed.length === 1 ? "" : "es"}.`;
|
|
2163
|
+
const manualText = report.manual_errors.length === 0 ? "No manual errors remain." : `${report.manual_errors.length} manual error${report.manual_errors.length === 1 ? "" : "s"} remain.`;
|
|
2164
|
+
return `${fixedText} ${manualText}`;
|
|
2165
|
+
}
|
|
2166
|
+
function isValidJsonLine(line) {
|
|
2167
|
+
try {
|
|
2168
|
+
JSON.parse(line);
|
|
2169
|
+
return true;
|
|
2170
|
+
} catch {
|
|
2171
|
+
return false;
|
|
2707
2172
|
}
|
|
2708
2173
|
}
|
|
2709
|
-
function
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2174
|
+
function normalizeTarget(targetInput) {
|
|
2175
|
+
return isAbsolute2(targetInput) ? targetInput : resolve3(process.cwd(), targetInput);
|
|
2176
|
+
}
|
|
2177
|
+
function normalizePath(path) {
|
|
2178
|
+
return posix.normalize(path.split("\\").join("/"));
|
|
2179
|
+
}
|
|
2180
|
+
function collectEntryPoints(root) {
|
|
2181
|
+
if (!existsSync4(root) || !statSync3(root).isDirectory()) {
|
|
2182
|
+
return [];
|
|
2713
2183
|
}
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2184
|
+
const entries = [];
|
|
2185
|
+
const stack = [root];
|
|
2186
|
+
while (stack.length > 0) {
|
|
2187
|
+
const current = stack.pop();
|
|
2188
|
+
if (current === void 0) {
|
|
2189
|
+
continue;
|
|
2718
2190
|
}
|
|
2719
|
-
const
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2191
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
2192
|
+
const absolutePath = join5(current, entry.name);
|
|
2193
|
+
const relativePath = normalizePath(absolutePath.slice(root.length + 1));
|
|
2194
|
+
if (relativePath.length === 0) {
|
|
2195
|
+
continue;
|
|
2196
|
+
}
|
|
2197
|
+
if (entry.isDirectory()) {
|
|
2198
|
+
if (!IGNORED_DIRECTORIES.has(entry.name)) {
|
|
2199
|
+
stack.push(absolutePath);
|
|
2200
|
+
}
|
|
2201
|
+
continue;
|
|
2202
|
+
}
|
|
2203
|
+
if (!entry.isFile()) {
|
|
2204
|
+
continue;
|
|
2205
|
+
}
|
|
2206
|
+
const reason = getEntryPointReason(relativePath);
|
|
2207
|
+
if (reason !== null) {
|
|
2208
|
+
entries.push({ path: relativePath, reason });
|
|
2209
|
+
}
|
|
2723
2210
|
}
|
|
2724
|
-
const cp = clientPaths;
|
|
2725
|
-
const presentKeys = LEGACY_CLIENT_PATH_KEYS.filter((key) => key in cp);
|
|
2726
|
-
return { presentKeys };
|
|
2727
|
-
} catch {
|
|
2728
|
-
return { presentKeys: [] };
|
|
2729
2211
|
}
|
|
2212
|
+
return entries.sort((left, right) => left.path.localeCompare(right.path));
|
|
2730
2213
|
}
|
|
2731
|
-
function
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
"warn",
|
|
2736
|
-
"warning",
|
|
2737
|
-
"legacy_client_path_present",
|
|
2738
|
-
`fabric.config.json contains deprecated clientPaths keys: ${inspection.presentKeys.join(", ")}. These clients are removed in 1.8.0; run --fix to clean now or accept the upcoming removal.`,
|
|
2739
|
-
"Run `fab doctor --fix` to remove deprecated clientPaths keys (windsurf, rooCode, geminiCLI) from fabric.config.json."
|
|
2740
|
-
);
|
|
2214
|
+
function getEntryPointReason(relativePath) {
|
|
2215
|
+
const extension = relativePath.slice(relativePath.lastIndexOf("."));
|
|
2216
|
+
if (!SCRIPT_EXTENSIONS.has(extension)) {
|
|
2217
|
+
return null;
|
|
2741
2218
|
}
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
return;
|
|
2219
|
+
const directory = posix.dirname(relativePath);
|
|
2220
|
+
const fileName = posix.basename(relativePath);
|
|
2221
|
+
const fileBase = fileName.slice(0, Math.max(fileName.lastIndexOf("."), 0));
|
|
2222
|
+
if (directory === "assets/scripts" || directory === "scripts") {
|
|
2223
|
+
return "top-level script";
|
|
2748
2224
|
}
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
const parsed = JSON.parse(readFileSync(configPath, "utf8"));
|
|
2752
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2753
|
-
return;
|
|
2754
|
-
}
|
|
2755
|
-
config = parsed;
|
|
2756
|
-
} catch {
|
|
2757
|
-
return;
|
|
2225
|
+
if (directory === "src" && /^(App|app|index|main)$/.test(fileBase)) {
|
|
2226
|
+
return "application entry";
|
|
2758
2227
|
}
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
return;
|
|
2228
|
+
if ((directory === "app" || directory.startsWith("app/")) && /^(layout|page|route)$/.test(fileBase)) {
|
|
2229
|
+
return "next app route";
|
|
2762
2230
|
}
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
for (const key of LEGACY_CLIENT_PATH_KEYS) {
|
|
2766
|
-
if (key in cp) {
|
|
2767
|
-
delete cp[key];
|
|
2768
|
-
removed.push(key);
|
|
2769
|
-
}
|
|
2231
|
+
if ((directory === "pages" || directory.startsWith("pages/")) && fileName !== "_app.d.ts") {
|
|
2232
|
+
return "next page route";
|
|
2770
2233
|
}
|
|
2771
|
-
|
|
2772
|
-
|
|
2234
|
+
return null;
|
|
2235
|
+
}
|
|
2236
|
+
function reduceStatus(statuses) {
|
|
2237
|
+
if (statuses.includes("error")) {
|
|
2238
|
+
return "error";
|
|
2773
2239
|
}
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2240
|
+
if (statuses.includes("warn")) {
|
|
2241
|
+
return "warn";
|
|
2242
|
+
}
|
|
2243
|
+
return "ok";
|
|
2244
|
+
}
|
|
2245
|
+
function isMissingFileError(error) {
|
|
2246
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
// src/services/get-rules.ts
|
|
2250
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
2251
|
+
import { join as join7 } from "path";
|
|
2252
|
+
import { minimatch } from "minimatch";
|
|
2253
|
+
|
|
2254
|
+
// src/services/audit-log.ts
|
|
2255
|
+
import { open, stat as stat2 } from "fs/promises";
|
|
2256
|
+
import { isAbsolute as isAbsolute3, join as join6, posix as posix2, relative as relative3, resolve as resolve4 } from "path";
|
|
2257
|
+
var AUDIT_LOG_FILE = `${FABRIC_DIR}/audit.jsonl`;
|
|
2258
|
+
var DEFAULT_AUDIT_WINDOW_MS = 5 * 60 * 1e3;
|
|
2259
|
+
async function appendGetRulesAuditEvent(projectRoot, input) {
|
|
2260
|
+
const entry = {
|
|
2261
|
+
kind: "audit-event",
|
|
2262
|
+
event: "get_rules",
|
|
2263
|
+
ts: input.ts ?? Date.now(),
|
|
2264
|
+
path: normalizeAuditPath(projectRoot, input.path),
|
|
2265
|
+
client_hash: input.client_hash
|
|
2266
|
+
};
|
|
2267
|
+
await appendAuditLogEventLedgerEvents(projectRoot, [entry], {
|
|
2268
|
+
rule_context: {
|
|
2269
|
+
required_stable_ids: input.required_stable_ids,
|
|
2270
|
+
ai_selectable_stable_ids: input.ai_selectable_stable_ids,
|
|
2271
|
+
final_stable_ids: input.final_stable_ids
|
|
2272
|
+
},
|
|
2273
|
+
correlation_id: input.correlation_id,
|
|
2274
|
+
session_id: input.session_id
|
|
2779
2275
|
});
|
|
2276
|
+
return entry;
|
|
2780
2277
|
}
|
|
2781
|
-
async function
|
|
2782
|
-
const
|
|
2783
|
-
|
|
2784
|
-
|
|
2278
|
+
async function appendRuleSelectionAuditEvent(projectRoot, input) {
|
|
2279
|
+
const entry = {
|
|
2280
|
+
kind: "audit-event",
|
|
2281
|
+
event: "knowledge_selection",
|
|
2282
|
+
ts: input.ts ?? Date.now(),
|
|
2283
|
+
path: normalizeAuditPath(projectRoot, input.path),
|
|
2284
|
+
selection_token: input.selection_token,
|
|
2285
|
+
target_paths: input.target_paths.map((path) => normalizeAuditPath(projectRoot, path)),
|
|
2286
|
+
required_stable_ids: input.required_stable_ids,
|
|
2287
|
+
ai_selectable_stable_ids: input.ai_selectable_stable_ids,
|
|
2288
|
+
ai_selected_stable_ids: input.ai_selected_stable_ids,
|
|
2289
|
+
final_stable_ids: input.final_stable_ids,
|
|
2290
|
+
ai_selection_reasons: input.ai_selection_reasons,
|
|
2291
|
+
rejected_stable_ids: input.rejected_stable_ids,
|
|
2292
|
+
ignored_stable_ids: input.ignored_stable_ids
|
|
2293
|
+
};
|
|
2294
|
+
await appendAuditLogEventLedgerEvents(projectRoot, [entry], {
|
|
2295
|
+
correlation_id: input.correlation_id,
|
|
2296
|
+
session_id: input.session_id
|
|
2297
|
+
});
|
|
2298
|
+
return entry;
|
|
2299
|
+
}
|
|
2300
|
+
function normalizeAuditPath(projectRoot, value) {
|
|
2301
|
+
const normalizedProjectRoot = resolve4(projectRoot);
|
|
2302
|
+
const candidate = isAbsolute3(value) ? resolve4(value) : resolve4(normalizedProjectRoot, value);
|
|
2303
|
+
const relativePath = relative3(normalizedProjectRoot, candidate);
|
|
2304
|
+
if (relativePath.length > 0 && relativePath !== "." && !relativePath.startsWith("..") && !isAbsolute3(relativePath)) {
|
|
2305
|
+
return posix2.normalize(relativePath.split("\\").join("/"));
|
|
2785
2306
|
}
|
|
2786
|
-
|
|
2307
|
+
return posix2.normalize(value.replaceAll("\\", "/"));
|
|
2308
|
+
}
|
|
2309
|
+
async function appendAuditLogEventLedgerEvents(projectRoot, entries, metadata = {}) {
|
|
2310
|
+
for (const entry of entries) {
|
|
2311
|
+
if (entry.event === "get_rules") {
|
|
2312
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
2313
|
+
event_type: "knowledge_context_planned",
|
|
2314
|
+
ts: entry.ts,
|
|
2315
|
+
target_paths: [entry.path],
|
|
2316
|
+
required_stable_ids: metadata.rule_context?.required_stable_ids ?? [],
|
|
2317
|
+
ai_selectable_stable_ids: metadata.rule_context?.ai_selectable_stable_ids ?? [],
|
|
2318
|
+
final_stable_ids: metadata.rule_context?.final_stable_ids ?? [],
|
|
2319
|
+
client_hash: entry.client_hash,
|
|
2320
|
+
correlation_id: metadata.correlation_id,
|
|
2321
|
+
session_id: metadata.session_id
|
|
2322
|
+
});
|
|
2323
|
+
continue;
|
|
2324
|
+
}
|
|
2325
|
+
if (entry.event === "knowledge_selection") {
|
|
2326
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
2327
|
+
event_type: "knowledge_selection",
|
|
2328
|
+
ts: entry.ts,
|
|
2329
|
+
selection_token: entry.selection_token,
|
|
2330
|
+
target_paths: entry.target_paths,
|
|
2331
|
+
required_stable_ids: entry.required_stable_ids,
|
|
2332
|
+
ai_selectable_stable_ids: entry.ai_selectable_stable_ids,
|
|
2333
|
+
ai_selected_stable_ids: entry.ai_selected_stable_ids,
|
|
2334
|
+
final_stable_ids: entry.final_stable_ids,
|
|
2335
|
+
ai_selection_reasons: entry.ai_selection_reasons,
|
|
2336
|
+
rejected_stable_ids: entry.rejected_stable_ids,
|
|
2337
|
+
ignored_stable_ids: entry.ignored_stable_ids,
|
|
2338
|
+
correlation_id: metadata.correlation_id,
|
|
2339
|
+
session_id: metadata.session_id
|
|
2340
|
+
});
|
|
2341
|
+
continue;
|
|
2342
|
+
}
|
|
2343
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
2344
|
+
event_type: "edit_intent_checked",
|
|
2345
|
+
ts: entry.ts,
|
|
2346
|
+
path: entry.path,
|
|
2347
|
+
compliant: entry.compliant,
|
|
2348
|
+
intent: entry.intent,
|
|
2349
|
+
ledger_entry_id: entry.ledger_entry_id,
|
|
2350
|
+
ledger_source: "ai",
|
|
2351
|
+
matched_rule_context_ts: entry.matched_get_rules_ts,
|
|
2352
|
+
window_ms: entry.window_ms,
|
|
2353
|
+
correlation_id: metadata.correlation_id,
|
|
2354
|
+
session_id: metadata.session_id
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
// src/services/get-rules.ts
|
|
2360
|
+
var PRIORITY_ORDER = {
|
|
2361
|
+
high: 0,
|
|
2362
|
+
medium: 1,
|
|
2363
|
+
low: 2
|
|
2364
|
+
};
|
|
2365
|
+
async function getRules(projectRoot, input) {
|
|
2366
|
+
const context = await loadGetRulesContext(projectRoot);
|
|
2367
|
+
const stale = input.client_hash !== void 0 && input.client_hash !== context.meta.revision;
|
|
2368
|
+
const matchedNodes = matchRuleNodes(context.meta, input.path);
|
|
2369
|
+
const requiredStableIds = matchedNodes.filter((node) => node.level === "L2").map((node) => node.stable_id);
|
|
2370
|
+
const aiSelectableStableIds = matchedNodes.filter((node) => node.level === "L1").map((node) => node.stable_id);
|
|
2371
|
+
const rules = await resolveRulesForPath(projectRoot, context, input.path);
|
|
2372
|
+
const result = {
|
|
2373
|
+
revision_hash: context.meta.revision,
|
|
2374
|
+
stale,
|
|
2375
|
+
rules
|
|
2376
|
+
};
|
|
2787
2377
|
try {
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2378
|
+
await appendGetRulesAuditEvent(projectRoot, {
|
|
2379
|
+
path: input.path,
|
|
2380
|
+
client_hash: input.client_hash,
|
|
2381
|
+
required_stable_ids: requiredStableIds,
|
|
2382
|
+
ai_selectable_stable_ids: aiSelectableStableIds,
|
|
2383
|
+
final_stable_ids: [...requiredStableIds, ...aiSelectableStableIds],
|
|
2384
|
+
correlation_id: input.correlation_id,
|
|
2385
|
+
session_id: input.session_id
|
|
2386
|
+
});
|
|
2793
2387
|
} catch {
|
|
2794
|
-
return;
|
|
2795
2388
|
}
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
if (Object.keys(remainingServers).length === 0) {
|
|
2803
|
-
delete cleaned.mcpServers;
|
|
2804
|
-
} else {
|
|
2805
|
-
cleaned.mcpServers = remainingServers;
|
|
2389
|
+
return result;
|
|
2390
|
+
}
|
|
2391
|
+
async function loadGetRulesContext(projectRoot) {
|
|
2392
|
+
const cached = contextCache.get("context", projectRoot);
|
|
2393
|
+
if (cached !== void 0) {
|
|
2394
|
+
return cached;
|
|
2806
2395
|
}
|
|
2807
|
-
|
|
2808
|
-
await
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2396
|
+
const meta = await readAgentsMeta(projectRoot);
|
|
2397
|
+
const l0Content = await readFile6(join7(projectRoot, ".fabric", "bootstrap", "README.md"), "utf8");
|
|
2398
|
+
const context = {
|
|
2399
|
+
meta,
|
|
2400
|
+
l0Content,
|
|
2401
|
+
humanLockedNearby: []
|
|
2402
|
+
};
|
|
2403
|
+
contextCache.set("context", projectRoot, context);
|
|
2404
|
+
return context;
|
|
2813
2405
|
}
|
|
2814
|
-
async function
|
|
2815
|
-
const
|
|
2816
|
-
await
|
|
2817
|
-
|
|
2406
|
+
async function resolveRulesForPath(projectRoot, context, path, options = {}) {
|
|
2407
|
+
const matchedNodes = matchRuleNodes(context.meta, path);
|
|
2408
|
+
const loaded = await loadMatchedRules(projectRoot, matchedNodes);
|
|
2409
|
+
return buildRulesPayload(context, loaded, options);
|
|
2818
2410
|
}
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
await ensureParentDirectory(path);
|
|
2822
|
-
await writeFile2(path, "", { encoding: "utf8", flag: "a" });
|
|
2411
|
+
function normalizeRulesPath(value) {
|
|
2412
|
+
return value.replaceAll("\\", "/");
|
|
2823
2413
|
}
|
|
2824
|
-
function
|
|
2825
|
-
const
|
|
2826
|
-
|
|
2827
|
-
|
|
2414
|
+
function matchRuleNodes(meta, path) {
|
|
2415
|
+
const requestedPath = normalizeRulesPath(path);
|
|
2416
|
+
return Object.entries(meta.nodes).filter(([, node]) => shouldLoadNodeForPath(requestedPath, node)).sort((left, right) => {
|
|
2417
|
+
const [leftId, leftNode] = left;
|
|
2418
|
+
const [rightId, rightNode] = right;
|
|
2419
|
+
const priorityDelta = PRIORITY_ORDER[leftNode.priority] - PRIORITY_ORDER[rightNode.priority];
|
|
2420
|
+
return priorityDelta !== 0 ? priorityDelta : leftId.localeCompare(rightId);
|
|
2421
|
+
}).map(([nodeId, node]) => ({
|
|
2422
|
+
node_id: nodeId,
|
|
2423
|
+
level: classifyNode(nodeId, node),
|
|
2424
|
+
stable_id: node.stable_id ?? nodeId,
|
|
2425
|
+
identity_source: node.identity_source ?? "derived",
|
|
2426
|
+
node
|
|
2427
|
+
}));
|
|
2828
2428
|
}
|
|
2829
|
-
function
|
|
2830
|
-
const
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
const files = [];
|
|
2835
|
-
const stack = [rulesRoot];
|
|
2836
|
-
while (stack.length > 0) {
|
|
2837
|
-
const current = stack.pop();
|
|
2838
|
-
if (current === void 0) {
|
|
2429
|
+
async function loadMatchedRules(projectRoot, matchedNodes, fileContentCache = /* @__PURE__ */ new Map()) {
|
|
2430
|
+
const rules = [];
|
|
2431
|
+
const stubs = [];
|
|
2432
|
+
for (const matchedNode of matchedNodes) {
|
|
2433
|
+
if (matchedNode.level === null) {
|
|
2839
2434
|
continue;
|
|
2840
2435
|
}
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
}
|
|
2436
|
+
if (matchedNode.node.activation?.tier === "description") {
|
|
2437
|
+
stubs.push({
|
|
2438
|
+
stable_id: matchedNode.stable_id,
|
|
2439
|
+
identity_source: matchedNode.identity_source,
|
|
2440
|
+
level: matchedNode.level,
|
|
2441
|
+
path: matchedNode.node.file,
|
|
2442
|
+
description: matchedNode.node.activation.description ?? ""
|
|
2443
|
+
});
|
|
2444
|
+
continue;
|
|
2849
2445
|
}
|
|
2446
|
+
rules.push({
|
|
2447
|
+
level: matchedNode.level,
|
|
2448
|
+
stable_id: matchedNode.stable_id,
|
|
2449
|
+
identity_source: matchedNode.identity_source,
|
|
2450
|
+
entry: {
|
|
2451
|
+
path: matchedNode.node.file,
|
|
2452
|
+
content: await readRuleContent(projectRoot, matchedNode.node.file, fileContentCache)
|
|
2453
|
+
}
|
|
2454
|
+
});
|
|
2850
2455
|
}
|
|
2851
|
-
return
|
|
2852
|
-
}
|
|
2853
|
-
function isValidJsonLine(line) {
|
|
2854
|
-
try {
|
|
2855
|
-
JSON.parse(line);
|
|
2856
|
-
return true;
|
|
2857
|
-
} catch {
|
|
2858
|
-
return false;
|
|
2859
|
-
}
|
|
2860
|
-
}
|
|
2861
|
-
function normalizeTarget(targetInput) {
|
|
2862
|
-
return isAbsolute3(targetInput) ? targetInput : resolve4(process.cwd(), targetInput);
|
|
2456
|
+
return { rules, stubs };
|
|
2863
2457
|
}
|
|
2864
|
-
function
|
|
2865
|
-
|
|
2458
|
+
function buildRulesPayload(context, loaded, options = {}) {
|
|
2459
|
+
const { L1, L2 } = partitionRulesByLevel(loaded.rules, options.dedupeByPath ?? false);
|
|
2460
|
+
return {
|
|
2461
|
+
L0: context.l0Content,
|
|
2462
|
+
L1,
|
|
2463
|
+
L2,
|
|
2464
|
+
human_locked_nearby: context.humanLockedNearby,
|
|
2465
|
+
description_stubs: loaded.stubs.length > 0 ? dedupeDescriptionStubsByPath(loaded.stubs).map(toDescriptionStub) : void 0
|
|
2466
|
+
};
|
|
2866
2467
|
}
|
|
2867
|
-
function
|
|
2868
|
-
if (
|
|
2869
|
-
return
|
|
2468
|
+
function classifyNode(nodeId, node) {
|
|
2469
|
+
if (nodeId.startsWith("L1/")) {
|
|
2470
|
+
return "L1";
|
|
2870
2471
|
}
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2472
|
+
if (nodeId.startsWith("L2/")) {
|
|
2473
|
+
return "L2";
|
|
2474
|
+
}
|
|
2475
|
+
return node.layer === "L0" ? null : node.layer;
|
|
2476
|
+
}
|
|
2477
|
+
function partitionRulesByLevel(loadedRules, dedupeByPath) {
|
|
2478
|
+
const l1 = [];
|
|
2479
|
+
const l2 = [];
|
|
2480
|
+
for (const rule of loadedRules) {
|
|
2481
|
+
if (rule.level === "L1") {
|
|
2482
|
+
l1.push(rule.entry);
|
|
2876
2483
|
continue;
|
|
2877
2484
|
}
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
const relativePath = normalizePath(absolutePath.slice(root.length + 1));
|
|
2881
|
-
if (relativePath.length === 0) {
|
|
2882
|
-
continue;
|
|
2883
|
-
}
|
|
2884
|
-
if (entry.isDirectory()) {
|
|
2885
|
-
if (!IGNORED_DIRECTORIES.has(entry.name)) {
|
|
2886
|
-
stack.push(absolutePath);
|
|
2887
|
-
}
|
|
2888
|
-
continue;
|
|
2889
|
-
}
|
|
2890
|
-
if (!entry.isFile()) {
|
|
2891
|
-
continue;
|
|
2892
|
-
}
|
|
2893
|
-
const reason = getEntryPointReason(relativePath);
|
|
2894
|
-
if (reason !== null) {
|
|
2895
|
-
entries.push({ path: relativePath, reason });
|
|
2896
|
-
}
|
|
2485
|
+
if (rule.level === "L2") {
|
|
2486
|
+
l2.push(rule.entry);
|
|
2897
2487
|
}
|
|
2898
2488
|
}
|
|
2899
|
-
return
|
|
2489
|
+
return {
|
|
2490
|
+
L1: dedupeByPath ? dedupeEntriesByPath(l1) : l1,
|
|
2491
|
+
L2: dedupeByPath ? dedupeEntriesByPath(l2) : l2
|
|
2492
|
+
};
|
|
2900
2493
|
}
|
|
2901
|
-
function
|
|
2902
|
-
const
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
return "top-level script";
|
|
2911
|
-
}
|
|
2912
|
-
if (directory === "src" && /^(App|app|index|main)$/.test(fileBase)) {
|
|
2913
|
-
return "application entry";
|
|
2914
|
-
}
|
|
2915
|
-
if ((directory === "app" || directory.startsWith("app/")) && /^(layout|page|route)$/.test(fileBase)) {
|
|
2916
|
-
return "next app route";
|
|
2917
|
-
}
|
|
2918
|
-
if ((directory === "pages" || directory.startsWith("pages/")) && fileName !== "_app.d.ts") {
|
|
2919
|
-
return "next page route";
|
|
2920
|
-
}
|
|
2921
|
-
return null;
|
|
2494
|
+
function dedupeEntriesByPath(entries) {
|
|
2495
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
2496
|
+
return entries.filter((entry) => {
|
|
2497
|
+
if (seenPaths.has(entry.path)) {
|
|
2498
|
+
return false;
|
|
2499
|
+
}
|
|
2500
|
+
seenPaths.add(entry.path);
|
|
2501
|
+
return true;
|
|
2502
|
+
});
|
|
2922
2503
|
}
|
|
2923
|
-
function
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2504
|
+
function shouldLoadNodeForPath(requestedPath, node) {
|
|
2505
|
+
switch (node.activation?.tier) {
|
|
2506
|
+
case "always":
|
|
2507
|
+
return true;
|
|
2508
|
+
case "description":
|
|
2509
|
+
return true;
|
|
2510
|
+
case "path":
|
|
2511
|
+
case void 0:
|
|
2512
|
+
return minimatch(requestedPath, normalizeRulesPath(node.scope_glob), { dot: true });
|
|
2929
2513
|
}
|
|
2930
|
-
return "ok";
|
|
2931
2514
|
}
|
|
2932
|
-
function
|
|
2933
|
-
|
|
2515
|
+
function dedupeDescriptionStubsByPath(stubs) {
|
|
2516
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
2517
|
+
return stubs.filter((stub) => {
|
|
2518
|
+
if (seenPaths.has(stub.path)) {
|
|
2519
|
+
return false;
|
|
2520
|
+
}
|
|
2521
|
+
seenPaths.add(stub.path);
|
|
2522
|
+
return true;
|
|
2523
|
+
});
|
|
2524
|
+
}
|
|
2525
|
+
function toDescriptionStub(stub) {
|
|
2526
|
+
return {
|
|
2527
|
+
path: stub.path,
|
|
2528
|
+
description: stub.description
|
|
2529
|
+
};
|
|
2530
|
+
}
|
|
2531
|
+
async function readRuleContent(projectRoot, file, fileContentCache) {
|
|
2532
|
+
const cached = fileContentCache.get(file);
|
|
2533
|
+
if (cached !== void 0) {
|
|
2534
|
+
return await cached;
|
|
2535
|
+
}
|
|
2536
|
+
const pending = readFile6(join7(projectRoot, file), "utf8");
|
|
2537
|
+
fileContentCache.set(file, pending);
|
|
2538
|
+
return await pending;
|
|
2934
2539
|
}
|
|
2935
2540
|
|
|
2936
2541
|
export {
|
|
2937
|
-
AGENTS_MD_RESOURCE_URI,
|
|
2938
2542
|
contextCache,
|
|
2939
2543
|
resolveProjectRoot,
|
|
2940
2544
|
readAgentsMeta,
|
|
@@ -2960,9 +2564,9 @@ export {
|
|
|
2960
2564
|
invalidateRuleSyncCooldown,
|
|
2961
2565
|
ensureRulesFresh,
|
|
2962
2566
|
reconcileRules,
|
|
2567
|
+
appendRuleSelectionAuditEvent,
|
|
2963
2568
|
getRules,
|
|
2964
|
-
|
|
2965
|
-
getRuleSections,
|
|
2569
|
+
normalizeRulesPath,
|
|
2966
2570
|
runDoctorReport,
|
|
2967
2571
|
runDoctorFix
|
|
2968
2572
|
};
|