@fenglimg/fabric-server 1.8.0-rc.3 → 2.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-ZZGARZL5.js → chunk-NRWDWAVO.js} +940 -1387
- package/dist/{http-PXFWUKCA.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-BSbndc76.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,846 +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 codexSkillLegacyPath = inspectCodexSkillLegacyPath(projectRoot);
|
|
1916
|
-
const preexistingRootFiles = inspectPreexistingRootFiles(projectRoot);
|
|
1917
|
-
const legacyClientPaths = inspectLegacyClientPaths(projectRoot);
|
|
1918
|
-
const taxonomyExists = existsSync4(join8(projectRoot, ".fabric", "INITIAL_TAXONOMY.md"));
|
|
1919
|
-
const bootstrapExists = existsSync4(join8(projectRoot, ".fabric", "bootstrap", "README.md"));
|
|
1920
|
-
const checks = [
|
|
1921
|
-
createBootstrapCheck(bootstrapExists),
|
|
1922
|
-
createTaxonomyCheck(taxonomyExists),
|
|
1923
|
-
createForensicCheck(forensic, framework.kind, entryPoints.length),
|
|
1924
|
-
createInitContextCheck(initContext),
|
|
1925
|
-
createMetaCheck(meta),
|
|
1926
|
-
createRuleContentRefCheck(meta),
|
|
1927
|
-
createRuleSectionsCheck(ruleSections),
|
|
1928
|
-
createRuleTestIndexCheck(ruleTestIndex),
|
|
1929
|
-
createEventLedgerCheck(eventLedger),
|
|
1930
|
-
createEventLedgerPartialWriteCheck(eventLedger),
|
|
1931
|
-
createMcpConfigInWrongFileCheck(mcpConfigInWrongFile),
|
|
1932
|
-
createMetaManuallyDivergedCheck(metaManuallyDiverged),
|
|
1933
|
-
createRulesDirUnindexedCheck(rulesDirUnindexed),
|
|
1934
|
-
createStableIdCollisionCheck(stableIdCollision),
|
|
1935
|
-
createClaudeSkillLegacyPathCheck(claudeSkillLegacyPath),
|
|
1936
|
-
createClaudeHookLegacyPathCheck(claudeHookLegacyPath),
|
|
1937
|
-
createCodexSkillLegacyPathCheck(codexSkillLegacyPath),
|
|
1938
|
-
createPreexistingRootFilesCheck(preexistingRootFiles),
|
|
1939
|
-
createLegacyClientPathCheck(legacyClientPaths)
|
|
1940
|
-
];
|
|
1941
|
-
const fixableErrors = collectIssues(checks, "fixable_error");
|
|
1942
|
-
const manualErrors = collectIssues(checks, "manual_error");
|
|
1943
|
-
const warnings = collectIssues(checks, "warning");
|
|
1944
|
-
const infos = collectIssues(checks, "info");
|
|
1945
|
-
return {
|
|
1946
|
-
status: reduceStatus(checks.map((check) => check.status)),
|
|
1947
|
-
checks,
|
|
1948
|
-
fixable_errors: fixableErrors,
|
|
1949
|
-
manual_errors: manualErrors,
|
|
1950
|
-
warnings,
|
|
1951
|
-
infos,
|
|
1952
|
-
summary: {
|
|
1953
|
-
target: projectRoot,
|
|
1954
|
-
framework: {
|
|
1955
|
-
kind: framework.kind,
|
|
1956
|
-
version: framework.version,
|
|
1957
|
-
subkind: framework.subkind
|
|
1958
|
-
},
|
|
1959
|
-
entryPoints,
|
|
1960
|
-
metaRevision: meta.revision,
|
|
1961
|
-
computedMetaRevision: meta.computedRevision,
|
|
1962
|
-
ruleCount: meta.ruleCount,
|
|
1963
|
-
eventLedgerPath: eventLedger.path,
|
|
1964
|
-
fixableErrorCount: fixableErrors.length,
|
|
1965
|
-
manualErrorCount: manualErrors.length,
|
|
1966
|
-
warningCount: warnings.length,
|
|
1967
|
-
infoCount: infos.length,
|
|
1968
|
-
targetFiles: Object.fromEntries(
|
|
1969
|
-
TARGET_FILE_PATHS.map((path) => [path, existsSync4(join8(projectRoot, path))])
|
|
1970
|
-
)
|
|
1971
|
-
}
|
|
1972
|
-
};
|
|
1973
|
-
}
|
|
1974
|
-
async function runDoctorFix(target) {
|
|
1975
|
-
const projectRoot = normalizeTarget(target);
|
|
1976
|
-
const before = await runDoctorReport(projectRoot);
|
|
1977
|
-
const fixed = [];
|
|
1978
|
-
if (before.fixable_errors.some((issue) => issue.code === "bootstrap_missing")) {
|
|
1979
|
-
await writeDefaultBootstrap(projectRoot);
|
|
1980
|
-
fixed.push(findIssue(before.fixable_errors, "bootstrap_missing"));
|
|
1981
|
-
}
|
|
1982
|
-
if (before.fixable_errors.some((issue) => issue.code === "event_ledger_missing")) {
|
|
1983
|
-
await ensureEventLedger(projectRoot);
|
|
1984
|
-
fixed.push(findIssue(before.fixable_errors, "event_ledger_missing"));
|
|
1395
|
+
if (before.fixable_errors.some((issue) => issue.code === "counter_desync")) {
|
|
1396
|
+
await fixCounterDesync(projectRoot);
|
|
1397
|
+
fixed.push(findIssue(before.fixable_errors, "counter_desync"));
|
|
1398
|
+
contextCache.invalidate("meta_write", projectRoot);
|
|
1985
1399
|
}
|
|
1986
1400
|
if (before.fixable_errors.some(
|
|
1987
1401
|
(issue) => [
|
|
@@ -1990,7 +1404,7 @@ async function runDoctorFix(target) {
|
|
|
1990
1404
|
"rule_test_index_missing",
|
|
1991
1405
|
"rule_test_index_stale",
|
|
1992
1406
|
"content_ref_missing",
|
|
1993
|
-
"
|
|
1407
|
+
"knowledge_dir_unindexed"
|
|
1994
1408
|
].includes(issue.code)
|
|
1995
1409
|
)) {
|
|
1996
1410
|
await reconcileRules(projectRoot, { trigger: "doctor" });
|
|
@@ -2001,12 +1415,14 @@ async function runDoctorFix(target) {
|
|
|
2001
1415
|
"rule_test_index_missing",
|
|
2002
1416
|
"rule_test_index_stale",
|
|
2003
1417
|
"content_ref_missing",
|
|
2004
|
-
"
|
|
1418
|
+
"knowledge_dir_unindexed"
|
|
2005
1419
|
].includes(candidate.code)
|
|
2006
1420
|
)) {
|
|
2007
1421
|
fixed.push(issue);
|
|
2008
1422
|
}
|
|
2009
1423
|
contextCache.invalidate("meta_write", projectRoot);
|
|
1424
|
+
await fixCounterDesync(projectRoot);
|
|
1425
|
+
contextCache.invalidate("meta_write", projectRoot);
|
|
2010
1426
|
}
|
|
2011
1427
|
if (before.fixable_errors.some((issue) => issue.code === "event_ledger_partial_write")) {
|
|
2012
1428
|
const ledgerPath = getEventLedgerPath(projectRoot);
|
|
@@ -2023,22 +1439,6 @@ async function runDoctorFix(target) {
|
|
|
2023
1439
|
await fixMcpConfigInWrongFile(projectRoot);
|
|
2024
1440
|
fixed.push(findIssue(before.fixable_errors, "mcp_config_in_wrong_file"));
|
|
2025
1441
|
}
|
|
2026
|
-
if (before.fixable_errors.some((issue) => issue.code === "claude_skill_legacy_path")) {
|
|
2027
|
-
await fixClaudeSkillLegacyPath(projectRoot);
|
|
2028
|
-
fixed.push(findIssue(before.fixable_errors, "claude_skill_legacy_path"));
|
|
2029
|
-
}
|
|
2030
|
-
if (before.fixable_errors.some((issue) => issue.code === "claude_hook_legacy_path")) {
|
|
2031
|
-
await fixClaudeHookLegacyPath(projectRoot);
|
|
2032
|
-
fixed.push(findIssue(before.fixable_errors, "claude_hook_legacy_path"));
|
|
2033
|
-
}
|
|
2034
|
-
if (before.fixable_errors.some((issue) => issue.code === "codex_skill_legacy_path")) {
|
|
2035
|
-
await fixCodexSkillLegacyPath(projectRoot);
|
|
2036
|
-
fixed.push(findIssue(before.fixable_errors, "codex_skill_legacy_path"));
|
|
2037
|
-
}
|
|
2038
|
-
if (before.warnings.some((issue) => issue.code === "legacy_client_path_present")) {
|
|
2039
|
-
await fixLegacyClientPaths(projectRoot);
|
|
2040
|
-
fixed.push(findIssue(before.warnings, "legacy_client_path_present"));
|
|
2041
|
-
}
|
|
2042
1442
|
const report = await runDoctorReport(projectRoot);
|
|
2043
1443
|
return {
|
|
2044
1444
|
changed: fixed.length > 0,
|
|
@@ -2050,9 +1450,9 @@ async function runDoctorFix(target) {
|
|
|
2050
1450
|
};
|
|
2051
1451
|
}
|
|
2052
1452
|
async function inspectForensic(projectRoot) {
|
|
2053
|
-
const path =
|
|
1453
|
+
const path = join5(projectRoot, ".fabric", "forensic.json");
|
|
2054
1454
|
try {
|
|
2055
|
-
const parsed = forensicReportSchema.parse(JSON.parse(await
|
|
1455
|
+
const parsed = forensicReportSchema.parse(JSON.parse(await readFile5(path, "utf8")));
|
|
2056
1456
|
return { present: true, valid: true, report: parsed };
|
|
2057
1457
|
} catch (error) {
|
|
2058
1458
|
if (isMissingFileError(error)) {
|
|
@@ -2061,20 +1461,8 @@ async function inspectForensic(projectRoot) {
|
|
|
2061
1461
|
return { present: true, valid: false, report: null, error: error instanceof Error ? error.message : String(error) };
|
|
2062
1462
|
}
|
|
2063
1463
|
}
|
|
2064
|
-
async function inspectInitContext(projectRoot) {
|
|
2065
|
-
const path = join8(projectRoot, ".fabric", "init-context.json");
|
|
2066
|
-
try {
|
|
2067
|
-
JSON.parse(await readFile7(path, "utf8"));
|
|
2068
|
-
return { exists: true, validJson: true };
|
|
2069
|
-
} catch (error) {
|
|
2070
|
-
if (isMissingFileError(error)) {
|
|
2071
|
-
return { exists: false, validJson: false, error: ".fabric/init-context.json is missing." };
|
|
2072
|
-
}
|
|
2073
|
-
return { exists: true, validJson: false, error: error instanceof Error ? error.message : String(error) };
|
|
2074
|
-
}
|
|
2075
|
-
}
|
|
2076
1464
|
function inspectMcpConfigInWrongFile(projectRoot) {
|
|
2077
|
-
const settingsPath =
|
|
1465
|
+
const settingsPath = join5(projectRoot, ".claude", "settings.json");
|
|
2078
1466
|
if (!existsSync4(settingsPath)) {
|
|
2079
1467
|
return { hasWrongEntry: false, settingsPath };
|
|
2080
1468
|
}
|
|
@@ -2095,10 +1483,10 @@ function inspectMcpConfigInWrongFile(projectRoot) {
|
|
|
2095
1483
|
}
|
|
2096
1484
|
}
|
|
2097
1485
|
async function inspectMeta(projectRoot) {
|
|
2098
|
-
const metaPath =
|
|
1486
|
+
const metaPath = join5(projectRoot, ".fabric", "agents.meta.json");
|
|
2099
1487
|
const built = await tryBuildRuleMeta(projectRoot);
|
|
2100
1488
|
try {
|
|
2101
|
-
const raw = await
|
|
1489
|
+
const raw = await readFile5(metaPath, "utf8");
|
|
2102
1490
|
const meta = agentsMetaSchema4.parse(JSON.parse(raw));
|
|
2103
1491
|
const contentRefIssues = inspectContentRefs(projectRoot, meta);
|
|
2104
1492
|
const changed = built === null ? false : built.changed;
|
|
@@ -2108,7 +1496,10 @@ async function inspectMeta(projectRoot) {
|
|
|
2108
1496
|
meta,
|
|
2109
1497
|
revision: meta.revision,
|
|
2110
1498
|
computedRevision: built?.meta.revision ?? null,
|
|
2111
|
-
ruleCount: Object.values(meta.nodes).filter((node) =>
|
|
1499
|
+
ruleCount: Object.values(meta.nodes).filter((node) => {
|
|
1500
|
+
const ref = node.content_ref ?? node.file;
|
|
1501
|
+
return ref.startsWith(".fabric/knowledge/") || ref.startsWith("~/.fabric/knowledge/");
|
|
1502
|
+
}).length,
|
|
2112
1503
|
missingContentRefs: contentRefIssues.missing,
|
|
2113
1504
|
invalidContentRefs: contentRefIssues.invalid,
|
|
2114
1505
|
stale: changed || built !== null && meta.revision !== built.meta.revision,
|
|
@@ -2156,17 +1547,16 @@ function inspectContentRefs(projectRoot, meta) {
|
|
|
2156
1547
|
const invalid = [];
|
|
2157
1548
|
for (const node of Object.values(meta.nodes)) {
|
|
2158
1549
|
const contentRef = normalizePath(node.content_ref ?? node.file);
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
1550
|
+
const isPersonalKnowledge = contentRef.startsWith("~/.fabric/knowledge/");
|
|
1551
|
+
const isTeamKnowledge = contentRef.startsWith(".fabric/knowledge/");
|
|
1552
|
+
if (!isPersonalKnowledge && !isTeamKnowledge) {
|
|
1553
|
+
invalid.push(contentRef);
|
|
2163
1554
|
continue;
|
|
2164
1555
|
}
|
|
2165
|
-
if (
|
|
2166
|
-
invalid.push(contentRef);
|
|
1556
|
+
if (isPersonalKnowledge) {
|
|
2167
1557
|
continue;
|
|
2168
1558
|
}
|
|
2169
|
-
if (!existsSync4(
|
|
1559
|
+
if (!existsSync4(join5(projectRoot, contentRef))) {
|
|
2170
1560
|
missing.push(contentRef);
|
|
2171
1561
|
}
|
|
2172
1562
|
}
|
|
@@ -2181,7 +1571,7 @@ async function inspectEventLedger(projectRoot) {
|
|
|
2181
1571
|
try {
|
|
2182
1572
|
await access(path, constants.W_OK);
|
|
2183
1573
|
const { warnings } = await readEventLedger(projectRoot);
|
|
2184
|
-
const raw = await
|
|
1574
|
+
const raw = await readFile5(path, "utf8");
|
|
2185
1575
|
const invalidLine = raw.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).find((line) => !isValidJsonLine(line));
|
|
2186
1576
|
const partialWarning = warnings.find((w) => w.kind === "partial_write_at_tail");
|
|
2187
1577
|
return {
|
|
@@ -2207,29 +1597,11 @@ async function inspectEventLedger(projectRoot) {
|
|
|
2207
1597
|
};
|
|
2208
1598
|
}
|
|
2209
1599
|
}
|
|
2210
|
-
async function inspectRuleSections(projectRoot) {
|
|
2211
|
-
const invalidFiles = [];
|
|
2212
|
-
const files = findRuleFiles2(projectRoot);
|
|
2213
|
-
for (const file of files) {
|
|
2214
|
-
try {
|
|
2215
|
-
parseRuleSections(await readFile7(join8(projectRoot, file), "utf8"));
|
|
2216
|
-
} catch (error) {
|
|
2217
|
-
invalidFiles.push({
|
|
2218
|
-
file,
|
|
2219
|
-
reason: error instanceof Error ? error.message : String(error)
|
|
2220
|
-
});
|
|
2221
|
-
}
|
|
2222
|
-
}
|
|
2223
|
-
return {
|
|
2224
|
-
checkedCount: files.length,
|
|
2225
|
-
invalidFiles
|
|
2226
|
-
};
|
|
2227
|
-
}
|
|
2228
1600
|
async function inspectRuleTestIndex(projectRoot) {
|
|
2229
|
-
const path =
|
|
1601
|
+
const path = join5(projectRoot, ".fabric", "rule-test.index.json");
|
|
2230
1602
|
const built = await tryBuildRuleMeta(projectRoot);
|
|
2231
1603
|
try {
|
|
2232
|
-
const index = ruleTestIndexSchema2.parse(JSON.parse(await
|
|
1604
|
+
const index = ruleTestIndexSchema2.parse(JSON.parse(await readFile5(path, "utf8")));
|
|
2233
1605
|
return {
|
|
2234
1606
|
present: true,
|
|
2235
1607
|
valid: true,
|
|
@@ -2248,17 +1620,56 @@ async function inspectRuleTestIndex(projectRoot) {
|
|
|
2248
1620
|
};
|
|
2249
1621
|
}
|
|
2250
1622
|
}
|
|
2251
|
-
function
|
|
2252
|
-
|
|
2253
|
-
|
|
1623
|
+
function inspectBootstrapAnchor(projectRoot) {
|
|
1624
|
+
return {
|
|
1625
|
+
hasAgentsMd: existsSync4(join5(projectRoot, "AGENTS.md")),
|
|
1626
|
+
hasClaudeMd: existsSync4(join5(projectRoot, "CLAUDE.md"))
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
function createBootstrapAnchorCheck(inspection) {
|
|
1630
|
+
if (!inspection.hasAgentsMd && !inspection.hasClaudeMd) {
|
|
1631
|
+
return issueCheck(
|
|
1632
|
+
"Bootstrap anchor",
|
|
1633
|
+
"error",
|
|
1634
|
+
"fixable_error",
|
|
1635
|
+
"bootstrap_anchor_missing",
|
|
1636
|
+
"Neither AGENTS.md nor CLAUDE.md exists at the repo root. Fabric requires a bootstrap anchor file at the project root.",
|
|
1637
|
+
"Run `fabric init` to generate the AGENTS.md / CLAUDE.md bootstrap anchor at the repo root."
|
|
1638
|
+
);
|
|
2254
1639
|
}
|
|
2255
|
-
|
|
1640
|
+
const present = [
|
|
1641
|
+
inspection.hasAgentsMd ? "AGENTS.md" : null,
|
|
1642
|
+
inspection.hasClaudeMd ? "CLAUDE.md" : null
|
|
1643
|
+
].filter((entry) => entry !== null).join(", ");
|
|
1644
|
+
return okCheck("Bootstrap anchor", `Bootstrap anchor present at repo root: ${present}.`);
|
|
2256
1645
|
}
|
|
2257
|
-
function
|
|
2258
|
-
|
|
2259
|
-
|
|
1646
|
+
function inspectKnowledgeDirMissing(projectRoot) {
|
|
1647
|
+
const knowledgeRoot = join5(projectRoot, ".fabric", "knowledge");
|
|
1648
|
+
const missingSubdirs = [];
|
|
1649
|
+
for (const sub of KNOWLEDGE_SUBDIRS2) {
|
|
1650
|
+
const path = join5(knowledgeRoot, sub);
|
|
1651
|
+
if (!existsSync4(path)) {
|
|
1652
|
+
missingSubdirs.push(`.fabric/knowledge/${sub}`);
|
|
1653
|
+
}
|
|
2260
1654
|
}
|
|
2261
|
-
return
|
|
1655
|
+
return { missingSubdirs };
|
|
1656
|
+
}
|
|
1657
|
+
function createKnowledgeDirMissingCheck(inspection) {
|
|
1658
|
+
if (inspection.missingSubdirs.length > 0) {
|
|
1659
|
+
const list = inspection.missingSubdirs.join(", ");
|
|
1660
|
+
return issueCheck(
|
|
1661
|
+
"Knowledge layout",
|
|
1662
|
+
"error",
|
|
1663
|
+
"fixable_error",
|
|
1664
|
+
"knowledge_dir_missing",
|
|
1665
|
+
`${inspection.missingSubdirs.length} required knowledge subdir${inspection.missingSubdirs.length === 1 ? " is" : "s are"} missing: ${list}.`,
|
|
1666
|
+
"Run `fab doctor --fix` to create the missing .fabric/knowledge/* subdirectories."
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1669
|
+
return okCheck(
|
|
1670
|
+
"Knowledge layout",
|
|
1671
|
+
`All ${KNOWLEDGE_SUBDIRS2.length} required .fabric/knowledge/* subdirectories exist.`
|
|
1672
|
+
);
|
|
2262
1673
|
}
|
|
2263
1674
|
function createForensicCheck(forensic, frameworkKind, entryPointCount) {
|
|
2264
1675
|
if (!forensic.present) {
|
|
@@ -2276,18 +1687,9 @@ function createForensicCheck(forensic, frameworkKind, entryPointCount) {
|
|
|
2276
1687
|
}
|
|
2277
1688
|
return okCheck("Scan evidence", `.fabric/forensic.json is valid for ${forensic.report?.framework.kind ?? "unknown"}.`);
|
|
2278
1689
|
}
|
|
2279
|
-
function createInitContextCheck(initContext) {
|
|
2280
|
-
if (!initContext.exists) {
|
|
2281
|
-
return issueCheck("Init context", "error", "manual_error", "init_context_missing", initContext.error ?? ".fabric/init-context.json is missing.", "Run the fabric-init skill in Claude Code or Codex CLI to complete initialization. See docs/migration-1.8.md FAQ.");
|
|
2282
|
-
}
|
|
2283
|
-
if (!initContext.validJson) {
|
|
2284
|
-
return issueCheck("Init context", "error", "manual_error", "init_context_invalid", initContext.error ?? ".fabric/init-context.json is invalid.", "Delete .fabric/init-context.json and run `fab init` to regenerate it.");
|
|
2285
|
-
}
|
|
2286
|
-
return okCheck("Init context", ".fabric/init-context.json is valid JSON.");
|
|
2287
|
-
}
|
|
2288
1690
|
function createMetaCheck(meta) {
|
|
2289
1691
|
if (!meta.present) {
|
|
2290
|
-
return issueCheck("Agents metadata", "error", "fixable_error", "agents_meta_missing", ".fabric/agents.meta.json is missing.", "Run `fab doctor --fix` to rebuild agents.meta.json from .fabric/
|
|
1692
|
+
return issueCheck("Agents metadata", "error", "fixable_error", "agents_meta_missing", ".fabric/agents.meta.json is missing.", "Run `fab doctor --fix` to rebuild agents.meta.json from .fabric/knowledge/.");
|
|
2291
1693
|
}
|
|
2292
1694
|
if (!meta.valid) {
|
|
2293
1695
|
return issueCheck("Agents metadata", "error", "manual_error", "agents_meta_invalid", meta.readError ?? ".fabric/agents.meta.json is invalid.", "Delete .fabric/agents.meta.json and run `fab doctor --fix` to regenerate it.");
|
|
@@ -2298,11 +1700,11 @@ function createMetaCheck(meta) {
|
|
|
2298
1700
|
"error",
|
|
2299
1701
|
"fixable_error",
|
|
2300
1702
|
"agents_meta_stale",
|
|
2301
|
-
`.fabric/agents.meta.json revision ${meta.revision} does not match .fabric/
|
|
2302
|
-
"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."
|
|
2303
1705
|
);
|
|
2304
1706
|
}
|
|
2305
|
-
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.`);
|
|
2306
1708
|
}
|
|
2307
1709
|
function createRuleContentRefCheck(meta) {
|
|
2308
1710
|
if (!meta.valid) {
|
|
@@ -2314,8 +1716,8 @@ function createRuleContentRefCheck(meta) {
|
|
|
2314
1716
|
"error",
|
|
2315
1717
|
"manual_error",
|
|
2316
1718
|
"content_ref_outside_rules",
|
|
2317
|
-
`${meta.invalidContentRefs.length} content_ref entr${meta.invalidContentRefs.length === 1 ? "y is" : "ies are"} outside .fabric/
|
|
2318
|
-
"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)."
|
|
2319
1721
|
);
|
|
2320
1722
|
}
|
|
2321
1723
|
if (meta.missingContentRefs.length > 0) {
|
|
@@ -2325,23 +1727,10 @@ function createRuleContentRefCheck(meta) {
|
|
|
2325
1727
|
"fixable_error",
|
|
2326
1728
|
"content_ref_missing",
|
|
2327
1729
|
`${meta.missingContentRefs.length} content_ref target${meta.missingContentRefs.length === 1 ? "" : "s"} are missing. Run \`fab doctor --fix\` to reconcile.`,
|
|
2328
|
-
"Run `fab doctor --fix` to reconcile agents.meta.json with the files present in .fabric/
|
|
2329
|
-
);
|
|
2330
|
-
}
|
|
2331
|
-
return okCheck("Rule content refs", "All content_ref entries resolve to .fabric/rules files or bootstrap README.");
|
|
2332
|
-
}
|
|
2333
|
-
function createRuleSectionsCheck(snapshot) {
|
|
2334
|
-
if (snapshot.invalidFiles.length > 0) {
|
|
2335
|
-
return issueCheck(
|
|
2336
|
-
"Rule sections",
|
|
2337
|
-
"error",
|
|
2338
|
-
"manual_error",
|
|
2339
|
-
"rule_sections_invalid",
|
|
2340
|
-
`${snapshot.invalidFiles.length} rule file${snapshot.invalidFiles.length === 1 ? "" : "s"} could not be parsed.`,
|
|
2341
|
-
"Edit the rule file(s) to fix the section structure, then re-run `fab doctor`."
|
|
1730
|
+
"Run `fab doctor --fix` to reconcile agents.meta.json with the files present in .fabric/knowledge/."
|
|
2342
1731
|
);
|
|
2343
1732
|
}
|
|
2344
|
-
return okCheck("Rule
|
|
1733
|
+
return okCheck("Rule content refs", "All content_ref entries resolve to .fabric/knowledge files.");
|
|
2345
1734
|
}
|
|
2346
1735
|
function createRuleTestIndexCheck(index) {
|
|
2347
1736
|
if (!index.present) {
|
|
@@ -2425,13 +1814,13 @@ function findIssue(issues, code) {
|
|
|
2425
1814
|
};
|
|
2426
1815
|
}
|
|
2427
1816
|
async function inspectMetaManuallyDiverged(projectRoot) {
|
|
2428
|
-
const metaPath =
|
|
1817
|
+
const metaPath = join5(projectRoot, ".fabric", "agents.meta.json");
|
|
2429
1818
|
if (!existsSync4(metaPath)) {
|
|
2430
1819
|
return { extraMetaEntries: [], hashMismatchEntries: [], readable: false };
|
|
2431
1820
|
}
|
|
2432
1821
|
let meta;
|
|
2433
1822
|
try {
|
|
2434
|
-
const raw = await
|
|
1823
|
+
const raw = await readFile5(metaPath, "utf8");
|
|
2435
1824
|
meta = agentsMetaSchema4.parse(JSON.parse(raw));
|
|
2436
1825
|
} catch (error) {
|
|
2437
1826
|
return {
|
|
@@ -2445,7 +1834,7 @@ async function inspectMetaManuallyDiverged(projectRoot) {
|
|
|
2445
1834
|
const hashMismatchEntries = [];
|
|
2446
1835
|
for (const node of Object.values(meta.nodes)) {
|
|
2447
1836
|
const contentRef = node.content_ref ?? node.file;
|
|
2448
|
-
const absPath =
|
|
1837
|
+
const absPath = join5(projectRoot, contentRef);
|
|
2449
1838
|
if (!existsSync4(absPath)) {
|
|
2450
1839
|
extraMetaEntries.push(contentRef);
|
|
2451
1840
|
continue;
|
|
@@ -2462,87 +1851,89 @@ async function inspectMetaManuallyDiverged(projectRoot) {
|
|
|
2462
1851
|
}
|
|
2463
1852
|
return { extraMetaEntries, hashMismatchEntries, readable: true };
|
|
2464
1853
|
}
|
|
2465
|
-
function
|
|
2466
|
-
const
|
|
2467
|
-
|
|
1854
|
+
function inspectKnowledgeDirUnindexed(projectRoot, meta) {
|
|
1855
|
+
const physicalMdFiles = /* @__PURE__ */ new Set();
|
|
1856
|
+
collectMdFilesUnder(physicalMdFiles, projectRoot, join5(projectRoot, ".fabric", "knowledge"), ".fabric/knowledge");
|
|
1857
|
+
if (physicalMdFiles.size === 0) {
|
|
2468
1858
|
return { unindexedFiles: [] };
|
|
2469
1859
|
}
|
|
2470
|
-
const
|
|
2471
|
-
|
|
1860
|
+
const indexedRefs = /* @__PURE__ */ new Set();
|
|
1861
|
+
if (meta.valid && meta.meta !== null) {
|
|
1862
|
+
for (const node of Object.values(meta.meta.nodes)) {
|
|
1863
|
+
const ref = normalizePath(node.content_ref ?? node.file);
|
|
1864
|
+
indexedRefs.add(ref);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
const unindexedFiles = [...physicalMdFiles].filter((f) => !indexedRefs.has(f)).sort();
|
|
1868
|
+
return { unindexedFiles };
|
|
1869
|
+
}
|
|
1870
|
+
function collectMdFilesUnder(out, projectRoot, rootDir, relPrefix) {
|
|
1871
|
+
if (!existsSync4(rootDir)) {
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1874
|
+
const stack = [rootDir];
|
|
2472
1875
|
while (stack.length > 0) {
|
|
2473
1876
|
const dir = stack.pop();
|
|
2474
1877
|
if (dir === void 0) {
|
|
2475
1878
|
continue;
|
|
2476
1879
|
}
|
|
2477
1880
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
2478
|
-
const abs =
|
|
1881
|
+
const abs = join5(dir, entry.name);
|
|
2479
1882
|
if (entry.isDirectory()) {
|
|
2480
1883
|
stack.push(abs);
|
|
2481
1884
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
2482
|
-
const rel =
|
|
2483
|
-
|
|
1885
|
+
const rel = posix.join(relPrefix, abs.slice(rootDir.length + 1).replace(/\\/gu, "/"));
|
|
1886
|
+
out.add(rel);
|
|
2484
1887
|
}
|
|
2485
1888
|
}
|
|
2486
1889
|
}
|
|
2487
|
-
const indexedRefs = /* @__PURE__ */ new Set();
|
|
2488
|
-
if (meta.valid && meta.meta !== null) {
|
|
2489
|
-
for (const node of Object.values(meta.meta.nodes)) {
|
|
2490
|
-
const ref = normalizePath(node.content_ref ?? node.file);
|
|
2491
|
-
indexedRefs.add(ref);
|
|
2492
|
-
}
|
|
2493
|
-
}
|
|
2494
|
-
const unindexedFiles = [...physicalMdFiles].filter((f) => !indexedRefs.has(f)).sort();
|
|
2495
|
-
return { unindexedFiles };
|
|
2496
1890
|
}
|
|
2497
|
-
function
|
|
1891
|
+
function createKnowledgeDirUnindexedCheck(inspection) {
|
|
2498
1892
|
if (inspection.unindexedFiles.length > 0) {
|
|
2499
1893
|
return issueCheck(
|
|
2500
|
-
"
|
|
1894
|
+
"Knowledge dir unindexed",
|
|
2501
1895
|
"error",
|
|
2502
1896
|
"fixable_error",
|
|
2503
|
-
"
|
|
2504
|
-
`${inspection.unindexedFiles.length} .md file${inspection.unindexedFiles.length === 1 ? "" : "s"} in .fabric/
|
|
2505
|
-
"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."
|
|
2506
1900
|
);
|
|
2507
1901
|
}
|
|
2508
|
-
return okCheck("
|
|
1902
|
+
return okCheck("Knowledge dir unindexed", "All .fabric/knowledge/ .md files are indexed in agents.meta.json.");
|
|
2509
1903
|
}
|
|
2510
1904
|
async function inspectStableIdCollisions(projectRoot) {
|
|
2511
|
-
const
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
1905
|
+
const found = [];
|
|
1906
|
+
const knowledgeDir = join5(projectRoot, ".fabric", "knowledge");
|
|
1907
|
+
if (existsSync4(knowledgeDir)) {
|
|
1908
|
+
const stack = [knowledgeDir];
|
|
1909
|
+
while (stack.length > 0) {
|
|
1910
|
+
const dir = stack.pop();
|
|
1911
|
+
if (dir === void 0) {
|
|
1912
|
+
continue;
|
|
1913
|
+
}
|
|
1914
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1915
|
+
const abs = join5(dir, entry.name);
|
|
1916
|
+
if (entry.isDirectory()) {
|
|
1917
|
+
stack.push(abs);
|
|
1918
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1919
|
+
let source;
|
|
1920
|
+
try {
|
|
1921
|
+
source = await readFile5(abs, "utf8");
|
|
1922
|
+
} catch {
|
|
1923
|
+
continue;
|
|
1924
|
+
}
|
|
1925
|
+
const id = extractKnowledgeFrontmatterId(source);
|
|
1926
|
+
if (id === null) {
|
|
1927
|
+
continue;
|
|
1928
|
+
}
|
|
1929
|
+
const relPath = posix.join(".fabric/knowledge", abs.slice(knowledgeDir.length + 1).replace(/\\/gu, "/"));
|
|
1930
|
+
found.push({ stableId: id, relPath });
|
|
1931
|
+
}
|
|
2528
1932
|
}
|
|
2529
1933
|
}
|
|
2530
1934
|
}
|
|
2531
1935
|
const stableIdToFiles = /* @__PURE__ */ new Map();
|
|
2532
|
-
const
|
|
2533
|
-
for (const absPath of mdFiles) {
|
|
2534
|
-
let source;
|
|
2535
|
-
try {
|
|
2536
|
-
source = await readFile7(absPath, "utf8");
|
|
2537
|
-
} catch {
|
|
2538
|
-
continue;
|
|
2539
|
-
}
|
|
2540
|
-
const match = DECLARED_ID_PATTERN.exec(source);
|
|
2541
|
-
if (match === null) {
|
|
2542
|
-
continue;
|
|
2543
|
-
}
|
|
2544
|
-
const stableId = match[1];
|
|
2545
|
-
const relPath = posix2.join(".fabric/rules", absPath.slice(rulesDir.length + 1).replace(/\\/gu, "/"));
|
|
1936
|
+
for (const { stableId, relPath } of found) {
|
|
2546
1937
|
const existing = stableIdToFiles.get(stableId) ?? [];
|
|
2547
1938
|
existing.push(relPath);
|
|
2548
1939
|
stableIdToFiles.set(stableId, existing);
|
|
@@ -2555,6 +1946,85 @@ async function inspectStableIdCollisions(projectRoot) {
|
|
|
2555
1946
|
}
|
|
2556
1947
|
return { collisions: collisions.sort((a, b) => a.stable_id.localeCompare(b.stable_id)) };
|
|
2557
1948
|
}
|
|
1949
|
+
function extractKnowledgeFrontmatterId(source) {
|
|
1950
|
+
const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
|
|
1951
|
+
const fm = FM_PATTERN.exec(source);
|
|
1952
|
+
if (fm === null) {
|
|
1953
|
+
return null;
|
|
1954
|
+
}
|
|
1955
|
+
const block = fm[1];
|
|
1956
|
+
const ID_LINE = /^id:\s*("?)(K[PT]-(?:MOD|DEC|GLD|PIT|PRO)-\d{4,})\1\s*$/mu;
|
|
1957
|
+
const idMatch = ID_LINE.exec(block);
|
|
1958
|
+
return idMatch === null ? null : idMatch[2];
|
|
1959
|
+
}
|
|
1960
|
+
function inspectCounterDesync(meta) {
|
|
1961
|
+
if (!meta.valid || meta.meta === null) {
|
|
1962
|
+
return { desyncs: [], correctedCounters: null };
|
|
1963
|
+
}
|
|
1964
|
+
const current = AgentsMetaCountersSchema.parse(meta.meta.counters ?? void 0);
|
|
1965
|
+
const observed = {
|
|
1966
|
+
KP: { MOD: 0, DEC: 0, GLD: 0, PIT: 0, PRO: 0 },
|
|
1967
|
+
KT: { MOD: 0, DEC: 0, GLD: 0, PIT: 0, PRO: 0 }
|
|
1968
|
+
};
|
|
1969
|
+
for (const node of Object.values(meta.meta.nodes)) {
|
|
1970
|
+
const id = node.stable_id;
|
|
1971
|
+
if (id === void 0) {
|
|
1972
|
+
continue;
|
|
1973
|
+
}
|
|
1974
|
+
const parsed = parseKnowledgeId2(id);
|
|
1975
|
+
if (parsed === null) {
|
|
1976
|
+
continue;
|
|
1977
|
+
}
|
|
1978
|
+
const layer = parsed.layer === "personal" ? "KP" : "KT";
|
|
1979
|
+
const typeCode = [
|
|
1980
|
+
["model", "MOD"],
|
|
1981
|
+
["decision", "DEC"],
|
|
1982
|
+
["guideline", "GLD"],
|
|
1983
|
+
["pitfall", "PIT"],
|
|
1984
|
+
["process", "PRO"]
|
|
1985
|
+
].find(([t]) => t === parsed.type)?.[1];
|
|
1986
|
+
if (typeCode === void 0) {
|
|
1987
|
+
continue;
|
|
1988
|
+
}
|
|
1989
|
+
if (parsed.counter > observed[layer][typeCode]) {
|
|
1990
|
+
observed[layer][typeCode] = parsed.counter;
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
const desyncs = [];
|
|
1994
|
+
const corrected = {
|
|
1995
|
+
KP: { ...current.KP },
|
|
1996
|
+
KT: { ...current.KT }
|
|
1997
|
+
};
|
|
1998
|
+
for (const layer of ["KP", "KT"]) {
|
|
1999
|
+
for (const code of COUNTER_TYPE_CODES) {
|
|
2000
|
+
const obs = observed[layer][code];
|
|
2001
|
+
const cur = current[layer][code];
|
|
2002
|
+
if (obs > cur) {
|
|
2003
|
+
desyncs.push({ layer, type: code, observed: obs, current: cur });
|
|
2004
|
+
corrected[layer][code] = obs;
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
return {
|
|
2009
|
+
desyncs,
|
|
2010
|
+
correctedCounters: desyncs.length === 0 ? null : corrected
|
|
2011
|
+
};
|
|
2012
|
+
}
|
|
2013
|
+
function createCounterDesyncCheck(inspection) {
|
|
2014
|
+
if (inspection.desyncs.length > 0) {
|
|
2015
|
+
const first = inspection.desyncs[0];
|
|
2016
|
+
const detail = `counters.${first.layer}.${first.type} = ${first.current} but observed K${first.layer === "KP" ? "P" : "T"}-${first.type}-${String(first.observed).padStart(4, "0")}`;
|
|
2017
|
+
return issueCheck(
|
|
2018
|
+
"Knowledge counter desync",
|
|
2019
|
+
"error",
|
|
2020
|
+
"fixable_error",
|
|
2021
|
+
"counter_desync",
|
|
2022
|
+
`${inspection.desyncs.length} knowledge counter${inspection.desyncs.length === 1 ? "" : "s"} desynced from observed stable_ids. ${detail}. Run \`fab doctor --fix\` to bump counters.`,
|
|
2023
|
+
"Run `fab doctor --fix` to bump agents.meta.json counters to the maximum observed counter value."
|
|
2024
|
+
);
|
|
2025
|
+
}
|
|
2026
|
+
return okCheck("Knowledge counter desync", "agents.meta.json counters envelope is consistent with observed stable_ids.");
|
|
2027
|
+
}
|
|
2558
2028
|
function createStableIdCollisionCheck(inspection) {
|
|
2559
2029
|
if (inspection.collisions.length > 0) {
|
|
2560
2030
|
const first = inspection.collisions[0];
|
|
@@ -2564,11 +2034,11 @@ function createStableIdCollisionCheck(inspection) {
|
|
|
2564
2034
|
"warn",
|
|
2565
2035
|
"warning",
|
|
2566
2036
|
"stable_id_collision",
|
|
2567
|
-
`${detail} Edit one of the
|
|
2568
|
-
"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."
|
|
2569
2039
|
);
|
|
2570
2040
|
}
|
|
2571
|
-
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/.");
|
|
2572
2042
|
}
|
|
2573
2043
|
function createMetaManuallyDivergedCheck(inspection) {
|
|
2574
2044
|
if (!inspection.readable) {
|
|
@@ -2598,7 +2068,7 @@ function createMetaManuallyDivergedCheck(inspection) {
|
|
|
2598
2068
|
}
|
|
2599
2069
|
function inspectPreexistingRootFiles(projectRoot) {
|
|
2600
2070
|
const candidates = ["CLAUDE.md", "AGENTS.md"];
|
|
2601
|
-
const detected = candidates.filter((name) => existsSync4(
|
|
2071
|
+
const detected = candidates.filter((name) => existsSync4(join5(projectRoot, name)));
|
|
2602
2072
|
return { detected };
|
|
2603
2073
|
}
|
|
2604
2074
|
function createPreexistingRootFilesCheck(inspection) {
|
|
@@ -2612,380 +2082,463 @@ function createPreexistingRootFilesCheck(inspection) {
|
|
|
2612
2082
|
code: "preexisting_root_claude_md",
|
|
2613
2083
|
fixable: false,
|
|
2614
2084
|
message: `${inspection.detected.join(", ")} detected at project root. These root files are not auto-loaded by Fabric MCP.`,
|
|
2615
|
-
actionHint: "Move
|
|
2085
|
+
actionHint: "Move knowledge content to `.fabric/knowledge/{type}/` if you want it available in MCP responses."
|
|
2616
2086
|
};
|
|
2617
2087
|
}
|
|
2618
|
-
function
|
|
2619
|
-
const
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
return { hasLegacy, legacyPath, newPath };
|
|
2623
|
-
}
|
|
2624
|
-
function createClaudeSkillLegacyPathCheck(inspection) {
|
|
2625
|
-
if (inspection.hasLegacy) {
|
|
2626
|
-
return issueCheck(
|
|
2627
|
-
"Claude skill path",
|
|
2628
|
-
"error",
|
|
2629
|
-
"fixable_error",
|
|
2630
|
-
"claude_skill_legacy_path",
|
|
2631
|
-
`.claude/skills/agents-md-init/SKILL.md exists at the legacy path. Run --fix to migrate it to .claude/skills/fabric-init/SKILL.md (user edits preserved).`,
|
|
2632
|
-
"Run `fab doctor --fix` to rename agents-md-init/ to fabric-init/, preserving any user edits to SKILL.md."
|
|
2633
|
-
);
|
|
2634
|
-
}
|
|
2635
|
-
return okCheck("Claude skill path", ".claude/skills/fabric-init/SKILL.md is at the canonical path (or not present).");
|
|
2636
|
-
}
|
|
2637
|
-
async function fixClaudeSkillLegacyPath(projectRoot) {
|
|
2638
|
-
const legacyPath = join8(projectRoot, ".claude", "skills", "agents-md-init", "SKILL.md");
|
|
2639
|
-
const newPath = join8(projectRoot, ".claude", "skills", "fabric-init", "SKILL.md");
|
|
2640
|
-
if (!existsSync4(legacyPath)) {
|
|
2641
|
-
return;
|
|
2642
|
-
}
|
|
2643
|
-
mkdirSync(join8(newPath, ".."), { recursive: true });
|
|
2644
|
-
renameSync(legacyPath, newPath);
|
|
2645
|
-
const legacyDir = join8(legacyPath, "..");
|
|
2646
|
-
try {
|
|
2647
|
-
rmdirSync(legacyDir);
|
|
2648
|
-
} catch {
|
|
2649
|
-
}
|
|
2650
|
-
await appendEventLedgerEvent(projectRoot, {
|
|
2651
|
-
event_type: "claude_skill_path_migrated",
|
|
2652
|
-
from: legacyPath,
|
|
2653
|
-
to: newPath
|
|
2654
|
-
});
|
|
2655
|
-
}
|
|
2656
|
-
var LEGACY_HOOK_FILENAME = "agents-md-init-reminder.cjs";
|
|
2657
|
-
var NEW_HOOK_FILENAME = "fabric-init-reminder.cjs";
|
|
2658
|
-
function inspectClaudeHookLegacyPath(projectRoot) {
|
|
2659
|
-
const legacyHookPath = join8(projectRoot, ".claude", "hooks", LEGACY_HOOK_FILENAME);
|
|
2660
|
-
const newHookPath = join8(projectRoot, ".claude", "hooks", NEW_HOOK_FILENAME);
|
|
2661
|
-
const settingsPath = join8(projectRoot, ".claude", "settings.json");
|
|
2662
|
-
const hasLegacyFile = existsSync4(legacyHookPath);
|
|
2663
|
-
let hasLegacySettingsCommand = false;
|
|
2664
|
-
if (existsSync4(settingsPath)) {
|
|
2665
|
-
try {
|
|
2666
|
-
const raw = readFileSync(settingsPath, "utf8");
|
|
2667
|
-
hasLegacySettingsCommand = raw.includes(LEGACY_HOOK_FILENAME);
|
|
2668
|
-
} catch {
|
|
2669
|
-
}
|
|
2670
|
-
}
|
|
2671
|
-
return { hasLegacyFile, hasLegacySettingsCommand, legacyHookPath, newHookPath, settingsPath };
|
|
2672
|
-
}
|
|
2673
|
-
function createClaudeHookLegacyPathCheck(inspection) {
|
|
2674
|
-
if (inspection.hasLegacyFile || inspection.hasLegacySettingsCommand) {
|
|
2675
|
-
return issueCheck(
|
|
2676
|
-
"Claude hook path",
|
|
2677
|
-
"error",
|
|
2678
|
-
"fixable_error",
|
|
2679
|
-
"claude_hook_legacy_path",
|
|
2680
|
-
`.claude/hooks/${LEGACY_HOOK_FILENAME} (or its reference in .claude/settings.json) exists at the legacy path. Run --fix to migrate to ${NEW_HOOK_FILENAME}.`,
|
|
2681
|
-
`Run \`fab doctor --fix\` to rename ${LEGACY_HOOK_FILENAME} to ${NEW_HOOK_FILENAME} and update .claude/settings.json hook commands.`
|
|
2682
|
-
);
|
|
2088
|
+
async function fixMcpConfigInWrongFile(projectRoot) {
|
|
2089
|
+
const settingsPath = join5(projectRoot, ".claude", "settings.json");
|
|
2090
|
+
if (!existsSync4(settingsPath)) {
|
|
2091
|
+
return;
|
|
2683
2092
|
}
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
if (existsSync4(newHookPath)) {
|
|
2690
|
-
unlinkSync(legacyHookPath);
|
|
2691
|
-
} else {
|
|
2692
|
-
mkdirSync(join8(newHookPath, ".."), { recursive: true });
|
|
2693
|
-
renameSync(legacyHookPath, newHookPath);
|
|
2093
|
+
let settings;
|
|
2094
|
+
try {
|
|
2095
|
+
const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
2096
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2097
|
+
return;
|
|
2694
2098
|
}
|
|
2099
|
+
settings = parsed;
|
|
2100
|
+
} catch {
|
|
2101
|
+
return;
|
|
2695
2102
|
}
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
const updated = raw.split(LEGACY_HOOK_FILENAME).join(NEW_HOOK_FILENAME);
|
|
2700
|
-
if (updated !== raw) {
|
|
2701
|
-
const parsed = JSON.parse(updated);
|
|
2702
|
-
await atomicWriteJson2(settingsPath, parsed);
|
|
2703
|
-
}
|
|
2704
|
-
} catch {
|
|
2705
|
-
}
|
|
2103
|
+
const mcpServers = settings.mcpServers;
|
|
2104
|
+
if (mcpServers === null || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
|
|
2105
|
+
return;
|
|
2706
2106
|
}
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2107
|
+
const { fabric: _removed, ...remainingServers } = mcpServers;
|
|
2108
|
+
const cleaned = { ...settings };
|
|
2109
|
+
if (Object.keys(remainingServers).length === 0) {
|
|
2110
|
+
delete cleaned.mcpServers;
|
|
2111
|
+
} else {
|
|
2112
|
+
cleaned.mcpServers = remainingServers;
|
|
2713
2113
|
}
|
|
2114
|
+
await atomicWriteJson2(settingsPath, cleaned, { indent: 2 });
|
|
2115
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
2116
|
+
event_type: "mcp_config_migrated",
|
|
2117
|
+
source: "doctor_fix",
|
|
2118
|
+
removed_from: ".claude/settings.json"
|
|
2119
|
+
});
|
|
2714
2120
|
}
|
|
2715
|
-
function
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
const hasLegacy = existsSync4(legacyPath) && !existsSync4(newPath);
|
|
2719
|
-
return { hasLegacy, legacyPath, newPath };
|
|
2720
|
-
}
|
|
2721
|
-
function createCodexSkillLegacyPathCheck(inspection) {
|
|
2722
|
-
if (inspection.hasLegacy) {
|
|
2723
|
-
return issueCheck(
|
|
2724
|
-
"Codex skill path",
|
|
2725
|
-
"error",
|
|
2726
|
-
"fixable_error",
|
|
2727
|
-
"codex_skill_legacy_path",
|
|
2728
|
-
`.agents/skills/fabric-init/SKILL.md exists at the legacy path. Codex CLI reads repo skills from .codex/skills/, not .agents/skills/. Run --fix to migrate it to .codex/skills/fabric-init/SKILL.md (user edits preserved).`,
|
|
2729
|
-
"Run `fab doctor --fix` to move .agents/skills/fabric-init/ to .codex/skills/fabric-init/, preserving any user edits to SKILL.md."
|
|
2730
|
-
);
|
|
2121
|
+
async function ensureKnowledgeSubdirs(projectRoot) {
|
|
2122
|
+
for (const sub of KNOWLEDGE_SUBDIRS2) {
|
|
2123
|
+
await mkdir3(join5(projectRoot, ".fabric", "knowledge", sub), { recursive: true });
|
|
2731
2124
|
}
|
|
2732
|
-
return okCheck("Codex skill path", ".codex/skills/fabric-init/SKILL.md is at the canonical path (or not present).");
|
|
2733
2125
|
}
|
|
2734
|
-
async function
|
|
2735
|
-
const
|
|
2736
|
-
if (!
|
|
2126
|
+
async function fixCounterDesync(projectRoot) {
|
|
2127
|
+
const metaPath = join5(projectRoot, ".fabric", "agents.meta.json");
|
|
2128
|
+
if (!existsSync4(metaPath)) {
|
|
2737
2129
|
return;
|
|
2738
2130
|
}
|
|
2739
|
-
|
|
2740
|
-
renameSync(legacyPath, newPath);
|
|
2741
|
-
const legacyDir = join8(legacyPath, "..");
|
|
2131
|
+
let meta;
|
|
2742
2132
|
try {
|
|
2743
|
-
|
|
2133
|
+
meta = agentsMetaSchema4.parse(JSON.parse(await readFile5(metaPath, "utf8")));
|
|
2744
2134
|
} catch {
|
|
2135
|
+
return;
|
|
2745
2136
|
}
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2137
|
+
const synthetic = {
|
|
2138
|
+
present: true,
|
|
2139
|
+
valid: true,
|
|
2140
|
+
meta,
|
|
2141
|
+
revision: meta.revision,
|
|
2142
|
+
computedRevision: null,
|
|
2143
|
+
ruleCount: 0,
|
|
2144
|
+
missingContentRefs: [],
|
|
2145
|
+
invalidContentRefs: [],
|
|
2146
|
+
stale: false,
|
|
2147
|
+
changed: false
|
|
2148
|
+
};
|
|
2149
|
+
const desync = inspectCounterDesync(synthetic);
|
|
2150
|
+
if (desync.desyncs.length === 0 || desync.correctedCounters === null) {
|
|
2151
|
+
return;
|
|
2749
2152
|
}
|
|
2153
|
+
const updated = { ...meta, counters: desync.correctedCounters };
|
|
2154
|
+
await atomicWriteJson2(metaPath, updated, { indent: 2 });
|
|
2155
|
+
}
|
|
2156
|
+
async function ensureEventLedger(projectRoot) {
|
|
2157
|
+
const path = getEventLedgerPath(projectRoot);
|
|
2158
|
+
await ensureParentDirectory(path);
|
|
2159
|
+
await writeFile2(path, "", { encoding: "utf8", flag: "a" });
|
|
2160
|
+
}
|
|
2161
|
+
function createFixMessage(fixed, report) {
|
|
2162
|
+
const fixedText = fixed.length === 0 ? "No deterministic doctor fixes were needed." : `Applied ${fixed.length} deterministic doctor fix${fixed.length === 1 ? "" : "es"}.`;
|
|
2163
|
+
const manualText = report.manual_errors.length === 0 ? "No manual errors remain." : `${report.manual_errors.length} manual error${report.manual_errors.length === 1 ? "" : "s"} remain.`;
|
|
2164
|
+
return `${fixedText} ${manualText}`;
|
|
2165
|
+
}
|
|
2166
|
+
function isValidJsonLine(line) {
|
|
2750
2167
|
try {
|
|
2751
|
-
|
|
2168
|
+
JSON.parse(line);
|
|
2169
|
+
return true;
|
|
2752
2170
|
} catch {
|
|
2171
|
+
return false;
|
|
2753
2172
|
}
|
|
2754
|
-
await appendEventLedgerEvent(projectRoot, {
|
|
2755
|
-
event_type: "codex_skill_path_migrated",
|
|
2756
|
-
from: legacyPath,
|
|
2757
|
-
to: newPath
|
|
2758
|
-
});
|
|
2759
2173
|
}
|
|
2760
|
-
function
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2174
|
+
function normalizeTarget(targetInput) {
|
|
2175
|
+
return isAbsolute2(targetInput) ? targetInput : resolve3(process.cwd(), targetInput);
|
|
2176
|
+
}
|
|
2177
|
+
function normalizePath(path) {
|
|
2178
|
+
return posix.normalize(path.split("\\").join("/"));
|
|
2179
|
+
}
|
|
2180
|
+
function collectEntryPoints(root) {
|
|
2181
|
+
if (!existsSync4(root) || !statSync3(root).isDirectory()) {
|
|
2182
|
+
return [];
|
|
2764
2183
|
}
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2184
|
+
const entries = [];
|
|
2185
|
+
const stack = [root];
|
|
2186
|
+
while (stack.length > 0) {
|
|
2187
|
+
const current = stack.pop();
|
|
2188
|
+
if (current === void 0) {
|
|
2189
|
+
continue;
|
|
2769
2190
|
}
|
|
2770
|
-
const
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2191
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
2192
|
+
const absolutePath = join5(current, entry.name);
|
|
2193
|
+
const relativePath = normalizePath(absolutePath.slice(root.length + 1));
|
|
2194
|
+
if (relativePath.length === 0) {
|
|
2195
|
+
continue;
|
|
2196
|
+
}
|
|
2197
|
+
if (entry.isDirectory()) {
|
|
2198
|
+
if (!IGNORED_DIRECTORIES.has(entry.name)) {
|
|
2199
|
+
stack.push(absolutePath);
|
|
2200
|
+
}
|
|
2201
|
+
continue;
|
|
2202
|
+
}
|
|
2203
|
+
if (!entry.isFile()) {
|
|
2204
|
+
continue;
|
|
2205
|
+
}
|
|
2206
|
+
const reason = getEntryPointReason(relativePath);
|
|
2207
|
+
if (reason !== null) {
|
|
2208
|
+
entries.push({ path: relativePath, reason });
|
|
2209
|
+
}
|
|
2774
2210
|
}
|
|
2775
|
-
const cp = clientPaths;
|
|
2776
|
-
const presentKeys = LEGACY_CLIENT_PATH_KEYS.filter((key) => key in cp);
|
|
2777
|
-
return { presentKeys };
|
|
2778
|
-
} catch {
|
|
2779
|
-
return { presentKeys: [] };
|
|
2780
2211
|
}
|
|
2212
|
+
return entries.sort((left, right) => left.path.localeCompare(right.path));
|
|
2781
2213
|
}
|
|
2782
|
-
function
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
"warn",
|
|
2787
|
-
"warning",
|
|
2788
|
-
"legacy_client_path_present",
|
|
2789
|
-
`fabric.config.json contains deprecated clientPaths keys: ${inspection.presentKeys.join(", ")}. These clients are removed in 1.8.0; run --fix to clean now or accept the upcoming removal.`,
|
|
2790
|
-
"Run `fab doctor --fix` to remove deprecated clientPaths keys (windsurf, rooCode, geminiCLI) from fabric.config.json."
|
|
2791
|
-
);
|
|
2214
|
+
function getEntryPointReason(relativePath) {
|
|
2215
|
+
const extension = relativePath.slice(relativePath.lastIndexOf("."));
|
|
2216
|
+
if (!SCRIPT_EXTENSIONS.has(extension)) {
|
|
2217
|
+
return null;
|
|
2792
2218
|
}
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
return;
|
|
2219
|
+
const directory = posix.dirname(relativePath);
|
|
2220
|
+
const fileName = posix.basename(relativePath);
|
|
2221
|
+
const fileBase = fileName.slice(0, Math.max(fileName.lastIndexOf("."), 0));
|
|
2222
|
+
if (directory === "assets/scripts" || directory === "scripts") {
|
|
2223
|
+
return "top-level script";
|
|
2799
2224
|
}
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
const parsed = JSON.parse(readFileSync(configPath, "utf8"));
|
|
2803
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2804
|
-
return;
|
|
2805
|
-
}
|
|
2806
|
-
config = parsed;
|
|
2807
|
-
} catch {
|
|
2808
|
-
return;
|
|
2225
|
+
if (directory === "src" && /^(App|app|index|main)$/.test(fileBase)) {
|
|
2226
|
+
return "application entry";
|
|
2809
2227
|
}
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
return;
|
|
2228
|
+
if ((directory === "app" || directory.startsWith("app/")) && /^(layout|page|route)$/.test(fileBase)) {
|
|
2229
|
+
return "next app route";
|
|
2813
2230
|
}
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
for (const key of LEGACY_CLIENT_PATH_KEYS) {
|
|
2817
|
-
if (key in cp) {
|
|
2818
|
-
delete cp[key];
|
|
2819
|
-
removed.push(key);
|
|
2820
|
-
}
|
|
2231
|
+
if ((directory === "pages" || directory.startsWith("pages/")) && fileName !== "_app.d.ts") {
|
|
2232
|
+
return "next page route";
|
|
2821
2233
|
}
|
|
2822
|
-
|
|
2823
|
-
|
|
2234
|
+
return null;
|
|
2235
|
+
}
|
|
2236
|
+
function reduceStatus(statuses) {
|
|
2237
|
+
if (statuses.includes("error")) {
|
|
2238
|
+
return "error";
|
|
2824
2239
|
}
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2240
|
+
if (statuses.includes("warn")) {
|
|
2241
|
+
return "warn";
|
|
2242
|
+
}
|
|
2243
|
+
return "ok";
|
|
2244
|
+
}
|
|
2245
|
+
function isMissingFileError(error) {
|
|
2246
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
// src/services/get-rules.ts
|
|
2250
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
2251
|
+
import { join as join7 } from "path";
|
|
2252
|
+
import { minimatch } from "minimatch";
|
|
2253
|
+
|
|
2254
|
+
// src/services/audit-log.ts
|
|
2255
|
+
import { open, stat as stat2 } from "fs/promises";
|
|
2256
|
+
import { isAbsolute as isAbsolute3, join as join6, posix as posix2, relative as relative3, resolve as resolve4 } from "path";
|
|
2257
|
+
var AUDIT_LOG_FILE = `${FABRIC_DIR}/audit.jsonl`;
|
|
2258
|
+
var DEFAULT_AUDIT_WINDOW_MS = 5 * 60 * 1e3;
|
|
2259
|
+
async function appendGetRulesAuditEvent(projectRoot, input) {
|
|
2260
|
+
const entry = {
|
|
2261
|
+
kind: "audit-event",
|
|
2262
|
+
event: "get_rules",
|
|
2263
|
+
ts: input.ts ?? Date.now(),
|
|
2264
|
+
path: normalizeAuditPath(projectRoot, input.path),
|
|
2265
|
+
client_hash: input.client_hash
|
|
2266
|
+
};
|
|
2267
|
+
await appendAuditLogEventLedgerEvents(projectRoot, [entry], {
|
|
2268
|
+
rule_context: {
|
|
2269
|
+
required_stable_ids: input.required_stable_ids,
|
|
2270
|
+
ai_selectable_stable_ids: input.ai_selectable_stable_ids,
|
|
2271
|
+
final_stable_ids: input.final_stable_ids
|
|
2272
|
+
},
|
|
2273
|
+
correlation_id: input.correlation_id,
|
|
2274
|
+
session_id: input.session_id
|
|
2830
2275
|
});
|
|
2276
|
+
return entry;
|
|
2831
2277
|
}
|
|
2832
|
-
async function
|
|
2833
|
-
const
|
|
2834
|
-
|
|
2835
|
-
|
|
2278
|
+
async function appendRuleSelectionAuditEvent(projectRoot, input) {
|
|
2279
|
+
const entry = {
|
|
2280
|
+
kind: "audit-event",
|
|
2281
|
+
event: "knowledge_selection",
|
|
2282
|
+
ts: input.ts ?? Date.now(),
|
|
2283
|
+
path: normalizeAuditPath(projectRoot, input.path),
|
|
2284
|
+
selection_token: input.selection_token,
|
|
2285
|
+
target_paths: input.target_paths.map((path) => normalizeAuditPath(projectRoot, path)),
|
|
2286
|
+
required_stable_ids: input.required_stable_ids,
|
|
2287
|
+
ai_selectable_stable_ids: input.ai_selectable_stable_ids,
|
|
2288
|
+
ai_selected_stable_ids: input.ai_selected_stable_ids,
|
|
2289
|
+
final_stable_ids: input.final_stable_ids,
|
|
2290
|
+
ai_selection_reasons: input.ai_selection_reasons,
|
|
2291
|
+
rejected_stable_ids: input.rejected_stable_ids,
|
|
2292
|
+
ignored_stable_ids: input.ignored_stable_ids
|
|
2293
|
+
};
|
|
2294
|
+
await appendAuditLogEventLedgerEvents(projectRoot, [entry], {
|
|
2295
|
+
correlation_id: input.correlation_id,
|
|
2296
|
+
session_id: input.session_id
|
|
2297
|
+
});
|
|
2298
|
+
return entry;
|
|
2299
|
+
}
|
|
2300
|
+
function normalizeAuditPath(projectRoot, value) {
|
|
2301
|
+
const normalizedProjectRoot = resolve4(projectRoot);
|
|
2302
|
+
const candidate = isAbsolute3(value) ? resolve4(value) : resolve4(normalizedProjectRoot, value);
|
|
2303
|
+
const relativePath = relative3(normalizedProjectRoot, candidate);
|
|
2304
|
+
if (relativePath.length > 0 && relativePath !== "." && !relativePath.startsWith("..") && !isAbsolute3(relativePath)) {
|
|
2305
|
+
return posix2.normalize(relativePath.split("\\").join("/"));
|
|
2836
2306
|
}
|
|
2837
|
-
|
|
2307
|
+
return posix2.normalize(value.replaceAll("\\", "/"));
|
|
2308
|
+
}
|
|
2309
|
+
async function appendAuditLogEventLedgerEvents(projectRoot, entries, metadata = {}) {
|
|
2310
|
+
for (const entry of entries) {
|
|
2311
|
+
if (entry.event === "get_rules") {
|
|
2312
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
2313
|
+
event_type: "knowledge_context_planned",
|
|
2314
|
+
ts: entry.ts,
|
|
2315
|
+
target_paths: [entry.path],
|
|
2316
|
+
required_stable_ids: metadata.rule_context?.required_stable_ids ?? [],
|
|
2317
|
+
ai_selectable_stable_ids: metadata.rule_context?.ai_selectable_stable_ids ?? [],
|
|
2318
|
+
final_stable_ids: metadata.rule_context?.final_stable_ids ?? [],
|
|
2319
|
+
client_hash: entry.client_hash,
|
|
2320
|
+
correlation_id: metadata.correlation_id,
|
|
2321
|
+
session_id: metadata.session_id
|
|
2322
|
+
});
|
|
2323
|
+
continue;
|
|
2324
|
+
}
|
|
2325
|
+
if (entry.event === "knowledge_selection") {
|
|
2326
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
2327
|
+
event_type: "knowledge_selection",
|
|
2328
|
+
ts: entry.ts,
|
|
2329
|
+
selection_token: entry.selection_token,
|
|
2330
|
+
target_paths: entry.target_paths,
|
|
2331
|
+
required_stable_ids: entry.required_stable_ids,
|
|
2332
|
+
ai_selectable_stable_ids: entry.ai_selectable_stable_ids,
|
|
2333
|
+
ai_selected_stable_ids: entry.ai_selected_stable_ids,
|
|
2334
|
+
final_stable_ids: entry.final_stable_ids,
|
|
2335
|
+
ai_selection_reasons: entry.ai_selection_reasons,
|
|
2336
|
+
rejected_stable_ids: entry.rejected_stable_ids,
|
|
2337
|
+
ignored_stable_ids: entry.ignored_stable_ids,
|
|
2338
|
+
correlation_id: metadata.correlation_id,
|
|
2339
|
+
session_id: metadata.session_id
|
|
2340
|
+
});
|
|
2341
|
+
continue;
|
|
2342
|
+
}
|
|
2343
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
2344
|
+
event_type: "edit_intent_checked",
|
|
2345
|
+
ts: entry.ts,
|
|
2346
|
+
path: entry.path,
|
|
2347
|
+
compliant: entry.compliant,
|
|
2348
|
+
intent: entry.intent,
|
|
2349
|
+
ledger_entry_id: entry.ledger_entry_id,
|
|
2350
|
+
ledger_source: "ai",
|
|
2351
|
+
matched_rule_context_ts: entry.matched_get_rules_ts,
|
|
2352
|
+
window_ms: entry.window_ms,
|
|
2353
|
+
correlation_id: metadata.correlation_id,
|
|
2354
|
+
session_id: metadata.session_id
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
// src/services/get-rules.ts
|
|
2360
|
+
var PRIORITY_ORDER = {
|
|
2361
|
+
high: 0,
|
|
2362
|
+
medium: 1,
|
|
2363
|
+
low: 2
|
|
2364
|
+
};
|
|
2365
|
+
async function getRules(projectRoot, input) {
|
|
2366
|
+
const context = await loadGetRulesContext(projectRoot);
|
|
2367
|
+
const stale = input.client_hash !== void 0 && input.client_hash !== context.meta.revision;
|
|
2368
|
+
const matchedNodes = matchRuleNodes(context.meta, input.path);
|
|
2369
|
+
const requiredStableIds = matchedNodes.filter((node) => node.level === "L2").map((node) => node.stable_id);
|
|
2370
|
+
const aiSelectableStableIds = matchedNodes.filter((node) => node.level === "L1").map((node) => node.stable_id);
|
|
2371
|
+
const rules = await resolveRulesForPath(projectRoot, context, input.path);
|
|
2372
|
+
const result = {
|
|
2373
|
+
revision_hash: context.meta.revision,
|
|
2374
|
+
stale,
|
|
2375
|
+
rules
|
|
2376
|
+
};
|
|
2838
2377
|
try {
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2378
|
+
await appendGetRulesAuditEvent(projectRoot, {
|
|
2379
|
+
path: input.path,
|
|
2380
|
+
client_hash: input.client_hash,
|
|
2381
|
+
required_stable_ids: requiredStableIds,
|
|
2382
|
+
ai_selectable_stable_ids: aiSelectableStableIds,
|
|
2383
|
+
final_stable_ids: [...requiredStableIds, ...aiSelectableStableIds],
|
|
2384
|
+
correlation_id: input.correlation_id,
|
|
2385
|
+
session_id: input.session_id
|
|
2386
|
+
});
|
|
2844
2387
|
} catch {
|
|
2845
|
-
return;
|
|
2846
2388
|
}
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
if (Object.keys(remainingServers).length === 0) {
|
|
2854
|
-
delete cleaned.mcpServers;
|
|
2855
|
-
} else {
|
|
2856
|
-
cleaned.mcpServers = remainingServers;
|
|
2389
|
+
return result;
|
|
2390
|
+
}
|
|
2391
|
+
async function loadGetRulesContext(projectRoot) {
|
|
2392
|
+
const cached = contextCache.get("context", projectRoot);
|
|
2393
|
+
if (cached !== void 0) {
|
|
2394
|
+
return cached;
|
|
2857
2395
|
}
|
|
2858
|
-
|
|
2859
|
-
await
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2396
|
+
const meta = await readAgentsMeta(projectRoot);
|
|
2397
|
+
const l0Content = await readFile6(join7(projectRoot, ".fabric", "bootstrap", "README.md"), "utf8");
|
|
2398
|
+
const context = {
|
|
2399
|
+
meta,
|
|
2400
|
+
l0Content,
|
|
2401
|
+
humanLockedNearby: []
|
|
2402
|
+
};
|
|
2403
|
+
contextCache.set("context", projectRoot, context);
|
|
2404
|
+
return context;
|
|
2864
2405
|
}
|
|
2865
|
-
async function
|
|
2866
|
-
const
|
|
2867
|
-
await
|
|
2868
|
-
|
|
2406
|
+
async function resolveRulesForPath(projectRoot, context, path, options = {}) {
|
|
2407
|
+
const matchedNodes = matchRuleNodes(context.meta, path);
|
|
2408
|
+
const loaded = await loadMatchedRules(projectRoot, matchedNodes);
|
|
2409
|
+
return buildRulesPayload(context, loaded, options);
|
|
2869
2410
|
}
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
await ensureParentDirectory(path);
|
|
2873
|
-
await writeFile2(path, "", { encoding: "utf8", flag: "a" });
|
|
2411
|
+
function normalizeRulesPath(value) {
|
|
2412
|
+
return value.replaceAll("\\", "/");
|
|
2874
2413
|
}
|
|
2875
|
-
function
|
|
2876
|
-
const
|
|
2877
|
-
|
|
2878
|
-
|
|
2414
|
+
function matchRuleNodes(meta, path) {
|
|
2415
|
+
const requestedPath = normalizeRulesPath(path);
|
|
2416
|
+
return Object.entries(meta.nodes).filter(([, node]) => shouldLoadNodeForPath(requestedPath, node)).sort((left, right) => {
|
|
2417
|
+
const [leftId, leftNode] = left;
|
|
2418
|
+
const [rightId, rightNode] = right;
|
|
2419
|
+
const priorityDelta = PRIORITY_ORDER[leftNode.priority] - PRIORITY_ORDER[rightNode.priority];
|
|
2420
|
+
return priorityDelta !== 0 ? priorityDelta : leftId.localeCompare(rightId);
|
|
2421
|
+
}).map(([nodeId, node]) => ({
|
|
2422
|
+
node_id: nodeId,
|
|
2423
|
+
level: classifyNode(nodeId, node),
|
|
2424
|
+
stable_id: node.stable_id ?? nodeId,
|
|
2425
|
+
identity_source: node.identity_source ?? "derived",
|
|
2426
|
+
node
|
|
2427
|
+
}));
|
|
2879
2428
|
}
|
|
2880
|
-
function
|
|
2881
|
-
const
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
const files = [];
|
|
2886
|
-
const stack = [rulesRoot];
|
|
2887
|
-
while (stack.length > 0) {
|
|
2888
|
-
const current = stack.pop();
|
|
2889
|
-
if (current === void 0) {
|
|
2429
|
+
async function loadMatchedRules(projectRoot, matchedNodes, fileContentCache = /* @__PURE__ */ new Map()) {
|
|
2430
|
+
const rules = [];
|
|
2431
|
+
const stubs = [];
|
|
2432
|
+
for (const matchedNode of matchedNodes) {
|
|
2433
|
+
if (matchedNode.level === null) {
|
|
2890
2434
|
continue;
|
|
2891
2435
|
}
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
}
|
|
2436
|
+
if (matchedNode.node.activation?.tier === "description") {
|
|
2437
|
+
stubs.push({
|
|
2438
|
+
stable_id: matchedNode.stable_id,
|
|
2439
|
+
identity_source: matchedNode.identity_source,
|
|
2440
|
+
level: matchedNode.level,
|
|
2441
|
+
path: matchedNode.node.file,
|
|
2442
|
+
description: matchedNode.node.activation.description ?? ""
|
|
2443
|
+
});
|
|
2444
|
+
continue;
|
|
2900
2445
|
}
|
|
2446
|
+
rules.push({
|
|
2447
|
+
level: matchedNode.level,
|
|
2448
|
+
stable_id: matchedNode.stable_id,
|
|
2449
|
+
identity_source: matchedNode.identity_source,
|
|
2450
|
+
entry: {
|
|
2451
|
+
path: matchedNode.node.file,
|
|
2452
|
+
content: await readRuleContent(projectRoot, matchedNode.node.file, fileContentCache)
|
|
2453
|
+
}
|
|
2454
|
+
});
|
|
2901
2455
|
}
|
|
2902
|
-
return
|
|
2903
|
-
}
|
|
2904
|
-
function isValidJsonLine(line) {
|
|
2905
|
-
try {
|
|
2906
|
-
JSON.parse(line);
|
|
2907
|
-
return true;
|
|
2908
|
-
} catch {
|
|
2909
|
-
return false;
|
|
2910
|
-
}
|
|
2911
|
-
}
|
|
2912
|
-
function normalizeTarget(targetInput) {
|
|
2913
|
-
return isAbsolute3(targetInput) ? targetInput : resolve4(process.cwd(), targetInput);
|
|
2456
|
+
return { rules, stubs };
|
|
2914
2457
|
}
|
|
2915
|
-
function
|
|
2916
|
-
|
|
2458
|
+
function buildRulesPayload(context, loaded, options = {}) {
|
|
2459
|
+
const { L1, L2 } = partitionRulesByLevel(loaded.rules, options.dedupeByPath ?? false);
|
|
2460
|
+
return {
|
|
2461
|
+
L0: context.l0Content,
|
|
2462
|
+
L1,
|
|
2463
|
+
L2,
|
|
2464
|
+
human_locked_nearby: context.humanLockedNearby,
|
|
2465
|
+
description_stubs: loaded.stubs.length > 0 ? dedupeDescriptionStubsByPath(loaded.stubs).map(toDescriptionStub) : void 0
|
|
2466
|
+
};
|
|
2917
2467
|
}
|
|
2918
|
-
function
|
|
2919
|
-
if (
|
|
2920
|
-
return
|
|
2468
|
+
function classifyNode(nodeId, node) {
|
|
2469
|
+
if (nodeId.startsWith("L1/")) {
|
|
2470
|
+
return "L1";
|
|
2921
2471
|
}
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2472
|
+
if (nodeId.startsWith("L2/")) {
|
|
2473
|
+
return "L2";
|
|
2474
|
+
}
|
|
2475
|
+
return node.layer === "L0" ? null : node.layer;
|
|
2476
|
+
}
|
|
2477
|
+
function partitionRulesByLevel(loadedRules, dedupeByPath) {
|
|
2478
|
+
const l1 = [];
|
|
2479
|
+
const l2 = [];
|
|
2480
|
+
for (const rule of loadedRules) {
|
|
2481
|
+
if (rule.level === "L1") {
|
|
2482
|
+
l1.push(rule.entry);
|
|
2927
2483
|
continue;
|
|
2928
2484
|
}
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
const relativePath = normalizePath(absolutePath.slice(root.length + 1));
|
|
2932
|
-
if (relativePath.length === 0) {
|
|
2933
|
-
continue;
|
|
2934
|
-
}
|
|
2935
|
-
if (entry.isDirectory()) {
|
|
2936
|
-
if (!IGNORED_DIRECTORIES.has(entry.name)) {
|
|
2937
|
-
stack.push(absolutePath);
|
|
2938
|
-
}
|
|
2939
|
-
continue;
|
|
2940
|
-
}
|
|
2941
|
-
if (!entry.isFile()) {
|
|
2942
|
-
continue;
|
|
2943
|
-
}
|
|
2944
|
-
const reason = getEntryPointReason(relativePath);
|
|
2945
|
-
if (reason !== null) {
|
|
2946
|
-
entries.push({ path: relativePath, reason });
|
|
2947
|
-
}
|
|
2485
|
+
if (rule.level === "L2") {
|
|
2486
|
+
l2.push(rule.entry);
|
|
2948
2487
|
}
|
|
2949
2488
|
}
|
|
2950
|
-
return
|
|
2489
|
+
return {
|
|
2490
|
+
L1: dedupeByPath ? dedupeEntriesByPath(l1) : l1,
|
|
2491
|
+
L2: dedupeByPath ? dedupeEntriesByPath(l2) : l2
|
|
2492
|
+
};
|
|
2951
2493
|
}
|
|
2952
|
-
function
|
|
2953
|
-
const
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
return "top-level script";
|
|
2962
|
-
}
|
|
2963
|
-
if (directory === "src" && /^(App|app|index|main)$/.test(fileBase)) {
|
|
2964
|
-
return "application entry";
|
|
2965
|
-
}
|
|
2966
|
-
if ((directory === "app" || directory.startsWith("app/")) && /^(layout|page|route)$/.test(fileBase)) {
|
|
2967
|
-
return "next app route";
|
|
2968
|
-
}
|
|
2969
|
-
if ((directory === "pages" || directory.startsWith("pages/")) && fileName !== "_app.d.ts") {
|
|
2970
|
-
return "next page route";
|
|
2971
|
-
}
|
|
2972
|
-
return null;
|
|
2494
|
+
function dedupeEntriesByPath(entries) {
|
|
2495
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
2496
|
+
return entries.filter((entry) => {
|
|
2497
|
+
if (seenPaths.has(entry.path)) {
|
|
2498
|
+
return false;
|
|
2499
|
+
}
|
|
2500
|
+
seenPaths.add(entry.path);
|
|
2501
|
+
return true;
|
|
2502
|
+
});
|
|
2973
2503
|
}
|
|
2974
|
-
function
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2504
|
+
function shouldLoadNodeForPath(requestedPath, node) {
|
|
2505
|
+
switch (node.activation?.tier) {
|
|
2506
|
+
case "always":
|
|
2507
|
+
return true;
|
|
2508
|
+
case "description":
|
|
2509
|
+
return true;
|
|
2510
|
+
case "path":
|
|
2511
|
+
case void 0:
|
|
2512
|
+
return minimatch(requestedPath, normalizeRulesPath(node.scope_glob), { dot: true });
|
|
2980
2513
|
}
|
|
2981
|
-
return "ok";
|
|
2982
2514
|
}
|
|
2983
|
-
function
|
|
2984
|
-
|
|
2515
|
+
function dedupeDescriptionStubsByPath(stubs) {
|
|
2516
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
2517
|
+
return stubs.filter((stub) => {
|
|
2518
|
+
if (seenPaths.has(stub.path)) {
|
|
2519
|
+
return false;
|
|
2520
|
+
}
|
|
2521
|
+
seenPaths.add(stub.path);
|
|
2522
|
+
return true;
|
|
2523
|
+
});
|
|
2524
|
+
}
|
|
2525
|
+
function toDescriptionStub(stub) {
|
|
2526
|
+
return {
|
|
2527
|
+
path: stub.path,
|
|
2528
|
+
description: stub.description
|
|
2529
|
+
};
|
|
2530
|
+
}
|
|
2531
|
+
async function readRuleContent(projectRoot, file, fileContentCache) {
|
|
2532
|
+
const cached = fileContentCache.get(file);
|
|
2533
|
+
if (cached !== void 0) {
|
|
2534
|
+
return await cached;
|
|
2535
|
+
}
|
|
2536
|
+
const pending = readFile6(join7(projectRoot, file), "utf8");
|
|
2537
|
+
fileContentCache.set(file, pending);
|
|
2538
|
+
return await pending;
|
|
2985
2539
|
}
|
|
2986
2540
|
|
|
2987
2541
|
export {
|
|
2988
|
-
AGENTS_MD_RESOURCE_URI,
|
|
2989
2542
|
contextCache,
|
|
2990
2543
|
resolveProjectRoot,
|
|
2991
2544
|
readAgentsMeta,
|
|
@@ -3011,9 +2564,9 @@ export {
|
|
|
3011
2564
|
invalidateRuleSyncCooldown,
|
|
3012
2565
|
ensureRulesFresh,
|
|
3013
2566
|
reconcileRules,
|
|
2567
|
+
appendRuleSelectionAuditEvent,
|
|
3014
2568
|
getRules,
|
|
3015
|
-
|
|
3016
|
-
getRuleSections,
|
|
2569
|
+
normalizeRulesPath,
|
|
3017
2570
|
runDoctorReport,
|
|
3018
2571
|
runDoctorFix
|
|
3019
2572
|
};
|