@fenglimg/fabric-server 2.2.0-rc.1 → 2.2.0-rc.3
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 +152 -2
- package/dist/index.js +1039 -340
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { existsSync as existsSync9 } from "fs";
|
|
3
|
-
import { readFile as
|
|
4
|
-
import { join as
|
|
3
|
+
import { readFile as readFile16 } from "fs/promises";
|
|
4
|
+
import { join as join14, resolve as resolve5 } 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";
|
|
@@ -162,7 +162,11 @@ import {
|
|
|
162
162
|
eventLedgerEventSchema,
|
|
163
163
|
redactSecrets
|
|
164
164
|
} from "@fenglimg/fabric-shared";
|
|
165
|
-
import {
|
|
165
|
+
import {
|
|
166
|
+
atomicWriteText as atomicWriteText2,
|
|
167
|
+
createLedgerWriteQueue,
|
|
168
|
+
withFileLock
|
|
169
|
+
} from "@fenglimg/fabric-shared/node/atomic-write";
|
|
166
170
|
|
|
167
171
|
// src/services/_shared.ts
|
|
168
172
|
import { dirname, join as join2, resolve, sep } from "path";
|
|
@@ -214,6 +218,15 @@ function isNodeError(error) {
|
|
|
214
218
|
|
|
215
219
|
// src/services/event-ledger.ts
|
|
216
220
|
var ledgerQueue = createLedgerWriteQueue();
|
|
221
|
+
function eventLedgerLockPath(eventPath) {
|
|
222
|
+
return `${eventPath}.lock`;
|
|
223
|
+
}
|
|
224
|
+
function exclusiveLedgerWrite(eventPath, fn) {
|
|
225
|
+
return withFileLock(
|
|
226
|
+
eventLedgerLockPath(eventPath),
|
|
227
|
+
() => ledgerQueue.runExclusive(eventPath, fn)
|
|
228
|
+
);
|
|
229
|
+
}
|
|
217
230
|
var __eventLedgerParseStats = { lineParses: 0 };
|
|
218
231
|
var EVENT_LEDGER_DEFAULT_RETENTION_DAYS = 30;
|
|
219
232
|
var EVENT_LEDGER_SIZE_WARN_BYTES = 50 * 1024 * 1024;
|
|
@@ -317,7 +330,10 @@ async function appendEventLedgerEvent(projectRoot, event) {
|
|
|
317
330
|
schema_version: 1
|
|
318
331
|
});
|
|
319
332
|
await ensureParentDirectory(eventPath);
|
|
320
|
-
await
|
|
333
|
+
await withFileLock(
|
|
334
|
+
eventLedgerLockPath(eventPath),
|
|
335
|
+
() => ledgerQueue.append(eventPath, JSON.stringify(nextEvent))
|
|
336
|
+
);
|
|
321
337
|
try {
|
|
322
338
|
const size = statSync(eventPath).size;
|
|
323
339
|
if (size > EVENT_LEDGER_SIZE_WARN_BYTES) {
|
|
@@ -385,24 +401,26 @@ async function readEventLedger(projectRoot, options = {}) {
|
|
|
385
401
|
return { events: filtered, warnings };
|
|
386
402
|
}
|
|
387
403
|
async function truncateLedgerToLastNewline(path2) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
404
|
+
return withFileLock(eventLedgerLockPath(path2), async () => {
|
|
405
|
+
const raw = await readFile2(path2);
|
|
406
|
+
const content = raw.toString("utf8");
|
|
407
|
+
if (content.endsWith("\n") || content.length === 0) {
|
|
408
|
+
return { truncated_bytes: 0, corrupted_path: "" };
|
|
409
|
+
}
|
|
410
|
+
const lastNewlineIndex = content.lastIndexOf("\n");
|
|
411
|
+
if (lastNewlineIndex === -1) {
|
|
412
|
+
const corruptedPath2 = `${path2}.corrupted.${Date.now()}`;
|
|
413
|
+
await writeFile(corruptedPath2, raw);
|
|
414
|
+
await truncate(path2, 0);
|
|
415
|
+
return { truncated_bytes: raw.length, corrupted_path: corruptedPath2 };
|
|
416
|
+
}
|
|
417
|
+
const keepByteLength = Buffer.byteLength(content.slice(0, lastNewlineIndex + 1), "utf8");
|
|
418
|
+
const corruptedBytes = raw.slice(keepByteLength);
|
|
419
|
+
const corruptedPath = `${path2}.corrupted.${Date.now()}`;
|
|
420
|
+
await writeFile(corruptedPath, corruptedBytes);
|
|
421
|
+
await truncate(path2, keepByteLength);
|
|
422
|
+
return { truncated_bytes: corruptedBytes.length, corrupted_path: corruptedPath };
|
|
423
|
+
});
|
|
406
424
|
}
|
|
407
425
|
function parseEventLedgerLine(line, index) {
|
|
408
426
|
try {
|
|
@@ -427,7 +445,7 @@ function isNodeError2(error) {
|
|
|
427
445
|
}
|
|
428
446
|
async function rotateEventLedgerIfNeeded(projectRoot, opts = {}) {
|
|
429
447
|
const eventPath = getEventLedgerPath(projectRoot);
|
|
430
|
-
return
|
|
448
|
+
return exclusiveLedgerWrite(eventPath, async () => {
|
|
431
449
|
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
432
450
|
const retentionDays = resolveRetentionDays(projectRoot, opts.retentionDays);
|
|
433
451
|
const cutoffMs = now.getTime() - retentionDays * 864e5;
|
|
@@ -523,7 +541,7 @@ async function rotateEventLedgerIfNeeded(projectRoot, opts = {}) {
|
|
|
523
541
|
}
|
|
524
542
|
async function dropEventsFromLedger(projectRoot, opts) {
|
|
525
543
|
const eventPath = getEventLedgerPath(projectRoot);
|
|
526
|
-
return
|
|
544
|
+
return exclusiveLedgerWrite(eventPath, async () => {
|
|
527
545
|
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
528
546
|
let raw;
|
|
529
547
|
try {
|
|
@@ -741,7 +759,7 @@ import { join as join5 } from "path";
|
|
|
741
759
|
import { RuleValidationError } from "@fenglimg/fabric-shared/errors";
|
|
742
760
|
|
|
743
761
|
// src/services/knowledge-meta-builder.ts
|
|
744
|
-
import {
|
|
762
|
+
import { readdir, readFile as readFile3, stat } from "fs/promises";
|
|
745
763
|
import { existsSync as existsSync2, statSync as statSync2 } from "fs";
|
|
746
764
|
import { homedir } from "os";
|
|
747
765
|
import { isAbsolute, join as join4, relative, resolve as resolve2, sep as sep2 } from "path";
|
|
@@ -760,7 +778,7 @@ import {
|
|
|
760
778
|
StableIdSchema,
|
|
761
779
|
parseKnowledgeId
|
|
762
780
|
} from "@fenglimg/fabric-shared";
|
|
763
|
-
import { atomicWriteText as atomicWriteText3 } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
781
|
+
import { atomicWriteText as atomicWriteText3, withFileLock as withFileLock2 } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
764
782
|
async function loadKbIdTypeMap(projectRootInput) {
|
|
765
783
|
const projectRoot = normalizeProjectRoot(projectRootInput);
|
|
766
784
|
const metaPath = join4(projectRoot, ".fabric", "agents.meta.json");
|
|
@@ -801,28 +819,30 @@ async function writeKnowledgeMeta(projectRootInput, options) {
|
|
|
801
819
|
const projectRoot = normalizeProjectRoot(projectRootInput);
|
|
802
820
|
const metaPath = join4(projectRoot, ".fabric", "agents.meta.json");
|
|
803
821
|
const knowledgeTestIndexPath = join4(projectRoot, ".fabric", ".cache", "knowledge-test.index.json");
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
822
|
+
return withFileLock2(`${metaPath}.lock`, async () => {
|
|
823
|
+
const existingMeta = await readExistingMeta(metaPath);
|
|
824
|
+
const result = await buildKnowledgeMeta(projectRoot);
|
|
825
|
+
if (!result.changed) {
|
|
826
|
+
return result;
|
|
827
|
+
}
|
|
828
|
+
await ensureParentDirectory(metaPath);
|
|
829
|
+
await atomicWriteText3(metaPath, `${JSON.stringify(result.meta, null, 2)}
|
|
811
830
|
`);
|
|
812
|
-
|
|
813
|
-
|
|
831
|
+
await ensureParentDirectory(knowledgeTestIndexPath);
|
|
832
|
+
await atomicWriteText3(knowledgeTestIndexPath, `${JSON.stringify(result.knowledgeTestIndex, null, 2)}
|
|
814
833
|
`);
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
834
|
+
if (existingMeta === void 0 || stableStringify(existingMeta) !== stableStringify(result.meta)) {
|
|
835
|
+
await recordBaselineSynced(projectRoot, {
|
|
836
|
+
previousRevision: existingMeta?.revision,
|
|
837
|
+
revision: result.meta.revision,
|
|
838
|
+
syncedFiles: collectSyncedFiles(existingMeta, result.meta),
|
|
839
|
+
acceptedStableIds: collectStableIds(result.meta),
|
|
840
|
+
driftDetails: collectDriftDetails(existingMeta, result.meta),
|
|
841
|
+
source: options.source
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
return result;
|
|
845
|
+
});
|
|
826
846
|
}
|
|
827
847
|
var knowledgeMetaCache = /* @__PURE__ */ new Map();
|
|
828
848
|
var __knowledgeMetaCacheStats = { fileReads: 0 };
|
|
@@ -992,13 +1012,6 @@ function resolveContentRefPath(projectRoot, contentRef) {
|
|
|
992
1012
|
async function findKnowledgeFiles(projectRoot) {
|
|
993
1013
|
const teamRoot = join4(projectRoot, ".fabric", "knowledge");
|
|
994
1014
|
const personalRoot = join4(resolvePersonalRoot(), ".fabric", "knowledge");
|
|
995
|
-
try {
|
|
996
|
-
await mkdir3(personalRoot, { recursive: true });
|
|
997
|
-
for (const sub of KNOWLEDGE_SUBDIRS) {
|
|
998
|
-
await mkdir3(join4(personalRoot, sub), { recursive: true });
|
|
999
|
-
}
|
|
1000
|
-
} catch {
|
|
1001
|
-
}
|
|
1002
1015
|
const files = [];
|
|
1003
1016
|
for (const [root, prefix] of [
|
|
1004
1017
|
[teamRoot, TEAM_CONTENT_REF_PREFIX],
|
|
@@ -1390,6 +1403,16 @@ function extractDescriptionFromFrontmatter(frontmatter) {
|
|
|
1390
1403
|
related: knowledge.related
|
|
1391
1404
|
};
|
|
1392
1405
|
}
|
|
1406
|
+
function isForbiddenCrossLayerEdge(sourceLayer, targetId) {
|
|
1407
|
+
if (sourceLayer !== "team") {
|
|
1408
|
+
return false;
|
|
1409
|
+
}
|
|
1410
|
+
const decoded = parseKnowledgeId(targetId);
|
|
1411
|
+
if (decoded === null) {
|
|
1412
|
+
return false;
|
|
1413
|
+
}
|
|
1414
|
+
return decoded.layer === "personal";
|
|
1415
|
+
}
|
|
1393
1416
|
function extractKnowledgeFieldsFromFrontmatter(frontmatter) {
|
|
1394
1417
|
const rawId = extractScalar(frontmatter, "id");
|
|
1395
1418
|
const rawType = extractScalar(frontmatter, "type");
|
|
@@ -1469,7 +1492,18 @@ function extractKnowledgeFieldsFromFrontmatter(frontmatter) {
|
|
|
1469
1492
|
const rawRelevanceScope = extractScalar(frontmatter, "relevance_scope");
|
|
1470
1493
|
const relevance_scope = rawRelevanceScope === "narrow" || rawRelevanceScope === "broad" ? rawRelevanceScope : "broad";
|
|
1471
1494
|
const relevance_paths = extractInlineArray(frontmatter, "relevance_paths");
|
|
1472
|
-
const
|
|
1495
|
+
const rawRelated = extractInlineArray(frontmatter, "related");
|
|
1496
|
+
const sourceLayer = knowledge_layer ?? (id !== void 0 ? parseKnowledgeId(id)?.layer ?? "team" : "team");
|
|
1497
|
+
const related = rawRelated.filter((targetId) => {
|
|
1498
|
+
if (isForbiddenCrossLayerEdge(sourceLayer, targetId)) {
|
|
1499
|
+
process.stderr.write(
|
|
1500
|
+
`[fabric] frontmatter: stripping forbidden cross-layer related edge ${id ?? "(team entry)"} \u2192 ${targetId} (KT\u2192KP topology leak; \xA74 privacy iron law)
|
|
1501
|
+
`
|
|
1502
|
+
);
|
|
1503
|
+
return false;
|
|
1504
|
+
}
|
|
1505
|
+
return true;
|
|
1506
|
+
});
|
|
1473
1507
|
return {
|
|
1474
1508
|
id,
|
|
1475
1509
|
knowledge_type,
|
|
@@ -1934,15 +1968,36 @@ function readSelectionTokenTtlMs(projectRoot) {
|
|
|
1934
1968
|
return void 0;
|
|
1935
1969
|
}
|
|
1936
1970
|
}
|
|
1971
|
+
var DEFAULT_EMBED_MODEL = "fast-bge-small-zh-v1.5";
|
|
1972
|
+
var SUPPORTED_EMBED_MODELS = /* @__PURE__ */ new Set([
|
|
1973
|
+
"fast-bge-small-zh-v1.5",
|
|
1974
|
+
"fast-multilingual-e5-large",
|
|
1975
|
+
"fast-bge-small-en-v1.5",
|
|
1976
|
+
"fast-bge-small-en",
|
|
1977
|
+
"fast-bge-base-en-v1.5",
|
|
1978
|
+
"fast-bge-base-en",
|
|
1979
|
+
"fast-all-MiniLM-L6-v2"
|
|
1980
|
+
]);
|
|
1937
1981
|
function readEmbedConfig(projectRoot) {
|
|
1938
1982
|
try {
|
|
1939
1983
|
const config = readFabricConfig(projectRoot);
|
|
1940
1984
|
const enabled = config.embed_enabled === true;
|
|
1941
1985
|
const rawWeight = config.embed_weight;
|
|
1942
1986
|
const weight = typeof rawWeight === "number" && Number.isInteger(rawWeight) && rawWeight >= 0 && rawWeight <= 49 ? rawWeight : 30;
|
|
1943
|
-
|
|
1987
|
+
const rawModel = config.embed_model;
|
|
1988
|
+
const model = typeof rawModel === "string" && SUPPORTED_EMBED_MODELS.has(rawModel) ? rawModel : DEFAULT_EMBED_MODEL;
|
|
1989
|
+
return { enabled, weight, model };
|
|
1990
|
+
} catch {
|
|
1991
|
+
return { enabled: false, weight: 30, model: DEFAULT_EMBED_MODEL };
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
function readDefaultLayerFilter(projectRoot) {
|
|
1995
|
+
try {
|
|
1996
|
+
const config = readFabricConfig(projectRoot);
|
|
1997
|
+
const raw = config.default_layer_filter;
|
|
1998
|
+
return raw === "team" || raw === "personal" ? raw : "both";
|
|
1944
1999
|
} catch {
|
|
1945
|
-
return
|
|
2000
|
+
return "both";
|
|
1946
2001
|
}
|
|
1947
2002
|
}
|
|
1948
2003
|
function readPlanContextTopK(projectRoot) {
|
|
@@ -1968,10 +2023,10 @@ function readOrphanDemoteThresholdDays(projectRoot) {
|
|
|
1968
2023
|
}
|
|
1969
2024
|
return v;
|
|
1970
2025
|
};
|
|
1971
|
-
const
|
|
1972
|
-
if (
|
|
1973
|
-
const
|
|
1974
|
-
if (
|
|
2026
|
+
const proven = validate(cfg.orphan_demote_proven_days) ?? validate(cfg.orphan_demote_stable_days);
|
|
2027
|
+
if (proven !== void 0) out.stable = proven;
|
|
2028
|
+
const verified = validate(cfg.orphan_demote_verified_days) ?? validate(cfg.orphan_demote_endorsed_days);
|
|
2029
|
+
if (verified !== void 0) out.endorsed = verified;
|
|
1975
2030
|
const d = validate(cfg.orphan_demote_draft_days);
|
|
1976
2031
|
if (d !== void 0) out.draft = d;
|
|
1977
2032
|
return out;
|
|
@@ -1979,12 +2034,27 @@ function readOrphanDemoteThresholdDays(projectRoot) {
|
|
|
1979
2034
|
return {};
|
|
1980
2035
|
}
|
|
1981
2036
|
}
|
|
2037
|
+
function readConflictLintThreshold(projectRoot) {
|
|
2038
|
+
try {
|
|
2039
|
+
const cfgPath = join6(projectRoot, ".fabric", "fabric-config.json");
|
|
2040
|
+
if (!existsSync4(cfgPath)) return void 0;
|
|
2041
|
+
const parsed = JSON.parse(readFileSync2(cfgPath, "utf8"));
|
|
2042
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
|
|
2043
|
+
const v = parsed.conflict_lint_similarity_threshold;
|
|
2044
|
+
if (typeof v === "number" && Number.isFinite(v) && v >= 0 && v <= 1) {
|
|
2045
|
+
return v;
|
|
2046
|
+
}
|
|
2047
|
+
return void 0;
|
|
2048
|
+
} catch {
|
|
2049
|
+
return void 0;
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
1982
2052
|
|
|
1983
2053
|
// src/services/extract-knowledge.ts
|
|
1984
2054
|
import { existsSync as existsSync5 } from "fs";
|
|
1985
2055
|
import { readFile as readFile5 } from "fs/promises";
|
|
1986
2056
|
import { homedir as homedir3 } from "os";
|
|
1987
|
-
import { join as
|
|
2057
|
+
import { join as join8, relative as relative2 } from "path";
|
|
1988
2058
|
import {
|
|
1989
2059
|
PROPOSED_REASON_DESCRIPTIONS
|
|
1990
2060
|
} from "@fenglimg/fabric-shared/schemas/api-contracts";
|
|
@@ -2090,8 +2160,44 @@ async function emitAutoHealEventBestEffort(projectRoot, payload) {
|
|
|
2090
2160
|
}
|
|
2091
2161
|
}
|
|
2092
2162
|
|
|
2163
|
+
// src/services/cross-store-write.ts
|
|
2164
|
+
import { join as join7 } from "path";
|
|
2165
|
+
import {
|
|
2166
|
+
STORE_LAYOUT,
|
|
2167
|
+
STORE_PENDING_DIR,
|
|
2168
|
+
buildStoreResolveInput,
|
|
2169
|
+
createStoreResolver,
|
|
2170
|
+
resolveGlobalRoot,
|
|
2171
|
+
storeRelativePath
|
|
2172
|
+
} from "@fenglimg/fabric-shared";
|
|
2173
|
+
import { StoreWriteTargetUnresolvedError } from "@fenglimg/fabric-shared/errors";
|
|
2174
|
+
function writeTargetUnresolved(layer) {
|
|
2175
|
+
const actionHint = layer === "personal" ? "run `fabric install --global` to mint your personal store, then retry" : "mount + select a team store: `fabric install --global` then `fabric store bind <alias>` and `fabric store switch-write <alias>`, then retry";
|
|
2176
|
+
return new StoreWriteTargetUnresolvedError(
|
|
2177
|
+
`no ${layer} write-target store resolved \u2014 knowledge writes are store-only (dual-root co-location removed)`,
|
|
2178
|
+
{ actionHint, fixable: true, details: { layer } }
|
|
2179
|
+
);
|
|
2180
|
+
}
|
|
2181
|
+
function resolveWriteTargetStoreDir(layer, projectRoot) {
|
|
2182
|
+
const input = buildStoreResolveInput(projectRoot);
|
|
2183
|
+
if (input === null) {
|
|
2184
|
+
throw writeTargetUnresolved(layer);
|
|
2185
|
+
}
|
|
2186
|
+
const scope = layer === "personal" ? "personal" : "team";
|
|
2187
|
+
const { target } = createStoreResolver().resolveWriteTarget(input, scope);
|
|
2188
|
+
if (target === null) {
|
|
2189
|
+
throw writeTargetUnresolved(layer);
|
|
2190
|
+
}
|
|
2191
|
+
return join7(resolveGlobalRoot(), storeRelativePath(target.store_uuid));
|
|
2192
|
+
}
|
|
2193
|
+
function resolveStorePendingBase(layer, projectRoot) {
|
|
2194
|
+
return join7(resolveWriteTargetStoreDir(layer, projectRoot), STORE_LAYOUT.knowledgeDir, STORE_PENDING_DIR);
|
|
2195
|
+
}
|
|
2196
|
+
function resolveStoreCanonicalBase(layer, projectRoot) {
|
|
2197
|
+
return join7(resolveWriteTargetStoreDir(layer, projectRoot), STORE_LAYOUT.knowledgeDir);
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2093
2200
|
// src/services/extract-knowledge.ts
|
|
2094
|
-
var TEAM_PENDING_REL = ".fabric/knowledge/pending";
|
|
2095
2201
|
var SLUG_MAX_LENGTH = 40;
|
|
2096
2202
|
var INJECTION_PATTERNS = [
|
|
2097
2203
|
{
|
|
@@ -2160,10 +2266,7 @@ function sanitizeInjectionFields(fields) {
|
|
|
2160
2266
|
return { sanitized: out, allRedactions };
|
|
2161
2267
|
}
|
|
2162
2268
|
function pendingBase(layer, projectRoot) {
|
|
2163
|
-
|
|
2164
|
-
return join7(resolvePersonalRoot3(), ".fabric", "knowledge", "pending");
|
|
2165
|
-
}
|
|
2166
|
-
return join7(projectRoot, TEAM_PENDING_REL);
|
|
2269
|
+
return resolveStorePendingBase(layer, projectRoot);
|
|
2167
2270
|
}
|
|
2168
2271
|
function resolvePersonalRoot3() {
|
|
2169
2272
|
return process.env.FABRIC_HOME ?? homedir3();
|
|
@@ -2277,7 +2380,7 @@ async function extractKnowledge(projectRoot, input) {
|
|
|
2277
2380
|
primarySession,
|
|
2278
2381
|
baseIdempotencyKey: idempotencyKey
|
|
2279
2382
|
});
|
|
2280
|
-
const reportedPath =
|
|
2383
|
+
const reportedPath = `~/${relative2(resolvePersonalRoot3(), absolutePath)}`;
|
|
2281
2384
|
const effectiveSanitizedSlug = chosenSlug;
|
|
2282
2385
|
const effectiveIdempotencyKey = chosenKey;
|
|
2283
2386
|
await ensureParentDirectory(absolutePath);
|
|
@@ -2361,7 +2464,7 @@ var SLUG_DISAMBIGUATE_MAX_VARIANTS = 9;
|
|
|
2361
2464
|
async function resolveDisambiguatedSlugPath(args) {
|
|
2362
2465
|
for (let n = 1; n <= SLUG_DISAMBIGUATE_MAX_VARIANTS; n += 1) {
|
|
2363
2466
|
const candidateSlug = n === 1 ? args.slug : `${args.slug}-${n}`;
|
|
2364
|
-
const candidatePath =
|
|
2467
|
+
const candidatePath = join8(args.baseDir, args.type, `${candidateSlug}.md`);
|
|
2365
2468
|
const candidateKey = n === 1 ? args.baseIdempotencyKey : sha256(
|
|
2366
2469
|
JSON.stringify({
|
|
2367
2470
|
source_session: args.primarySession,
|
|
@@ -2599,11 +2702,11 @@ function readFrontmatterKey(content, key) {
|
|
|
2599
2702
|
}
|
|
2600
2703
|
for (const rawLine of block.split(/\r?\n/u)) {
|
|
2601
2704
|
const line = rawLine.trim();
|
|
2602
|
-
const
|
|
2603
|
-
if (
|
|
2604
|
-
const k = line.slice(0,
|
|
2705
|
+
const sep5 = line.indexOf(":");
|
|
2706
|
+
if (sep5 === -1) continue;
|
|
2707
|
+
const k = line.slice(0, sep5).trim();
|
|
2605
2708
|
if (k === key) {
|
|
2606
|
-
return line.slice(
|
|
2709
|
+
return line.slice(sep5 + 1).trim();
|
|
2607
2710
|
}
|
|
2608
2711
|
}
|
|
2609
2712
|
return void 0;
|
|
@@ -2674,7 +2777,7 @@ import { tokenize as tokenize2 } from "@fenglimg/fabric-shared";
|
|
|
2674
2777
|
|
|
2675
2778
|
// src/services/get-knowledge.ts
|
|
2676
2779
|
import { readFile as readFile6 } from "fs/promises";
|
|
2677
|
-
import { join as
|
|
2780
|
+
import { join as join9 } from "path";
|
|
2678
2781
|
import { deriveAgentsMetaLayer as deriveAgentsMetaLayer2 } from "@fenglimg/fabric-shared";
|
|
2679
2782
|
import { minimatch } from "minimatch";
|
|
2680
2783
|
async function getKnowledge(projectRoot, input) {
|
|
@@ -2714,7 +2817,7 @@ async function loadGetKnowledgeContext(projectRoot) {
|
|
|
2714
2817
|
return cached;
|
|
2715
2818
|
}
|
|
2716
2819
|
const meta = await readAgentsMeta(projectRoot);
|
|
2717
|
-
const l0Content = await readFile6(
|
|
2820
|
+
const l0Content = await readFile6(join9(projectRoot, ".fabric", "bootstrap", "README.md"), "utf8");
|
|
2718
2821
|
const context = {
|
|
2719
2822
|
meta,
|
|
2720
2823
|
l0Content,
|
|
@@ -2849,11 +2952,98 @@ async function readRuleContent(projectRoot, file, fileContentCache) {
|
|
|
2849
2952
|
if (cached !== void 0) {
|
|
2850
2953
|
return await cached;
|
|
2851
2954
|
}
|
|
2852
|
-
const pending = readFile6(
|
|
2955
|
+
const pending = readFile6(join9(projectRoot, file), "utf8");
|
|
2853
2956
|
fileContentCache.set(file, pending);
|
|
2854
2957
|
return await pending;
|
|
2855
2958
|
}
|
|
2856
2959
|
|
|
2960
|
+
// src/services/cross-store-recall.ts
|
|
2961
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2962
|
+
import { join as join10 } from "path";
|
|
2963
|
+
import {
|
|
2964
|
+
buildStoreResolveInput as buildStoreResolveInput2,
|
|
2965
|
+
createStoreResolver as createStoreResolver2,
|
|
2966
|
+
readKnowledgeAcrossStores,
|
|
2967
|
+
resolveGlobalRoot as resolveGlobalRoot2,
|
|
2968
|
+
storeRelativePath as storeRelativePath2
|
|
2969
|
+
} from "@fenglimg/fabric-shared";
|
|
2970
|
+
function walkReadSetStores(projectRoot) {
|
|
2971
|
+
const resolveInput = buildStoreResolveInput2(projectRoot);
|
|
2972
|
+
if (resolveInput === null) {
|
|
2973
|
+
return [];
|
|
2974
|
+
}
|
|
2975
|
+
const readSet = createStoreResolver2().resolveReadSet(resolveInput);
|
|
2976
|
+
if (readSet.stores.length === 0) {
|
|
2977
|
+
return [];
|
|
2978
|
+
}
|
|
2979
|
+
const personalUuids = new Set(
|
|
2980
|
+
resolveInput.mountedStores.filter((s) => s.personal).map((s) => s.store_uuid)
|
|
2981
|
+
);
|
|
2982
|
+
const globalRoot = resolveGlobalRoot2();
|
|
2983
|
+
const dirs = readSet.stores.map((entry) => ({
|
|
2984
|
+
store_uuid: entry.store_uuid,
|
|
2985
|
+
alias: entry.alias,
|
|
2986
|
+
dir: join10(globalRoot, storeRelativePath2(entry.store_uuid))
|
|
2987
|
+
}));
|
|
2988
|
+
const entries = [];
|
|
2989
|
+
for (const ref of readKnowledgeAcrossStores(dirs)) {
|
|
2990
|
+
let source;
|
|
2991
|
+
try {
|
|
2992
|
+
source = readFileSync3(ref.file, "utf8");
|
|
2993
|
+
} catch {
|
|
2994
|
+
continue;
|
|
2995
|
+
}
|
|
2996
|
+
const stableId = deriveRuleIdentity(ref.file, source, void 0).stableId;
|
|
2997
|
+
const layer = personalUuids.has(ref.store_uuid) ? "personal" : "team";
|
|
2998
|
+
entries.push({
|
|
2999
|
+
qualifiedId: `${ref.alias}:${stableId}`,
|
|
3000
|
+
file: ref.file,
|
|
3001
|
+
alias: ref.alias,
|
|
3002
|
+
layer,
|
|
3003
|
+
source
|
|
3004
|
+
});
|
|
3005
|
+
}
|
|
3006
|
+
return entries;
|
|
3007
|
+
}
|
|
3008
|
+
async function buildCrossStoreRawItems(projectRoot) {
|
|
3009
|
+
const items = [];
|
|
3010
|
+
for (const entry of walkReadSetStores(projectRoot)) {
|
|
3011
|
+
const baseDescription = extractRuleDescription(entry.source);
|
|
3012
|
+
if (baseDescription === void 0) {
|
|
3013
|
+
continue;
|
|
3014
|
+
}
|
|
3015
|
+
items.push({
|
|
3016
|
+
stable_id: entry.qualifiedId,
|
|
3017
|
+
description: { ...baseDescription, knowledge_layer: entry.layer }
|
|
3018
|
+
});
|
|
3019
|
+
}
|
|
3020
|
+
return items;
|
|
3021
|
+
}
|
|
3022
|
+
function buildCrossStoreBodyIndex(projectRoot) {
|
|
3023
|
+
const index = /* @__PURE__ */ new Map();
|
|
3024
|
+
for (const entry of walkReadSetStores(projectRoot)) {
|
|
3025
|
+
if (!index.has(entry.qualifiedId)) {
|
|
3026
|
+
index.set(entry.qualifiedId, { file: entry.file, layer: entry.layer });
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
return index;
|
|
3030
|
+
}
|
|
3031
|
+
function collectStoreKnowledgeSummaries(projectRoot) {
|
|
3032
|
+
const out = [];
|
|
3033
|
+
for (const entry of walkReadSetStores(projectRoot)) {
|
|
3034
|
+
const description = extractRuleDescription(entry.source);
|
|
3035
|
+
if (description === void 0) {
|
|
3036
|
+
continue;
|
|
3037
|
+
}
|
|
3038
|
+
out.push({
|
|
3039
|
+
stableId: entry.qualifiedId,
|
|
3040
|
+
summary: description.summary ?? "",
|
|
3041
|
+
layer: entry.layer
|
|
3042
|
+
});
|
|
3043
|
+
}
|
|
3044
|
+
return out;
|
|
3045
|
+
}
|
|
3046
|
+
|
|
2857
3047
|
// src/services/id-redirect.ts
|
|
2858
3048
|
var DEFAULT_REDIRECT_WINDOW_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
2859
3049
|
async function loadIdRedirectMap(projectRoot, options = {}) {
|
|
@@ -3066,7 +3256,14 @@ function buildQueryTerms(text) {
|
|
|
3066
3256
|
// src/services/vector-retrieval.ts
|
|
3067
3257
|
var embedderLoad;
|
|
3068
3258
|
var OPTIONAL_EMBED_PACKAGE = "fastembed";
|
|
3069
|
-
|
|
3259
|
+
function buildEmbedInitOptions(modelName) {
|
|
3260
|
+
return {
|
|
3261
|
+
maxLength: 512,
|
|
3262
|
+
cacheDir: process.env.FABRIC_EMBED_CACHE_DIR,
|
|
3263
|
+
...typeof modelName === "string" && modelName.length > 0 ? { model: modelName } : {}
|
|
3264
|
+
};
|
|
3265
|
+
}
|
|
3266
|
+
async function loadEmbedder(modelName) {
|
|
3070
3267
|
if (embedderLoad === void 0) {
|
|
3071
3268
|
embedderLoad = (async () => {
|
|
3072
3269
|
try {
|
|
@@ -3075,10 +3272,7 @@ async function loadEmbedder() {
|
|
|
3075
3272
|
if (mod?.FlagEmbedding?.init === void 0) {
|
|
3076
3273
|
return null;
|
|
3077
3274
|
}
|
|
3078
|
-
const model = await mod.FlagEmbedding.init(
|
|
3079
|
-
maxLength: 512,
|
|
3080
|
-
cacheDir: process.env.FABRIC_EMBED_CACHE_DIR
|
|
3081
|
-
});
|
|
3275
|
+
const model = await mod.FlagEmbedding.init(buildEmbedInitOptions(modelName));
|
|
3082
3276
|
return {
|
|
3083
3277
|
async embed(texts) {
|
|
3084
3278
|
const out = [];
|
|
@@ -3245,7 +3439,11 @@ async function planContext(projectRoot, input) {
|
|
|
3245
3439
|
targetPaths: input.target_paths ?? dedupePaths(input.paths),
|
|
3246
3440
|
queryTerms: buildQueryTerms(queryText)
|
|
3247
3441
|
};
|
|
3248
|
-
const { rawItems, suppressedStableIds } = buildRawDescriptionItems(meta);
|
|
3442
|
+
const { rawItems: projectRawItems, suppressedStableIds } = buildRawDescriptionItems(meta);
|
|
3443
|
+
const storeRawItems = await buildCrossStoreRawItems(projectRoot).catch(() => []);
|
|
3444
|
+
const allRawItems = [...projectRawItems, ...storeRawItems];
|
|
3445
|
+
const effectiveLayerFilter = input.layer_filter ?? readDefaultLayerFilter(projectRoot);
|
|
3446
|
+
const rawItems = effectiveLayerFilter === "both" ? allRawItems : allRawItems.filter((item) => item.description.knowledge_layer === effectiveLayerFilter);
|
|
3249
3447
|
const docTexts = /* @__PURE__ */ new Map();
|
|
3250
3448
|
for (const item of rawItems) {
|
|
3251
3449
|
docTexts.set(item.stable_id, documentTextForItem(item.description));
|
|
@@ -3256,7 +3454,7 @@ async function planContext(projectRoot, input) {
|
|
|
3256
3454
|
}
|
|
3257
3455
|
const embedConfig = readEmbedConfig(projectRoot);
|
|
3258
3456
|
if (embedConfig.enabled && queryText.trim().length > 0 && rawItems.length > 0) {
|
|
3259
|
-
const embedder = await loadEmbedder();
|
|
3457
|
+
const embedder = await loadEmbedder(embedConfig.model);
|
|
3260
3458
|
const vectorScores = await buildVectorScores(
|
|
3261
3459
|
embedder,
|
|
3262
3460
|
queryText,
|
|
@@ -3274,7 +3472,27 @@ async function planContext(projectRoot, input) {
|
|
|
3274
3472
|
const rankedCandidates = dedupeDescriptionIndex(builtItems);
|
|
3275
3473
|
const topK = readPlanContextTopK(projectRoot);
|
|
3276
3474
|
const omittedCandidateCount = Math.max(0, rankedCandidates.length - topK);
|
|
3277
|
-
const
|
|
3475
|
+
const topKCandidates = omittedCandidateCount > 0 ? rankedCandidates.slice(0, topK) : rankedCandidates;
|
|
3476
|
+
let candidates = topKCandidates;
|
|
3477
|
+
const relatedAppended = {};
|
|
3478
|
+
if (input.include_related === true) {
|
|
3479
|
+
const inTopK = new Set(topKCandidates.map((item) => item.stable_id));
|
|
3480
|
+
const rankedById = new Map(rankedCandidates.map((item) => [item.stable_id, item]));
|
|
3481
|
+
const appended = [];
|
|
3482
|
+
for (const surfaced of topKCandidates) {
|
|
3483
|
+
for (const rel of surfaced.description.related ?? []) {
|
|
3484
|
+
if (inTopK.has(rel)) continue;
|
|
3485
|
+
if (relatedAppended[rel] !== void 0) continue;
|
|
3486
|
+
const neighbour = rankedById.get(rel);
|
|
3487
|
+
if (neighbour === void 0) continue;
|
|
3488
|
+
relatedAppended[rel] = surfaced.stable_id;
|
|
3489
|
+
appended.push(neighbour);
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
if (appended.length > 0) {
|
|
3493
|
+
candidates = [...topKCandidates, ...appended];
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3278
3496
|
const entries = uniquePaths.map((path2) => ({
|
|
3279
3497
|
path: path2,
|
|
3280
3498
|
requirement_profile: buildRequirementProfile(path2, input)
|
|
@@ -3308,7 +3526,11 @@ async function planContext(projectRoot, input) {
|
|
|
3308
3526
|
auto_healed: true,
|
|
3309
3527
|
previous_revision_hash: firstSeenPreviousRevision
|
|
3310
3528
|
} : {},
|
|
3311
|
-
...redirects !== void 0 ? { redirects } : {}
|
|
3529
|
+
...redirects !== void 0 ? { redirects } : {},
|
|
3530
|
+
// lifecycle-refactor W3-T2 (§7): surface the related-expansion provenance map
|
|
3531
|
+
// ONLY when at least one neighbour was actually appended. Empty (graph-empty
|
|
3532
|
+
// no-op) → field omitted, steady-state wire shape unchanged.
|
|
3533
|
+
...Object.keys(relatedAppended).length > 0 ? { related_appended: relatedAppended } : {}
|
|
3312
3534
|
};
|
|
3313
3535
|
bumpCounter(projectRoot, METRIC_COUNTER_NAMES.knowledge_context_planned);
|
|
3314
3536
|
try {
|
|
@@ -3594,7 +3816,7 @@ function registerPlanContext(server, tracker) {
|
|
|
3594
3816
|
outputSchema: planContextOutputSchema,
|
|
3595
3817
|
annotations: planContextAnnotations
|
|
3596
3818
|
},
|
|
3597
|
-
async ({ paths, intent, known_tech, detected_entities, client_hash, correlation_id, session_id }) => {
|
|
3819
|
+
async ({ paths, intent, known_tech, detected_entities, client_hash, correlation_id, session_id, target_paths, layer_filter }) => {
|
|
3598
3820
|
const requestId = randomUUID3();
|
|
3599
3821
|
tracker?.enter(requestId);
|
|
3600
3822
|
try {
|
|
@@ -3609,7 +3831,12 @@ function registerPlanContext(server, tracker) {
|
|
|
3609
3831
|
detected_entities,
|
|
3610
3832
|
client_hash,
|
|
3611
3833
|
correlation_id,
|
|
3612
|
-
session_id
|
|
3834
|
+
session_id,
|
|
3835
|
+
// F54 (ISS-20260531-090): these were declared in
|
|
3836
|
+
// planContextInputSchema but never forwarded to the service, so any
|
|
3837
|
+
// client/LLM-supplied value was silently discarded.
|
|
3838
|
+
target_paths,
|
|
3839
|
+
layer_filter
|
|
3613
3840
|
});
|
|
3614
3841
|
let response = {
|
|
3615
3842
|
...result,
|
|
@@ -3687,7 +3914,7 @@ import { enforcePayloadLimit as enforcePayloadLimit3 } from "@fenglimg/fabric-sh
|
|
|
3687
3914
|
// src/services/knowledge-sections.ts
|
|
3688
3915
|
import { readFile as readFile8 } from "fs/promises";
|
|
3689
3916
|
import { homedir as homedir4 } from "os";
|
|
3690
|
-
import { join as
|
|
3917
|
+
import { join as join11 } from "path";
|
|
3691
3918
|
import { deriveAgentsMetaLayer as deriveAgentsMetaLayer3 } from "@fenglimg/fabric-shared";
|
|
3692
3919
|
import { McpToolError } from "@fenglimg/fabric-shared/errors";
|
|
3693
3920
|
var PRIORITY_ORDER = {
|
|
@@ -3729,13 +3956,25 @@ async function getKnowledgeSections(projectRoot, input) {
|
|
|
3729
3956
|
const { meta } = await loadActiveMeta(projectRoot, { caller: "getKnowledgeSections" });
|
|
3730
3957
|
const selectedStableIds = [...token.required_stable_ids, ...rewrittenAiSelected];
|
|
3731
3958
|
const ruleNodeIndex = buildRuleNodeIndex(meta);
|
|
3959
|
+
const storeBodyIndex = buildCrossStoreBodyIndex(projectRoot);
|
|
3960
|
+
const unresolvedSelectedIds = [];
|
|
3961
|
+
const storeSelected = [];
|
|
3732
3962
|
const selectedRules = sortRuleNodes(
|
|
3733
|
-
selectedStableIds.
|
|
3963
|
+
selectedStableIds.flatMap((stableId) => {
|
|
3734
3964
|
const entry = ruleNodeIndex.get(stableId);
|
|
3735
3965
|
if (entry === void 0) {
|
|
3966
|
+
if (stableId.includes(":")) {
|
|
3967
|
+
const ref = storeBodyIndex.get(stableId);
|
|
3968
|
+
if (ref !== void 0) {
|
|
3969
|
+
storeSelected.push({ stableId, ref });
|
|
3970
|
+
} else {
|
|
3971
|
+
unresolvedSelectedIds.push(stableId);
|
|
3972
|
+
}
|
|
3973
|
+
return [];
|
|
3974
|
+
}
|
|
3736
3975
|
throw new Error(`Selected rule is not present in agents.meta.json: ${stableId}`);
|
|
3737
3976
|
}
|
|
3738
|
-
return entry;
|
|
3977
|
+
return [entry];
|
|
3739
3978
|
})
|
|
3740
3979
|
);
|
|
3741
3980
|
const diagnostics = [];
|
|
@@ -3759,6 +3998,29 @@ async function getKnowledgeSections(projectRoot, input) {
|
|
|
3759
3998
|
body
|
|
3760
3999
|
});
|
|
3761
4000
|
}
|
|
4001
|
+
for (const { stableId, ref } of storeSelected) {
|
|
4002
|
+
let content;
|
|
4003
|
+
try {
|
|
4004
|
+
content = await readFile8(ref.file, "utf8");
|
|
4005
|
+
} catch {
|
|
4006
|
+
unresolvedSelectedIds.push(stableId);
|
|
4007
|
+
continue;
|
|
4008
|
+
}
|
|
4009
|
+
rules.push({
|
|
4010
|
+
stable_id: stableId,
|
|
4011
|
+
level: "L1",
|
|
4012
|
+
path: ref.file,
|
|
4013
|
+
body: extractBody(content)
|
|
4014
|
+
});
|
|
4015
|
+
}
|
|
4016
|
+
for (const stableId of unresolvedSelectedIds) {
|
|
4017
|
+
diagnostics.push({
|
|
4018
|
+
code: "unresolved_selected_id",
|
|
4019
|
+
severity: "warn",
|
|
4020
|
+
stable_id: stableId,
|
|
4021
|
+
message: `Selected rule '${stableId}' is not present in the project's agents.meta.json or any read-set store \u2014 skipped (deleted, layer-flipped, or its store is not bound).`
|
|
4022
|
+
});
|
|
4023
|
+
}
|
|
3762
4024
|
const result = {
|
|
3763
4025
|
revision_hash: meta.revision,
|
|
3764
4026
|
selected_stable_ids: rules.map((rule) => rule.stable_id),
|
|
@@ -3835,12 +4097,6 @@ function validateAiSelections(aiSelectableStableIds, aiSelectedStableIds, aiSele
|
|
|
3835
4097
|
`Invalid rule selection "${stableId}": not in this token's plan-context candidates. Pass only stable_ids from fab_plan_context candidates[].stable_id.`
|
|
3836
4098
|
);
|
|
3837
4099
|
}
|
|
3838
|
-
if (aiSelectionReasons[stableId]?.trim() === "") {
|
|
3839
|
-
throw new Error(`Missing AI selection reason for ${stableId}`);
|
|
3840
|
-
}
|
|
3841
|
-
if (aiSelectionReasons[stableId] === void 0) {
|
|
3842
|
-
throw new Error(`Missing AI selection reason for ${stableId}`);
|
|
3843
|
-
}
|
|
3844
4100
|
}
|
|
3845
4101
|
}
|
|
3846
4102
|
function buildRuleNodeIndex(meta) {
|
|
@@ -3887,9 +4143,9 @@ function outputLevelOrder(level) {
|
|
|
3887
4143
|
function resolveRuleSourcePath(projectRoot, contentRef) {
|
|
3888
4144
|
if (contentRef.startsWith("~/.fabric/knowledge/")) {
|
|
3889
4145
|
const home = process.env.FABRIC_HOME ?? homedir4();
|
|
3890
|
-
return
|
|
4146
|
+
return join11(home, ".fabric", "knowledge", contentRef.slice("~/.fabric/knowledge/".length));
|
|
3891
4147
|
}
|
|
3892
|
-
return
|
|
4148
|
+
return join11(projectRoot, contentRef);
|
|
3893
4149
|
}
|
|
3894
4150
|
function pickSelectionReasons(selectedStableIds, reasons) {
|
|
3895
4151
|
return Object.fromEntries(selectedStableIds.map((stableId) => [stableId, reasons[stableId] ?? ""]));
|
|
@@ -3964,12 +4220,20 @@ async function recall(projectRoot, input) {
|
|
|
3964
4220
|
});
|
|
3965
4221
|
return {
|
|
3966
4222
|
...planResult,
|
|
3967
|
-
rules: sectionsResult.rules,
|
|
4223
|
+
rules: sectionsResult.rules.map(attachStoreProvenance),
|
|
3968
4224
|
selected_stable_ids: sectionsResult.selected_stable_ids,
|
|
3969
4225
|
diagnostics: sectionsResult.diagnostics,
|
|
3970
4226
|
...packaging
|
|
3971
4227
|
};
|
|
3972
4228
|
}
|
|
4229
|
+
function attachStoreProvenance(rule) {
|
|
4230
|
+
const colon = rule.stable_id.indexOf(":");
|
|
4231
|
+
if (colon <= 0) {
|
|
4232
|
+
return rule;
|
|
4233
|
+
}
|
|
4234
|
+
const alias = rule.stable_id.slice(0, colon);
|
|
4235
|
+
return { ...rule, store: { alias } };
|
|
4236
|
+
}
|
|
3973
4237
|
function buildRecallPackaging(planResult, relatedAvailableNotIncluded) {
|
|
3974
4238
|
const omitted = planResult.omitted_candidate_count ?? 0;
|
|
3975
4239
|
const nextSteps = [];
|
|
@@ -4009,6 +4273,7 @@ function registerRecall(server, tracker) {
|
|
|
4009
4273
|
correlation_id,
|
|
4010
4274
|
session_id,
|
|
4011
4275
|
target_paths,
|
|
4276
|
+
layer_filter,
|
|
4012
4277
|
ids,
|
|
4013
4278
|
include_related
|
|
4014
4279
|
}) => {
|
|
@@ -4031,6 +4296,9 @@ function registerRecall(server, tracker) {
|
|
|
4031
4296
|
correlation_id,
|
|
4032
4297
|
session_id,
|
|
4033
4298
|
target_paths,
|
|
4299
|
+
// F54 (ISS-20260531-090): forwarded so recall→planContext honors the
|
|
4300
|
+
// declared layer restriction instead of silently discarding it.
|
|
4301
|
+
layer_filter,
|
|
4034
4302
|
ids,
|
|
4035
4303
|
include_related
|
|
4036
4304
|
};
|
|
@@ -4286,19 +4554,19 @@ import { execFileSync } from "child_process";
|
|
|
4286
4554
|
import { existsSync as existsSync6 } from "fs";
|
|
4287
4555
|
import { readFile as readFile10, readdir as readdir3, unlink } from "fs/promises";
|
|
4288
4556
|
import { homedir as homedir5 } from "os";
|
|
4289
|
-
import { basename, join as
|
|
4557
|
+
import { basename, join as join12, relative as relative3, resolve as resolve3, sep as sep3 } from "path";
|
|
4290
4558
|
|
|
4291
4559
|
// src/services/knowledge-id-allocator.ts
|
|
4292
4560
|
import { readFile as readFile9, writeFile as writeFile2 } from "fs/promises";
|
|
4293
4561
|
import { dirname as dirname2 } from "path";
|
|
4294
|
-
import { mkdir as
|
|
4562
|
+
import { mkdir as mkdir3 } from "fs/promises";
|
|
4295
4563
|
import {
|
|
4296
4564
|
AgentsMetaCountersSchema,
|
|
4297
4565
|
agentsMetaSchema as agentsMetaSchema4,
|
|
4298
4566
|
allocateKnowledgeId,
|
|
4299
4567
|
defaultAgentsMetaCounters as defaultAgentsMetaCounters2
|
|
4300
4568
|
} from "@fenglimg/fabric-shared";
|
|
4301
|
-
import { atomicWriteJson as atomicWriteJson2, withFileLock } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
4569
|
+
import { atomicWriteJson as atomicWriteJson2, withFileLock as withFileLock3 } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
4302
4570
|
import { GenericIOError } from "@fenglimg/fabric-shared/errors";
|
|
4303
4571
|
var KnowledgeIdAllocator = class {
|
|
4304
4572
|
constructor(metaPath) {
|
|
@@ -4310,7 +4578,7 @@ var KnowledgeIdAllocator = class {
|
|
|
4310
4578
|
* the advanced counter to `agents.meta.json`.
|
|
4311
4579
|
*/
|
|
4312
4580
|
async allocate(layer, type) {
|
|
4313
|
-
return
|
|
4581
|
+
return withFileLock3(`${this.metaPath}.lock`, async () => {
|
|
4314
4582
|
const meta = await this.readMeta();
|
|
4315
4583
|
const counters2 = this.normalizeCounters(meta.counters);
|
|
4316
4584
|
const { id, nextCounters } = allocateKnowledgeId(layer, type, counters2);
|
|
@@ -4373,20 +4641,13 @@ var KnowledgeIdAllocator = class {
|
|
|
4373
4641
|
}
|
|
4374
4642
|
};
|
|
4375
4643
|
async function ensureParentDirectory2(filePath) {
|
|
4376
|
-
await
|
|
4644
|
+
await mkdir3(dirname2(filePath), { recursive: true });
|
|
4377
4645
|
}
|
|
4378
4646
|
function isNodeError4(err) {
|
|
4379
4647
|
return err instanceof Error && typeof err.code === "string";
|
|
4380
4648
|
}
|
|
4381
4649
|
|
|
4382
4650
|
// src/services/review.ts
|
|
4383
|
-
var PENDING_BASE_TEAM_REL = ".fabric/knowledge/pending";
|
|
4384
|
-
function pendingBaseAbs(layer, projectRoot) {
|
|
4385
|
-
if (layer === "personal") {
|
|
4386
|
-
return join10(resolvePersonalRoot4(), ".fabric", "knowledge", "pending");
|
|
4387
|
-
}
|
|
4388
|
-
return join10(projectRoot, PENDING_BASE_TEAM_REL);
|
|
4389
|
-
}
|
|
4390
4651
|
var PLURAL_TYPES = [
|
|
4391
4652
|
"decisions",
|
|
4392
4653
|
"pitfalls",
|
|
@@ -4444,29 +4705,50 @@ async function reviewKnowledge(projectRoot, input) {
|
|
|
4444
4705
|
}
|
|
4445
4706
|
}
|
|
4446
4707
|
}
|
|
4708
|
+
function storeKnowledgeRoots(projectRoot) {
|
|
4709
|
+
const roots = [];
|
|
4710
|
+
for (const layer of ["team", "personal"]) {
|
|
4711
|
+
try {
|
|
4712
|
+
roots.push(resolve3(resolveStoreCanonicalBase(layer, projectRoot)));
|
|
4713
|
+
} catch {
|
|
4714
|
+
}
|
|
4715
|
+
}
|
|
4716
|
+
return roots;
|
|
4717
|
+
}
|
|
4718
|
+
function isUnder(abs, root) {
|
|
4719
|
+
return abs === root || abs.startsWith(root + "/");
|
|
4720
|
+
}
|
|
4447
4721
|
function resolveSandboxedPath(projectRoot, candidate, options = {}) {
|
|
4448
4722
|
if (candidate.length === 0) {
|
|
4449
4723
|
throw new Error("path is empty");
|
|
4450
4724
|
}
|
|
4451
4725
|
const projectKnowledgeRoot = resolve3(projectRoot, ".fabric", "knowledge");
|
|
4452
4726
|
const personalKnowledgeRoot = resolve3(resolvePersonalRoot4(), ".fabric", "knowledge");
|
|
4727
|
+
const storeRoots = storeKnowledgeRoots(projectRoot);
|
|
4453
4728
|
if (candidate.startsWith("~/")) {
|
|
4454
4729
|
if (options.allowPersonal !== true) {
|
|
4455
4730
|
throw new Error(`personal-root path not allowed for this action: ${candidate}`);
|
|
4456
4731
|
}
|
|
4457
4732
|
const abs = resolve3(resolvePersonalRoot4(), candidate.slice(2));
|
|
4458
|
-
if (abs
|
|
4733
|
+
if (!isUnder(abs, personalKnowledgeRoot)) {
|
|
4459
4734
|
throw new Error(`path escapes personal knowledge root: ${candidate}`);
|
|
4460
4735
|
}
|
|
4461
4736
|
return { abs, isInProjectTree: false };
|
|
4462
4737
|
}
|
|
4738
|
+
if (candidate.startsWith("/")) {
|
|
4739
|
+
const abs = resolve3(candidate);
|
|
4740
|
+
if (storeRoots.some((root) => isUnder(abs, root))) {
|
|
4741
|
+
return { abs, isInProjectTree: false };
|
|
4742
|
+
}
|
|
4743
|
+
throw new Error(`path escapes knowledge root: ${candidate}`);
|
|
4744
|
+
}
|
|
4463
4745
|
const projectAbs = resolve3(projectRoot, candidate);
|
|
4464
|
-
if (projectAbs
|
|
4746
|
+
if (isUnder(projectAbs, projectKnowledgeRoot)) {
|
|
4465
4747
|
return { abs: projectAbs, isInProjectTree: true };
|
|
4466
4748
|
}
|
|
4467
4749
|
if (options.allowPersonal === true) {
|
|
4468
4750
|
const personalAbs = resolve3(resolvePersonalRoot4(), candidate);
|
|
4469
|
-
if (personalAbs
|
|
4751
|
+
if (isUnder(personalAbs, personalKnowledgeRoot)) {
|
|
4470
4752
|
return { abs: personalAbs, isInProjectTree: false };
|
|
4471
4753
|
}
|
|
4472
4754
|
}
|
|
@@ -4488,13 +4770,24 @@ function isVisibleByLifecycle(fm, filters) {
|
|
|
4488
4770
|
async function listPending(projectRoot, filters) {
|
|
4489
4771
|
const items = [];
|
|
4490
4772
|
const typesToScan = filters?.type !== void 0 ? [filters.type] : PLURAL_TYPES;
|
|
4491
|
-
const sources = [
|
|
4492
|
-
|
|
4493
|
-
{
|
|
4494
|
-
|
|
4773
|
+
const sources = [];
|
|
4774
|
+
for (const origin of ["team", "personal"]) {
|
|
4775
|
+
try {
|
|
4776
|
+
const pendingRoot = resolveStorePendingBase(origin, projectRoot);
|
|
4777
|
+
sources.push({ origin, root: pendingRoot, isStore: true });
|
|
4778
|
+
if (filters?.include_rejected === true) {
|
|
4779
|
+
sources.push({
|
|
4780
|
+
origin,
|
|
4781
|
+
root: pendingRoot.replace(`${sep3}pending`, `${sep3}rejected`),
|
|
4782
|
+
isStore: true
|
|
4783
|
+
});
|
|
4784
|
+
}
|
|
4785
|
+
} catch {
|
|
4786
|
+
}
|
|
4787
|
+
}
|
|
4495
4788
|
for (const source of sources) {
|
|
4496
4789
|
for (const type of typesToScan) {
|
|
4497
|
-
const dir =
|
|
4790
|
+
const dir = join12(source.root, type);
|
|
4498
4791
|
if (!existsSync6(dir)) {
|
|
4499
4792
|
continue;
|
|
4500
4793
|
}
|
|
@@ -4506,7 +4799,7 @@ async function listPending(projectRoot, filters) {
|
|
|
4506
4799
|
}
|
|
4507
4800
|
for (const name of entries) {
|
|
4508
4801
|
if (!name.endsWith(".md")) continue;
|
|
4509
|
-
const absolutePath =
|
|
4802
|
+
const absolutePath = join12(dir, name);
|
|
4510
4803
|
let content;
|
|
4511
4804
|
try {
|
|
4512
4805
|
content = await readFile10(absolutePath, "utf8");
|
|
@@ -4536,13 +4829,15 @@ async function listPending(projectRoot, filters) {
|
|
|
4536
4829
|
if (!isVisibleByLifecycle(fm, filters)) {
|
|
4537
4830
|
continue;
|
|
4538
4831
|
}
|
|
4539
|
-
const reportedPath = source.origin === "personal" ? `~/${relative3(resolvePersonalRoot4(), absolutePath)}` : relative3(projectRoot, absolutePath);
|
|
4832
|
+
const reportedPath = source.isStore ? absolutePath : source.origin === "personal" ? `~/${relative3(resolvePersonalRoot4(), absolutePath)}` : relative3(projectRoot, absolutePath);
|
|
4540
4833
|
items.push({
|
|
4541
4834
|
pending_path: reportedPath,
|
|
4542
4835
|
// v2.0.0-rc.27 TASK-001 (§2.12): absolute path companion for
|
|
4543
4836
|
// personal entries so programmatic consumers (Read, fs.readFile)
|
|
4544
|
-
// don't need to shell-expand the `~` themselves.
|
|
4545
|
-
|
|
4837
|
+
// don't need to shell-expand the `~` themselves. Store entries
|
|
4838
|
+
// already report an absolute pending_path, so the companion is
|
|
4839
|
+
// emitted for non-store personal entries only.
|
|
4840
|
+
...source.origin === "personal" && !source.isStore ? { pending_path_absolute: absolutePath } : {},
|
|
4546
4841
|
type,
|
|
4547
4842
|
layer,
|
|
4548
4843
|
maturity,
|
|
@@ -4562,7 +4857,7 @@ async function listPending(projectRoot, filters) {
|
|
|
4562
4857
|
}
|
|
4563
4858
|
async function approveAll(projectRoot, pendingPaths) {
|
|
4564
4859
|
const allocator = new KnowledgeIdAllocator(
|
|
4565
|
-
|
|
4860
|
+
join12(projectRoot, ".fabric", "agents.meta.json")
|
|
4566
4861
|
);
|
|
4567
4862
|
const approved = [];
|
|
4568
4863
|
for (const pendingPath of pendingPaths) {
|
|
@@ -4576,17 +4871,26 @@ async function approveAll(projectRoot, pendingPaths) {
|
|
|
4576
4871
|
async function approveOne(projectRoot, pendingPath, allocator) {
|
|
4577
4872
|
let sourceAbs;
|
|
4578
4873
|
let sourceOrigin;
|
|
4874
|
+
let sourceIsStore = false;
|
|
4579
4875
|
try {
|
|
4580
4876
|
const sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
|
|
4581
|
-
const
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4877
|
+
const resolvePendingBaseOrNull = (layer) => {
|
|
4878
|
+
try {
|
|
4879
|
+
return resolveStorePendingBase(layer, projectRoot);
|
|
4880
|
+
} catch {
|
|
4881
|
+
return null;
|
|
4882
|
+
}
|
|
4883
|
+
};
|
|
4884
|
+
const teamPendingAbs = resolvePendingBaseOrNull("team");
|
|
4885
|
+
const personalPendingAbs = resolvePendingBaseOrNull("personal");
|
|
4886
|
+
const inTeamPending = teamPendingAbs !== null && (sandboxed.abs === teamPendingAbs || sandboxed.abs.startsWith(teamPendingAbs + "/"));
|
|
4887
|
+
const inPersonalPending = personalPendingAbs !== null && (sandboxed.abs === personalPendingAbs || sandboxed.abs.startsWith(personalPendingAbs + "/"));
|
|
4585
4888
|
if (!inTeamPending && !inPersonalPending) {
|
|
4586
4889
|
throw new Error(`approve path is outside .fabric/knowledge/pending/: ${pendingPath}`);
|
|
4587
4890
|
}
|
|
4588
4891
|
sourceAbs = sandboxed.abs;
|
|
4589
4892
|
sourceOrigin = inPersonalPending ? "personal" : "team";
|
|
4893
|
+
sourceIsStore = true;
|
|
4590
4894
|
} catch (err) {
|
|
4591
4895
|
const reason = err instanceof Error ? err.message : String(err);
|
|
4592
4896
|
await emitEventBestEffort2(projectRoot, {
|
|
@@ -4621,13 +4925,16 @@ async function approveOne(projectRoot, pendingPath, allocator) {
|
|
|
4621
4925
|
const stableId = await allocator.allocate(layer, pluralType);
|
|
4622
4926
|
allocatedId = stableId;
|
|
4623
4927
|
const newFilename = `${stableId}--${slug}.md`;
|
|
4624
|
-
|
|
4625
|
-
targetAbs = join10(layerRoot, "knowledge", pluralType, newFilename);
|
|
4928
|
+
targetAbs = join12(resolveStoreCanonicalBase(layer, projectRoot), pluralType, newFilename);
|
|
4626
4929
|
await ensureParentDirectory(targetAbs);
|
|
4627
4930
|
const rewritten = rewriteFrontmatterForPromote(content, stableId);
|
|
4628
4931
|
await atomicWriteText(targetAbs, rewritten);
|
|
4629
4932
|
writtenTarget = true;
|
|
4630
|
-
if (
|
|
4933
|
+
if (sourceIsStore) {
|
|
4934
|
+
if (existsSync6(sourceAbs)) {
|
|
4935
|
+
await unlink(sourceAbs);
|
|
4936
|
+
}
|
|
4937
|
+
} else if (sourceOrigin === "team") {
|
|
4631
4938
|
try {
|
|
4632
4939
|
execFileSync("git", ["rm", "--quiet", "-f", pendingPath], {
|
|
4633
4940
|
cwd: projectRoot,
|
|
@@ -4679,7 +4986,12 @@ async function rejectAll(projectRoot, pendingPaths, reason) {
|
|
|
4679
4986
|
if (existsSync6(sandboxed.abs)) {
|
|
4680
4987
|
const content = await readFile10(sandboxed.abs, "utf8");
|
|
4681
4988
|
const merged = rewriteFrontmatterMerge(content, { status: "rejected" });
|
|
4682
|
-
|
|
4989
|
+
const rejectedAbs = sandboxed.abs.includes(`${sep3}pending${sep3}`) ? sandboxed.abs.replace(`${sep3}pending${sep3}`, `${sep3}rejected${sep3}`) : null;
|
|
4990
|
+
if (rejectedAbs !== null) {
|
|
4991
|
+
await ensureParentDirectory(rejectedAbs);
|
|
4992
|
+
await atomicWriteText(rejectedAbs, merged);
|
|
4993
|
+
await unlink(sandboxed.abs);
|
|
4994
|
+
} else if (merged !== content) {
|
|
4683
4995
|
await atomicWriteText(sandboxed.abs, merged);
|
|
4684
4996
|
}
|
|
4685
4997
|
}
|
|
@@ -4759,11 +5071,14 @@ async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
|
|
|
4759
5071
|
const fromScope = fm.relevance_scope ?? "broad";
|
|
4760
5072
|
const shouldAutoDegrade = fromScope === "narrow" && fromLayer === "team" && toLayer === "personal";
|
|
4761
5073
|
const allocator = new KnowledgeIdAllocator(
|
|
4762
|
-
|
|
5074
|
+
join12(projectRoot, ".fabric", "agents.meta.json")
|
|
4763
5075
|
);
|
|
4764
5076
|
const newStableId = await allocator.allocate(toLayer, pluralType);
|
|
4765
|
-
const
|
|
4766
|
-
|
|
5077
|
+
const toAbs = join12(
|
|
5078
|
+
resolveStoreCanonicalBase(toLayer, projectRoot),
|
|
5079
|
+
pluralType,
|
|
5080
|
+
`${newStableId}--${slug}.md`
|
|
5081
|
+
);
|
|
4767
5082
|
await ensureParentDirectory(toAbs);
|
|
4768
5083
|
await emitEventBestEffort2(projectRoot, {
|
|
4769
5084
|
event_type: "knowledge_promote_started",
|
|
@@ -4842,16 +5157,22 @@ async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
|
|
|
4842
5157
|
async function searchEntries(projectRoot, query, filters) {
|
|
4843
5158
|
const lowerQuery = query.toLowerCase();
|
|
4844
5159
|
const items = [];
|
|
4845
|
-
const sources = [
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
{
|
|
4849
|
-
|
|
4850
|
-
|
|
5160
|
+
const sources = [];
|
|
5161
|
+
for (const layer of ["team", "personal"]) {
|
|
5162
|
+
const isPersonal = layer === "personal";
|
|
5163
|
+
try {
|
|
5164
|
+
sources.push({ root: resolveStorePendingBase(layer, projectRoot), isPending: true, isPersonal, isStore: true });
|
|
5165
|
+
} catch {
|
|
5166
|
+
}
|
|
5167
|
+
try {
|
|
5168
|
+
sources.push({ root: resolveStoreCanonicalBase(layer, projectRoot), isPending: false, isPersonal, isStore: true });
|
|
5169
|
+
} catch {
|
|
5170
|
+
}
|
|
5171
|
+
}
|
|
4851
5172
|
const typesToScan = filters?.type !== void 0 ? [filters.type] : PLURAL_TYPES;
|
|
4852
5173
|
for (const source of sources) {
|
|
4853
5174
|
for (const type of typesToScan) {
|
|
4854
|
-
const dir =
|
|
5175
|
+
const dir = join12(source.root, type);
|
|
4855
5176
|
if (!existsSync6(dir)) continue;
|
|
4856
5177
|
let entries;
|
|
4857
5178
|
try {
|
|
@@ -4861,7 +5182,7 @@ async function searchEntries(projectRoot, query, filters) {
|
|
|
4861
5182
|
}
|
|
4862
5183
|
for (const name of entries) {
|
|
4863
5184
|
if (!name.endsWith(".md")) continue;
|
|
4864
|
-
const absolutePath =
|
|
5185
|
+
const absolutePath = join12(dir, name);
|
|
4865
5186
|
let content;
|
|
4866
5187
|
try {
|
|
4867
5188
|
content = await readFile10(absolutePath, "utf8");
|
|
@@ -4901,7 +5222,7 @@ async function searchEntries(projectRoot, query, filters) {
|
|
|
4901
5222
|
].map((s) => s.toLowerCase());
|
|
4902
5223
|
const matches = haystacks.some((h) => h.includes(lowerQuery));
|
|
4903
5224
|
if (!matches) continue;
|
|
4904
|
-
const reportedPath = source.isPersonal ? `~/${relative3(resolvePersonalRoot4(), absolutePath)}` : relative3(projectRoot, absolutePath);
|
|
5225
|
+
const reportedPath = source.isStore ? absolutePath : source.isPersonal ? `~/${relative3(resolvePersonalRoot4(), absolutePath)}` : relative3(projectRoot, absolutePath);
|
|
4905
5226
|
items.push({
|
|
4906
5227
|
area: source.isPending ? "pending" : "canonical",
|
|
4907
5228
|
path: reportedPath,
|
|
@@ -4970,10 +5291,10 @@ function parseFrontmatter(content) {
|
|
|
4970
5291
|
for (const rawLine of block.split(/\r?\n/u)) {
|
|
4971
5292
|
const line = rawLine.trim();
|
|
4972
5293
|
if (line.length === 0) continue;
|
|
4973
|
-
const
|
|
4974
|
-
if (
|
|
4975
|
-
const key = line.slice(0,
|
|
4976
|
-
const value = line.slice(
|
|
5294
|
+
const sep5 = line.indexOf(":");
|
|
5295
|
+
if (sep5 === -1) continue;
|
|
5296
|
+
const key = line.slice(0, sep5).trim();
|
|
5297
|
+
const value = line.slice(sep5 + 1).trim();
|
|
4977
5298
|
switch (key) {
|
|
4978
5299
|
case "id":
|
|
4979
5300
|
out.id = stripQuotes(value);
|
|
@@ -5082,17 +5403,17 @@ ${content}`;
|
|
|
5082
5403
|
if (patch.summary !== void 0) updates.summary = `summary: ${quoteIfNeeded(patch.summary)}`;
|
|
5083
5404
|
if (patch.layer !== void 0) updates.layer = `layer: ${patch.layer}`;
|
|
5084
5405
|
if (patch.maturity !== void 0) updates.maturity = `maturity: ${patch.maturity}`;
|
|
5085
|
-
if (patch.tags !== void 0) updates.tags = `tags:
|
|
5406
|
+
if (patch.tags !== void 0) updates.tags = `tags: ${flowArray(patch.tags)}`;
|
|
5086
5407
|
if (patch.relevance_scope !== void 0) updates.relevance_scope = `relevance_scope: ${patch.relevance_scope}`;
|
|
5087
|
-
if (patch.relevance_paths !== void 0) updates.relevance_paths = `relevance_paths:
|
|
5408
|
+
if (patch.relevance_paths !== void 0) updates.relevance_paths = `relevance_paths: ${flowArray(patch.relevance_paths)}`;
|
|
5088
5409
|
if (patch.status !== void 0) updates.status = `status: ${patch.status}`;
|
|
5089
5410
|
if (patch.deferred_until !== void 0) updates.deferred_until = `deferred_until: ${quoteIfNeeded(patch.deferred_until)}`;
|
|
5090
5411
|
const lines = block.split(/\r?\n/u);
|
|
5091
5412
|
const seen = /* @__PURE__ */ new Set();
|
|
5092
5413
|
const newLines = [];
|
|
5093
5414
|
for (const line of lines) {
|
|
5094
|
-
const
|
|
5095
|
-
const key =
|
|
5415
|
+
const sep5 = line.indexOf(":");
|
|
5416
|
+
const key = sep5 === -1 ? "" : line.slice(0, sep5).trim();
|
|
5096
5417
|
if (key in updates) {
|
|
5097
5418
|
newLines.push(updates[key]);
|
|
5098
5419
|
seen.add(key);
|
|
@@ -5117,18 +5438,27 @@ function appendPatchLines(lines, patch) {
|
|
|
5117
5438
|
if (patch.summary !== void 0) lines.push(`summary: ${quoteIfNeeded(patch.summary)}`);
|
|
5118
5439
|
if (patch.layer !== void 0) lines.push(`layer: ${patch.layer}`);
|
|
5119
5440
|
if (patch.maturity !== void 0) lines.push(`maturity: ${patch.maturity}`);
|
|
5120
|
-
if (patch.tags !== void 0) lines.push(`tags:
|
|
5441
|
+
if (patch.tags !== void 0) lines.push(`tags: ${flowArray(patch.tags)}`);
|
|
5121
5442
|
if (patch.relevance_scope !== void 0) lines.push(`relevance_scope: ${patch.relevance_scope}`);
|
|
5122
|
-
if (patch.relevance_paths !== void 0) lines.push(`relevance_paths:
|
|
5443
|
+
if (patch.relevance_paths !== void 0) lines.push(`relevance_paths: ${flowArray(patch.relevance_paths)}`);
|
|
5123
5444
|
if (patch.status !== void 0) lines.push(`status: ${patch.status}`);
|
|
5124
5445
|
if (patch.deferred_until !== void 0) lines.push(`deferred_until: ${quoteIfNeeded(patch.deferred_until)}`);
|
|
5125
5446
|
}
|
|
5447
|
+
function flowArrayElement(value) {
|
|
5448
|
+
if (/[\n\r,\[\]{}"#:]/u.test(value) || /^\s|\s$/u.test(value)) {
|
|
5449
|
+
return JSON.stringify(value);
|
|
5450
|
+
}
|
|
5451
|
+
return value;
|
|
5452
|
+
}
|
|
5453
|
+
function flowArray(values) {
|
|
5454
|
+
return `[${values.map(flowArrayElement).join(", ")}]`;
|
|
5455
|
+
}
|
|
5126
5456
|
function quoteIfNeeded(value) {
|
|
5127
5457
|
if (/[\n\r]/u.test(value)) {
|
|
5128
5458
|
return JSON.stringify(value);
|
|
5129
5459
|
}
|
|
5130
|
-
if (/[
|
|
5131
|
-
return `"${value.replace(/"/gu, '\\"')}"`;
|
|
5460
|
+
if (/[\\:#\[\]{}&*!|>'"%@`,]|^\s|\s$/u.test(value)) {
|
|
5461
|
+
return `"${value.replace(/\\/gu, "\\\\").replace(/"/gu, '\\"')}"`;
|
|
5132
5462
|
}
|
|
5133
5463
|
return value;
|
|
5134
5464
|
}
|
|
@@ -5242,11 +5572,11 @@ function registerKnowledgeSections(server, tracker) {
|
|
|
5242
5572
|
|
|
5243
5573
|
// src/services/doctor.ts
|
|
5244
5574
|
import { execFileSync as execFileSync2, spawnSync } from "child_process";
|
|
5245
|
-
import { existsSync as existsSync8, readdirSync, readFileSync as
|
|
5246
|
-
import { access as access2, mkdir as
|
|
5575
|
+
import { existsSync as existsSync8, readdirSync, readFileSync as readFileSync4, statSync as statSync4 } from "fs";
|
|
5576
|
+
import { access as access2, mkdir as mkdir5, readFile as readFile13, rename as rename2, unlink as unlink3, writeFile as writeFile4 } from "fs/promises";
|
|
5247
5577
|
import { constants } from "fs";
|
|
5248
5578
|
import { homedir as homedir6 } from "os";
|
|
5249
|
-
import { isAbsolute as isAbsolute2, join as
|
|
5579
|
+
import { isAbsolute as isAbsolute2, join as join13, posix, relative as nodeRelative, resolve as resolve4, sep as sep4 } from "path";
|
|
5250
5580
|
import { Script } from "vm";
|
|
5251
5581
|
import { minimatch as minimatch3 } from "minimatch";
|
|
5252
5582
|
import { ZodError } from "zod";
|
|
@@ -5271,7 +5601,7 @@ import {
|
|
|
5271
5601
|
PAYLOAD_LIMIT_DEFAULT_HARD_BYTES,
|
|
5272
5602
|
PAYLOAD_LIMIT_DEFAULT_WARN_BYTES
|
|
5273
5603
|
} from "@fenglimg/fabric-shared/node/mcp-payload-guard";
|
|
5274
|
-
import { atomicWriteJson as atomicWriteJson3, atomicWriteText as atomicWriteText4 } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
5604
|
+
import { atomicWriteJson as atomicWriteJson3, atomicWriteText as atomicWriteText4, withFileLock as withFileLock4 } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
5275
5605
|
|
|
5276
5606
|
// src/services/legacy-serve-lock-probe.ts
|
|
5277
5607
|
import fs from "fs";
|
|
@@ -5502,6 +5832,54 @@ function matchesRelevancePath(editPath, relevancePaths) {
|
|
|
5502
5832
|
}
|
|
5503
5833
|
return false;
|
|
5504
5834
|
}
|
|
5835
|
+
function recallPathOverlaps(editPath, recallPaths) {
|
|
5836
|
+
if (recallPaths.length === 0) return false;
|
|
5837
|
+
const e = normalizePath(editPath);
|
|
5838
|
+
if (e.length === 0) return false;
|
|
5839
|
+
for (const rp of recallPaths) {
|
|
5840
|
+
const r = normalizePath(rp);
|
|
5841
|
+
if (r.length === 0) continue;
|
|
5842
|
+
if (e === r) return true;
|
|
5843
|
+
if (e.endsWith("/" + r) || r.endsWith("/" + e)) return true;
|
|
5844
|
+
if (e.startsWith(r + "/") || r.startsWith(e + "/")) return true;
|
|
5845
|
+
}
|
|
5846
|
+
return false;
|
|
5847
|
+
}
|
|
5848
|
+
function isSpecificGlob(glob) {
|
|
5849
|
+
const g = glob.trim();
|
|
5850
|
+
if (g.length === 0) return false;
|
|
5851
|
+
return g !== "**/*" && g !== "**" && g !== "*";
|
|
5852
|
+
}
|
|
5853
|
+
function computeExposedAndMutated(args) {
|
|
5854
|
+
const { narrowSurfacedBySession, dismissedBySession, editPathsBySession, kbIndex, idTypeMap } = args;
|
|
5855
|
+
const qualifiedIds = /* @__PURE__ */ new Set();
|
|
5856
|
+
let count = 0;
|
|
5857
|
+
for (const [sessionId, surfacedIds] of narrowSurfacedBySession) {
|
|
5858
|
+
const editPaths = editPathsBySession.get(sessionId);
|
|
5859
|
+
if (editPaths === void 0 || editPaths.length === 0) continue;
|
|
5860
|
+
const dismissed = dismissedBySession.get(sessionId);
|
|
5861
|
+
for (const id of surfacedIds) {
|
|
5862
|
+
if (dismissed !== void 0 && dismissed.has(id)) continue;
|
|
5863
|
+
const kb = kbIndex.get(id);
|
|
5864
|
+
if (kb === void 0 || kb.relevance_scope !== "narrow") continue;
|
|
5865
|
+
const specificGlobs = kb.relevance_paths.filter(isSpecificGlob);
|
|
5866
|
+
if (specificGlobs.length === 0) continue;
|
|
5867
|
+
const type = idTypeMap.get(id);
|
|
5868
|
+
if (type === "guidelines" || type === "processes") continue;
|
|
5869
|
+
let mutated = false;
|
|
5870
|
+
for (const p of editPaths) {
|
|
5871
|
+
if (matchesRelevancePath(p, specificGlobs)) {
|
|
5872
|
+
mutated = true;
|
|
5873
|
+
break;
|
|
5874
|
+
}
|
|
5875
|
+
}
|
|
5876
|
+
if (!mutated) continue;
|
|
5877
|
+
count += 1;
|
|
5878
|
+
qualifiedIds.add(id);
|
|
5879
|
+
}
|
|
5880
|
+
}
|
|
5881
|
+
return { count, ids: [...qualifiedIds].sort() };
|
|
5882
|
+
}
|
|
5505
5883
|
var ASSISTANT_TURN_COUNTER_PREFIX = "assistant_turn_observed";
|
|
5506
5884
|
function sumFoldedTurnCounters(rows, options) {
|
|
5507
5885
|
let sum = 0;
|
|
@@ -5525,6 +5903,7 @@ function sumFoldedTurnCounters(rows, options) {
|
|
|
5525
5903
|
}
|
|
5526
5904
|
async function runDoctorCiteCoverage(projectRoot, options) {
|
|
5527
5905
|
const layerFilter = options.layer ?? "all";
|
|
5906
|
+
const recallWindowMs = typeof options.recallWindowMs === "number" && options.recallWindowMs >= 0 ? options.recallWindowMs : 30 * 6e4;
|
|
5528
5907
|
const marker = await ensureCitePolicyActivatedMarker(projectRoot);
|
|
5529
5908
|
const contractMarker = await ensureCiteContractPolicyActivatedMarker(projectRoot);
|
|
5530
5909
|
const idTypeMap = await loadKbIdTypeMap(projectRoot);
|
|
@@ -5591,6 +5970,10 @@ async function runDoctorCiteCoverage(projectRoot, options) {
|
|
|
5591
5970
|
const assistantTurns = [];
|
|
5592
5971
|
const editEvents = [];
|
|
5593
5972
|
const fetchEvents = [];
|
|
5973
|
+
const plannedEvents = [];
|
|
5974
|
+
const hookSurfaceEvents = [];
|
|
5975
|
+
const fileMutatedEvents = [];
|
|
5976
|
+
const sessionEndedEvents = [];
|
|
5594
5977
|
for (const event of ledgerEvents) {
|
|
5595
5978
|
switch (event.event_type) {
|
|
5596
5979
|
case "assistant_turn_observed":
|
|
@@ -5602,10 +5985,33 @@ async function runDoctorCiteCoverage(projectRoot, options) {
|
|
|
5602
5985
|
case "knowledge_sections_fetched":
|
|
5603
5986
|
fetchEvents.push(event);
|
|
5604
5987
|
break;
|
|
5988
|
+
case "knowledge_context_planned":
|
|
5989
|
+
plannedEvents.push(event);
|
|
5990
|
+
break;
|
|
5991
|
+
case "hook_surface_emitted":
|
|
5992
|
+
hookSurfaceEvents.push(event);
|
|
5993
|
+
break;
|
|
5994
|
+
case "file_mutated":
|
|
5995
|
+
fileMutatedEvents.push(event);
|
|
5996
|
+
break;
|
|
5997
|
+
case "session_ended":
|
|
5998
|
+
sessionEndedEvents.push(event);
|
|
5999
|
+
break;
|
|
5605
6000
|
default:
|
|
5606
6001
|
break;
|
|
5607
6002
|
}
|
|
5608
6003
|
}
|
|
6004
|
+
const plannedBySession = /* @__PURE__ */ new Map();
|
|
6005
|
+
for (const planned of plannedEvents) {
|
|
6006
|
+
const sid = planned.session_id;
|
|
6007
|
+
if (typeof sid !== "string" || sid.length === 0) continue;
|
|
6008
|
+
const list = plannedBySession.get(sid) ?? [];
|
|
6009
|
+
list.push({ ts: planned.ts, target_paths: planned.target_paths ?? [] });
|
|
6010
|
+
plannedBySession.set(sid, list);
|
|
6011
|
+
}
|
|
6012
|
+
for (const list of plannedBySession.values()) {
|
|
6013
|
+
list.sort((a, b) => a.ts - b.ts);
|
|
6014
|
+
}
|
|
5609
6015
|
const filteredTurns = options.client === "all" ? assistantTurns : assistantTurns.filter((t) => t.client === options.client);
|
|
5610
6016
|
let clientSessionIds = null;
|
|
5611
6017
|
if (options.client !== "all") {
|
|
@@ -5677,6 +6083,7 @@ async function runDoctorCiteCoverage(projectRoot, options) {
|
|
|
5677
6083
|
perClientAccum.set(client, existing);
|
|
5678
6084
|
};
|
|
5679
6085
|
const sessionCitedKbs = /* @__PURE__ */ new Map();
|
|
6086
|
+
const dismissedBySession = /* @__PURE__ */ new Map();
|
|
5680
6087
|
const sessionEditPaths = /* @__PURE__ */ new Map();
|
|
5681
6088
|
for (const edit of editEvents) {
|
|
5682
6089
|
const sid = edit.session_id;
|
|
@@ -5754,6 +6161,8 @@ async function runDoctorCiteCoverage(projectRoot, options) {
|
|
|
5754
6161
|
let totalTurns = 0;
|
|
5755
6162
|
let qualifyingCites = 0;
|
|
5756
6163
|
let recalledUnverified = 0;
|
|
6164
|
+
const STORE_LOCAL_KEY = "local";
|
|
6165
|
+
const byStoreQualifying = {};
|
|
5757
6166
|
for (const turn of filteredTurns) {
|
|
5758
6167
|
totalTurns += 1;
|
|
5759
6168
|
bumpClient(turn.client, (m) => {
|
|
@@ -5766,6 +6175,15 @@ async function runDoctorCiteCoverage(projectRoot, options) {
|
|
|
5766
6175
|
set.add(id);
|
|
5767
6176
|
}
|
|
5768
6177
|
sessionCitedKbs.set(sid, set);
|
|
6178
|
+
for (let i = 0; i < turn.cite_tags.length; i += 1) {
|
|
6179
|
+
const id = turn.cite_ids[i];
|
|
6180
|
+
if (typeof id !== "string" || id.length === 0) continue;
|
|
6181
|
+
if (categorizeCiteTag(turn.cite_tags[i]).category === "dismissed") {
|
|
6182
|
+
const dset = dismissedBySession.get(sid) ?? /* @__PURE__ */ new Set();
|
|
6183
|
+
dset.add(id);
|
|
6184
|
+
dismissedBySession.set(sid, dset);
|
|
6185
|
+
}
|
|
6186
|
+
}
|
|
5769
6187
|
}
|
|
5770
6188
|
let turnHadApplied = false;
|
|
5771
6189
|
for (const tag of turn.cite_tags) {
|
|
@@ -5792,6 +6210,14 @@ async function runDoctorCiteCoverage(projectRoot, options) {
|
|
|
5792
6210
|
break;
|
|
5793
6211
|
}
|
|
5794
6212
|
}
|
|
6213
|
+
const turnCiteStores = turn.cite_stores ?? [];
|
|
6214
|
+
for (let i = 0; i < turn.cite_ids.length; i += 1) {
|
|
6215
|
+
const tag = turn.cite_tags[i];
|
|
6216
|
+
if (categorizeCiteTag(typeof tag === "string" ? tag : "none").category !== "applied") continue;
|
|
6217
|
+
const rawStore = turnCiteStores[i];
|
|
6218
|
+
const storeKey = typeof rawStore === "string" && rawStore.length > 0 ? rawStore : STORE_LOCAL_KEY;
|
|
6219
|
+
byStoreQualifying[storeKey] = (byStoreQualifying[storeKey] ?? 0) + 1;
|
|
6220
|
+
}
|
|
5795
6221
|
if (turnHadApplied && !isRecallVerified(turn)) {
|
|
5796
6222
|
recalledUnverified += 1;
|
|
5797
6223
|
bumpClient(turn.client, (m) => {
|
|
@@ -5836,6 +6262,7 @@ async function runDoctorCiteCoverage(projectRoot, options) {
|
|
|
5836
6262
|
let editsTouched = 0;
|
|
5837
6263
|
let expectedButMissed = 0;
|
|
5838
6264
|
let uncorrelatableEdits = 0;
|
|
6265
|
+
let recallBackedEdits = 0;
|
|
5839
6266
|
for (const edit of editEvents) {
|
|
5840
6267
|
const sid = edit.session_id;
|
|
5841
6268
|
const hasSid = typeof sid === "string" && sid.length > 0;
|
|
@@ -5846,6 +6273,17 @@ async function runDoctorCiteCoverage(projectRoot, options) {
|
|
|
5846
6273
|
}
|
|
5847
6274
|
editsTouched += 1;
|
|
5848
6275
|
if (!hasSid) continue;
|
|
6276
|
+
const recalls = plannedBySession.get(sid);
|
|
6277
|
+
if (recalls !== void 0) {
|
|
6278
|
+
for (const recall2 of recalls) {
|
|
6279
|
+
if (recall2.ts > edit.ts) break;
|
|
6280
|
+
if (recallWindowMs > 0 && edit.ts - recall2.ts > recallWindowMs) continue;
|
|
6281
|
+
if (recallPathOverlaps(edit.path, recall2.target_paths)) {
|
|
6282
|
+
recallBackedEdits += 1;
|
|
6283
|
+
break;
|
|
6284
|
+
}
|
|
6285
|
+
}
|
|
6286
|
+
}
|
|
5849
6287
|
const citedSet = sessionCitedKbs.get(sid) ?? /* @__PURE__ */ new Set();
|
|
5850
6288
|
for (const [kbId, kb] of kbIndex) {
|
|
5851
6289
|
if (kb.relevance_scope !== "narrow") continue;
|
|
@@ -5860,6 +6298,56 @@ async function runDoctorCiteCoverage(projectRoot, options) {
|
|
|
5860
6298
|
const noncompliantCites = expectedButMissed;
|
|
5861
6299
|
const complianceDenom = compliantCites + noncompliantCites;
|
|
5862
6300
|
const citeComplianceRate = complianceDenom > 0 ? compliantCites / complianceDenom : null;
|
|
6301
|
+
const recallCoverageRate = editsTouched > 0 ? recallBackedEdits / editsTouched : null;
|
|
6302
|
+
const narrowSurfacedBySession = /* @__PURE__ */ new Map();
|
|
6303
|
+
for (const surface of hookSurfaceEvents) {
|
|
6304
|
+
if (surface.hook_name !== "knowledge-hint-narrow") continue;
|
|
6305
|
+
if (surface.delivery_status !== "delivered") continue;
|
|
6306
|
+
const sid = surface.session_id;
|
|
6307
|
+
if (typeof sid !== "string" || sid.length === 0) continue;
|
|
6308
|
+
const set = narrowSurfacedBySession.get(sid) ?? /* @__PURE__ */ new Set();
|
|
6309
|
+
for (const id of surface.rendered_ids) {
|
|
6310
|
+
if (typeof id === "string" && id.length > 0) set.add(id);
|
|
6311
|
+
}
|
|
6312
|
+
narrowSurfacedBySession.set(sid, set);
|
|
6313
|
+
}
|
|
6314
|
+
const exposedAndMutated = computeExposedAndMutated({
|
|
6315
|
+
narrowSurfacedBySession,
|
|
6316
|
+
dismissedBySession,
|
|
6317
|
+
editPathsBySession: sessionEditPaths,
|
|
6318
|
+
kbIndex,
|
|
6319
|
+
idTypeMap
|
|
6320
|
+
});
|
|
6321
|
+
const surfacedIdsByEvent = /* @__PURE__ */ new Map();
|
|
6322
|
+
for (const surface of hookSurfaceEvents) {
|
|
6323
|
+
surfacedIdsByEvent.set(surface.id, surface.rendered_ids);
|
|
6324
|
+
}
|
|
6325
|
+
const seenMutationKeys = /* @__PURE__ */ new Set();
|
|
6326
|
+
const attributionKeys = /* @__PURE__ */ new Set();
|
|
6327
|
+
let mutationsObserved = 0;
|
|
6328
|
+
let unattributedWorkspaceDirty = 0;
|
|
6329
|
+
for (const mutation of fileMutatedEvents) {
|
|
6330
|
+
if (seenMutationKeys.has(mutation.tool_call_id)) continue;
|
|
6331
|
+
seenMutationKeys.add(mutation.tool_call_id);
|
|
6332
|
+
mutationsObserved += 1;
|
|
6333
|
+
const sourceEventId = mutation.source_event_id;
|
|
6334
|
+
const surfacedRenderedIds = typeof sourceEventId === "string" && sourceEventId.length > 0 ? surfacedIdsByEvent.get(sourceEventId) : void 0;
|
|
6335
|
+
if (surfacedRenderedIds === void 0 || surfacedRenderedIds.length === 0) {
|
|
6336
|
+
unattributedWorkspaceDirty += 1;
|
|
6337
|
+
continue;
|
|
6338
|
+
}
|
|
6339
|
+
const storeId = mutation.store_id ?? "";
|
|
6340
|
+
for (const stableId of surfacedRenderedIds) {
|
|
6341
|
+
if (typeof stableId !== "string" || stableId.length === 0) continue;
|
|
6342
|
+
attributionKeys.add(`${storeId}|${stableId}|${sourceEventId}`);
|
|
6343
|
+
}
|
|
6344
|
+
}
|
|
6345
|
+
const mutationPoolAttributed = attributionKeys.size;
|
|
6346
|
+
const closedSessions = /* @__PURE__ */ new Set();
|
|
6347
|
+
for (const ended of sessionEndedEvents) {
|
|
6348
|
+
const sid = typeof ended.session_id === "string" && ended.session_id.length > 0 ? ended.session_id : ended.id;
|
|
6349
|
+
closedSessions.add(sid);
|
|
6350
|
+
}
|
|
5863
6351
|
const metrics = {
|
|
5864
6352
|
edits_touched: editsTouched,
|
|
5865
6353
|
qualifying_cites: qualifyingCites,
|
|
@@ -5869,7 +6357,29 @@ async function runDoctorCiteCoverage(projectRoot, options) {
|
|
|
5869
6357
|
cite_compliance_rate: citeComplianceRate,
|
|
5870
6358
|
compliant_cites: compliantCites,
|
|
5871
6359
|
noncompliant_cites: noncompliantCites,
|
|
5872
|
-
uncorrelatable_edits: uncorrelatableEdits
|
|
6360
|
+
uncorrelatable_edits: uncorrelatableEdits,
|
|
6361
|
+
recall_backed_edits: recallBackedEdits,
|
|
6362
|
+
recall_coverage_rate: recallCoverageRate,
|
|
6363
|
+
exposed_and_mutated: {
|
|
6364
|
+
count: exposedAndMutated.count,
|
|
6365
|
+
...exposedAndMutated.ids.length > 0 ? { ids: exposedAndMutated.ids } : {}
|
|
6366
|
+
},
|
|
6367
|
+
// lifecycle-refactor W2-T4: PostToolUse mutation funnel (own fields, NEVER
|
|
6368
|
+
// folded into cite_compliance_rate — honesty 铁律).
|
|
6369
|
+
mutations_observed: { count: mutationsObserved },
|
|
6370
|
+
mutation_pool: {
|
|
6371
|
+
attributed: mutationPoolAttributed,
|
|
6372
|
+
unattributed_workspace_dirty: unattributedWorkspaceDirty
|
|
6373
|
+
},
|
|
6374
|
+
sessions_closed: { count: closedSessions.size },
|
|
6375
|
+
// lifecycle-refactor W3-T4 (§2 store 轴): per-store qualifying-cite breakdown.
|
|
6376
|
+
// Diagnostic split of qualifying_cites only — never touches compliance.
|
|
6377
|
+
// Omitted when no cite was observed (empty map → no field).
|
|
6378
|
+
...Object.keys(byStoreQualifying).length > 0 ? {
|
|
6379
|
+
by_store: Object.fromEntries(
|
|
6380
|
+
Object.entries(byStoreQualifying).map(([store, count]) => [store, { qualifying_cites: count }])
|
|
6381
|
+
)
|
|
6382
|
+
} : {}
|
|
5873
6383
|
};
|
|
5874
6384
|
let rollupDaysMerged = 0;
|
|
5875
6385
|
let rollupTrend;
|
|
@@ -5893,9 +6403,11 @@ async function runDoctorCiteCoverage(projectRoot, options) {
|
|
|
5893
6403
|
metrics.compliant_cites = (metrics.compliant_cites ?? 0) + (r.metrics.compliant_cites ?? 0);
|
|
5894
6404
|
metrics.noncompliant_cites = (metrics.noncompliant_cites ?? 0) + (r.metrics.noncompliant_cites ?? 0);
|
|
5895
6405
|
metrics.uncorrelatable_edits = (metrics.uncorrelatable_edits ?? 0) + (r.metrics.uncorrelatable_edits ?? 0);
|
|
6406
|
+
metrics.recall_backed_edits = (metrics.recall_backed_edits ?? 0) + (r.metrics.recall_backed_edits ?? 0);
|
|
5896
6407
|
}
|
|
5897
6408
|
const mergedDenom = (metrics.compliant_cites ?? 0) + (metrics.noncompliant_cites ?? 0);
|
|
5898
6409
|
metrics.cite_compliance_rate = mergedDenom > 0 ? (metrics.compliant_cites ?? 0) / mergedDenom : null;
|
|
6410
|
+
metrics.recall_coverage_rate = metrics.edits_touched > 0 ? (metrics.recall_backed_edits ?? 0) / metrics.edits_touched : null;
|
|
5899
6411
|
}
|
|
5900
6412
|
try {
|
|
5901
6413
|
const metricsRows = await readMetrics(projectRoot);
|
|
@@ -6223,7 +6735,15 @@ var HINT_SILENCE_COUNTER_FILE_REL = posix.join(
|
|
|
6223
6735
|
"hint-silence-counter"
|
|
6224
6736
|
);
|
|
6225
6737
|
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
6226
|
-
var MATURITY_LINE_PATTERN = /^maturity:\s*("?)(stable|endorsed|draft)\1\s*$/mu;
|
|
6738
|
+
var MATURITY_LINE_PATTERN = /^maturity:\s*("?)(stable|endorsed|draft|verified|proven)\1\s*$/mu;
|
|
6739
|
+
var CANONICAL_TO_LINT_MATURITY = {
|
|
6740
|
+
proven: "stable",
|
|
6741
|
+
verified: "endorsed",
|
|
6742
|
+
draft: "draft",
|
|
6743
|
+
// legacy values pass through unchanged.
|
|
6744
|
+
stable: "stable",
|
|
6745
|
+
endorsed: "endorsed"
|
|
6746
|
+
};
|
|
6227
6747
|
var CREATED_AT_LINE_PATTERN = /^created_at:\s*("?)([^"\n]+)\1\s*$/mu;
|
|
6228
6748
|
var TAGS_LINE_PATTERN = /^tags:\s*\[(.*)\]\s*$/mu;
|
|
6229
6749
|
var RELEVANCE_SCOPE_LINE_PATTERN = /^relevance_scope:\s*("?)(narrow|broad)\1\s*$/mu;
|
|
@@ -6479,7 +6999,13 @@ async function runDoctorReport(target) {
|
|
|
6479
6999
|
// werewolf-eval failure mode where description.summary == stable_id so
|
|
6480
7000
|
// hint output is "KT-PIT-0001 · KT-PIT-0001" (AI skips fetch). Built
|
|
6481
7001
|
// from the same MetaInspection so no extra disk reads.
|
|
6482
|
-
createKnowledgeSummaryOpaqueCheck(
|
|
7002
|
+
createKnowledgeSummaryOpaqueCheck(
|
|
7003
|
+
t,
|
|
7004
|
+
// v2.2 全砍 F10: also scan the read-set stores (team + personal) so opaque
|
|
7005
|
+
// store summaries — the personal layer the dogfood flagged — are caught,
|
|
7006
|
+
// not just the project agents.meta entries.
|
|
7007
|
+
inspectKnowledgeSummaryOpaque(meta, collectStoreKnowledgeSummaries(target))
|
|
7008
|
+
),
|
|
6483
7009
|
// rc.31 BUG-G2/G5: promote-ledger invariant. Sits adjacent to onboard
|
|
6484
7010
|
// coverage — both are observability advisories built off events.jsonl.
|
|
6485
7011
|
...promoteLedgerInvariant === null ? [] : [createPromoteLedgerInvariantCheck(t, promoteLedgerInvariant)],
|
|
@@ -6523,7 +7049,7 @@ async function runDoctorReport(target) {
|
|
|
6523
7049
|
warningCount: warnings.length,
|
|
6524
7050
|
infoCount: infos.length,
|
|
6525
7051
|
targetFiles: Object.fromEntries(
|
|
6526
|
-
TARGET_FILE_PATHS.map((path2) => [path2, existsSync8(
|
|
7052
|
+
TARGET_FILE_PATHS.map((path2) => [path2, existsSync8(join13(projectRoot, path2))])
|
|
6527
7053
|
),
|
|
6528
7054
|
// v2.0.0-rc.29 TASK-008 (BUG-F2): resolve and surface payload thresholds.
|
|
6529
7055
|
// Best-effort: a corrupt fabric.config.json should not fail doctor; on
|
|
@@ -6568,7 +7094,7 @@ async function runDoctorFix(target) {
|
|
|
6568
7094
|
}
|
|
6569
7095
|
}
|
|
6570
7096
|
if (before.fixable_errors.some((issue) => issue.code === "bootstrap_snapshot_drift")) {
|
|
6571
|
-
const snapshotPath =
|
|
7097
|
+
const snapshotPath = join13(projectRoot, ".fabric", "AGENTS.md");
|
|
6572
7098
|
await ensureParentDirectory(snapshotPath);
|
|
6573
7099
|
await atomicWriteText4(snapshotPath, BOOTSTRAP_CANONICAL);
|
|
6574
7100
|
fixed.push(findIssue(before.fixable_errors, "bootstrap_snapshot_drift"));
|
|
@@ -6663,6 +7189,10 @@ async function runDoctorFix(target) {
|
|
|
6663
7189
|
path: rotateResult.archivePath
|
|
6664
7190
|
});
|
|
6665
7191
|
}
|
|
7192
|
+
try {
|
|
7193
|
+
await flushMetrics(projectRoot);
|
|
7194
|
+
} catch {
|
|
7195
|
+
}
|
|
6666
7196
|
if (before.fixable_errors.some((issue) => issue.code === "mcp_config_in_wrong_file")) {
|
|
6667
7197
|
await fixMcpConfigInWrongFile(projectRoot);
|
|
6668
7198
|
fixed.push(findIssue(before.fixable_errors, "mcp_config_in_wrong_file"));
|
|
@@ -6670,7 +7200,7 @@ async function runDoctorFix(target) {
|
|
|
6670
7200
|
if (before.infos.some((issue) => issue.code === "stale_serve_lock")) {
|
|
6671
7201
|
const lockInspection = inspectStaleServeLock(projectRoot, Date.now());
|
|
6672
7202
|
if (lockInspection.present && !lockInspection.pidAlive) {
|
|
6673
|
-
const lockFilePath =
|
|
7203
|
+
const lockFilePath = join13(projectRoot, ".fabric", ".serve.lock");
|
|
6674
7204
|
try {
|
|
6675
7205
|
await unlink3(lockFilePath);
|
|
6676
7206
|
} catch (err) {
|
|
@@ -6812,6 +7342,10 @@ function createApplyLintMessage(succeeded, failed, manualErrorCount) {
|
|
|
6812
7342
|
);
|
|
6813
7343
|
return parts.join(" ");
|
|
6814
7344
|
}
|
|
7345
|
+
var LINT_TO_CANONICAL_MATURITY = {
|
|
7346
|
+
endorsed: "verified",
|
|
7347
|
+
draft: "draft"
|
|
7348
|
+
};
|
|
6815
7349
|
function rewriteFrontmatterMaturity(source, newMaturity) {
|
|
6816
7350
|
const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
|
|
6817
7351
|
const fm = FM_PATTERN.exec(source);
|
|
@@ -6819,12 +7353,16 @@ function rewriteFrontmatterMaturity(source, newMaturity) {
|
|
|
6819
7353
|
return null;
|
|
6820
7354
|
}
|
|
6821
7355
|
const block = fm[1];
|
|
6822
|
-
|
|
7356
|
+
const matMatch = MATURITY_LINE_PATTERN.exec(block);
|
|
7357
|
+
if (matMatch === null) {
|
|
6823
7358
|
return null;
|
|
6824
7359
|
}
|
|
7360
|
+
const currentValue = matMatch[2];
|
|
7361
|
+
const isCanonicalVocab = currentValue === "proven" || currentValue === "verified" || currentValue === "draft";
|
|
7362
|
+
const replacement = isCanonicalVocab ? LINT_TO_CANONICAL_MATURITY[newMaturity] : newMaturity;
|
|
6825
7363
|
const replacedBlock = block.replace(
|
|
6826
7364
|
MATURITY_LINE_PATTERN,
|
|
6827
|
-
(line) => line.replace(/(stable|endorsed|draft)/u,
|
|
7365
|
+
(line) => line.replace(/(stable|endorsed|draft|verified|proven)/u, replacement)
|
|
6828
7366
|
);
|
|
6829
7367
|
const blockStart = source.indexOf(block);
|
|
6830
7368
|
if (blockStart < 0) {
|
|
@@ -6862,7 +7400,7 @@ async function applyOrphanDemote(projectRoot, candidate, now) {
|
|
|
6862
7400
|
};
|
|
6863
7401
|
}
|
|
6864
7402
|
const detail = `${candidate.maturity} -> ${next}`;
|
|
6865
|
-
const absPath =
|
|
7403
|
+
const absPath = join13(projectRoot, candidate.path);
|
|
6866
7404
|
try {
|
|
6867
7405
|
const source = await readFile13(absPath, "utf8");
|
|
6868
7406
|
const rewritten = rewriteFrontmatterMaturity(source, next);
|
|
@@ -6929,11 +7467,11 @@ async function applyOrphanDemote(projectRoot, candidate, now) {
|
|
|
6929
7467
|
}
|
|
6930
7468
|
}
|
|
6931
7469
|
async function applyStaleArchive(projectRoot, candidate, now) {
|
|
6932
|
-
const sourceAbs =
|
|
6933
|
-
const destAbs =
|
|
7470
|
+
const sourceAbs = join13(projectRoot, candidate.path);
|
|
7471
|
+
const destAbs = join13(projectRoot, candidate.archive_path);
|
|
6934
7472
|
const detail = `${candidate.path} -> ${candidate.archive_path}`;
|
|
6935
7473
|
try {
|
|
6936
|
-
await
|
|
7474
|
+
await mkdir5(join13(destAbs, ".."), { recursive: true });
|
|
6937
7475
|
try {
|
|
6938
7476
|
await rename2(sourceAbs, destAbs);
|
|
6939
7477
|
} catch (renameError) {
|
|
@@ -6992,7 +7530,7 @@ async function applyStaleArchive(projectRoot, candidate, now) {
|
|
|
6992
7530
|
async function applyPendingAutoArchive(projectRoot, candidate, now) {
|
|
6993
7531
|
const detail = `${candidate.pending_path} -> ${candidate.archived_to}`;
|
|
6994
7532
|
try {
|
|
6995
|
-
await
|
|
7533
|
+
await mkdir5(join13(candidate.archived_to_abs, ".."), { recursive: true });
|
|
6996
7534
|
let moved = false;
|
|
6997
7535
|
if (candidate.layer === "team") {
|
|
6998
7536
|
try {
|
|
@@ -7065,11 +7603,11 @@ async function applyPendingAutoArchive(projectRoot, candidate, now) {
|
|
|
7065
7603
|
}
|
|
7066
7604
|
function relativePosix(projectRoot, absolutePath) {
|
|
7067
7605
|
const rel = nodeRelative(projectRoot, absolutePath);
|
|
7068
|
-
return rel.split(
|
|
7606
|
+
return rel.split(sep4).join("/");
|
|
7069
7607
|
}
|
|
7070
7608
|
async function applySessionHintsStaleCleanup(projectRoot, candidate) {
|
|
7071
7609
|
const detail = `deleted (${candidate.age_days}d old)`;
|
|
7072
|
-
const absPath =
|
|
7610
|
+
const absPath = join13(projectRoot, candidate.path);
|
|
7073
7611
|
try {
|
|
7074
7612
|
const { unlink: unlink4 } = await import("fs/promises");
|
|
7075
7613
|
await unlink4(absPath);
|
|
@@ -7090,21 +7628,23 @@ async function applySessionHintsStaleCleanup(projectRoot, candidate) {
|
|
|
7090
7628
|
}
|
|
7091
7629
|
}
|
|
7092
7630
|
async function applyIndexDriftFix(projectRoot, inspection) {
|
|
7093
|
-
const metaPath =
|
|
7631
|
+
const metaPath = join13(projectRoot, ".fabric", "agents.meta.json");
|
|
7094
7632
|
const detailParts = [];
|
|
7095
7633
|
try {
|
|
7096
|
-
|
|
7097
|
-
|
|
7098
|
-
|
|
7099
|
-
|
|
7100
|
-
|
|
7101
|
-
|
|
7102
|
-
|
|
7103
|
-
|
|
7104
|
-
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
|
|
7634
|
+
await withFileLock4(`${metaPath}.lock`, async () => {
|
|
7635
|
+
const meta = agentsMetaSchema5.parse(JSON.parse(await readFile13(metaPath, "utf8")));
|
|
7636
|
+
const baseCounters = AgentsMetaCountersSchema2.parse(meta.counters ?? void 0);
|
|
7637
|
+
const updatedCounters = {
|
|
7638
|
+
KP: { ...baseCounters.KP },
|
|
7639
|
+
KT: { ...baseCounters.KT }
|
|
7640
|
+
};
|
|
7641
|
+
for (const drift of inspection.drifts) {
|
|
7642
|
+
updatedCounters[drift.layer][drift.type] = drift.proposed_after;
|
|
7643
|
+
detailParts.push(`${drift.layer}.${drift.type}: ${drift.counter} -> ${drift.proposed_after}`);
|
|
7644
|
+
}
|
|
7645
|
+
const updated = { ...meta, counters: updatedCounters };
|
|
7646
|
+
await atomicWriteJson3(metaPath, updated, { indent: 2 });
|
|
7647
|
+
});
|
|
7108
7648
|
return {
|
|
7109
7649
|
kind: "knowledge_index_drift",
|
|
7110
7650
|
path: "agents.meta.json#counters",
|
|
@@ -7126,7 +7666,7 @@ function truncateErrorMessage(error) {
|
|
|
7126
7666
|
return raw.length > 240 ? `${raw.slice(0, 237)}...` : raw;
|
|
7127
7667
|
}
|
|
7128
7668
|
async function inspectForensic(projectRoot) {
|
|
7129
|
-
const path2 =
|
|
7669
|
+
const path2 = join13(projectRoot, ".fabric", "forensic.json");
|
|
7130
7670
|
try {
|
|
7131
7671
|
const parsed = forensicReportSchema.parse(JSON.parse(await readFile13(path2, "utf8")));
|
|
7132
7672
|
return { present: true, valid: true, report: parsed };
|
|
@@ -7138,12 +7678,12 @@ async function inspectForensic(projectRoot) {
|
|
|
7138
7678
|
}
|
|
7139
7679
|
}
|
|
7140
7680
|
function inspectMcpConfigInWrongFile(projectRoot) {
|
|
7141
|
-
const settingsPath =
|
|
7681
|
+
const settingsPath = join13(projectRoot, ".claude", "settings.json");
|
|
7142
7682
|
if (!existsSync8(settingsPath)) {
|
|
7143
7683
|
return { hasWrongEntry: false, settingsPath };
|
|
7144
7684
|
}
|
|
7145
7685
|
try {
|
|
7146
|
-
const parsed = JSON.parse(
|
|
7686
|
+
const parsed = JSON.parse(readFileSync4(settingsPath, "utf8"));
|
|
7147
7687
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
7148
7688
|
return { hasWrongEntry: false, settingsPath };
|
|
7149
7689
|
}
|
|
@@ -7159,7 +7699,7 @@ function inspectMcpConfigInWrongFile(projectRoot) {
|
|
|
7159
7699
|
}
|
|
7160
7700
|
}
|
|
7161
7701
|
async function inspectMeta(projectRoot) {
|
|
7162
|
-
const metaPath =
|
|
7702
|
+
const metaPath = join13(projectRoot, ".fabric", "agents.meta.json");
|
|
7163
7703
|
const built = await tryBuildRuleMeta(projectRoot);
|
|
7164
7704
|
try {
|
|
7165
7705
|
const raw = await readFile13(metaPath, "utf8");
|
|
@@ -7245,7 +7785,7 @@ function inspectContentRefs(projectRoot, meta) {
|
|
|
7245
7785
|
if (isPersonalKnowledge) {
|
|
7246
7786
|
continue;
|
|
7247
7787
|
}
|
|
7248
|
-
if (!existsSync8(
|
|
7788
|
+
if (!existsSync8(join13(projectRoot, contentRef))) {
|
|
7249
7789
|
missing.push(contentRef);
|
|
7250
7790
|
}
|
|
7251
7791
|
}
|
|
@@ -7329,8 +7869,8 @@ function inspectSkillRefMirror(projectRoot) {
|
|
|
7329
7869
|
const skillSlugs = ["fabric-archive", "fabric-review", "fabric-import"];
|
|
7330
7870
|
const driftedPaths = [];
|
|
7331
7871
|
for (const slug of skillSlugs) {
|
|
7332
|
-
const claudeRef =
|
|
7333
|
-
const codexRef =
|
|
7872
|
+
const claudeRef = join13(projectRoot, ".claude", "skills", slug, "ref");
|
|
7873
|
+
const codexRef = join13(projectRoot, ".codex", "skills", slug, "ref");
|
|
7334
7874
|
let claudeFiles = null;
|
|
7335
7875
|
let codexFiles = null;
|
|
7336
7876
|
try {
|
|
@@ -7355,12 +7895,12 @@ function inspectSkillRefMirror(projectRoot) {
|
|
|
7355
7895
|
let claudeBody;
|
|
7356
7896
|
let codexBody;
|
|
7357
7897
|
try {
|
|
7358
|
-
claudeBody =
|
|
7898
|
+
claudeBody = readFileSync4(join13(claudeRef, fname), "utf8");
|
|
7359
7899
|
} catch {
|
|
7360
7900
|
continue;
|
|
7361
7901
|
}
|
|
7362
7902
|
try {
|
|
7363
|
-
codexBody =
|
|
7903
|
+
codexBody = readFileSync4(join13(codexRef, fname), "utf8");
|
|
7364
7904
|
} catch {
|
|
7365
7905
|
continue;
|
|
7366
7906
|
}
|
|
@@ -7379,10 +7919,10 @@ function inspectSkillTokenBudget(projectRoot) {
|
|
|
7379
7919
|
const overSize = [];
|
|
7380
7920
|
let highestSeverity = "ok";
|
|
7381
7921
|
for (const slug of skillSlugs) {
|
|
7382
|
-
const skillMdPath =
|
|
7922
|
+
const skillMdPath = join13(projectRoot, ".claude", "skills", slug, "SKILL.md");
|
|
7383
7923
|
let body;
|
|
7384
7924
|
try {
|
|
7385
|
-
body =
|
|
7925
|
+
body = readFileSync4(skillMdPath, "utf8");
|
|
7386
7926
|
} catch {
|
|
7387
7927
|
continue;
|
|
7388
7928
|
}
|
|
@@ -7404,10 +7944,10 @@ function inspectSkillDescription(projectRoot) {
|
|
|
7404
7944
|
const CJK_PATTERN = /[㐀-䶿一-鿿]/;
|
|
7405
7945
|
const ASCII_PATTERN = /[a-zA-Z]{2,}/;
|
|
7406
7946
|
for (const slug of skillSlugs) {
|
|
7407
|
-
const skillMdPath =
|
|
7947
|
+
const skillMdPath = join13(projectRoot, ".claude", "skills", slug, "SKILL.md");
|
|
7408
7948
|
let body;
|
|
7409
7949
|
try {
|
|
7410
|
-
body =
|
|
7950
|
+
body = readFileSync4(skillMdPath, "utf8");
|
|
7411
7951
|
} catch {
|
|
7412
7952
|
continue;
|
|
7413
7953
|
}
|
|
@@ -7522,9 +8062,9 @@ function inspectDraftBacklog(projectRoot) {
|
|
|
7522
8062
|
const MIN_TOTAL_FOR_RATIO = 10;
|
|
7523
8063
|
let draftCount = 0;
|
|
7524
8064
|
let totalCount = 0;
|
|
7525
|
-
const knowledgeRoot =
|
|
8065
|
+
const knowledgeRoot = join13(projectRoot, ".fabric", "knowledge");
|
|
7526
8066
|
for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
|
|
7527
|
-
const dir =
|
|
8067
|
+
const dir = join13(knowledgeRoot, typeDir);
|
|
7528
8068
|
if (!existsSync8(dir)) continue;
|
|
7529
8069
|
let entries;
|
|
7530
8070
|
try {
|
|
@@ -7537,7 +8077,7 @@ function inspectDraftBacklog(projectRoot) {
|
|
|
7537
8077
|
if (CANONICAL_KNOWLEDGE_FILENAME_PATTERN.exec(entry.name) === null) continue;
|
|
7538
8078
|
let maturity;
|
|
7539
8079
|
try {
|
|
7540
|
-
maturity = extractMaturityFull(
|
|
8080
|
+
maturity = extractMaturityFull(readFileSync4(join13(dir, entry.name), "utf8"));
|
|
7541
8081
|
} catch {
|
|
7542
8082
|
continue;
|
|
7543
8083
|
}
|
|
@@ -7581,7 +8121,7 @@ async function inspectDraftAutoPromote(projectRoot, now = Date.now()) {
|
|
|
7581
8121
|
if (drifted.has(entry.stable_id)) continue;
|
|
7582
8122
|
let createdAt;
|
|
7583
8123
|
try {
|
|
7584
|
-
createdAt = extractKnowledgeFrontmatterCreatedAt(
|
|
8124
|
+
createdAt = extractKnowledgeFrontmatterCreatedAt(readFileSync4(entry.absPath, "utf8"));
|
|
7585
8125
|
} catch {
|
|
7586
8126
|
continue;
|
|
7587
8127
|
}
|
|
@@ -7624,7 +8164,7 @@ async function applyDraftAutoPromote(projectRoot, candidates) {
|
|
|
7624
8164
|
for (const candidate of candidates) {
|
|
7625
8165
|
let source;
|
|
7626
8166
|
try {
|
|
7627
|
-
source =
|
|
8167
|
+
source = readFileSync4(candidate.absPath, "utf8");
|
|
7628
8168
|
} catch {
|
|
7629
8169
|
continue;
|
|
7630
8170
|
}
|
|
@@ -7654,7 +8194,7 @@ function inspectKnowledgeTagsEmpty(projectRoot) {
|
|
|
7654
8194
|
for (const entry of iterateCanonicalEntries(projectRoot, /* @__PURE__ */ new Map())) {
|
|
7655
8195
|
let source;
|
|
7656
8196
|
try {
|
|
7657
|
-
source =
|
|
8197
|
+
source = readFileSync4(entry.absPath, "utf8");
|
|
7658
8198
|
} catch {
|
|
7659
8199
|
continue;
|
|
7660
8200
|
}
|
|
@@ -7676,7 +8216,7 @@ function inspectKnowledgeTagsEmpty(projectRoot) {
|
|
|
7676
8216
|
};
|
|
7677
8217
|
}
|
|
7678
8218
|
async function inspectKnowledgeTestIndex(projectRoot) {
|
|
7679
|
-
const path2 =
|
|
8219
|
+
const path2 = join13(projectRoot, ".fabric", ".cache", "knowledge-test.index.json");
|
|
7680
8220
|
const built = await tryBuildRuleMeta(projectRoot);
|
|
7681
8221
|
try {
|
|
7682
8222
|
const index = knowledgeTestIndexSchema2.parse(JSON.parse(await readFile13(path2, "utf8")));
|
|
@@ -7700,8 +8240,8 @@ async function inspectKnowledgeTestIndex(projectRoot) {
|
|
|
7700
8240
|
}
|
|
7701
8241
|
function inspectBootstrapAnchor(projectRoot) {
|
|
7702
8242
|
return {
|
|
7703
|
-
hasAgentsMd: existsSync8(
|
|
7704
|
-
hasClaudeMd: existsSync8(
|
|
8243
|
+
hasAgentsMd: existsSync8(join13(projectRoot, "AGENTS.md")),
|
|
8244
|
+
hasClaudeMd: existsSync8(join13(projectRoot, "CLAUDE.md"))
|
|
7705
8245
|
};
|
|
7706
8246
|
}
|
|
7707
8247
|
var BOOTSTRAP_MARKER_MIGRATION_TARGETS = [
|
|
@@ -7713,7 +8253,7 @@ var BOOTSTRAP_MARKER_MIGRATION_TARGETS = [
|
|
|
7713
8253
|
async function inspectBootstrapMarkerMigration(target) {
|
|
7714
8254
|
const filesNeedingMigration = [];
|
|
7715
8255
|
for (const rel of BOOTSTRAP_MARKER_MIGRATION_TARGETS) {
|
|
7716
|
-
const abs =
|
|
8256
|
+
const abs = join13(target, rel);
|
|
7717
8257
|
if (!existsSync8(abs)) {
|
|
7718
8258
|
continue;
|
|
7719
8259
|
}
|
|
@@ -7751,7 +8291,7 @@ function createBootstrapMarkerMigrationCheck(t, inspection) {
|
|
|
7751
8291
|
);
|
|
7752
8292
|
}
|
|
7753
8293
|
async function inspectL1BootstrapSnapshotDrift(target) {
|
|
7754
|
-
const abs =
|
|
8294
|
+
const abs = join13(target, ".fabric", "AGENTS.md");
|
|
7755
8295
|
if (!existsSync8(abs)) {
|
|
7756
8296
|
return { status: "missing", canonical: BOOTSTRAP_CANONICAL, onDisk: null };
|
|
7757
8297
|
}
|
|
@@ -7783,7 +8323,7 @@ function createL1BootstrapSnapshotDriftCheck(t, inspection) {
|
|
|
7783
8323
|
);
|
|
7784
8324
|
}
|
|
7785
8325
|
async function inspectL2ManagedBlockDrift(target) {
|
|
7786
|
-
const snapshotPath =
|
|
8326
|
+
const snapshotPath = join13(target, ".fabric", "AGENTS.md");
|
|
7787
8327
|
if (!existsSync8(snapshotPath)) {
|
|
7788
8328
|
return { status: "ok", drifted: [] };
|
|
7789
8329
|
}
|
|
@@ -7793,7 +8333,7 @@ async function inspectL2ManagedBlockDrift(target) {
|
|
|
7793
8333
|
} catch {
|
|
7794
8334
|
return { status: "ok", drifted: [] };
|
|
7795
8335
|
}
|
|
7796
|
-
const projectRulesPath =
|
|
8336
|
+
const projectRulesPath = join13(target, ".fabric", "project-rules.md");
|
|
7797
8337
|
let expectedBody = snapshot;
|
|
7798
8338
|
if (existsSync8(projectRulesPath)) {
|
|
7799
8339
|
try {
|
|
@@ -7807,8 +8347,8 @@ ${projectRules}`;
|
|
|
7807
8347
|
const drifted = [];
|
|
7808
8348
|
let anyManagedBlockFound = false;
|
|
7809
8349
|
const blockTargets = [
|
|
7810
|
-
|
|
7811
|
-
|
|
8350
|
+
join13(target, "AGENTS.md"),
|
|
8351
|
+
join13(target, ".cursor", "rules", "fabric-bootstrap.mdc")
|
|
7812
8352
|
];
|
|
7813
8353
|
for (const abs of blockTargets) {
|
|
7814
8354
|
if (!existsSync8(abs)) {
|
|
@@ -7842,7 +8382,7 @@ ${projectRules}`;
|
|
|
7842
8382
|
drifted.push({ path: abs, expected: expectedBody, actual: body });
|
|
7843
8383
|
}
|
|
7844
8384
|
}
|
|
7845
|
-
const claudeMdPath =
|
|
8385
|
+
const claudeMdPath = join13(target, "CLAUDE.md");
|
|
7846
8386
|
if (existsSync8(claudeMdPath)) {
|
|
7847
8387
|
let claudeContent;
|
|
7848
8388
|
try {
|
|
@@ -7913,10 +8453,10 @@ function createBootstrapAnchorCheck(t, inspection) {
|
|
|
7913
8453
|
);
|
|
7914
8454
|
}
|
|
7915
8455
|
function inspectKnowledgeDirMissing(projectRoot) {
|
|
7916
|
-
const knowledgeRoot =
|
|
8456
|
+
const knowledgeRoot = join13(projectRoot, ".fabric", "knowledge");
|
|
7917
8457
|
const missingSubdirs = [];
|
|
7918
8458
|
for (const sub of KNOWLEDGE_SUBDIRS3) {
|
|
7919
|
-
const path2 =
|
|
8459
|
+
const path2 = join13(knowledgeRoot, sub);
|
|
7920
8460
|
if (!existsSync8(path2)) {
|
|
7921
8461
|
missingSubdirs.push(`.fabric/knowledge/${sub}`);
|
|
7922
8462
|
}
|
|
@@ -7925,12 +8465,12 @@ function inspectKnowledgeDirMissing(projectRoot) {
|
|
|
7925
8465
|
}
|
|
7926
8466
|
function inspectBaselineFilenameFormat(projectRoot) {
|
|
7927
8467
|
const offenders = [];
|
|
7928
|
-
const knowledgeRoot =
|
|
8468
|
+
const knowledgeRoot = join13(projectRoot, ".fabric", "knowledge");
|
|
7929
8469
|
if (!existsSync8(knowledgeRoot)) {
|
|
7930
8470
|
return { offenders };
|
|
7931
8471
|
}
|
|
7932
8472
|
for (const sub of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
|
|
7933
|
-
const dir =
|
|
8473
|
+
const dir = join13(knowledgeRoot, sub);
|
|
7934
8474
|
if (!existsSync8(dir)) {
|
|
7935
8475
|
continue;
|
|
7936
8476
|
}
|
|
@@ -7948,10 +8488,10 @@ function inspectBaselineFilenameFormat(projectRoot) {
|
|
|
7948
8488
|
if (BASELINE_ID_PREFIXED_FILENAME_PATTERN.test(entryName)) {
|
|
7949
8489
|
continue;
|
|
7950
8490
|
}
|
|
7951
|
-
const abs =
|
|
8491
|
+
const abs = join13(dir, entryName);
|
|
7952
8492
|
let source;
|
|
7953
8493
|
try {
|
|
7954
|
-
source =
|
|
8494
|
+
source = readFileSync4(abs, "utf8");
|
|
7955
8495
|
} catch {
|
|
7956
8496
|
continue;
|
|
7957
8497
|
}
|
|
@@ -8529,17 +9069,17 @@ function okCheck(name, message) {
|
|
|
8529
9069
|
return { name, status: "ok", message };
|
|
8530
9070
|
}
|
|
8531
9071
|
function inspectHooksWired(projectRoot) {
|
|
8532
|
-
const claudeDir =
|
|
9072
|
+
const claudeDir = join13(projectRoot, ".claude");
|
|
8533
9073
|
if (!existsSync8(claudeDir)) {
|
|
8534
9074
|
return { status: "skipped", missingHooks: [] };
|
|
8535
9075
|
}
|
|
8536
|
-
const settingsPath =
|
|
9076
|
+
const settingsPath = join13(projectRoot, ".claude", "settings.json");
|
|
8537
9077
|
if (!existsSync8(settingsPath)) {
|
|
8538
9078
|
return { status: "missing-settings", missingHooks: [] };
|
|
8539
9079
|
}
|
|
8540
9080
|
let raw;
|
|
8541
9081
|
try {
|
|
8542
|
-
raw =
|
|
9082
|
+
raw = readFileSync4(settingsPath, "utf8");
|
|
8543
9083
|
} catch {
|
|
8544
9084
|
return { status: "missing-settings", missingHooks: [] };
|
|
8545
9085
|
}
|
|
@@ -8624,7 +9164,7 @@ function createHooksWiredCheck(t, inspection) {
|
|
|
8624
9164
|
function inspectHooksContentDrift(projectRoot) {
|
|
8625
9165
|
const hookFilesByBasename = /* @__PURE__ */ new Map();
|
|
8626
9166
|
for (const { client, dir } of HOOKS_RUNTIME_CLIENT_DIRS) {
|
|
8627
|
-
const absDir =
|
|
9167
|
+
const absDir = join13(projectRoot, dir);
|
|
8628
9168
|
if (!existsSync8(absDir)) continue;
|
|
8629
9169
|
let entries;
|
|
8630
9170
|
try {
|
|
@@ -8634,7 +9174,7 @@ function inspectHooksContentDrift(projectRoot) {
|
|
|
8634
9174
|
}
|
|
8635
9175
|
for (const name of entries) {
|
|
8636
9176
|
if (!name.endsWith(".cjs")) continue;
|
|
8637
|
-
const abs =
|
|
9177
|
+
const abs = join13(absDir, name);
|
|
8638
9178
|
let stat3;
|
|
8639
9179
|
try {
|
|
8640
9180
|
stat3 = statSync4(abs);
|
|
@@ -8655,7 +9195,7 @@ function inspectHooksContentDrift(projectRoot) {
|
|
|
8655
9195
|
const hashes = [];
|
|
8656
9196
|
for (const { client, abs } of copies) {
|
|
8657
9197
|
try {
|
|
8658
|
-
const body =
|
|
9198
|
+
const body = readFileSync4(abs);
|
|
8659
9199
|
hashes.push({ client, sha: sha256(body.toString("utf8")) });
|
|
8660
9200
|
} catch {
|
|
8661
9201
|
}
|
|
@@ -8711,7 +9251,7 @@ function inspectHooksRuntime(projectRoot) {
|
|
|
8711
9251
|
const issues = [];
|
|
8712
9252
|
let scanned = 0;
|
|
8713
9253
|
for (const { client, dir } of HOOKS_RUNTIME_CLIENT_DIRS) {
|
|
8714
|
-
const absDir =
|
|
9254
|
+
const absDir = join13(projectRoot, dir);
|
|
8715
9255
|
if (!existsSync8(absDir)) continue;
|
|
8716
9256
|
let entries;
|
|
8717
9257
|
try {
|
|
@@ -8721,7 +9261,7 @@ function inspectHooksRuntime(projectRoot) {
|
|
|
8721
9261
|
}
|
|
8722
9262
|
for (const name of entries) {
|
|
8723
9263
|
if (!name.endsWith(".cjs")) continue;
|
|
8724
|
-
const abs =
|
|
9264
|
+
const abs = join13(absDir, name);
|
|
8725
9265
|
const displayPath = `${dir}/${name}`;
|
|
8726
9266
|
let stat3;
|
|
8727
9267
|
try {
|
|
@@ -8733,7 +9273,7 @@ function inspectHooksRuntime(projectRoot) {
|
|
|
8733
9273
|
scanned += 1;
|
|
8734
9274
|
let body;
|
|
8735
9275
|
try {
|
|
8736
|
-
body =
|
|
9276
|
+
body = readFileSync4(abs, "utf8");
|
|
8737
9277
|
} catch (err) {
|
|
8738
9278
|
issues.push({
|
|
8739
9279
|
path: displayPath,
|
|
@@ -8887,15 +9427,17 @@ function inspectGlobalCliVersion(spawn = defaultGlobalCliSpawn) {
|
|
|
8887
9427
|
return { status: "unparseable", detail: `exit ${res.status ?? "?"}` };
|
|
8888
9428
|
}
|
|
8889
9429
|
const raw = (res.stdout ?? "").trim();
|
|
8890
|
-
const m = /(\d+)\.(\d+)\.(\d+)
|
|
9430
|
+
const m = /(\d+)\.(\d+)\.(\d+)(?:-rc\.(\d+))?/.exec(raw);
|
|
8891
9431
|
if (!m) {
|
|
8892
9432
|
return { status: "unparseable", detail: raw.slice(0, 80) };
|
|
8893
9433
|
}
|
|
8894
|
-
const
|
|
8895
|
-
const
|
|
8896
|
-
const
|
|
8897
|
-
const
|
|
8898
|
-
|
|
9434
|
+
const hasRc = m[4] !== void 0;
|
|
9435
|
+
const version = hasRc ? `${m[1]}.${m[2]}.${m[3]}-rc.${m[4]}` : `${m[1]}.${m[2]}.${m[3]}`;
|
|
9436
|
+
const minM = /(\d+)\.(\d+)\.(\d+)(?:-rc\.(\d+))?/.exec(MIN_SUPPORTED_GLOBAL_CLI_VERSION);
|
|
9437
|
+
const observed = [Number(m[1]), Number(m[2]), Number(m[3]), hasRc ? Number(m[4]) : Infinity];
|
|
9438
|
+
const min = minM ? [Number(minM[1]), Number(minM[2]), Number(minM[3]), minM[4] !== void 0 ? Number(minM[4]) : Infinity] : [0, 0, 0, 0];
|
|
9439
|
+
const diffAt = observed.findIndex((v, i) => v !== min[i]);
|
|
9440
|
+
if (diffAt !== -1 && observed[diffAt] < min[diffAt]) {
|
|
8899
9441
|
return { status: "outdated", version, minVersion: MIN_SUPPORTED_GLOBAL_CLI_VERSION };
|
|
8900
9442
|
}
|
|
8901
9443
|
return { status: "ok", version };
|
|
@@ -8940,7 +9482,7 @@ function createGlobalCliVersionCheck(t, inspection) {
|
|
|
8940
9482
|
);
|
|
8941
9483
|
}
|
|
8942
9484
|
var KNOWLEDGE_SUMMARY_OPAQUE_THRESHOLD = 0.3;
|
|
8943
|
-
function inspectKnowledgeSummaryOpaque(meta) {
|
|
9485
|
+
function inspectKnowledgeSummaryOpaque(meta, storeSummaries = []) {
|
|
8944
9486
|
const baseline = {
|
|
8945
9487
|
totalWithDescription: 0,
|
|
8946
9488
|
opaqueCount: 0,
|
|
@@ -8965,6 +9507,14 @@ function inspectKnowledgeSummaryOpaque(meta) {
|
|
|
8965
9507
|
opaqueIds.push(stableId);
|
|
8966
9508
|
}
|
|
8967
9509
|
}
|
|
9510
|
+
for (const entry of storeSummaries) {
|
|
9511
|
+
total += 1;
|
|
9512
|
+
const summary = entry.summary.trim();
|
|
9513
|
+
const localId = entry.stableId.includes(":") ? entry.stableId.slice(entry.stableId.indexOf(":") + 1) : entry.stableId;
|
|
9514
|
+
if (summary.length === 0 || summary === entry.stableId.trim() || summary === localId.trim()) {
|
|
9515
|
+
opaqueIds.push(entry.stableId);
|
|
9516
|
+
}
|
|
9517
|
+
}
|
|
8968
9518
|
if (total === 0) {
|
|
8969
9519
|
return { status: "ok", ...baseline };
|
|
8970
9520
|
}
|
|
@@ -9040,7 +9590,7 @@ function findIssue(issues, code) {
|
|
|
9040
9590
|
};
|
|
9041
9591
|
}
|
|
9042
9592
|
async function inspectMetaManuallyDiverged(projectRoot) {
|
|
9043
|
-
const metaPath =
|
|
9593
|
+
const metaPath = join13(projectRoot, ".fabric", "agents.meta.json");
|
|
9044
9594
|
if (!existsSync8(metaPath)) {
|
|
9045
9595
|
return { extraMetaEntries: [], hashMismatchEntries: [], readable: false };
|
|
9046
9596
|
}
|
|
@@ -9066,7 +9616,7 @@ async function inspectMetaManuallyDiverged(projectRoot) {
|
|
|
9066
9616
|
continue;
|
|
9067
9617
|
}
|
|
9068
9618
|
try {
|
|
9069
|
-
const content =
|
|
9619
|
+
const content = readFileSync4(absPath, "utf8");
|
|
9070
9620
|
const diskHash = sha256(content);
|
|
9071
9621
|
if (node.hash !== "" && node.hash !== diskHash) {
|
|
9072
9622
|
hashMismatchEntries.push(contentRef);
|
|
@@ -9079,7 +9629,7 @@ async function inspectMetaManuallyDiverged(projectRoot) {
|
|
|
9079
9629
|
}
|
|
9080
9630
|
function inspectKnowledgeDirUnindexed(projectRoot, meta) {
|
|
9081
9631
|
const physicalMdFiles = /* @__PURE__ */ new Set();
|
|
9082
|
-
collectMdFilesUnder(physicalMdFiles, projectRoot,
|
|
9632
|
+
collectMdFilesUnder(physicalMdFiles, projectRoot, join13(projectRoot, ".fabric", "knowledge"), ".fabric/knowledge");
|
|
9083
9633
|
if (physicalMdFiles.size === 0) {
|
|
9084
9634
|
return { unindexedFiles: [] };
|
|
9085
9635
|
}
|
|
@@ -9104,7 +9654,7 @@ function collectMdFilesUnder(out, projectRoot, rootDir, relPrefix) {
|
|
|
9104
9654
|
continue;
|
|
9105
9655
|
}
|
|
9106
9656
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
9107
|
-
const abs =
|
|
9657
|
+
const abs = join13(dir, entry.name);
|
|
9108
9658
|
if (entry.isDirectory()) {
|
|
9109
9659
|
if (entry.name !== "pending" && entry.name !== "archive") {
|
|
9110
9660
|
stack.push(abs);
|
|
@@ -9137,7 +9687,7 @@ function createKnowledgeDirUnindexedCheck(t, inspection) {
|
|
|
9137
9687
|
}
|
|
9138
9688
|
async function inspectStableIdCollisions(projectRoot) {
|
|
9139
9689
|
const found = [];
|
|
9140
|
-
const knowledgeDir =
|
|
9690
|
+
const knowledgeDir = join13(projectRoot, ".fabric", "knowledge");
|
|
9141
9691
|
if (existsSync8(knowledgeDir)) {
|
|
9142
9692
|
const stack = [knowledgeDir];
|
|
9143
9693
|
while (stack.length > 0) {
|
|
@@ -9146,7 +9696,7 @@ async function inspectStableIdCollisions(projectRoot) {
|
|
|
9146
9696
|
continue;
|
|
9147
9697
|
}
|
|
9148
9698
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
9149
|
-
const abs =
|
|
9699
|
+
const abs = join13(dir, entry.name);
|
|
9150
9700
|
if (entry.isDirectory()) {
|
|
9151
9701
|
stack.push(abs);
|
|
9152
9702
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
@@ -9325,17 +9875,17 @@ function createMetaManuallyDivergedCheck(t, inspection) {
|
|
|
9325
9875
|
}
|
|
9326
9876
|
function inspectPreexistingRootFiles(projectRoot) {
|
|
9327
9877
|
const candidates = ["CLAUDE.md", "AGENTS.md"];
|
|
9328
|
-
const detected = candidates.filter((name) => existsSync8(
|
|
9878
|
+
const detected = candidates.filter((name) => existsSync8(join13(projectRoot, name)));
|
|
9329
9879
|
return { detected };
|
|
9330
9880
|
}
|
|
9331
9881
|
async function inspectFilesystemEditFallback(projectRoot) {
|
|
9332
|
-
const knowledgeRoot =
|
|
9882
|
+
const knowledgeRoot = join13(projectRoot, ".fabric", "knowledge");
|
|
9333
9883
|
if (!existsSync8(knowledgeRoot)) {
|
|
9334
9884
|
return { synthesized: 0, synthesizedStableIds: [] };
|
|
9335
9885
|
}
|
|
9336
9886
|
const canonicalIds = /* @__PURE__ */ new Set();
|
|
9337
9887
|
for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
|
|
9338
|
-
const dir =
|
|
9888
|
+
const dir = join13(knowledgeRoot, typeDir);
|
|
9339
9889
|
if (!existsSync8(dir)) {
|
|
9340
9890
|
continue;
|
|
9341
9891
|
}
|
|
@@ -9571,7 +10121,10 @@ function extractKnowledgeFrontmatterMaturity(source) {
|
|
|
9571
10121
|
return null;
|
|
9572
10122
|
}
|
|
9573
10123
|
const match = MATURITY_LINE_PATTERN.exec(fm[1]);
|
|
9574
|
-
|
|
10124
|
+
if (match === null) {
|
|
10125
|
+
return null;
|
|
10126
|
+
}
|
|
10127
|
+
return CANONICAL_TO_LINT_MATURITY[match[2]] ?? null;
|
|
9575
10128
|
}
|
|
9576
10129
|
function isKnowledgeFrontmatterTagsEmpty(source) {
|
|
9577
10130
|
const FM_PATTERN = /^(?:)?---\r?\n([\s\S]*?)\r?\n---/u;
|
|
@@ -9600,12 +10153,12 @@ function extractKnowledgeFrontmatterCreatedAt(source) {
|
|
|
9600
10153
|
return Number.isFinite(parsed) ? parsed : null;
|
|
9601
10154
|
}
|
|
9602
10155
|
function* iterateCanonicalEntries(projectRoot, lastActiveIndex) {
|
|
9603
|
-
const knowledgeRoot =
|
|
10156
|
+
const knowledgeRoot = join13(projectRoot, ".fabric", "knowledge");
|
|
9604
10157
|
if (!existsSync8(knowledgeRoot)) {
|
|
9605
10158
|
return;
|
|
9606
10159
|
}
|
|
9607
10160
|
for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
|
|
9608
|
-
const dir =
|
|
10161
|
+
const dir = join13(knowledgeRoot, typeDir);
|
|
9609
10162
|
if (!existsSync8(dir)) {
|
|
9610
10163
|
continue;
|
|
9611
10164
|
}
|
|
@@ -9624,10 +10177,10 @@ function* iterateCanonicalEntries(projectRoot, lastActiveIndex) {
|
|
|
9624
10177
|
continue;
|
|
9625
10178
|
}
|
|
9626
10179
|
const stableId = match[1];
|
|
9627
|
-
const absPath =
|
|
10180
|
+
const absPath = join13(dir, entry.name);
|
|
9628
10181
|
let source;
|
|
9629
10182
|
try {
|
|
9630
|
-
source =
|
|
10183
|
+
source = readFileSync4(absPath, "utf8");
|
|
9631
10184
|
} catch {
|
|
9632
10185
|
continue;
|
|
9633
10186
|
}
|
|
@@ -9701,8 +10254,8 @@ async function inspectStaleArchive(projectRoot, now) {
|
|
|
9701
10254
|
return { candidates };
|
|
9702
10255
|
}
|
|
9703
10256
|
function* iteratePendingFiles(projectRoot, now) {
|
|
9704
|
-
const teamRoot =
|
|
9705
|
-
const personalRoot =
|
|
10257
|
+
const teamRoot = join13(projectRoot, ".fabric", "knowledge", "pending");
|
|
10258
|
+
const personalRoot = join13(resolvePersonalRootForPending(), ".fabric", "knowledge", "pending");
|
|
9706
10259
|
for (const [layer, root, displayPrefix] of [
|
|
9707
10260
|
["team", teamRoot, ".fabric/knowledge/pending"],
|
|
9708
10261
|
["personal", personalRoot, "~/.fabric/knowledge/pending"]
|
|
@@ -9717,7 +10270,7 @@ function* iteratePendingFiles(projectRoot, now) {
|
|
|
9717
10270
|
continue;
|
|
9718
10271
|
}
|
|
9719
10272
|
for (const typeDir of typeDirs) {
|
|
9720
|
-
const dir =
|
|
10273
|
+
const dir = join13(root, typeDir);
|
|
9721
10274
|
let entries;
|
|
9722
10275
|
try {
|
|
9723
10276
|
entries = readdirSync(dir, { withFileTypes: true });
|
|
@@ -9728,10 +10281,10 @@ function* iteratePendingFiles(projectRoot, now) {
|
|
|
9728
10281
|
if (!entry.isFile() || !entry.name.endsWith(".md")) {
|
|
9729
10282
|
continue;
|
|
9730
10283
|
}
|
|
9731
|
-
const absPath =
|
|
10284
|
+
const absPath = join13(dir, entry.name);
|
|
9732
10285
|
let source = "";
|
|
9733
10286
|
try {
|
|
9734
|
-
source =
|
|
10287
|
+
source = readFileSync4(absPath, "utf8");
|
|
9735
10288
|
} catch {
|
|
9736
10289
|
continue;
|
|
9737
10290
|
}
|
|
@@ -9803,7 +10356,7 @@ function inspectPendingAutoArchive(projectRoot, now) {
|
|
|
9803
10356
|
pending_path: visit.pending_path,
|
|
9804
10357
|
pending_path_abs: visit.pending_path_abs,
|
|
9805
10358
|
archived_to: archivedToRel,
|
|
9806
|
-
archived_to_abs:
|
|
10359
|
+
archived_to_abs: join13(projectRoot, archivedToRel),
|
|
9807
10360
|
age_days: visit.age_days
|
|
9808
10361
|
});
|
|
9809
10362
|
} else {
|
|
@@ -9812,7 +10365,7 @@ function inspectPendingAutoArchive(projectRoot, now) {
|
|
|
9812
10365
|
visit.type,
|
|
9813
10366
|
visit.filename
|
|
9814
10367
|
);
|
|
9815
|
-
const archivedToAbs =
|
|
10368
|
+
const archivedToAbs = join13(
|
|
9816
10369
|
resolvePersonalRootForPending(),
|
|
9817
10370
|
".fabric",
|
|
9818
10371
|
".archive",
|
|
@@ -9836,11 +10389,11 @@ function inspectPendingAutoArchive(projectRoot, now) {
|
|
|
9836
10389
|
}
|
|
9837
10390
|
function inspectUnderseeded(projectRoot) {
|
|
9838
10391
|
const threshold = readUnderseedThresholdFromConfig(projectRoot);
|
|
9839
|
-
const knowledgeRoot =
|
|
10392
|
+
const knowledgeRoot = join13(projectRoot, ".fabric", "knowledge");
|
|
9840
10393
|
let nodeCount = 0;
|
|
9841
10394
|
if (existsSync8(knowledgeRoot)) {
|
|
9842
10395
|
for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
|
|
9843
|
-
const dir =
|
|
10396
|
+
const dir = join13(knowledgeRoot, typeDir);
|
|
9844
10397
|
if (!existsSync8(dir)) continue;
|
|
9845
10398
|
let entries;
|
|
9846
10399
|
try {
|
|
@@ -9862,7 +10415,7 @@ function inspectUnderseeded(projectRoot) {
|
|
|
9862
10415
|
};
|
|
9863
10416
|
}
|
|
9864
10417
|
function inspectSessionHintsStale(projectRoot, now) {
|
|
9865
|
-
const cacheDir =
|
|
10418
|
+
const cacheDir = join13(projectRoot, ".fabric", ".cache");
|
|
9866
10419
|
if (!existsSync8(cacheDir)) {
|
|
9867
10420
|
return { candidates: [] };
|
|
9868
10421
|
}
|
|
@@ -9877,7 +10430,7 @@ function inspectSessionHintsStale(projectRoot, now) {
|
|
|
9877
10430
|
if (!entry.isFile()) continue;
|
|
9878
10431
|
if (!entry.name.startsWith(SESSION_HINTS_FILE_PREFIX)) continue;
|
|
9879
10432
|
if (!entry.name.endsWith(SESSION_HINTS_FILE_SUFFIX)) continue;
|
|
9880
|
-
const absPath =
|
|
10433
|
+
const absPath = join13(cacheDir, entry.name);
|
|
9881
10434
|
let mtimeMs = 0;
|
|
9882
10435
|
try {
|
|
9883
10436
|
mtimeMs = statSync4(absPath).mtimeMs;
|
|
@@ -9921,11 +10474,11 @@ function inspectNarrowTooFew(projectRoot, now) {
|
|
|
9921
10474
|
const structuralFlagged = total >= NARROW_MIN_TOTAL && narrowRatio < NARROW_RATIO_THRESHOLD;
|
|
9922
10475
|
const windowStartMs = now - SILENCE_WINDOW_DAYS * MS_PER_DAY;
|
|
9923
10476
|
const editFires = readCounterTimestamps(
|
|
9924
|
-
|
|
10477
|
+
join13(projectRoot, EDIT_COUNTER_FILE_REL),
|
|
9925
10478
|
windowStartMs
|
|
9926
10479
|
);
|
|
9927
10480
|
const silenceFires = readCounterTimestamps(
|
|
9928
|
-
|
|
10481
|
+
join13(projectRoot, HINT_SILENCE_COUNTER_FILE_REL),
|
|
9929
10482
|
windowStartMs
|
|
9930
10483
|
);
|
|
9931
10484
|
const telemetrySkipped = editFires === 0;
|
|
@@ -9947,7 +10500,7 @@ function readCounterTimestamps(absPath, windowStartMs) {
|
|
|
9947
10500
|
if (!existsSync8(absPath)) return 0;
|
|
9948
10501
|
let raw;
|
|
9949
10502
|
try {
|
|
9950
|
-
raw =
|
|
10503
|
+
raw = readFileSync4(absPath, "utf8");
|
|
9951
10504
|
} catch {
|
|
9952
10505
|
return 0;
|
|
9953
10506
|
}
|
|
@@ -9963,10 +10516,10 @@ function readCounterTimestamps(absPath, windowStartMs) {
|
|
|
9963
10516
|
return count;
|
|
9964
10517
|
}
|
|
9965
10518
|
function readUnderseedThresholdFromConfig(projectRoot) {
|
|
9966
|
-
const configPath =
|
|
10519
|
+
const configPath = join13(projectRoot, ".fabric", "fabric-config.json");
|
|
9967
10520
|
if (!existsSync8(configPath)) return DEFAULT_UNDERSEED_NODE_THRESHOLD;
|
|
9968
10521
|
try {
|
|
9969
|
-
const raw =
|
|
10522
|
+
const raw = readFileSync4(configPath, "utf8");
|
|
9970
10523
|
const parsed = JSON.parse(raw);
|
|
9971
10524
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
9972
10525
|
const v = parsed.underseed_node_threshold;
|
|
@@ -10160,11 +10713,11 @@ function extractKnowledgeFrontmatterRelevancePaths(source) {
|
|
|
10160
10713
|
}
|
|
10161
10714
|
function* iterateRelevanceFrontmatter(projectRoot) {
|
|
10162
10715
|
for (const visit of iterateCanonicalFilenames(projectRoot)) {
|
|
10163
|
-
const layerRoot = visit.layer === "team" ?
|
|
10164
|
-
const absPath =
|
|
10716
|
+
const layerRoot = visit.layer === "team" ? join13(projectRoot, ".fabric", "knowledge") : resolvePersonalKnowledgeRoot();
|
|
10717
|
+
const absPath = join13(layerRoot, visit.type, visit.filename);
|
|
10165
10718
|
let source;
|
|
10166
10719
|
try {
|
|
10167
|
-
source =
|
|
10720
|
+
source = readFileSync4(absPath, "utf8");
|
|
10168
10721
|
} catch {
|
|
10169
10722
|
continue;
|
|
10170
10723
|
}
|
|
@@ -10250,7 +10803,7 @@ function collectWorkspacePathsForGlobMatch(projectRoot) {
|
|
|
10250
10803
|
continue;
|
|
10251
10804
|
}
|
|
10252
10805
|
for (const entry of entries) {
|
|
10253
|
-
const abs =
|
|
10806
|
+
const abs = join13(current, entry.name);
|
|
10254
10807
|
const rel = normalizePath(abs.slice(projectRoot.length + 1));
|
|
10255
10808
|
if (rel.length === 0) continue;
|
|
10256
10809
|
if (entry.isDirectory()) {
|
|
@@ -10466,11 +11019,11 @@ function createPersonalLayerPathMisclassifyCheck(t, inspection) {
|
|
|
10466
11019
|
function inspectSuspiciousKb(projectRoot) {
|
|
10467
11020
|
const candidates = [];
|
|
10468
11021
|
for (const visit of iterateCanonicalFilenames(projectRoot)) {
|
|
10469
|
-
const layerRoot = visit.layer === "team" ?
|
|
10470
|
-
const absPath =
|
|
11022
|
+
const layerRoot = visit.layer === "team" ? join13(projectRoot, ".fabric", "knowledge") : resolvePersonalKnowledgeRoot();
|
|
11023
|
+
const absPath = join13(layerRoot, visit.type, visit.filename);
|
|
10471
11024
|
let body;
|
|
10472
11025
|
try {
|
|
10473
|
-
body =
|
|
11026
|
+
body = readFileSync4(absPath, "utf8");
|
|
10474
11027
|
} catch {
|
|
10475
11028
|
continue;
|
|
10476
11029
|
}
|
|
@@ -10519,8 +11072,8 @@ function inspectRelevanceFieldsMissing(projectRoot) {
|
|
|
10519
11072
|
const candidates = [];
|
|
10520
11073
|
let scannedCount = 0;
|
|
10521
11074
|
const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
|
|
10522
|
-
const teamRoot =
|
|
10523
|
-
const personalRoot =
|
|
11075
|
+
const teamRoot = join13(projectRoot, ".fabric", "knowledge", "pending");
|
|
11076
|
+
const personalRoot = join13(
|
|
10524
11077
|
resolvePersonalRootForPending(),
|
|
10525
11078
|
".fabric",
|
|
10526
11079
|
"knowledge",
|
|
@@ -10540,7 +11093,7 @@ function inspectRelevanceFieldsMissing(projectRoot) {
|
|
|
10540
11093
|
continue;
|
|
10541
11094
|
}
|
|
10542
11095
|
for (const typeDir of typeDirs) {
|
|
10543
|
-
const dir =
|
|
11096
|
+
const dir = join13(root, typeDir);
|
|
10544
11097
|
let entries;
|
|
10545
11098
|
try {
|
|
10546
11099
|
entries = readdirSync(dir, { withFileTypes: true });
|
|
@@ -10551,10 +11104,10 @@ function inspectRelevanceFieldsMissing(projectRoot) {
|
|
|
10551
11104
|
if (!entry.isFile() || !entry.name.endsWith(".md")) {
|
|
10552
11105
|
continue;
|
|
10553
11106
|
}
|
|
10554
|
-
const absPath =
|
|
11107
|
+
const absPath = join13(dir, entry.name);
|
|
10555
11108
|
let source;
|
|
10556
11109
|
try {
|
|
10557
|
-
source =
|
|
11110
|
+
source = readFileSync4(absPath, "utf8");
|
|
10558
11111
|
} catch {
|
|
10559
11112
|
continue;
|
|
10560
11113
|
}
|
|
@@ -10686,7 +11239,7 @@ var SKILL_QUOTED_VALUE_LEADS = /* @__PURE__ */ new Set(['"', "'", "[", "{", ">",
|
|
|
10686
11239
|
function inspectSkillMdYamlInvalid(projectRoot) {
|
|
10687
11240
|
const candidates = [];
|
|
10688
11241
|
for (const rootRel of SKILL_MD_FRONTMATTER_ROOTS) {
|
|
10689
|
-
const rootAbs =
|
|
11242
|
+
const rootAbs = join13(projectRoot, rootRel);
|
|
10690
11243
|
if (!existsSync8(rootAbs)) continue;
|
|
10691
11244
|
let dirEntries;
|
|
10692
11245
|
try {
|
|
@@ -10696,11 +11249,11 @@ function inspectSkillMdYamlInvalid(projectRoot) {
|
|
|
10696
11249
|
}
|
|
10697
11250
|
for (const dirEntry of dirEntries) {
|
|
10698
11251
|
if (!dirEntry.isDirectory()) continue;
|
|
10699
|
-
const skillFile =
|
|
11252
|
+
const skillFile = join13(rootAbs, dirEntry.name, "SKILL.md");
|
|
10700
11253
|
if (!existsSync8(skillFile)) continue;
|
|
10701
11254
|
let raw;
|
|
10702
11255
|
try {
|
|
10703
|
-
raw =
|
|
11256
|
+
raw = readFileSync4(skillFile, "utf8");
|
|
10704
11257
|
} catch {
|
|
10705
11258
|
continue;
|
|
10706
11259
|
}
|
|
@@ -10782,10 +11335,10 @@ function inspectOnboardCoverage(projectRoot) {
|
|
|
10782
11335
|
for (const slot of ONBOARD_SLOT_NAMES) {
|
|
10783
11336
|
filled[slot] = [];
|
|
10784
11337
|
}
|
|
10785
|
-
const knowledgeRoot =
|
|
11338
|
+
const knowledgeRoot = join13(projectRoot, ".fabric", "knowledge");
|
|
10786
11339
|
if (existsSync8(knowledgeRoot)) {
|
|
10787
11340
|
for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS_FOR_ONBOARD) {
|
|
10788
|
-
const dir =
|
|
11341
|
+
const dir = join13(knowledgeRoot, typeDir);
|
|
10789
11342
|
if (!existsSync8(dir)) continue;
|
|
10790
11343
|
let entries;
|
|
10791
11344
|
try {
|
|
@@ -10796,10 +11349,10 @@ function inspectOnboardCoverage(projectRoot) {
|
|
|
10796
11349
|
for (const entry of entries) {
|
|
10797
11350
|
if (!entry.isFile()) continue;
|
|
10798
11351
|
if (!entry.name.endsWith(".md")) continue;
|
|
10799
|
-
const filePath =
|
|
11352
|
+
const filePath = join13(dir, entry.name);
|
|
10800
11353
|
let content;
|
|
10801
11354
|
try {
|
|
10802
|
-
content =
|
|
11355
|
+
content = readFileSync4(filePath, "utf8");
|
|
10803
11356
|
} catch {
|
|
10804
11357
|
continue;
|
|
10805
11358
|
}
|
|
@@ -10823,11 +11376,11 @@ function inspectOnboardCoverage(projectRoot) {
|
|
|
10823
11376
|
return { filled, missing, opted_out: optedOut };
|
|
10824
11377
|
}
|
|
10825
11378
|
function readOnboardOptedOut(projectRoot) {
|
|
10826
|
-
const path2 =
|
|
11379
|
+
const path2 = join13(projectRoot, ".fabric", "fabric-config.json");
|
|
10827
11380
|
if (!existsSync8(path2)) return [];
|
|
10828
11381
|
let raw;
|
|
10829
11382
|
try {
|
|
10830
|
-
raw =
|
|
11383
|
+
raw = readFileSync4(path2, "utf8");
|
|
10831
11384
|
} catch {
|
|
10832
11385
|
return [];
|
|
10833
11386
|
}
|
|
@@ -10851,10 +11404,10 @@ function readFrontmatterScalar(content, key) {
|
|
|
10851
11404
|
if (block === void 0) return void 0;
|
|
10852
11405
|
for (const rawLine of block.split(/\r?\n/u)) {
|
|
10853
11406
|
const line = rawLine.trim();
|
|
10854
|
-
const
|
|
10855
|
-
if (
|
|
10856
|
-
if (line.slice(0,
|
|
10857
|
-
let value = line.slice(
|
|
11407
|
+
const sep5 = line.indexOf(":");
|
|
11408
|
+
if (sep5 === -1) continue;
|
|
11409
|
+
if (line.slice(0, sep5).trim() !== key) continue;
|
|
11410
|
+
let value = line.slice(sep5 + 1).trim();
|
|
10858
11411
|
if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
|
|
10859
11412
|
value = value.slice(1, -1);
|
|
10860
11413
|
}
|
|
@@ -10943,7 +11496,7 @@ function createNarrowTooFewCheck(t, inspection) {
|
|
|
10943
11496
|
}
|
|
10944
11497
|
function resolvePersonalKnowledgeRoot() {
|
|
10945
11498
|
const home = process.env.FABRIC_HOME ?? homedir6();
|
|
10946
|
-
return
|
|
11499
|
+
return join13(home, ".fabric", "knowledge");
|
|
10947
11500
|
}
|
|
10948
11501
|
function parseStableIdFromCanonicalFilename(filename) {
|
|
10949
11502
|
const match = CANONICAL_KNOWLEDGE_FILENAME_PATTERN.exec(filename);
|
|
@@ -10963,7 +11516,7 @@ function parseStableIdFromCanonicalFilename(filename) {
|
|
|
10963
11516
|
};
|
|
10964
11517
|
}
|
|
10965
11518
|
function* iterateCanonicalFilenames(projectRoot) {
|
|
10966
|
-
const teamRoot =
|
|
11519
|
+
const teamRoot = join13(projectRoot, ".fabric", "knowledge");
|
|
10967
11520
|
const personalRoot = resolvePersonalKnowledgeRoot();
|
|
10968
11521
|
for (const [layer, root, displayPrefix] of [
|
|
10969
11522
|
["team", teamRoot, ".fabric/knowledge"],
|
|
@@ -10973,7 +11526,7 @@ function* iterateCanonicalFilenames(projectRoot) {
|
|
|
10973
11526
|
continue;
|
|
10974
11527
|
}
|
|
10975
11528
|
for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
|
|
10976
|
-
const dir =
|
|
11529
|
+
const dir = join13(root, typeDir);
|
|
10977
11530
|
if (!existsSync8(dir)) {
|
|
10978
11531
|
continue;
|
|
10979
11532
|
}
|
|
@@ -11145,7 +11698,7 @@ async function migrateBootstrapMarkers(projectRoot) {
|
|
|
11145
11698
|
const paths = [];
|
|
11146
11699
|
const countPerPath = {};
|
|
11147
11700
|
for (const rel of BOOTSTRAP_MARKER_MIGRATION_TARGETS) {
|
|
11148
|
-
const abs =
|
|
11701
|
+
const abs = join13(projectRoot, rel);
|
|
11149
11702
|
if (!existsSync8(abs)) {
|
|
11150
11703
|
continue;
|
|
11151
11704
|
}
|
|
@@ -11172,7 +11725,7 @@ async function migrateBootstrapMarkers(projectRoot) {
|
|
|
11172
11725
|
return { paths, countPerPath };
|
|
11173
11726
|
}
|
|
11174
11727
|
async function rewriteThreeEndManagedBlocks(projectRoot) {
|
|
11175
|
-
const snapshotPath =
|
|
11728
|
+
const snapshotPath = join13(projectRoot, ".fabric", "AGENTS.md");
|
|
11176
11729
|
if (!existsSync8(snapshotPath)) {
|
|
11177
11730
|
return;
|
|
11178
11731
|
}
|
|
@@ -11182,7 +11735,7 @@ async function rewriteThreeEndManagedBlocks(projectRoot) {
|
|
|
11182
11735
|
} catch {
|
|
11183
11736
|
return;
|
|
11184
11737
|
}
|
|
11185
|
-
const projectRulesPath =
|
|
11738
|
+
const projectRulesPath = join13(projectRoot, ".fabric", "project-rules.md");
|
|
11186
11739
|
const hasProjectRules = existsSync8(projectRulesPath);
|
|
11187
11740
|
let expectedBody = snapshot;
|
|
11188
11741
|
if (hasProjectRules) {
|
|
@@ -11198,8 +11751,8 @@ ${projectRules}`;
|
|
|
11198
11751
|
${expectedBody}
|
|
11199
11752
|
${BOOTSTRAP_MARKER_END}`;
|
|
11200
11753
|
const blockTargets = [
|
|
11201
|
-
|
|
11202
|
-
|
|
11754
|
+
join13(projectRoot, "AGENTS.md"),
|
|
11755
|
+
join13(projectRoot, ".cursor", "rules", "fabric-bootstrap.mdc")
|
|
11203
11756
|
];
|
|
11204
11757
|
for (const abs of blockTargets) {
|
|
11205
11758
|
if (!existsSync8(abs)) {
|
|
@@ -11232,7 +11785,7 @@ ${managedBlock}
|
|
|
11232
11785
|
}
|
|
11233
11786
|
await atomicWriteText4(abs, next);
|
|
11234
11787
|
}
|
|
11235
|
-
const claudeMdPath =
|
|
11788
|
+
const claudeMdPath = join13(projectRoot, "CLAUDE.md");
|
|
11236
11789
|
if (existsSync8(claudeMdPath)) {
|
|
11237
11790
|
let claudeContent;
|
|
11238
11791
|
try {
|
|
@@ -11261,13 +11814,13 @@ ${managedBlock}
|
|
|
11261
11814
|
}
|
|
11262
11815
|
}
|
|
11263
11816
|
async function fixMcpConfigInWrongFile(projectRoot) {
|
|
11264
|
-
const settingsPath =
|
|
11817
|
+
const settingsPath = join13(projectRoot, ".claude", "settings.json");
|
|
11265
11818
|
if (!existsSync8(settingsPath)) {
|
|
11266
11819
|
return;
|
|
11267
11820
|
}
|
|
11268
11821
|
let settings;
|
|
11269
11822
|
try {
|
|
11270
|
-
const parsed = JSON.parse(
|
|
11823
|
+
const parsed = JSON.parse(readFileSync4(settingsPath, "utf8"));
|
|
11271
11824
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
11272
11825
|
return;
|
|
11273
11826
|
}
|
|
@@ -11295,38 +11848,40 @@ async function fixMcpConfigInWrongFile(projectRoot) {
|
|
|
11295
11848
|
}
|
|
11296
11849
|
async function ensureKnowledgeSubdirs(projectRoot) {
|
|
11297
11850
|
for (const sub of KNOWLEDGE_SUBDIRS3) {
|
|
11298
|
-
await
|
|
11851
|
+
await mkdir5(join13(projectRoot, ".fabric", "knowledge", sub), { recursive: true });
|
|
11299
11852
|
}
|
|
11300
11853
|
}
|
|
11301
11854
|
async function fixCounterDesync(projectRoot) {
|
|
11302
|
-
const metaPath =
|
|
11855
|
+
const metaPath = join13(projectRoot, ".fabric", "agents.meta.json");
|
|
11303
11856
|
if (!existsSync8(metaPath)) {
|
|
11304
11857
|
return;
|
|
11305
11858
|
}
|
|
11306
|
-
|
|
11307
|
-
|
|
11308
|
-
|
|
11309
|
-
|
|
11310
|
-
|
|
11311
|
-
|
|
11312
|
-
|
|
11313
|
-
|
|
11314
|
-
|
|
11315
|
-
|
|
11316
|
-
|
|
11317
|
-
|
|
11318
|
-
|
|
11319
|
-
|
|
11320
|
-
|
|
11321
|
-
|
|
11322
|
-
|
|
11323
|
-
|
|
11324
|
-
|
|
11325
|
-
|
|
11326
|
-
|
|
11327
|
-
|
|
11328
|
-
|
|
11329
|
-
|
|
11859
|
+
await withFileLock4(`${metaPath}.lock`, async () => {
|
|
11860
|
+
let meta;
|
|
11861
|
+
try {
|
|
11862
|
+
meta = agentsMetaSchema5.parse(JSON.parse(await readFile13(metaPath, "utf8")));
|
|
11863
|
+
} catch {
|
|
11864
|
+
return;
|
|
11865
|
+
}
|
|
11866
|
+
const synthetic = {
|
|
11867
|
+
present: true,
|
|
11868
|
+
valid: true,
|
|
11869
|
+
meta,
|
|
11870
|
+
revision: meta.revision,
|
|
11871
|
+
computedRevision: null,
|
|
11872
|
+
ruleCount: 0,
|
|
11873
|
+
missingContentRefs: [],
|
|
11874
|
+
invalidContentRefs: [],
|
|
11875
|
+
stale: false,
|
|
11876
|
+
changed: false
|
|
11877
|
+
};
|
|
11878
|
+
const desync = inspectCounterDesync(synthetic);
|
|
11879
|
+
if (desync.desyncs.length === 0 || desync.correctedCounters === null) {
|
|
11880
|
+
return;
|
|
11881
|
+
}
|
|
11882
|
+
const updated = { ...meta, counters: desync.correctedCounters };
|
|
11883
|
+
await atomicWriteJson3(metaPath, updated, { indent: 2 });
|
|
11884
|
+
});
|
|
11330
11885
|
}
|
|
11331
11886
|
async function ensureEventLedger(projectRoot) {
|
|
11332
11887
|
const path2 = getEventLedgerPath(projectRoot);
|
|
@@ -11364,7 +11919,7 @@ function collectEntryPoints(root) {
|
|
|
11364
11919
|
continue;
|
|
11365
11920
|
}
|
|
11366
11921
|
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
11367
|
-
const absolutePath =
|
|
11922
|
+
const absolutePath = join13(current, entry.name);
|
|
11368
11923
|
const relativePath = normalizePath(absolutePath.slice(root.length + 1));
|
|
11369
11924
|
if (relativePath.length === 0) {
|
|
11370
11925
|
continue;
|
|
@@ -11436,8 +11991,8 @@ async function enrichDescriptions(projectRoot, opts = {}) {
|
|
|
11436
11991
|
let modified = 0;
|
|
11437
11992
|
let skipped = 0;
|
|
11438
11993
|
for (const visit of iterateCanonicalFilenames(projectRoot)) {
|
|
11439
|
-
const layerRoot = visit.layer === "team" ?
|
|
11440
|
-
const absPath =
|
|
11994
|
+
const layerRoot = visit.layer === "team" ? join13(projectRoot, ".fabric", "knowledge") : resolvePersonalKnowledgeRoot();
|
|
11995
|
+
const absPath = join13(layerRoot, visit.type, visit.filename);
|
|
11441
11996
|
scanned += 1;
|
|
11442
11997
|
let source;
|
|
11443
11998
|
try {
|
|
@@ -11538,6 +12093,144 @@ function yamlQuoteIfNeeded(value) {
|
|
|
11538
12093
|
return value;
|
|
11539
12094
|
}
|
|
11540
12095
|
|
|
12096
|
+
// src/services/doctor-conflict.ts
|
|
12097
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
12098
|
+
|
|
12099
|
+
// src/services/conflict-lint.ts
|
|
12100
|
+
var DEFAULT_CONFLICT_SIMILARITY_THRESHOLD = 0.5;
|
|
12101
|
+
function groupKey(entry) {
|
|
12102
|
+
return `${entry.layer}\0${entry.knowledge_type}`;
|
|
12103
|
+
}
|
|
12104
|
+
function pairSimilarity(model, a, b) {
|
|
12105
|
+
const selfA = model.scoreDoc(a.id, a.tokens);
|
|
12106
|
+
const selfB = model.scoreDoc(b.id, b.tokens);
|
|
12107
|
+
if (selfA <= 0 || selfB <= 0) return 0;
|
|
12108
|
+
const aToB = model.scoreDoc(b.id, a.tokens) / selfB;
|
|
12109
|
+
const bToA = model.scoreDoc(a.id, b.tokens) / selfA;
|
|
12110
|
+
const sim = Math.min(aToB, bToA);
|
|
12111
|
+
return sim < 0 ? 0 : sim > 1 ? 1 : sim;
|
|
12112
|
+
}
|
|
12113
|
+
function findConflictCandidates(entries, opts = {}) {
|
|
12114
|
+
const threshold = typeof opts.threshold === "number" && opts.threshold >= 0 && opts.threshold <= 1 ? opts.threshold : DEFAULT_CONFLICT_SIMILARITY_THRESHOLD;
|
|
12115
|
+
const groups = /* @__PURE__ */ new Map();
|
|
12116
|
+
for (const entry of entries) {
|
|
12117
|
+
if (typeof entry.stable_id !== "string" || entry.stable_id.length === 0) continue;
|
|
12118
|
+
const list = groups.get(groupKey(entry)) ?? [];
|
|
12119
|
+
list.push(entry);
|
|
12120
|
+
groups.set(groupKey(entry), list);
|
|
12121
|
+
}
|
|
12122
|
+
const pairs = [];
|
|
12123
|
+
for (const group of groups.values()) {
|
|
12124
|
+
if (group.length < 2) continue;
|
|
12125
|
+
const docs = group.map((e) => ({ id: e.stable_id, tokens: buildQueryTerms(e.text) }));
|
|
12126
|
+
const tokensById = new Map(docs.map((d) => [d.id, d.tokens]));
|
|
12127
|
+
const model = buildBm25Model(docs);
|
|
12128
|
+
for (let i = 0; i < group.length; i += 1) {
|
|
12129
|
+
for (let j = i + 1; j < group.length; j += 1) {
|
|
12130
|
+
const ea = group[i];
|
|
12131
|
+
const eb = group[j];
|
|
12132
|
+
const sim = pairSimilarity(
|
|
12133
|
+
model,
|
|
12134
|
+
{ id: ea.stable_id, tokens: tokensById.get(ea.stable_id) ?? [] },
|
|
12135
|
+
{ id: eb.stable_id, tokens: tokensById.get(eb.stable_id) ?? [] }
|
|
12136
|
+
);
|
|
12137
|
+
if (sim < threshold) continue;
|
|
12138
|
+
const [a, b] = ea.stable_id <= eb.stable_id ? [ea.stable_id, eb.stable_id] : [eb.stable_id, ea.stable_id];
|
|
12139
|
+
pairs.push({
|
|
12140
|
+
a,
|
|
12141
|
+
b,
|
|
12142
|
+
knowledge_type: ea.knowledge_type,
|
|
12143
|
+
layer: ea.layer,
|
|
12144
|
+
similarity: sim,
|
|
12145
|
+
verdict: "unknown"
|
|
12146
|
+
});
|
|
12147
|
+
}
|
|
12148
|
+
}
|
|
12149
|
+
}
|
|
12150
|
+
pairs.sort((x, y) => y.similarity - x.similarity || (x.a < y.a ? -1 : x.a > y.a ? 1 : x.b < y.b ? -1 : 1));
|
|
12151
|
+
return pairs;
|
|
12152
|
+
}
|
|
12153
|
+
async function lintConflicts(entries, opts = {}) {
|
|
12154
|
+
const candidates = findConflictCandidates(entries, { threshold: opts.threshold });
|
|
12155
|
+
if (opts.judge === void 0 || candidates.length === 0) {
|
|
12156
|
+
return candidates;
|
|
12157
|
+
}
|
|
12158
|
+
const byId = new Map(entries.map((e) => [e.stable_id, e]));
|
|
12159
|
+
const judged = [];
|
|
12160
|
+
for (const pair of candidates) {
|
|
12161
|
+
const ea = byId.get(pair.a);
|
|
12162
|
+
const eb = byId.get(pair.b);
|
|
12163
|
+
if (ea === void 0 || eb === void 0) {
|
|
12164
|
+
judged.push(pair);
|
|
12165
|
+
continue;
|
|
12166
|
+
}
|
|
12167
|
+
try {
|
|
12168
|
+
const verdict = await opts.judge(ea, eb);
|
|
12169
|
+
judged.push({
|
|
12170
|
+
...pair,
|
|
12171
|
+
verdict: verdict.isConflict ? "conflict" : "similar",
|
|
12172
|
+
rationale: verdict.rationale
|
|
12173
|
+
});
|
|
12174
|
+
} catch {
|
|
12175
|
+
judged.push(pair);
|
|
12176
|
+
}
|
|
12177
|
+
}
|
|
12178
|
+
return judged;
|
|
12179
|
+
}
|
|
12180
|
+
|
|
12181
|
+
// src/services/doctor-conflict.ts
|
|
12182
|
+
function stripFrontmatter(content) {
|
|
12183
|
+
if (!content.startsWith("---")) return content;
|
|
12184
|
+
const end = content.indexOf("\n---", 3);
|
|
12185
|
+
if (end === -1) return content;
|
|
12186
|
+
const after = content.indexOf("\n", end + 1);
|
|
12187
|
+
return after === -1 ? "" : content.slice(after + 1);
|
|
12188
|
+
}
|
|
12189
|
+
async function loadConflictEntries(projectRoot) {
|
|
12190
|
+
let meta;
|
|
12191
|
+
try {
|
|
12192
|
+
meta = await readAgentsMeta(projectRoot);
|
|
12193
|
+
} catch {
|
|
12194
|
+
return [];
|
|
12195
|
+
}
|
|
12196
|
+
const entries = [];
|
|
12197
|
+
for (const node of Object.values(meta.nodes)) {
|
|
12198
|
+
const stableId = node.stable_id;
|
|
12199
|
+
if (typeof stableId !== "string" || stableId.length === 0) continue;
|
|
12200
|
+
const contentRef = node.content_ref;
|
|
12201
|
+
if (typeof contentRef !== "string" || contentRef.length === 0) continue;
|
|
12202
|
+
if (contentRef.includes("/pending/")) continue;
|
|
12203
|
+
const knowledgeType = node.description?.knowledge_type;
|
|
12204
|
+
if (typeof knowledgeType !== "string" || knowledgeType.length === 0) continue;
|
|
12205
|
+
const layer = stableId.startsWith("KP-") ? "personal" : "team";
|
|
12206
|
+
let body;
|
|
12207
|
+
try {
|
|
12208
|
+
body = await readFile14(resolveContentRefPath2(projectRoot, contentRef), "utf8");
|
|
12209
|
+
} catch {
|
|
12210
|
+
continue;
|
|
12211
|
+
}
|
|
12212
|
+
entries.push({ stable_id: stableId, knowledge_type: knowledgeType, layer, text: stripFrontmatter(body) });
|
|
12213
|
+
}
|
|
12214
|
+
return entries;
|
|
12215
|
+
}
|
|
12216
|
+
async function runDoctorConflictLint(projectRoot, opts = {}) {
|
|
12217
|
+
const threshold = typeof opts.threshold === "number" ? opts.threshold : readConflictLintThreshold(projectRoot) ?? DEFAULT_CONFLICT_SIMILARITY_THRESHOLD;
|
|
12218
|
+
const deep = opts.deep === true && opts.judge !== void 0;
|
|
12219
|
+
const entries = await loadConflictEntries(projectRoot);
|
|
12220
|
+
const pairs = await lintConflicts(entries, {
|
|
12221
|
+
threshold,
|
|
12222
|
+
judge: deep ? opts.judge : void 0
|
|
12223
|
+
});
|
|
12224
|
+
return {
|
|
12225
|
+
status: "ok",
|
|
12226
|
+
threshold,
|
|
12227
|
+
deep,
|
|
12228
|
+
candidate_count: pairs.length,
|
|
12229
|
+
conflict_count: pairs.filter((p) => p.verdict === "conflict").length,
|
|
12230
|
+
pairs
|
|
12231
|
+
};
|
|
12232
|
+
}
|
|
12233
|
+
|
|
11541
12234
|
// src/services/rotation-tick.ts
|
|
11542
12235
|
var DEFAULT_TICK_INTERVAL_MS = 6 * 60 * 60 * 1e3;
|
|
11543
12236
|
var tickTimers = /* @__PURE__ */ new Map();
|
|
@@ -11573,7 +12266,7 @@ import { IOFabricError as IOFabricError2, RuleError } from "@fenglimg/fabric-sha
|
|
|
11573
12266
|
|
|
11574
12267
|
// src/services/read-ledger.ts
|
|
11575
12268
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
11576
|
-
import { access as access3, copyFile, readFile as
|
|
12269
|
+
import { access as access3, copyFile, readFile as readFile15, rm } from "fs/promises";
|
|
11577
12270
|
import { ledgerEntrySchema } from "@fenglimg/fabric-shared";
|
|
11578
12271
|
async function resolveLedgerPaths(projectRoot) {
|
|
11579
12272
|
const primaryPath = getLedgerPath(projectRoot);
|
|
@@ -11601,7 +12294,7 @@ async function readLegacyLedger(projectRoot) {
|
|
|
11601
12294
|
const { readPath } = await resolveLedgerPaths(projectRoot);
|
|
11602
12295
|
let raw;
|
|
11603
12296
|
try {
|
|
11604
|
-
raw = await
|
|
12297
|
+
raw = await readFile15(readPath, "utf8");
|
|
11605
12298
|
} catch (error) {
|
|
11606
12299
|
if (isNodeError(error) && error.code === "ENOENT") {
|
|
11607
12300
|
return [];
|
|
@@ -11858,8 +12551,8 @@ function formatError(error) {
|
|
|
11858
12551
|
}
|
|
11859
12552
|
function formatPreexistingRootMessage(projectRoot) {
|
|
11860
12553
|
const preexisting = [];
|
|
11861
|
-
if (existsSync9(
|
|
11862
|
-
if (existsSync9(
|
|
12554
|
+
if (existsSync9(join14(projectRoot, "CLAUDE.md"))) preexisting.push("CLAUDE.md");
|
|
12555
|
+
if (existsSync9(join14(projectRoot, "AGENTS.md"))) preexisting.push("AGENTS.md");
|
|
11863
12556
|
if (preexisting.length === 0) return null;
|
|
11864
12557
|
return `[startup] info: detected ${preexisting.join(", ")} at project root. Note: Fabric serves knowledge from .fabric/knowledge/ via MCP \u2014 root markdown files are not auto-loaded into the AI context.`;
|
|
11865
12558
|
}
|
|
@@ -11887,7 +12580,7 @@ function createFabricServer(tracker) {
|
|
|
11887
12580
|
const server = new McpServer(
|
|
11888
12581
|
{
|
|
11889
12582
|
name: "fabric-knowledge-server",
|
|
11890
|
-
version: "2.2.0-rc.
|
|
12583
|
+
version: "2.2.0-rc.3"
|
|
11891
12584
|
},
|
|
11892
12585
|
{
|
|
11893
12586
|
instructions: FABRIC_SERVER_INSTRUCTIONS
|
|
@@ -11908,10 +12601,10 @@ function createFabricServer(tracker) {
|
|
|
11908
12601
|
},
|
|
11909
12602
|
async (_uri) => {
|
|
11910
12603
|
const projectRoot = process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
|
|
11911
|
-
const path2 =
|
|
12604
|
+
const path2 = join14(projectRoot, ".fabric", "bootstrap", "README.md");
|
|
11912
12605
|
let text = "";
|
|
11913
12606
|
if (existsSync9(path2)) {
|
|
11914
|
-
text = await
|
|
12607
|
+
text = await readFile16(path2, "utf8");
|
|
11915
12608
|
}
|
|
11916
12609
|
return {
|
|
11917
12610
|
contents: [
|
|
@@ -12016,6 +12709,7 @@ if (isMainModule) {
|
|
|
12016
12709
|
}
|
|
12017
12710
|
export {
|
|
12018
12711
|
AGENTS_MD_RESOURCE_URI,
|
|
12712
|
+
DEFAULT_CONFLICT_SIMILARITY_THRESHOLD,
|
|
12019
12713
|
EVENT_LEDGER_PATH,
|
|
12020
12714
|
FABRIC_SERVER_INSTRUCTIONS,
|
|
12021
12715
|
KnowledgeIdAllocator,
|
|
@@ -12038,6 +12732,7 @@ export {
|
|
|
12038
12732
|
enrichDescriptions,
|
|
12039
12733
|
ensureKnowledgeFresh,
|
|
12040
12734
|
extractKnowledge,
|
|
12735
|
+
findConflictCandidates,
|
|
12041
12736
|
flushAndSyncEventLedger,
|
|
12042
12737
|
flushMetrics,
|
|
12043
12738
|
formatPreexistingRootMessage,
|
|
@@ -12048,7 +12743,10 @@ export {
|
|
|
12048
12743
|
getMetricsLedgerPath,
|
|
12049
12744
|
invalidateKnowledgeSyncCooldown,
|
|
12050
12745
|
isSameKnowledgeTestIndex,
|
|
12746
|
+
lintConflicts,
|
|
12747
|
+
loadConflictEntries,
|
|
12051
12748
|
loadKbIdTypeMap,
|
|
12749
|
+
pairSimilarity,
|
|
12052
12750
|
planContext,
|
|
12053
12751
|
readAgentsMeta,
|
|
12054
12752
|
readEventLedger,
|
|
@@ -12063,6 +12761,7 @@ export {
|
|
|
12063
12761
|
runDoctorApplyLint,
|
|
12064
12762
|
runDoctorArchiveHistory,
|
|
12065
12763
|
runDoctorCiteCoverage,
|
|
12764
|
+
runDoctorConflictLint,
|
|
12066
12765
|
runDoctorFix,
|
|
12067
12766
|
runDoctorHistoryAll,
|
|
12068
12767
|
runDoctorReport,
|