@fenglimg/fabric-server 2.2.0-rc.8 → 2.2.0
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/index.d.ts +35 -1
- package/dist/index.js +436 -186
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { existsSync as
|
|
3
|
-
import { readFile as
|
|
4
|
-
import { join as
|
|
2
|
+
import { existsSync as existsSync9 } from "fs";
|
|
3
|
+
import { readFile as readFile17 } from "fs/promises";
|
|
4
|
+
import { join as join21, resolve as resolve4 } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
7
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -700,13 +700,9 @@ function appendPayloadWarning(warnings, guardResult, actionHint) {
|
|
|
700
700
|
// src/config-loader.ts
|
|
701
701
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
702
702
|
import { join as join4 } from "path";
|
|
703
|
-
import { selectionTokenTtlMsSchema, planContextTopKSchema
|
|
704
|
-
var RETRIEVAL_BUDGET_PROFILES = ["conservative", "balanced", "generous"];
|
|
705
|
-
function readRetrievalBudgetProfile(config) {
|
|
706
|
-
const raw = config.retrieval_budget_profile;
|
|
707
|
-
return typeof raw === "string" && RETRIEVAL_BUDGET_PROFILES.includes(raw) ? raw : void 0;
|
|
708
|
-
}
|
|
703
|
+
import { selectionTokenTtlMsSchema, planContextTopKSchema } from "@fenglimg/fabric-shared";
|
|
709
704
|
var PLAN_CONTEXT_TOP_K_DEFAULT = 24;
|
|
705
|
+
var RECALL_RELEVANCE_RATIO_DEFAULT = 0.25;
|
|
710
706
|
function readFabricConfig(projectRoot) {
|
|
711
707
|
const configPath = join4(projectRoot, ".fabric", "fabric-config.json");
|
|
712
708
|
if (!existsSync2(configPath)) {
|
|
@@ -719,18 +715,7 @@ function readFabricConfig(projectRoot) {
|
|
|
719
715
|
return parsed;
|
|
720
716
|
}
|
|
721
717
|
function readPayloadLimits(projectRoot) {
|
|
722
|
-
|
|
723
|
-
const explicit = config.mcpPayloadLimits;
|
|
724
|
-
const profile = readRetrievalBudgetProfile(config);
|
|
725
|
-
if (profile === void 0 && explicit === void 0) {
|
|
726
|
-
return void 0;
|
|
727
|
-
}
|
|
728
|
-
const resolved = resolveRetrievalBudget({
|
|
729
|
-
profile,
|
|
730
|
-
payloadWarnBytes: explicit?.warnBytes,
|
|
731
|
-
payloadHardBytes: explicit?.hardBytes
|
|
732
|
-
});
|
|
733
|
-
return { warnBytes: resolved.payloadWarnBytes, hardBytes: resolved.payloadHardBytes };
|
|
718
|
+
return readFabricConfig(projectRoot).mcpPayloadLimits;
|
|
734
719
|
}
|
|
735
720
|
function readSelectionTokenTtlMs(projectRoot) {
|
|
736
721
|
try {
|
|
@@ -776,17 +761,27 @@ function readDefaultLayerFilter(projectRoot) {
|
|
|
776
761
|
}
|
|
777
762
|
function readPlanContextTopK(projectRoot) {
|
|
778
763
|
try {
|
|
779
|
-
const
|
|
780
|
-
const raw = config.plan_context_top_k;
|
|
764
|
+
const raw = readFabricConfig(projectRoot).plan_context_top_k;
|
|
781
765
|
if (raw !== void 0) {
|
|
782
766
|
const parsed = planContextTopKSchema.safeParse(raw);
|
|
783
767
|
if (parsed.success) return parsed.data;
|
|
784
768
|
}
|
|
785
|
-
return
|
|
769
|
+
return PLAN_CONTEXT_TOP_K_DEFAULT;
|
|
786
770
|
} catch {
|
|
787
771
|
return PLAN_CONTEXT_TOP_K_DEFAULT;
|
|
788
772
|
}
|
|
789
773
|
}
|
|
774
|
+
function readRecallRelevanceRatio(projectRoot) {
|
|
775
|
+
try {
|
|
776
|
+
const raw = readFabricConfig(projectRoot).recall_relevance_ratio;
|
|
777
|
+
if (typeof raw === "number" && Number.isFinite(raw) && raw >= 0 && raw <= 1) {
|
|
778
|
+
return raw;
|
|
779
|
+
}
|
|
780
|
+
return RECALL_RELEVANCE_RATIO_DEFAULT;
|
|
781
|
+
} catch {
|
|
782
|
+
return RECALL_RELEVANCE_RATIO_DEFAULT;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
790
785
|
function readOrphanDemoteThresholdDays(projectRoot) {
|
|
791
786
|
try {
|
|
792
787
|
const cfg = readFabricConfig(projectRoot);
|
|
@@ -2033,6 +2028,8 @@ async function buildKnowledgeCensus(projectRoot) {
|
|
|
2033
2028
|
const census = {
|
|
2034
2029
|
by_type: {},
|
|
2035
2030
|
by_layer: { team: 0, personal: 0, project: 0 },
|
|
2031
|
+
broad_by_type: {},
|
|
2032
|
+
narrow_total: 0,
|
|
2036
2033
|
dropped_other_project: 0,
|
|
2037
2034
|
total: 0
|
|
2038
2035
|
};
|
|
@@ -2042,10 +2039,16 @@ async function buildKnowledgeCensus(projectRoot) {
|
|
|
2042
2039
|
const kept = filterByActiveProject(all, activeProject);
|
|
2043
2040
|
census.dropped_other_project = all.length - kept.length;
|
|
2044
2041
|
for (const entry of kept) {
|
|
2045
|
-
const
|
|
2042
|
+
const desc = extractRuleDescription(entry.source);
|
|
2043
|
+
const type = desc?.knowledge_type;
|
|
2044
|
+
const isNarrow = desc?.relevance_scope === "narrow";
|
|
2046
2045
|
if (typeof type === "string") {
|
|
2047
2046
|
census.by_type[type] = (census.by_type[type] ?? 0) + 1;
|
|
2047
|
+
if (!isNarrow) {
|
|
2048
|
+
census.broad_by_type[type] = (census.broad_by_type[type] ?? 0) + 1;
|
|
2049
|
+
}
|
|
2048
2050
|
}
|
|
2051
|
+
if (isNarrow) census.narrow_total += 1;
|
|
2049
2052
|
if (scopeRoot(entry.semanticScope) === "project") {
|
|
2050
2053
|
census.by_layer.project += 1;
|
|
2051
2054
|
} else {
|
|
@@ -2069,6 +2072,7 @@ async function buildAlwaysActiveBodies(projectRoot) {
|
|
|
2069
2072
|
if (desc === void 0) continue;
|
|
2070
2073
|
const type = desc.knowledge_type;
|
|
2071
2074
|
if (typeof type !== "string" || !ALWAYS_ACTIVE_TYPES.has(type)) continue;
|
|
2075
|
+
if (desc.relevance_scope === "narrow") continue;
|
|
2072
2076
|
out.push({
|
|
2073
2077
|
stable_id: entry.qualifiedId,
|
|
2074
2078
|
type,
|
|
@@ -2643,11 +2647,23 @@ async function planContext(projectRoot, input) {
|
|
|
2643
2647
|
scoringContext.vectorWeight = embedConfig.weight;
|
|
2644
2648
|
}
|
|
2645
2649
|
}
|
|
2646
|
-
const
|
|
2647
|
-
const
|
|
2650
|
+
const scoredSorted = sortDescriptionItems(rawItems, scoringContext);
|
|
2651
|
+
const seenStableIds = /* @__PURE__ */ new Set();
|
|
2652
|
+
const rankedScored = scoredSorted.filter(({ item }) => {
|
|
2653
|
+
if (seenStableIds.has(item.stable_id)) return false;
|
|
2654
|
+
seenStableIds.add(item.stable_id);
|
|
2655
|
+
return true;
|
|
2656
|
+
});
|
|
2657
|
+
const rankedCandidates = rankedScored.map((entry) => entry.item);
|
|
2648
2658
|
const topK = readPlanContextTopK(projectRoot);
|
|
2649
|
-
const
|
|
2650
|
-
const
|
|
2659
|
+
const cappedScored = rankedScored.slice(0, topK);
|
|
2660
|
+
const relevanceRatio = readRecallRelevanceRatio(projectRoot);
|
|
2661
|
+
const hasQuery = scoringContext.queryTerms.length > 0;
|
|
2662
|
+
const maxScore = rankedScored.length > 0 ? rankedScored[0].score : 0;
|
|
2663
|
+
const relevanceFloor = maxScore * relevanceRatio;
|
|
2664
|
+
const survivingScored = hasQuery && maxScore > 0 && relevanceRatio > 0 ? cappedScored.filter((entry) => entry.score >= relevanceFloor) : cappedScored;
|
|
2665
|
+
const topKCandidates = survivingScored.map((entry) => entry.item);
|
|
2666
|
+
const omittedCandidateCount = Math.max(0, rankedCandidates.length - topKCandidates.length);
|
|
2651
2667
|
let candidates = topKCandidates;
|
|
2652
2668
|
const relatedAppended = {};
|
|
2653
2669
|
if (input.include_related === true) {
|
|
@@ -2889,7 +2905,7 @@ function compareScopeThenId(left, right, scopeRank) {
|
|
|
2889
2905
|
}
|
|
2890
2906
|
function sortDescriptionItems(rawItems, scoringContext) {
|
|
2891
2907
|
if (scoringContext === void 0) {
|
|
2892
|
-
return [...rawItems].sort((left, right) => compareStableIds(left.stable_id, right.stable_id));
|
|
2908
|
+
return [...rawItems].sort((left, right) => compareStableIds(left.stable_id, right.stable_id)).map((item) => ({ item, score: 0 }));
|
|
2893
2909
|
}
|
|
2894
2910
|
const scored = rawItems.map((item) => ({
|
|
2895
2911
|
item,
|
|
@@ -2899,7 +2915,7 @@ function sortDescriptionItems(rawItems, scoringContext) {
|
|
|
2899
2915
|
(left, right) => left.score !== right.score ? right.score - left.score : compareScopeThenId(left.item, right.item, scoringContext.scopeRank)
|
|
2900
2916
|
// W2/A4 scope tie-break
|
|
2901
2917
|
);
|
|
2902
|
-
return scored
|
|
2918
|
+
return scored;
|
|
2903
2919
|
}
|
|
2904
2920
|
function documentTextForItem(description) {
|
|
2905
2921
|
return [
|
|
@@ -2930,16 +2946,6 @@ function buildPreflightDiagnostics(suppressedStableIds) {
|
|
|
2930
2946
|
function dedupeStableIds(stableIds) {
|
|
2931
2947
|
return Array.from(new Set(stableIds));
|
|
2932
2948
|
}
|
|
2933
|
-
function dedupeDescriptionIndex(items) {
|
|
2934
|
-
const seenStableIds = /* @__PURE__ */ new Set();
|
|
2935
|
-
return items.filter((item) => {
|
|
2936
|
-
if (seenStableIds.has(item.stable_id)) {
|
|
2937
|
-
return false;
|
|
2938
|
-
}
|
|
2939
|
-
seenStableIds.add(item.stable_id);
|
|
2940
|
-
return true;
|
|
2941
|
-
});
|
|
2942
|
-
}
|
|
2943
2949
|
var RECENCY_WINDOW_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
2944
2950
|
var RECENCY_BOOST = 25;
|
|
2945
2951
|
var LOCALITY_SAME_FILE = 100;
|
|
@@ -3091,7 +3097,7 @@ function buildNextSteps(planResult, paths, candidateById, candidateLookup) {
|
|
|
3091
3097
|
const omitted = planResult.omitted_candidate_count ?? 0;
|
|
3092
3098
|
if (omitted > 0) {
|
|
3093
3099
|
nextSteps.push(
|
|
3094
|
-
`${omitted} lower-ranked candidate(s) were omitted by the retrieval budget \u2014 pass a narrower intent (or raise plan_context_top_k
|
|
3100
|
+
`${omitted} lower-ranked candidate(s) were omitted by the retrieval budget \u2014 pass a narrower intent (or raise plan_context_top_k) to surface them.`
|
|
3095
3101
|
);
|
|
3096
3102
|
}
|
|
3097
3103
|
const surfacedPaths = new Set(paths.map((p) => p.stable_id));
|
|
@@ -3409,11 +3415,155 @@ import { enforcePayloadLimit as enforcePayloadLimit4 } from "@fenglimg/fabric-sh
|
|
|
3409
3415
|
|
|
3410
3416
|
// src/services/review.ts
|
|
3411
3417
|
import { execFileSync } from "child_process";
|
|
3412
|
-
import { existsSync as
|
|
3413
|
-
import { readFile as
|
|
3418
|
+
import { existsSync as existsSync5 } from "fs";
|
|
3419
|
+
import { readFile as readFile6, readdir as readdir2, stat as stat2, unlink as unlink2 } from "fs/promises";
|
|
3414
3420
|
import { homedir } from "os";
|
|
3415
|
-
import { basename, isAbsolute, join as
|
|
3421
|
+
import { basename, isAbsolute, join as join10, relative, resolve as resolve2, sep as sep2 } from "path";
|
|
3416
3422
|
import { allocateStoreKnowledgeId, isPersonalScope as isPersonalScope2 } from "@fenglimg/fabric-shared";
|
|
3423
|
+
|
|
3424
|
+
// src/services/pending-dedupe.ts
|
|
3425
|
+
import { existsSync as existsSync4 } from "fs";
|
|
3426
|
+
import { readdir, readFile as readFile5, unlink } from "fs/promises";
|
|
3427
|
+
import { join as join9 } from "path";
|
|
3428
|
+
var PENDING_TYPES = ["decisions", "pitfalls", "guidelines", "models", "processes"];
|
|
3429
|
+
var DISAMBIGUATION_SUFFIX = /^(.+)-([2-9])\.md$/u;
|
|
3430
|
+
var SOURCE_SESSIONS_LINE = /^source_sessions:\s*\[(.*)\]\s*$/mu;
|
|
3431
|
+
var CREATED_AT_LINE = /^created_at:\s*(.+)$/mu;
|
|
3432
|
+
function parseSourceSessions(content) {
|
|
3433
|
+
const m = SOURCE_SESSIONS_LINE.exec(content);
|
|
3434
|
+
if (!m) return [];
|
|
3435
|
+
try {
|
|
3436
|
+
const arr = JSON.parse(`[${m[1]}]`);
|
|
3437
|
+
return Array.isArray(arr) ? arr.filter((s) => typeof s === "string") : [];
|
|
3438
|
+
} catch {
|
|
3439
|
+
return [];
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
function parseCreatedAt(content) {
|
|
3443
|
+
const m = CREATED_AT_LINE.exec(content);
|
|
3444
|
+
return m ? m[1].trim() : "";
|
|
3445
|
+
}
|
|
3446
|
+
function resolveBaseSlug(name, present) {
|
|
3447
|
+
const m = DISAMBIGUATION_SUFFIX.exec(name);
|
|
3448
|
+
if (m && present.has(`${m[1]}.md`)) return m[1];
|
|
3449
|
+
return name.replace(/\.md$/u, "");
|
|
3450
|
+
}
|
|
3451
|
+
function unionSessions(survivor, twins) {
|
|
3452
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3453
|
+
const out = [];
|
|
3454
|
+
for (const s of [survivor, ...twins]) {
|
|
3455
|
+
for (const sid of s.sourceSessions) {
|
|
3456
|
+
if (sid.length > 0 && !seen.has(sid)) {
|
|
3457
|
+
seen.add(sid);
|
|
3458
|
+
out.push(sid);
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3462
|
+
return out;
|
|
3463
|
+
}
|
|
3464
|
+
function buildMergedContent(survivor, twins) {
|
|
3465
|
+
const union = unionSessions(survivor, twins);
|
|
3466
|
+
let merged = survivor.content.replace(
|
|
3467
|
+
SOURCE_SESSIONS_LINE,
|
|
3468
|
+
`source_sessions: [${union.map((s) => JSON.stringify(s)).join(", ")}]`
|
|
3469
|
+
);
|
|
3470
|
+
if (!merged.endsWith("\n")) merged += "\n";
|
|
3471
|
+
for (const twin of twins) {
|
|
3472
|
+
const body = extractBody(twin.content).trim();
|
|
3473
|
+
if (body.length === 0) continue;
|
|
3474
|
+
merged += `
|
|
3475
|
+
## Evidence (merged from session ${twin.primarySession || "unknown"})
|
|
3476
|
+
|
|
3477
|
+
${body}
|
|
3478
|
+
`;
|
|
3479
|
+
}
|
|
3480
|
+
return merged;
|
|
3481
|
+
}
|
|
3482
|
+
function chooseSurvivor(baseSlug, group) {
|
|
3483
|
+
const exact = group.find((p) => p.name === `${baseSlug}.md`);
|
|
3484
|
+
if (exact) return exact;
|
|
3485
|
+
return [...group].sort((a, b) => {
|
|
3486
|
+
if (a.createdAt !== b.createdAt) return a.createdAt < b.createdAt ? -1 : 1;
|
|
3487
|
+
return a.name < b.name ? -1 : 1;
|
|
3488
|
+
})[0];
|
|
3489
|
+
}
|
|
3490
|
+
async function mergePendingTwins(projectRoot) {
|
|
3491
|
+
const merged = [];
|
|
3492
|
+
for (const layer of ["team", "personal"]) {
|
|
3493
|
+
let pendingBase2;
|
|
3494
|
+
try {
|
|
3495
|
+
pendingBase2 = resolveStorePendingBase(layer, projectRoot);
|
|
3496
|
+
} catch {
|
|
3497
|
+
continue;
|
|
3498
|
+
}
|
|
3499
|
+
for (const type of PENDING_TYPES) {
|
|
3500
|
+
const dir = join9(pendingBase2, type);
|
|
3501
|
+
if (!existsSync4(dir)) continue;
|
|
3502
|
+
let names;
|
|
3503
|
+
try {
|
|
3504
|
+
names = (await readdir(dir)).filter((n) => n.endsWith(".md"));
|
|
3505
|
+
} catch {
|
|
3506
|
+
continue;
|
|
3507
|
+
}
|
|
3508
|
+
if (names.length < 2) continue;
|
|
3509
|
+
const present = new Set(names);
|
|
3510
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3511
|
+
for (const name of names) {
|
|
3512
|
+
const base = resolveBaseSlug(name, present);
|
|
3513
|
+
const arr = groups.get(base);
|
|
3514
|
+
if (arr) arr.push(name);
|
|
3515
|
+
else groups.set(base, [name]);
|
|
3516
|
+
}
|
|
3517
|
+
for (const [baseSlug, groupNames] of groups) {
|
|
3518
|
+
if (groupNames.length < 2) continue;
|
|
3519
|
+
const parsed = [];
|
|
3520
|
+
for (const name of groupNames) {
|
|
3521
|
+
const abs = join9(dir, name);
|
|
3522
|
+
let content;
|
|
3523
|
+
try {
|
|
3524
|
+
content = await readFile5(abs, "utf8");
|
|
3525
|
+
} catch {
|
|
3526
|
+
continue;
|
|
3527
|
+
}
|
|
3528
|
+
const sourceSessions = parseSourceSessions(content);
|
|
3529
|
+
parsed.push({
|
|
3530
|
+
name,
|
|
3531
|
+
abs,
|
|
3532
|
+
content,
|
|
3533
|
+
sourceSessions,
|
|
3534
|
+
primarySession: sourceSessions[0] ?? "",
|
|
3535
|
+
createdAt: parseCreatedAt(content)
|
|
3536
|
+
});
|
|
3537
|
+
}
|
|
3538
|
+
if (parsed.length < 2) continue;
|
|
3539
|
+
const distinctPrimaries = new Set(parsed.map((p) => p.primarySession).filter((s) => s.length > 0));
|
|
3540
|
+
if (distinctPrimaries.size < 2) continue;
|
|
3541
|
+
const survivor = chooseSurvivor(baseSlug, parsed);
|
|
3542
|
+
const twins = parsed.filter((p) => p.abs !== survivor.abs);
|
|
3543
|
+
const mergedContent = buildMergedContent(survivor, twins);
|
|
3544
|
+
try {
|
|
3545
|
+
await atomicWriteText(survivor.abs, mergedContent);
|
|
3546
|
+
for (const twin of twins) {
|
|
3547
|
+
await unlink(twin.abs);
|
|
3548
|
+
}
|
|
3549
|
+
} catch {
|
|
3550
|
+
continue;
|
|
3551
|
+
}
|
|
3552
|
+
merged.push({
|
|
3553
|
+
layer,
|
|
3554
|
+
type,
|
|
3555
|
+
base_slug: baseSlug,
|
|
3556
|
+
survivor: survivor.abs,
|
|
3557
|
+
removed: twins.map((t) => t.abs),
|
|
3558
|
+
source_sessions: unionSessions(survivor, twins)
|
|
3559
|
+
});
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
return { merged };
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3566
|
+
// src/services/review.ts
|
|
3417
3567
|
var PLURAL_TYPES = [
|
|
3418
3568
|
"decisions",
|
|
3419
3569
|
"pitfalls",
|
|
@@ -3520,6 +3670,10 @@ function isVisibleByLifecycle(fm, filters) {
|
|
|
3520
3670
|
return true;
|
|
3521
3671
|
}
|
|
3522
3672
|
async function listPending(projectRoot, filters) {
|
|
3673
|
+
try {
|
|
3674
|
+
await mergePendingTwins(projectRoot);
|
|
3675
|
+
} catch {
|
|
3676
|
+
}
|
|
3523
3677
|
const items = [];
|
|
3524
3678
|
const typesToScan = filters?.type !== void 0 ? [filters.type] : PLURAL_TYPES;
|
|
3525
3679
|
const sources = [];
|
|
@@ -3539,22 +3693,22 @@ async function listPending(projectRoot, filters) {
|
|
|
3539
3693
|
}
|
|
3540
3694
|
for (const source of sources) {
|
|
3541
3695
|
for (const type of typesToScan) {
|
|
3542
|
-
const dir =
|
|
3543
|
-
if (!
|
|
3696
|
+
const dir = join10(source.root, type);
|
|
3697
|
+
if (!existsSync5(dir)) {
|
|
3544
3698
|
continue;
|
|
3545
3699
|
}
|
|
3546
3700
|
let entries;
|
|
3547
3701
|
try {
|
|
3548
|
-
entries = await
|
|
3702
|
+
entries = await readdir2(dir);
|
|
3549
3703
|
} catch {
|
|
3550
3704
|
continue;
|
|
3551
3705
|
}
|
|
3552
3706
|
for (const name of entries) {
|
|
3553
3707
|
if (!name.endsWith(".md")) continue;
|
|
3554
|
-
const absolutePath =
|
|
3708
|
+
const absolutePath = join10(dir, name);
|
|
3555
3709
|
let content;
|
|
3556
3710
|
try {
|
|
3557
|
-
content = await
|
|
3711
|
+
content = await readFile6(absolutePath, "utf8");
|
|
3558
3712
|
} catch {
|
|
3559
3713
|
continue;
|
|
3560
3714
|
}
|
|
@@ -3664,7 +3818,7 @@ async function approveOne(projectRoot, pendingPath) {
|
|
|
3664
3818
|
let targetAbs;
|
|
3665
3819
|
let writtenTarget = false;
|
|
3666
3820
|
try {
|
|
3667
|
-
const content = await
|
|
3821
|
+
const content = await readFile6(sourceAbs, "utf8");
|
|
3668
3822
|
const fm = parseFrontmatter(content);
|
|
3669
3823
|
const pluralType = fm.type;
|
|
3670
3824
|
if (pluralType === void 0 || !PLURAL_TYPES.includes(pluralType)) {
|
|
@@ -3678,14 +3832,14 @@ async function approveOne(projectRoot, pendingPath) {
|
|
|
3678
3832
|
);
|
|
3679
3833
|
allocatedId = stableId;
|
|
3680
3834
|
const newFilename = `${stableId}--${slug}.md`;
|
|
3681
|
-
targetAbs =
|
|
3835
|
+
targetAbs = join10(resolveStoreCanonicalBase(layer, projectRoot), pluralType, newFilename);
|
|
3682
3836
|
await ensureParentDirectory(targetAbs);
|
|
3683
3837
|
const rewritten = rewriteFrontmatterForPromote(content, stableId);
|
|
3684
3838
|
await atomicWriteText(targetAbs, rewritten);
|
|
3685
3839
|
writtenTarget = true;
|
|
3686
3840
|
if (sourceIsStore) {
|
|
3687
|
-
if (
|
|
3688
|
-
await
|
|
3841
|
+
if (existsSync5(sourceAbs)) {
|
|
3842
|
+
await unlink2(sourceAbs);
|
|
3689
3843
|
}
|
|
3690
3844
|
} else if (sourceOrigin === "team") {
|
|
3691
3845
|
try {
|
|
@@ -3694,13 +3848,13 @@ async function approveOne(projectRoot, pendingPath) {
|
|
|
3694
3848
|
stdio: ["ignore", "pipe", "pipe"]
|
|
3695
3849
|
});
|
|
3696
3850
|
} catch {
|
|
3697
|
-
if (
|
|
3698
|
-
await
|
|
3851
|
+
if (existsSync5(sourceAbs)) {
|
|
3852
|
+
await unlink2(sourceAbs);
|
|
3699
3853
|
}
|
|
3700
3854
|
}
|
|
3701
3855
|
} else {
|
|
3702
|
-
if (
|
|
3703
|
-
await
|
|
3856
|
+
if (existsSync5(sourceAbs)) {
|
|
3857
|
+
await unlink2(sourceAbs);
|
|
3704
3858
|
}
|
|
3705
3859
|
}
|
|
3706
3860
|
await emitEventBestEffort2(projectRoot, {
|
|
@@ -3711,9 +3865,9 @@ async function approveOne(projectRoot, pendingPath) {
|
|
|
3711
3865
|
});
|
|
3712
3866
|
return { pending_path: pendingPath, stable_id: stableId };
|
|
3713
3867
|
} catch (err) {
|
|
3714
|
-
if (writtenTarget && targetAbs !== void 0 &&
|
|
3868
|
+
if (writtenTarget && targetAbs !== void 0 && existsSync5(targetAbs)) {
|
|
3715
3869
|
try {
|
|
3716
|
-
await
|
|
3870
|
+
await unlink2(targetAbs);
|
|
3717
3871
|
} catch {
|
|
3718
3872
|
}
|
|
3719
3873
|
}
|
|
@@ -3732,14 +3886,14 @@ async function rejectAll(projectRoot, pendingPaths, reason) {
|
|
|
3732
3886
|
for (const pendingPath of pendingPaths) {
|
|
3733
3887
|
try {
|
|
3734
3888
|
const sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
|
|
3735
|
-
if (
|
|
3736
|
-
const content = await
|
|
3889
|
+
if (existsSync5(sandboxed.abs)) {
|
|
3890
|
+
const content = await readFile6(sandboxed.abs, "utf8");
|
|
3737
3891
|
const merged = rewriteFrontmatterMerge(content, { status: "rejected" });
|
|
3738
3892
|
const rejectedAbs = sandboxed.abs.includes(`${sep2}pending${sep2}`) ? sandboxed.abs.replace(`${sep2}pending${sep2}`, `${sep2}rejected${sep2}`) : null;
|
|
3739
3893
|
if (rejectedAbs !== null) {
|
|
3740
3894
|
await ensureParentDirectory(rejectedAbs);
|
|
3741
3895
|
await atomicWriteText(rejectedAbs, merged);
|
|
3742
|
-
await
|
|
3896
|
+
await unlink2(sandboxed.abs);
|
|
3743
3897
|
} else if (merged !== content) {
|
|
3744
3898
|
await atomicWriteText(sandboxed.abs, merged);
|
|
3745
3899
|
}
|
|
@@ -3760,7 +3914,7 @@ async function modifyEntry(projectRoot, pendingPath, changes) {
|
|
|
3760
3914
|
if (target === null) {
|
|
3761
3915
|
throw new Error(`modify target not found: ${pendingPath}`);
|
|
3762
3916
|
}
|
|
3763
|
-
const content = await
|
|
3917
|
+
const content = await readFile6(target.absPath, "utf8");
|
|
3764
3918
|
const fm = parseFrontmatter(content);
|
|
3765
3919
|
const currentLayer = fm.layer ?? "team";
|
|
3766
3920
|
if (changes.semantic_scope !== void 0 && isPersonalScope2(changes.semantic_scope)) {
|
|
@@ -3805,7 +3959,7 @@ function resolveModifyTarget(projectRoot, pendingPath) {
|
|
|
3805
3959
|
} catch {
|
|
3806
3960
|
return null;
|
|
3807
3961
|
}
|
|
3808
|
-
if (
|
|
3962
|
+
if (existsSync5(sandboxed.abs)) {
|
|
3809
3963
|
return {
|
|
3810
3964
|
absPath: sandboxed.abs,
|
|
3811
3965
|
isInProjectTree: sandboxed.isInProjectTree,
|
|
@@ -3852,7 +4006,7 @@ async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
|
|
|
3852
4006
|
pluralType,
|
|
3853
4007
|
resolveWriteTargetStoreDir(toLayer, projectRoot)
|
|
3854
4008
|
);
|
|
3855
|
-
const toAbs =
|
|
4009
|
+
const toAbs = join10(
|
|
3856
4010
|
resolveStoreCanonicalBase(toLayer, projectRoot),
|
|
3857
4011
|
pluralType,
|
|
3858
4012
|
`${newStableId}--${slug}.md`
|
|
@@ -3880,12 +4034,12 @@ async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
|
|
|
3880
4034
|
stdio: ["ignore", "pipe", "pipe"]
|
|
3881
4035
|
});
|
|
3882
4036
|
} catch {
|
|
3883
|
-
if (
|
|
3884
|
-
await
|
|
4037
|
+
if (existsSync5(target.absPath)) {
|
|
4038
|
+
await unlink2(target.absPath);
|
|
3885
4039
|
}
|
|
3886
4040
|
}
|
|
3887
|
-
} else if (
|
|
3888
|
-
await
|
|
4041
|
+
} else if (existsSync5(target.absPath)) {
|
|
4042
|
+
await unlink2(target.absPath);
|
|
3889
4043
|
}
|
|
3890
4044
|
const flipReason = `layer_flip:${priorStableId ?? "<unassigned>"}->${newStableId}`;
|
|
3891
4045
|
const flipTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -3948,10 +4102,10 @@ function getSearchDirectoryCache(cacheKey) {
|
|
|
3948
4102
|
return created;
|
|
3949
4103
|
}
|
|
3950
4104
|
async function listIndexedSearchEntries(source, type) {
|
|
3951
|
-
const dir =
|
|
4105
|
+
const dir = join10(source.root, type);
|
|
3952
4106
|
let entries;
|
|
3953
4107
|
try {
|
|
3954
|
-
entries = await
|
|
4108
|
+
entries = await readdir2(dir);
|
|
3955
4109
|
} catch {
|
|
3956
4110
|
return [];
|
|
3957
4111
|
}
|
|
@@ -3961,7 +4115,7 @@ async function listIndexedSearchEntries(source, type) {
|
|
|
3961
4115
|
const indexed = [];
|
|
3962
4116
|
for (const name of entries) {
|
|
3963
4117
|
if (!name.endsWith(".md")) continue;
|
|
3964
|
-
const absolutePath =
|
|
4118
|
+
const absolutePath = join10(dir, name);
|
|
3965
4119
|
let fingerprint;
|
|
3966
4120
|
try {
|
|
3967
4121
|
const st = await stat2(absolutePath);
|
|
@@ -3978,7 +4132,7 @@ async function listIndexedSearchEntries(source, type) {
|
|
|
3978
4132
|
}
|
|
3979
4133
|
let content;
|
|
3980
4134
|
try {
|
|
3981
|
-
content = await
|
|
4135
|
+
content = await readFile6(absolutePath, "utf8");
|
|
3982
4136
|
searchEntryIndexContentReads += 1;
|
|
3983
4137
|
} catch {
|
|
3984
4138
|
directoryCache.files.delete(absolutePath);
|
|
@@ -4098,8 +4252,8 @@ async function deferAll(projectRoot, pendingPaths, until, reason) {
|
|
|
4098
4252
|
let stableId;
|
|
4099
4253
|
try {
|
|
4100
4254
|
const sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
|
|
4101
|
-
if (
|
|
4102
|
-
const content = await
|
|
4255
|
+
if (existsSync5(sandboxed.abs)) {
|
|
4256
|
+
const content = await readFile6(sandboxed.abs, "utf8");
|
|
4103
4257
|
stableId = parseFrontmatter(content).id;
|
|
4104
4258
|
const patch = {
|
|
4105
4259
|
status: "deferred",
|
|
@@ -4371,9 +4525,9 @@ function registerReview(server, tracker) {
|
|
|
4371
4525
|
}
|
|
4372
4526
|
|
|
4373
4527
|
// src/services/doctor.ts
|
|
4374
|
-
import { access as access4, readFile as
|
|
4528
|
+
import { access as access4, readFile as readFile15, readdir as readdirAsync, stat as statAsync, unlink as unlink4, writeFile as writeFile3 } from "fs/promises";
|
|
4375
4529
|
import { constants as constants2 } from "fs";
|
|
4376
|
-
import { isAbsolute as isAbsolute2, join as
|
|
4530
|
+
import { isAbsolute as isAbsolute2, join as join20, posix as posix4, resolve as resolve3 } from "path";
|
|
4377
4531
|
import {
|
|
4378
4532
|
createTranslator,
|
|
4379
4533
|
forensicReportSchema,
|
|
@@ -4587,8 +4741,8 @@ function createKnowledgeSummaryOpaqueCheck(t, inspection) {
|
|
|
4587
4741
|
}
|
|
4588
4742
|
|
|
4589
4743
|
// src/services/doctor-stable-id-collision.ts
|
|
4590
|
-
import { readFile as
|
|
4591
|
-
import { basename as basename2, join as
|
|
4744
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
4745
|
+
import { basename as basename2, join as join11 } from "path";
|
|
4592
4746
|
import {
|
|
4593
4747
|
buildStoreResolveInput as buildStoreResolveInput4,
|
|
4594
4748
|
createStoreResolver as createStoreResolver4,
|
|
@@ -4615,7 +4769,7 @@ function resolveIntegrityStores(projectRoot) {
|
|
|
4615
4769
|
return {
|
|
4616
4770
|
store_uuid: entry.store_uuid,
|
|
4617
4771
|
alias: entry.alias,
|
|
4618
|
-
dir:
|
|
4772
|
+
dir: join11(globalRoot, storeRelativePathForMount3(mounted ?? { store_uuid: entry.store_uuid }))
|
|
4619
4773
|
};
|
|
4620
4774
|
});
|
|
4621
4775
|
return { dirs, personalUuids };
|
|
@@ -4634,7 +4788,7 @@ async function inspectStoreStableIdIntegrity(projectRoot) {
|
|
|
4634
4788
|
for (const ref of await readKnowledgeAcrossStores2(resolved.dirs)) {
|
|
4635
4789
|
let source;
|
|
4636
4790
|
try {
|
|
4637
|
-
source = await
|
|
4791
|
+
source = await readFile7(ref.file, "utf8");
|
|
4638
4792
|
} catch {
|
|
4639
4793
|
continue;
|
|
4640
4794
|
}
|
|
@@ -4718,8 +4872,8 @@ function createLayerMismatchCheck(t, inspection) {
|
|
|
4718
4872
|
|
|
4719
4873
|
// src/services/doctor-relevance-paths.ts
|
|
4720
4874
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
4721
|
-
import { existsSync as
|
|
4722
|
-
import { join as
|
|
4875
|
+
import { existsSync as existsSync6, readdirSync, statSync as statSync3 } from "fs";
|
|
4876
|
+
import { join as join12, sep as sep3 } from "path";
|
|
4723
4877
|
import { minimatch } from "minimatch";
|
|
4724
4878
|
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
4725
4879
|
var RELEVANCE_PATHS_DRIFT_WINDOW_DAYS = 90;
|
|
@@ -4740,7 +4894,7 @@ function expandGlob(rawGlob) {
|
|
|
4740
4894
|
return rawGlob.endsWith("/") ? `${rawGlob}**` : rawGlob;
|
|
4741
4895
|
}
|
|
4742
4896
|
function collectWorkspacePaths(projectRoot) {
|
|
4743
|
-
if (!
|
|
4897
|
+
if (!existsSync6(projectRoot)) {
|
|
4744
4898
|
return [];
|
|
4745
4899
|
}
|
|
4746
4900
|
try {
|
|
@@ -4762,7 +4916,7 @@ function collectWorkspacePaths(projectRoot) {
|
|
|
4762
4916
|
continue;
|
|
4763
4917
|
}
|
|
4764
4918
|
for (const entry of entries) {
|
|
4765
|
-
const abs =
|
|
4919
|
+
const abs = join12(current, entry.name);
|
|
4766
4920
|
const rel = toPosix(abs.slice(projectRoot.length + 1));
|
|
4767
4921
|
if (rel.length === 0) continue;
|
|
4768
4922
|
if (entry.isDirectory()) {
|
|
@@ -4955,16 +5109,16 @@ function createNarrowNoPathsCheck(t, inspection) {
|
|
|
4955
5109
|
}
|
|
4956
5110
|
|
|
4957
5111
|
// src/services/doctor-broad-index.ts
|
|
4958
|
-
import { readFile as
|
|
4959
|
-
import { join as
|
|
5112
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
5113
|
+
import { join as join13 } from "path";
|
|
4960
5114
|
var DEFAULT_BROAD_INDEX_BACKSTOP = 50;
|
|
4961
5115
|
var BROAD_INDEX_BACKSTOP_MIN = 20;
|
|
4962
5116
|
var BROAD_INDEX_BACKSTOP_MAX = 500;
|
|
4963
5117
|
var BROAD_INDEX_DRIFT_RATIO = 0.8;
|
|
4964
5118
|
async function readBroadIndexBackstop(projectRoot) {
|
|
4965
|
-
const configPath =
|
|
5119
|
+
const configPath = join13(projectRoot, ".fabric", "fabric-config.json");
|
|
4966
5120
|
try {
|
|
4967
|
-
const raw = await
|
|
5121
|
+
const raw = await readFile8(configPath, "utf8");
|
|
4968
5122
|
const parsed = JSON.parse(raw);
|
|
4969
5123
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
4970
5124
|
const v = parsed.broad_index_backstop;
|
|
@@ -5145,8 +5299,8 @@ function createStaleArchiveCheck(t, inspection) {
|
|
|
5145
5299
|
}
|
|
5146
5300
|
|
|
5147
5301
|
// src/services/doctor-scope-lint.ts
|
|
5148
|
-
import { readFile as
|
|
5149
|
-
import { join as
|
|
5302
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
5303
|
+
import { join as join14 } from "path";
|
|
5150
5304
|
import {
|
|
5151
5305
|
buildStoreResolveInput as buildStoreResolveInput5,
|
|
5152
5306
|
createStoreResolver as createStoreResolver5,
|
|
@@ -5181,7 +5335,7 @@ async function resolveLintStores(projectRoot) {
|
|
|
5181
5335
|
const globalRoot = resolveGlobalRoot4();
|
|
5182
5336
|
return Promise.all(readSet.stores.map(async (entry) => {
|
|
5183
5337
|
const mounted = input.mountedStores.find((s) => s.store_uuid === entry.store_uuid);
|
|
5184
|
-
const dir =
|
|
5338
|
+
const dir = join14(
|
|
5185
5339
|
globalRoot,
|
|
5186
5340
|
storeRelativePathForMount4(mounted ?? { store_uuid: entry.store_uuid })
|
|
5187
5341
|
);
|
|
@@ -5213,7 +5367,7 @@ async function lintStoreScopes(projectRoot) {
|
|
|
5213
5367
|
}
|
|
5214
5368
|
let source;
|
|
5215
5369
|
try {
|
|
5216
|
-
source = await
|
|
5370
|
+
source = await readFile9(ref.file, "utf8");
|
|
5217
5371
|
} catch {
|
|
5218
5372
|
continue;
|
|
5219
5373
|
}
|
|
@@ -5335,8 +5489,8 @@ function createUnboundProjectCheck(t, violation) {
|
|
|
5335
5489
|
}
|
|
5336
5490
|
|
|
5337
5491
|
// src/services/doctor-store-counters.ts
|
|
5338
|
-
import { existsSync as
|
|
5339
|
-
import { join as
|
|
5492
|
+
import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
|
|
5493
|
+
import { join as join15 } from "path";
|
|
5340
5494
|
import {
|
|
5341
5495
|
buildStoreResolveInput as buildStoreResolveInput6,
|
|
5342
5496
|
createStoreResolver as createStoreResolver6,
|
|
@@ -5363,7 +5517,7 @@ function resolveCounterStores(projectRoot) {
|
|
|
5363
5517
|
return readSet.stores.map((entry) => ({
|
|
5364
5518
|
uuid: entry.store_uuid,
|
|
5365
5519
|
alias: entry.alias,
|
|
5366
|
-
dir:
|
|
5520
|
+
dir: join15(
|
|
5367
5521
|
globalRoot,
|
|
5368
5522
|
storeRelativePathForMount5(
|
|
5369
5523
|
input.mountedStores.find((s) => s.store_uuid === entry.store_uuid) ?? {
|
|
@@ -5391,8 +5545,8 @@ function readEntryId(file) {
|
|
|
5391
5545
|
function computeStoreDiskMax(storeDir) {
|
|
5392
5546
|
const max = defaultAgentsMetaCounters();
|
|
5393
5547
|
for (const type of STORE_KNOWLEDGE_TYPE_DIRS) {
|
|
5394
|
-
const dir =
|
|
5395
|
-
if (!
|
|
5548
|
+
const dir = join15(storeDir, STORE_LAYOUT2.knowledgeDir, type);
|
|
5549
|
+
if (!existsSync7(dir)) {
|
|
5396
5550
|
continue;
|
|
5397
5551
|
}
|
|
5398
5552
|
let names;
|
|
@@ -5405,7 +5559,7 @@ function computeStoreDiskMax(storeDir) {
|
|
|
5405
5559
|
if (!name.endsWith(".md")) {
|
|
5406
5560
|
continue;
|
|
5407
5561
|
}
|
|
5408
|
-
const parsed = parseKnowledgeId2(readEntryId(
|
|
5562
|
+
const parsed = parseKnowledgeId2(readEntryId(join15(dir, name)) ?? "");
|
|
5409
5563
|
if (parsed === null) {
|
|
5410
5564
|
continue;
|
|
5411
5565
|
}
|
|
@@ -5488,7 +5642,7 @@ function createStoreCounterCheck(t, drifts) {
|
|
|
5488
5642
|
|
|
5489
5643
|
// src/services/doctor-store-orphan.ts
|
|
5490
5644
|
import { readdirSync as readdirSync3 } from "fs";
|
|
5491
|
-
import { join as
|
|
5645
|
+
import { join as join16 } from "path";
|
|
5492
5646
|
import {
|
|
5493
5647
|
STORES_ROOT_DIR,
|
|
5494
5648
|
addMountedStore,
|
|
@@ -5512,15 +5666,15 @@ function inspectStoreOrphans(globalRoot = resolveGlobalRoot6()) {
|
|
|
5512
5666
|
return [];
|
|
5513
5667
|
}
|
|
5514
5668
|
const registered = new Set(config.stores.map((s) => s.store_uuid));
|
|
5515
|
-
const storesRoot =
|
|
5669
|
+
const storesRoot = join16(globalRoot, STORES_ROOT_DIR);
|
|
5516
5670
|
const orphans = [];
|
|
5517
5671
|
for (const group of listDir(storesRoot)) {
|
|
5518
5672
|
if (group === STORE_BY_ALIAS_DIR) {
|
|
5519
5673
|
continue;
|
|
5520
5674
|
}
|
|
5521
|
-
const groupDir =
|
|
5675
|
+
const groupDir = join16(storesRoot, group);
|
|
5522
5676
|
for (const mount of listDir(groupDir)) {
|
|
5523
|
-
const dir =
|
|
5677
|
+
const dir = join16(groupDir, mount);
|
|
5524
5678
|
const identity = readStoreIdentity(dir);
|
|
5525
5679
|
if (identity === null || registered.has(identity.store_uuid)) {
|
|
5526
5680
|
continue;
|
|
@@ -5593,7 +5747,7 @@ import {
|
|
|
5593
5747
|
|
|
5594
5748
|
// src/services/events-jsonl-gates.ts
|
|
5595
5749
|
import { promises as fs } from "fs";
|
|
5596
|
-
import { existsSync as
|
|
5750
|
+
import { existsSync as existsSync8 } from "fs";
|
|
5597
5751
|
var EVENTS_JSONL_SIZE_WARN_BYTES = 10 * 1024 * 1024;
|
|
5598
5752
|
var METRICS_STALE_WARN_MS = 10 * 60 * 1e3;
|
|
5599
5753
|
var ROTATION_OVERDUE_WARN_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
@@ -5615,7 +5769,7 @@ async function inspectEventsJsonlGates(projectRoot, options = {}) {
|
|
|
5615
5769
|
if (!(isNodeError(error) && error.code === "ENOENT")) throw error;
|
|
5616
5770
|
}
|
|
5617
5771
|
let metricsStalenessMs = null;
|
|
5618
|
-
if (
|
|
5772
|
+
if (existsSync8(metricsPath)) {
|
|
5619
5773
|
try {
|
|
5620
5774
|
const stat4 = await fs.stat(metricsPath);
|
|
5621
5775
|
metricsStalenessMs = Math.max(0, now.getTime() - stat4.mtimeMs);
|
|
@@ -5657,9 +5811,18 @@ async function inspectEventsJsonlGates(projectRoot, options = {}) {
|
|
|
5657
5811
|
}
|
|
5658
5812
|
|
|
5659
5813
|
// src/services/doctor-skill-lints.ts
|
|
5660
|
-
import { readdir as
|
|
5661
|
-
import { join as
|
|
5814
|
+
import { readdir as readdir3, readFile as readFile10 } from "fs/promises";
|
|
5815
|
+
import { join as join17, posix as posix2 } from "path";
|
|
5662
5816
|
var FABRIC_SKILL_SLUGS = ["fabric-archive", "fabric-review", "fabric-import"];
|
|
5817
|
+
var ROUTER_VALID_LEAF_SLUGS = /* @__PURE__ */ new Set([
|
|
5818
|
+
"fabric-archive",
|
|
5819
|
+
"fabric-review",
|
|
5820
|
+
"fabric-import",
|
|
5821
|
+
"fabric-sync",
|
|
5822
|
+
"fabric-store",
|
|
5823
|
+
"fabric-audit",
|
|
5824
|
+
"fabric-connect"
|
|
5825
|
+
]);
|
|
5663
5826
|
var SKILL_MD_FRONTMATTER_ROOTS = [".claude/skills", ".codex/skills"];
|
|
5664
5827
|
var SKILL_FRONTMATTER_KEY_PATTERN = /^([A-Za-z_][A-Za-z0-9_-]*):[ \t]+(.+?)[ \t]*$/u;
|
|
5665
5828
|
var SKILL_QUOTED_VALUE_LEADS = /* @__PURE__ */ new Set(['"', "'", "[", "{", ">", "|"]);
|
|
@@ -5680,7 +5843,7 @@ function issueCheck(name, status, kind, code, message, actionHint, audience) {
|
|
|
5680
5843
|
}
|
|
5681
5844
|
async function listMarkdownFiles(dir) {
|
|
5682
5845
|
try {
|
|
5683
|
-
return (await
|
|
5846
|
+
return (await readdir3(dir)).filter((name) => name.endsWith(".md"));
|
|
5684
5847
|
} catch {
|
|
5685
5848
|
return null;
|
|
5686
5849
|
}
|
|
@@ -5688,8 +5851,8 @@ async function listMarkdownFiles(dir) {
|
|
|
5688
5851
|
async function inspectSkillRefMirror(projectRoot) {
|
|
5689
5852
|
const driftedPaths = [];
|
|
5690
5853
|
for (const slug of FABRIC_SKILL_SLUGS) {
|
|
5691
|
-
const claudeRef =
|
|
5692
|
-
const codexRef =
|
|
5854
|
+
const claudeRef = join17(projectRoot, ".claude", "skills", slug, "ref");
|
|
5855
|
+
const codexRef = join17(projectRoot, ".codex", "skills", slug, "ref");
|
|
5693
5856
|
const [claudeFiles, codexFiles] = await Promise.all([listMarkdownFiles(claudeRef), listMarkdownFiles(codexRef)]);
|
|
5694
5857
|
if (claudeFiles === null || codexFiles === null) continue;
|
|
5695
5858
|
const claudeSet = new Set(claudeFiles);
|
|
@@ -5706,8 +5869,8 @@ async function inspectSkillRefMirror(projectRoot) {
|
|
|
5706
5869
|
let codexBody;
|
|
5707
5870
|
try {
|
|
5708
5871
|
[claudeBody, codexBody] = await Promise.all([
|
|
5709
|
-
|
|
5710
|
-
|
|
5872
|
+
readFile10(join17(claudeRef, fname), "utf8"),
|
|
5873
|
+
readFile10(join17(codexRef, fname), "utf8")
|
|
5711
5874
|
]);
|
|
5712
5875
|
} catch {
|
|
5713
5876
|
continue;
|
|
@@ -5726,10 +5889,10 @@ async function inspectSkillTokenBudget(projectRoot) {
|
|
|
5726
5889
|
const overSize = [];
|
|
5727
5890
|
let highestSeverity = "ok";
|
|
5728
5891
|
for (const slug of FABRIC_SKILL_SLUGS) {
|
|
5729
|
-
const skillMdPath =
|
|
5892
|
+
const skillMdPath = join17(projectRoot, ".claude", "skills", slug, "SKILL.md");
|
|
5730
5893
|
let body;
|
|
5731
5894
|
try {
|
|
5732
|
-
body = await
|
|
5895
|
+
body = await readFile10(skillMdPath, "utf8");
|
|
5733
5896
|
} catch {
|
|
5734
5897
|
continue;
|
|
5735
5898
|
}
|
|
@@ -5750,10 +5913,10 @@ async function inspectSkillDescription(projectRoot) {
|
|
|
5750
5913
|
const CJK_PATTERN = /[\u3400-\u4dbf\u4e00-\u9fff]/u;
|
|
5751
5914
|
const ASCII_PATTERN = /[a-zA-Z]{2,}/u;
|
|
5752
5915
|
for (const slug of FABRIC_SKILL_SLUGS) {
|
|
5753
|
-
const skillMdPath =
|
|
5916
|
+
const skillMdPath = join17(projectRoot, ".claude", "skills", slug, "SKILL.md");
|
|
5754
5917
|
let body;
|
|
5755
5918
|
try {
|
|
5756
|
-
body = await
|
|
5919
|
+
body = await readFile10(skillMdPath, "utf8");
|
|
5757
5920
|
} catch {
|
|
5758
5921
|
continue;
|
|
5759
5922
|
}
|
|
@@ -5784,19 +5947,19 @@ async function inspectSkillDescription(projectRoot) {
|
|
|
5784
5947
|
async function inspectSkillMdYamlInvalid(projectRoot) {
|
|
5785
5948
|
const candidates = [];
|
|
5786
5949
|
for (const rootRel of SKILL_MD_FRONTMATTER_ROOTS) {
|
|
5787
|
-
const rootAbs =
|
|
5950
|
+
const rootAbs = join17(projectRoot, rootRel);
|
|
5788
5951
|
let dirEntries;
|
|
5789
5952
|
try {
|
|
5790
|
-
dirEntries = await
|
|
5953
|
+
dirEntries = await readdir3(rootAbs, { withFileTypes: true });
|
|
5791
5954
|
} catch {
|
|
5792
5955
|
continue;
|
|
5793
5956
|
}
|
|
5794
5957
|
for (const dirEntry of dirEntries) {
|
|
5795
5958
|
if (!dirEntry.isDirectory()) continue;
|
|
5796
|
-
const skillFile =
|
|
5959
|
+
const skillFile = join17(rootAbs, dirEntry.name, "SKILL.md");
|
|
5797
5960
|
let raw;
|
|
5798
5961
|
try {
|
|
5799
|
-
raw = await
|
|
5962
|
+
raw = await readFile10(skillFile, "utf8");
|
|
5800
5963
|
} catch {
|
|
5801
5964
|
continue;
|
|
5802
5965
|
}
|
|
@@ -5844,6 +6007,68 @@ function extractSkillFrontmatterLines(raw) {
|
|
|
5844
6007
|
}
|
|
5845
6008
|
return null;
|
|
5846
6009
|
}
|
|
6010
|
+
function extractMarkdownSectionBody(markdown, sectionName) {
|
|
6011
|
+
const lines = markdown.split(/\r?\n/u);
|
|
6012
|
+
const headingRe = /^(#{2,3})\s+(.+?)\s*$/u;
|
|
6013
|
+
let start = -1;
|
|
6014
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6015
|
+
const h = headingRe.exec(lines[i]);
|
|
6016
|
+
if (h && h[2] === sectionName) {
|
|
6017
|
+
start = i + 1;
|
|
6018
|
+
break;
|
|
6019
|
+
}
|
|
6020
|
+
}
|
|
6021
|
+
if (start === -1) return null;
|
|
6022
|
+
const out = [];
|
|
6023
|
+
for (let i = start; i < lines.length; i++) {
|
|
6024
|
+
if (headingRe.test(lines[i])) break;
|
|
6025
|
+
out.push(lines[i]);
|
|
6026
|
+
}
|
|
6027
|
+
return out.join("\n");
|
|
6028
|
+
}
|
|
6029
|
+
async function inspectRouterChainRef(projectRoot) {
|
|
6030
|
+
const candidatePaths = [
|
|
6031
|
+
join17(projectRoot, ".claude", "skills", "fabric", "SKILL.md"),
|
|
6032
|
+
join17(projectRoot, ".codex", "skills", "fabric", "SKILL.md")
|
|
6033
|
+
];
|
|
6034
|
+
let body = null;
|
|
6035
|
+
for (const candidate of candidatePaths) {
|
|
6036
|
+
try {
|
|
6037
|
+
body = await readFile10(candidate, "utf8");
|
|
6038
|
+
break;
|
|
6039
|
+
} catch {
|
|
6040
|
+
}
|
|
6041
|
+
}
|
|
6042
|
+
if (body === null) return { status: "ok", unknownRefs: [] };
|
|
6043
|
+
const chainSection = extractMarkdownSectionBody(body, "S_CHAIN");
|
|
6044
|
+
if (chainSection === null) return { status: "ok", unknownRefs: [] };
|
|
6045
|
+
const refs = /* @__PURE__ */ new Set();
|
|
6046
|
+
const tokenRe = /`(fabric-[a-z]+)`/gu;
|
|
6047
|
+
let match;
|
|
6048
|
+
while ((match = tokenRe.exec(chainSection)) !== null) {
|
|
6049
|
+
refs.add(match[1]);
|
|
6050
|
+
}
|
|
6051
|
+
const unknownRefs = [...refs].filter((slug) => !ROUTER_VALID_LEAF_SLUGS.has(slug)).sort();
|
|
6052
|
+
return unknownRefs.length === 0 ? { status: "ok", unknownRefs: [] } : { status: "drift", unknownRefs };
|
|
6053
|
+
}
|
|
6054
|
+
function createRouterChainRefCheck(t, inspection) {
|
|
6055
|
+
if (inspection.status === "ok") {
|
|
6056
|
+
return okCheck(t("doctor.check.router_chain_ref.name"), t("doctor.check.router_chain_ref.ok"));
|
|
6057
|
+
}
|
|
6058
|
+
const count = inspection.unknownRefs.length;
|
|
6059
|
+
return issueCheck(
|
|
6060
|
+
t("doctor.check.router_chain_ref.name"),
|
|
6061
|
+
"warn",
|
|
6062
|
+
"warning",
|
|
6063
|
+
"router_chain_ref_drift",
|
|
6064
|
+
t(`doctor.check.router_chain_ref.message.${count === 1 ? "singular" : "plural"}`, {
|
|
6065
|
+
count: String(count),
|
|
6066
|
+
list: inspection.unknownRefs.join(", ")
|
|
6067
|
+
}),
|
|
6068
|
+
t("doctor.check.router_chain_ref.remediation"),
|
|
6069
|
+
"maintainer"
|
|
6070
|
+
);
|
|
6071
|
+
}
|
|
5847
6072
|
function createSkillRefMirrorCheck(t, inspection) {
|
|
5848
6073
|
if (inspection.status === "ok") {
|
|
5849
6074
|
return okCheck(t("doctor.check.skill_ref_mirror.name"), t("doctor.check.skill_ref_mirror.ok"));
|
|
@@ -5920,8 +6145,8 @@ function createSkillMdYamlInvalidCheck(t, inspection) {
|
|
|
5920
6145
|
|
|
5921
6146
|
// src/services/doctor-hooks-lints.ts
|
|
5922
6147
|
import { constants } from "fs";
|
|
5923
|
-
import { access, readdir as
|
|
5924
|
-
import { join as
|
|
6148
|
+
import { access, readdir as readdir4, readFile as readFile11, stat as stat3 } from "fs/promises";
|
|
6149
|
+
import { join as join18, posix as posix3 } from "path";
|
|
5925
6150
|
import { Script } from "vm";
|
|
5926
6151
|
var HOOKS_RUNTIME_CLIENT_DIRS = [
|
|
5927
6152
|
{ client: "claude", dir: ".claude/hooks" },
|
|
@@ -5970,7 +6195,7 @@ function isHookWiredForEvent(hooks, event, hookFile) {
|
|
|
5970
6195
|
}
|
|
5971
6196
|
async function readDirectoryFileNames(dir) {
|
|
5972
6197
|
try {
|
|
5973
|
-
return await
|
|
6198
|
+
return await readdir4(dir);
|
|
5974
6199
|
} catch {
|
|
5975
6200
|
return null;
|
|
5976
6201
|
}
|
|
@@ -5983,14 +6208,14 @@ async function isFile(absPath) {
|
|
|
5983
6208
|
}
|
|
5984
6209
|
}
|
|
5985
6210
|
async function inspectHooksWired(projectRoot) {
|
|
5986
|
-
const claudeEntries = await readDirectoryFileNames(
|
|
6211
|
+
const claudeEntries = await readDirectoryFileNames(join18(projectRoot, ".claude"));
|
|
5987
6212
|
if (claudeEntries === null) {
|
|
5988
6213
|
return { status: "skipped", missingHooks: [] };
|
|
5989
6214
|
}
|
|
5990
|
-
const settingsPath =
|
|
6215
|
+
const settingsPath = join18(projectRoot, ".claude", "settings.json");
|
|
5991
6216
|
let parsed;
|
|
5992
6217
|
try {
|
|
5993
|
-
parsed = JSON.parse(await
|
|
6218
|
+
parsed = JSON.parse(await readFile11(settingsPath, "utf8"));
|
|
5994
6219
|
} catch {
|
|
5995
6220
|
return { status: "missing-settings", missingHooks: [] };
|
|
5996
6221
|
}
|
|
@@ -6013,8 +6238,8 @@ async function inspectHooksWired(projectRoot) {
|
|
|
6013
6238
|
}
|
|
6014
6239
|
async function inspectHookCacheWritability(projectRoot) {
|
|
6015
6240
|
const relPath = posix3.join(".fabric", ".cache");
|
|
6016
|
-
const fabricDir =
|
|
6017
|
-
const cacheDir =
|
|
6241
|
+
const fabricDir = join18(projectRoot, ".fabric");
|
|
6242
|
+
const cacheDir = join18(projectRoot, ".fabric", ".cache");
|
|
6018
6243
|
try {
|
|
6019
6244
|
try {
|
|
6020
6245
|
const cacheStats = await stat3(cacheDir);
|
|
@@ -6063,12 +6288,12 @@ async function inspectHookCacheWritability(projectRoot) {
|
|
|
6063
6288
|
async function inspectHooksContentDrift(projectRoot) {
|
|
6064
6289
|
const hookFilesByBasename = /* @__PURE__ */ new Map();
|
|
6065
6290
|
for (const { client, dir } of HOOKS_RUNTIME_CLIENT_DIRS) {
|
|
6066
|
-
const absDir =
|
|
6291
|
+
const absDir = join18(projectRoot, dir);
|
|
6067
6292
|
const entries = await readDirectoryFileNames(absDir);
|
|
6068
6293
|
if (entries === null) continue;
|
|
6069
6294
|
for (const name of entries) {
|
|
6070
6295
|
if (!name.endsWith(".cjs")) continue;
|
|
6071
|
-
const abs =
|
|
6296
|
+
const abs = join18(absDir, name);
|
|
6072
6297
|
if (!await isFile(abs)) continue;
|
|
6073
6298
|
const arr = hookFilesByBasename.get(name) ?? [];
|
|
6074
6299
|
arr.push({ client, abs });
|
|
@@ -6083,7 +6308,7 @@ async function inspectHooksContentDrift(projectRoot) {
|
|
|
6083
6308
|
const hashes = [];
|
|
6084
6309
|
for (const { client, abs } of copies) {
|
|
6085
6310
|
try {
|
|
6086
|
-
const body = await
|
|
6311
|
+
const body = await readFile11(abs, "utf8");
|
|
6087
6312
|
hashes.push({ client, sha: sha256(body) });
|
|
6088
6313
|
} catch {
|
|
6089
6314
|
}
|
|
@@ -6105,18 +6330,18 @@ async function inspectHooksRuntime(projectRoot) {
|
|
|
6105
6330
|
const issues = [];
|
|
6106
6331
|
let scanned = 0;
|
|
6107
6332
|
for (const { client, dir } of HOOKS_RUNTIME_CLIENT_DIRS) {
|
|
6108
|
-
const absDir =
|
|
6333
|
+
const absDir = join18(projectRoot, dir);
|
|
6109
6334
|
const entries = await readDirectoryFileNames(absDir);
|
|
6110
6335
|
if (entries === null) continue;
|
|
6111
6336
|
for (const name of entries) {
|
|
6112
6337
|
if (!name.endsWith(".cjs")) continue;
|
|
6113
|
-
const abs =
|
|
6338
|
+
const abs = join18(absDir, name);
|
|
6114
6339
|
const displayPath = `${dir}/${name}`;
|
|
6115
6340
|
if (!await isFile(abs)) continue;
|
|
6116
6341
|
scanned += 1;
|
|
6117
6342
|
let body;
|
|
6118
6343
|
try {
|
|
6119
|
-
body = await
|
|
6344
|
+
body = await readFile11(abs, "utf8");
|
|
6120
6345
|
} catch (err) {
|
|
6121
6346
|
issues.push({
|
|
6122
6347
|
path: displayPath,
|
|
@@ -6253,8 +6478,8 @@ function createHookCacheWritabilityCheck(t, inspection) {
|
|
|
6253
6478
|
}
|
|
6254
6479
|
|
|
6255
6480
|
// src/services/doctor-bootstrap-lints.ts
|
|
6256
|
-
import { access as access2, readFile as
|
|
6257
|
-
import { join as
|
|
6481
|
+
import { access as access2, readFile as readFile12 } from "fs/promises";
|
|
6482
|
+
import { join as join19 } from "path";
|
|
6258
6483
|
import {
|
|
6259
6484
|
BOOTSTRAP_MARKER_BEGIN,
|
|
6260
6485
|
BOOTSTRAP_MARKER_END,
|
|
@@ -6286,17 +6511,17 @@ async function fileExists(path) {
|
|
|
6286
6511
|
}
|
|
6287
6512
|
async function inspectBootstrapAnchor(projectRoot) {
|
|
6288
6513
|
const [hasAgentsMd, hasClaudeMd] = await Promise.all([
|
|
6289
|
-
fileExists(
|
|
6290
|
-
fileExists(
|
|
6514
|
+
fileExists(join19(projectRoot, "AGENTS.md")),
|
|
6515
|
+
fileExists(join19(projectRoot, "CLAUDE.md"))
|
|
6291
6516
|
]);
|
|
6292
6517
|
return { hasAgentsMd, hasClaudeMd };
|
|
6293
6518
|
}
|
|
6294
6519
|
async function inspectL1BootstrapSnapshotDrift(target) {
|
|
6295
|
-
const abs =
|
|
6520
|
+
const abs = join19(target, ".fabric", "AGENTS.md");
|
|
6296
6521
|
const canonical = resolveBootstrapCanonical();
|
|
6297
6522
|
let onDisk;
|
|
6298
6523
|
try {
|
|
6299
|
-
onDisk = await
|
|
6524
|
+
onDisk = await readFile12(abs, "utf8");
|
|
6300
6525
|
} catch {
|
|
6301
6526
|
return { status: "missing", canonical, onDisk: null };
|
|
6302
6527
|
}
|
|
@@ -6322,17 +6547,17 @@ function createL1BootstrapSnapshotDriftCheck(t, inspection) {
|
|
|
6322
6547
|
);
|
|
6323
6548
|
}
|
|
6324
6549
|
async function inspectL2ManagedBlockDrift(target) {
|
|
6325
|
-
const snapshotPath =
|
|
6550
|
+
const snapshotPath = join19(target, ".fabric", "AGENTS.md");
|
|
6326
6551
|
let snapshot;
|
|
6327
6552
|
try {
|
|
6328
|
-
snapshot = await
|
|
6553
|
+
snapshot = await readFile12(snapshotPath, "utf8");
|
|
6329
6554
|
} catch {
|
|
6330
6555
|
return { status: "ok", drifted: [] };
|
|
6331
6556
|
}
|
|
6332
|
-
const projectRulesPath =
|
|
6557
|
+
const projectRulesPath = join19(target, ".fabric", "project-rules.md");
|
|
6333
6558
|
let expectedBody = snapshot;
|
|
6334
6559
|
try {
|
|
6335
|
-
const projectRules = await
|
|
6560
|
+
const projectRules = await readFile12(projectRulesPath, "utf8");
|
|
6336
6561
|
expectedBody = `${snapshot}
|
|
6337
6562
|
---
|
|
6338
6563
|
${projectRules}`;
|
|
@@ -6341,12 +6566,12 @@ ${projectRules}`;
|
|
|
6341
6566
|
const drifted = [];
|
|
6342
6567
|
let anyManagedBlockFound = false;
|
|
6343
6568
|
const blockTargets = [
|
|
6344
|
-
|
|
6569
|
+
join19(target, "AGENTS.md")
|
|
6345
6570
|
];
|
|
6346
6571
|
for (const abs of blockTargets) {
|
|
6347
6572
|
let content;
|
|
6348
6573
|
try {
|
|
6349
|
-
content = await
|
|
6574
|
+
content = await readFile12(abs, "utf8");
|
|
6350
6575
|
} catch {
|
|
6351
6576
|
continue;
|
|
6352
6577
|
}
|
|
@@ -6369,9 +6594,9 @@ ${projectRules}`;
|
|
|
6369
6594
|
drifted.push({ path: abs, expected: expectedBody, actual: body });
|
|
6370
6595
|
}
|
|
6371
6596
|
}
|
|
6372
|
-
const claudeMdPath =
|
|
6597
|
+
const claudeMdPath = join19(target, "CLAUDE.md");
|
|
6373
6598
|
try {
|
|
6374
|
-
const claudeContent = await
|
|
6599
|
+
const claudeContent = await readFile12(claudeMdPath, "utf8");
|
|
6375
6600
|
anyManagedBlockFound = true;
|
|
6376
6601
|
const lines = claudeContent.split(/\r?\n/u);
|
|
6377
6602
|
const hasAtImport = lines.some((line) => line.trim() === "@.fabric/AGENTS.md");
|
|
@@ -6439,7 +6664,7 @@ import { appendFile as appendFile3 } from "fs/promises";
|
|
|
6439
6664
|
import { minimatch as minimatch2 } from "minimatch";
|
|
6440
6665
|
|
|
6441
6666
|
// src/services/cite-rollup.ts
|
|
6442
|
-
import { readFile as
|
|
6667
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
6443
6668
|
import { createLedgerWriteQueue as createLedgerWriteQueue3 } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
6444
6669
|
var citeRollupQueue = createLedgerWriteQueue3();
|
|
6445
6670
|
async function appendCiteRollupRow(projectRoot, row) {
|
|
@@ -6451,7 +6676,7 @@ async function readCiteRollup(projectRoot) {
|
|
|
6451
6676
|
const path = getCiteRollupPath(projectRoot);
|
|
6452
6677
|
let raw;
|
|
6453
6678
|
try {
|
|
6454
|
-
raw = await
|
|
6679
|
+
raw = await readFile13(path, "utf8");
|
|
6455
6680
|
} catch (error) {
|
|
6456
6681
|
if (isNodeError(error) && error.code === "ENOENT") return [];
|
|
6457
6682
|
throw error;
|
|
@@ -7565,6 +7790,7 @@ async function runDoctorReport(target) {
|
|
|
7565
7790
|
const hookCacheWritability = await inspectHookCacheWritability(projectRoot);
|
|
7566
7791
|
const staleServeLock = inspectStaleServeLock(projectRoot, lintNow);
|
|
7567
7792
|
const skillMdYamlInvalid = await inspectSkillMdYamlInvalid(projectRoot);
|
|
7793
|
+
const routerChainRef = await inspectRouterChainRef(projectRoot);
|
|
7568
7794
|
const onboardCoverage = await inspectOnboardCoverage(projectRoot);
|
|
7569
7795
|
const [hooksWired, hooksRuntime, hooksContentDrift] = await Promise.all([
|
|
7570
7796
|
inspectHooksWired(projectRoot),
|
|
@@ -7575,7 +7801,7 @@ async function runDoctorReport(target) {
|
|
|
7575
7801
|
const globalCliVersion = process.env.VITEST === "true" ? { status: "ok", version: "test-skipped" } : inspectGlobalCliVersion();
|
|
7576
7802
|
const targetFiles = Object.fromEntries(
|
|
7577
7803
|
await Promise.all(
|
|
7578
|
-
TARGET_FILE_PATHS.map(async (path) => [path, await pathExists(
|
|
7804
|
+
TARGET_FILE_PATHS.map(async (path) => [path, await pathExists(join20(projectRoot, path))])
|
|
7579
7805
|
)
|
|
7580
7806
|
);
|
|
7581
7807
|
const checks = [
|
|
@@ -7641,6 +7867,9 @@ async function runDoctorReport(target) {
|
|
|
7641
7867
|
// rc.12 lint #29: skill_md_yaml_invalid. Warning kind — surfaces
|
|
7642
7868
|
// SKILL.md frontmatter that Codex CLI silently drops at load.
|
|
7643
7869
|
createSkillMdYamlInvalidCheck(t, skillMdYamlInvalid),
|
|
7870
|
+
// B2 skill-router (A4): S_CHAIN reference backstop. Warning kind — flags an
|
|
7871
|
+
// S_CHAIN `fabric-*` reference to a leaf no longer in the install set.
|
|
7872
|
+
createRouterChainRefCheck(t, routerChainRef),
|
|
7644
7873
|
// v2.0.0-rc.23 TASK-014 (F8c): Onboard coverage advisory. Info kind.
|
|
7645
7874
|
// Surfaces uncovered S5 onboard slots and recommends /fabric-archive
|
|
7646
7875
|
// first-run phase. Sits adjacent to Skill markdown YAML — both are
|
|
@@ -7772,7 +8001,7 @@ async function runDoctorFix(target) {
|
|
|
7772
8001
|
const fixed = [];
|
|
7773
8002
|
const ledgerWarnings = [];
|
|
7774
8003
|
if (before.fixable_errors.some((issue) => issue.code === "bootstrap_snapshot_drift")) {
|
|
7775
|
-
const snapshotPath =
|
|
8004
|
+
const snapshotPath = join20(projectRoot, ".fabric", "AGENTS.md");
|
|
7776
8005
|
await ensureParentDirectory(snapshotPath);
|
|
7777
8006
|
await atomicWriteText4(snapshotPath, resolveBootstrapCanonical2());
|
|
7778
8007
|
fixed.push(findIssue(before.fixable_errors, "bootstrap_snapshot_drift"));
|
|
@@ -7845,9 +8074,9 @@ async function runDoctorFix(target) {
|
|
|
7845
8074
|
if (before.infos.some((issue) => issue.code === "stale_serve_lock")) {
|
|
7846
8075
|
const lockInspection = inspectStaleServeLock(projectRoot, Date.now());
|
|
7847
8076
|
if (lockInspection.present && !lockInspection.pidAlive) {
|
|
7848
|
-
const lockFilePath =
|
|
8077
|
+
const lockFilePath = join20(projectRoot, ".fabric", ".serve.lock");
|
|
7849
8078
|
try {
|
|
7850
|
-
await
|
|
8079
|
+
await unlink4(lockFilePath);
|
|
7851
8080
|
} catch (err) {
|
|
7852
8081
|
const errno = err;
|
|
7853
8082
|
if (errno.code !== "ENOENT") throw err;
|
|
@@ -7962,10 +8191,10 @@ function createApplyLintMessage(succeeded, failed, manualErrorCount) {
|
|
|
7962
8191
|
}
|
|
7963
8192
|
async function applySessionHintsStaleCleanup(projectRoot, candidate) {
|
|
7964
8193
|
const detail = `deleted (${candidate.age_days}d old)`;
|
|
7965
|
-
const absPath =
|
|
8194
|
+
const absPath = join20(projectRoot, candidate.path);
|
|
7966
8195
|
try {
|
|
7967
|
-
const { unlink:
|
|
7968
|
-
await
|
|
8196
|
+
const { unlink: unlink5 } = await import("fs/promises");
|
|
8197
|
+
await unlink5(absPath);
|
|
7969
8198
|
return {
|
|
7970
8199
|
kind: "knowledge_session_hints_stale_cleanup",
|
|
7971
8200
|
path: candidate.path,
|
|
@@ -7987,9 +8216,9 @@ function truncateErrorMessage(error) {
|
|
|
7987
8216
|
return raw.length > 240 ? `${raw.slice(0, 237)}...` : raw;
|
|
7988
8217
|
}
|
|
7989
8218
|
async function inspectForensic(projectRoot) {
|
|
7990
|
-
const path =
|
|
8219
|
+
const path = join20(projectRoot, ".fabric", "forensic.json");
|
|
7991
8220
|
try {
|
|
7992
|
-
const parsed = forensicReportSchema.parse(JSON.parse(await
|
|
8221
|
+
const parsed = forensicReportSchema.parse(JSON.parse(await readFile15(path, "utf8")));
|
|
7993
8222
|
return { present: true, valid: true, report: parsed };
|
|
7994
8223
|
} catch (error) {
|
|
7995
8224
|
if (isMissingFileError(error)) {
|
|
@@ -8019,7 +8248,7 @@ async function inspectEventLedger(projectRoot) {
|
|
|
8019
8248
|
try {
|
|
8020
8249
|
await access4(path, constants2.W_OK);
|
|
8021
8250
|
const { warnings } = await readEventLedger(projectRoot);
|
|
8022
|
-
const raw = await
|
|
8251
|
+
const raw = await readFile15(path, "utf8");
|
|
8023
8252
|
const invalidLine = raw.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).find((line) => !isValidJsonLine(line));
|
|
8024
8253
|
const partialWarning = warnings.find((w) => w.kind === "partial_write_at_tail");
|
|
8025
8254
|
const schemaVersionSamples = [];
|
|
@@ -8564,7 +8793,7 @@ async function inspectPreexistingRootFiles(projectRoot) {
|
|
|
8564
8793
|
const candidates = ["CLAUDE.md", "AGENTS.md"];
|
|
8565
8794
|
const detected = [];
|
|
8566
8795
|
for (const name of candidates) {
|
|
8567
|
-
if (await pathExists(
|
|
8796
|
+
if (await pathExists(join20(projectRoot, name))) {
|
|
8568
8797
|
detected.push(name);
|
|
8569
8798
|
}
|
|
8570
8799
|
}
|
|
@@ -8649,7 +8878,7 @@ async function buildLastActiveIndex(projectRoot) {
|
|
|
8649
8878
|
return map;
|
|
8650
8879
|
}
|
|
8651
8880
|
async function inspectSessionHintsStale(projectRoot, now) {
|
|
8652
|
-
const cacheDir =
|
|
8881
|
+
const cacheDir = join20(projectRoot, ".fabric", ".cache");
|
|
8653
8882
|
let entries;
|
|
8654
8883
|
try {
|
|
8655
8884
|
entries = await readdirAsync(cacheDir, { withFileTypes: true });
|
|
@@ -8661,7 +8890,7 @@ async function inspectSessionHintsStale(projectRoot, now) {
|
|
|
8661
8890
|
if (!entry.isFile()) continue;
|
|
8662
8891
|
if (!entry.name.startsWith(SESSION_HINTS_FILE_PREFIX)) continue;
|
|
8663
8892
|
if (!entry.name.endsWith(SESSION_HINTS_FILE_SUFFIX)) continue;
|
|
8664
|
-
const absPath =
|
|
8893
|
+
const absPath = join20(cacheDir, entry.name);
|
|
8665
8894
|
let mtimeMs = 0;
|
|
8666
8895
|
try {
|
|
8667
8896
|
mtimeMs = (await statAsync(absPath)).mtimeMs;
|
|
@@ -8693,9 +8922,9 @@ function inspectStaleServeLock(projectRoot, now) {
|
|
|
8693
8922
|
};
|
|
8694
8923
|
}
|
|
8695
8924
|
async function readUnderseedThresholdFromConfig(projectRoot) {
|
|
8696
|
-
const configPath =
|
|
8925
|
+
const configPath = join20(projectRoot, ".fabric", "fabric-config.json");
|
|
8697
8926
|
try {
|
|
8698
|
-
const raw = await
|
|
8927
|
+
const raw = await readFile15(configPath, "utf8");
|
|
8699
8928
|
const parsed = JSON.parse(raw);
|
|
8700
8929
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
8701
8930
|
const v = parsed.underseed_node_threshold;
|
|
@@ -8811,10 +9040,10 @@ async function inspectOnboardCoverage(projectRoot) {
|
|
|
8811
9040
|
return { filled, missing, opted_out: optedOut };
|
|
8812
9041
|
}
|
|
8813
9042
|
async function readOnboardOptedOut(projectRoot) {
|
|
8814
|
-
const path =
|
|
9043
|
+
const path = join20(projectRoot, ".fabric", "fabric-config.json");
|
|
8815
9044
|
let raw;
|
|
8816
9045
|
try {
|
|
8817
|
-
raw = await
|
|
9046
|
+
raw = await readFile15(path, "utf8");
|
|
8818
9047
|
} catch {
|
|
8819
9048
|
return [];
|
|
8820
9049
|
}
|
|
@@ -8916,22 +9145,22 @@ async function* iterateCanonicalFilenames(projectRoot) {
|
|
|
8916
9145
|
}
|
|
8917
9146
|
}
|
|
8918
9147
|
async function rewriteThreeEndManagedBlocks(projectRoot) {
|
|
8919
|
-
const snapshotPath =
|
|
9148
|
+
const snapshotPath = join20(projectRoot, ".fabric", "AGENTS.md");
|
|
8920
9149
|
if (!await pathExists(snapshotPath)) {
|
|
8921
9150
|
return;
|
|
8922
9151
|
}
|
|
8923
9152
|
let snapshot;
|
|
8924
9153
|
try {
|
|
8925
|
-
snapshot = await
|
|
9154
|
+
snapshot = await readFile15(snapshotPath, "utf8");
|
|
8926
9155
|
} catch {
|
|
8927
9156
|
return;
|
|
8928
9157
|
}
|
|
8929
|
-
const projectRulesPath =
|
|
9158
|
+
const projectRulesPath = join20(projectRoot, ".fabric", "project-rules.md");
|
|
8930
9159
|
const hasProjectRules = await pathExists(projectRulesPath);
|
|
8931
9160
|
let expectedBody = snapshot;
|
|
8932
9161
|
if (hasProjectRules) {
|
|
8933
9162
|
try {
|
|
8934
|
-
const projectRules = await
|
|
9163
|
+
const projectRules = await readFile15(projectRulesPath, "utf8");
|
|
8935
9164
|
expectedBody = `${snapshot}
|
|
8936
9165
|
---
|
|
8937
9166
|
${projectRules}`;
|
|
@@ -8942,7 +9171,7 @@ ${projectRules}`;
|
|
|
8942
9171
|
${expectedBody}
|
|
8943
9172
|
${BOOTSTRAP_MARKER_END2}`;
|
|
8944
9173
|
const blockTargets = [
|
|
8945
|
-
|
|
9174
|
+
join20(projectRoot, "AGENTS.md")
|
|
8946
9175
|
];
|
|
8947
9176
|
for (const abs of blockTargets) {
|
|
8948
9177
|
if (!await pathExists(abs)) {
|
|
@@ -8950,7 +9179,7 @@ ${BOOTSTRAP_MARKER_END2}`;
|
|
|
8950
9179
|
}
|
|
8951
9180
|
let existing;
|
|
8952
9181
|
try {
|
|
8953
|
-
existing = await
|
|
9182
|
+
existing = await readFile15(abs, "utf8");
|
|
8954
9183
|
} catch {
|
|
8955
9184
|
continue;
|
|
8956
9185
|
}
|
|
@@ -8975,11 +9204,11 @@ ${managedBlock}
|
|
|
8975
9204
|
}
|
|
8976
9205
|
await atomicWriteText4(abs, next);
|
|
8977
9206
|
}
|
|
8978
|
-
const claudeMdPath =
|
|
9207
|
+
const claudeMdPath = join20(projectRoot, "CLAUDE.md");
|
|
8979
9208
|
if (await pathExists(claudeMdPath)) {
|
|
8980
9209
|
let claudeContent;
|
|
8981
9210
|
try {
|
|
8982
|
-
claudeContent = await
|
|
9211
|
+
claudeContent = await readFile15(claudeMdPath, "utf8");
|
|
8983
9212
|
} catch {
|
|
8984
9213
|
return;
|
|
8985
9214
|
}
|
|
@@ -9045,7 +9274,7 @@ async function collectEntryPoints(root) {
|
|
|
9045
9274
|
continue;
|
|
9046
9275
|
}
|
|
9047
9276
|
for (const entry of await readdirAsync(current, { withFileTypes: true })) {
|
|
9048
|
-
const absolutePath =
|
|
9277
|
+
const absolutePath = join20(current, entry.name);
|
|
9049
9278
|
const relativePath = normalizePath2(absolutePath.slice(root.length + 1));
|
|
9050
9279
|
if (relativePath.length === 0) {
|
|
9051
9280
|
continue;
|
|
@@ -9121,7 +9350,7 @@ async function enrichDescriptions(projectRoot, opts = {}) {
|
|
|
9121
9350
|
scanned += 1;
|
|
9122
9351
|
let source;
|
|
9123
9352
|
try {
|
|
9124
|
-
source = await
|
|
9353
|
+
source = await readFile15(absPath, "utf8");
|
|
9125
9354
|
} catch {
|
|
9126
9355
|
continue;
|
|
9127
9356
|
}
|
|
@@ -9340,6 +9569,25 @@ async function runDoctorConflictLint(projectRoot, opts = {}) {
|
|
|
9340
9569
|
};
|
|
9341
9570
|
}
|
|
9342
9571
|
|
|
9572
|
+
// src/services/summary-cold-eval.ts
|
|
9573
|
+
var COLD_EVAL_RUBRIC = [
|
|
9574
|
+
"You are a ZERO-CONTEXT judge. You are shown ONLY a one-line knowledge summary \u2014",
|
|
9575
|
+
"never the full entry body. For each summary decide: could a reader who has NOT",
|
|
9576
|
+
"seen the body ACT on this line alone (apply the decision / avoid the pitfall /",
|
|
9577
|
+
"follow the rule)?",
|
|
9578
|
+
"",
|
|
9579
|
+
"PASS (self_sufficient=true): the line states the thesis \u2014 the what + the",
|
|
9580
|
+
"operative so-what. FAIL (self_sufficient=false): the line only POINTS at the",
|
|
9581
|
+
"body ('explains the approach', 'covers the edge cases') without stating it.",
|
|
9582
|
+
"When you FAIL one, return a suggested_summary that states the thesis in one line."
|
|
9583
|
+
].join("\n");
|
|
9584
|
+
function buildColdEvalBatch(candidates) {
|
|
9585
|
+
const judgeable = candidates.filter(
|
|
9586
|
+
(c) => typeof c.summary === "string" && c.summary.trim().length > 0
|
|
9587
|
+
);
|
|
9588
|
+
return { rubric: COLD_EVAL_RUBRIC, candidates: judgeable };
|
|
9589
|
+
}
|
|
9590
|
+
|
|
9343
9591
|
// src/services/rotation-tick.ts
|
|
9344
9592
|
var DEFAULT_TICK_INTERVAL_MS = 6 * 60 * 60 * 1e3;
|
|
9345
9593
|
var tickTimers = /* @__PURE__ */ new Map();
|
|
@@ -9375,7 +9623,7 @@ import { IOFabricError as IOFabricError2, RuleError } from "@fenglimg/fabric-sha
|
|
|
9375
9623
|
|
|
9376
9624
|
// src/services/read-ledger.ts
|
|
9377
9625
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
9378
|
-
import { access as access5, copyFile, readFile as
|
|
9626
|
+
import { access as access5, copyFile, readFile as readFile16, rm } from "fs/promises";
|
|
9379
9627
|
import { ledgerEntrySchema } from "@fenglimg/fabric-shared";
|
|
9380
9628
|
async function resolveLedgerPaths(projectRoot) {
|
|
9381
9629
|
const primaryPath = getLedgerPath(projectRoot);
|
|
@@ -9403,7 +9651,7 @@ async function readLegacyLedger(projectRoot) {
|
|
|
9403
9651
|
const { readPath } = await resolveLedgerPaths(projectRoot);
|
|
9404
9652
|
let raw;
|
|
9405
9653
|
try {
|
|
9406
|
-
raw = await
|
|
9654
|
+
raw = await readFile16(readPath, "utf8");
|
|
9407
9655
|
} catch (error) {
|
|
9408
9656
|
if (isNodeError(error) && error.code === "ENOENT") {
|
|
9409
9657
|
return [];
|
|
@@ -9658,8 +9906,8 @@ function formatError(error) {
|
|
|
9658
9906
|
}
|
|
9659
9907
|
function formatPreexistingRootMessage(projectRoot) {
|
|
9660
9908
|
const preexisting = [];
|
|
9661
|
-
if (
|
|
9662
|
-
if (
|
|
9909
|
+
if (existsSync9(join21(projectRoot, "CLAUDE.md"))) preexisting.push("CLAUDE.md");
|
|
9910
|
+
if (existsSync9(join21(projectRoot, "AGENTS.md"))) preexisting.push("AGENTS.md");
|
|
9663
9911
|
if (preexisting.length === 0) return null;
|
|
9664
9912
|
return `[startup] info: detected ${preexisting.join(", ")} at project root. Note: Fabric serves knowledge from mounted stores via MCP \u2014 root markdown files are not auto-loaded into the AI context.`;
|
|
9665
9913
|
}
|
|
@@ -9685,7 +9933,7 @@ function createFabricServer(tracker) {
|
|
|
9685
9933
|
const server = new McpServer(
|
|
9686
9934
|
{
|
|
9687
9935
|
name: "fabric-knowledge-server",
|
|
9688
|
-
version: "2.2.0
|
|
9936
|
+
version: "2.2.0"
|
|
9689
9937
|
},
|
|
9690
9938
|
{
|
|
9691
9939
|
instructions: FABRIC_SERVER_INSTRUCTIONS
|
|
@@ -9704,10 +9952,10 @@ function createFabricServer(tracker) {
|
|
|
9704
9952
|
},
|
|
9705
9953
|
async (_uri) => {
|
|
9706
9954
|
const projectRoot = process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
|
|
9707
|
-
const path =
|
|
9955
|
+
const path = join21(projectRoot, ".fabric", "bootstrap", "README.md");
|
|
9708
9956
|
let text = "";
|
|
9709
|
-
if (
|
|
9710
|
-
text = await
|
|
9957
|
+
if (existsSync9(path)) {
|
|
9958
|
+
text = await readFile17(path, "utf8");
|
|
9711
9959
|
}
|
|
9712
9960
|
return {
|
|
9713
9961
|
contents: [
|
|
@@ -9797,6 +10045,7 @@ if (isMainModule) {
|
|
|
9797
10045
|
}
|
|
9798
10046
|
export {
|
|
9799
10047
|
AGENTS_MD_RESOURCE_URI,
|
|
10048
|
+
COLD_EVAL_RUBRIC,
|
|
9800
10049
|
DEFAULT_CONFLICT_SIMILARITY_THRESHOLD,
|
|
9801
10050
|
EVENT_LEDGER_PATH,
|
|
9802
10051
|
FABRIC_SERVER_INSTRUCTIONS,
|
|
@@ -9806,6 +10055,7 @@ export {
|
|
|
9806
10055
|
METRIC_COUNTER_NAMES,
|
|
9807
10056
|
appendEventLedgerEvent,
|
|
9808
10057
|
buildAlwaysActiveBodies,
|
|
10058
|
+
buildColdEvalBatch,
|
|
9809
10059
|
buildKnowledgeCensus,
|
|
9810
10060
|
bumpCounter,
|
|
9811
10061
|
contextCache,
|