@fenglimg/fabric-server 2.0.0-rc.22 → 2.0.0-rc.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-7N3FW5LX.js → chunk-HAXROPQM.js} +746 -57
- package/dist/{http-FF5NZCJK.js → http-QBGLHCHA.js} +1 -1
- package/dist/index.d.ts +97 -6
- package/dist/index.js +212 -217
- package/package.json +2 -2
|
@@ -160,8 +160,8 @@ function getLegacyLedgerPath(projectRoot) {
|
|
|
160
160
|
function getEventLedgerPath(projectRoot) {
|
|
161
161
|
return join2(projectRoot, EVENT_LEDGER_PATH);
|
|
162
162
|
}
|
|
163
|
-
async function ensureParentDirectory(
|
|
164
|
-
await mkdir(dirname(
|
|
163
|
+
async function ensureParentDirectory(path2) {
|
|
164
|
+
await mkdir(dirname(path2), { recursive: true });
|
|
165
165
|
}
|
|
166
166
|
function sha256(content) {
|
|
167
167
|
return `sha256:${createHash("sha256").update(content).digest("hex")}`;
|
|
@@ -241,24 +241,24 @@ async function readEventLedger(projectRoot, options = {}) {
|
|
|
241
241
|
const events = lines.map((line) => line.trim()).filter((line) => line.length > 0).map((line, index) => parseEventLedgerLine(line, index)).filter((entry) => entry !== null).filter((entry) => options.event_type === void 0 || entry.event_type === options.event_type).filter((entry) => options.since === void 0 || entry.ts >= options.since).filter((entry) => options.correlation_id === void 0 || entry.correlation_id === options.correlation_id).filter((entry) => options.session_id === void 0 || entry.session_id === options.session_id);
|
|
242
242
|
return { events, warnings };
|
|
243
243
|
}
|
|
244
|
-
async function truncateLedgerToLastNewline(
|
|
245
|
-
const raw = await readFile2(
|
|
244
|
+
async function truncateLedgerToLastNewline(path2) {
|
|
245
|
+
const raw = await readFile2(path2);
|
|
246
246
|
const content = raw.toString("utf8");
|
|
247
247
|
if (content.endsWith("\n") || content.length === 0) {
|
|
248
248
|
return { truncated_bytes: 0, corrupted_path: "" };
|
|
249
249
|
}
|
|
250
250
|
const lastNewlineIndex = content.lastIndexOf("\n");
|
|
251
251
|
if (lastNewlineIndex === -1) {
|
|
252
|
-
const corruptedPath2 = `${
|
|
252
|
+
const corruptedPath2 = `${path2}.corrupted.${Date.now()}`;
|
|
253
253
|
await writeFile(corruptedPath2, raw);
|
|
254
|
-
await truncate(
|
|
254
|
+
await truncate(path2, 0);
|
|
255
255
|
return { truncated_bytes: raw.length, corrupted_path: corruptedPath2 };
|
|
256
256
|
}
|
|
257
257
|
const keepByteLength = Buffer.byteLength(content.slice(0, lastNewlineIndex + 1), "utf8");
|
|
258
258
|
const corruptedBytes = raw.slice(keepByteLength);
|
|
259
|
-
const corruptedPath = `${
|
|
259
|
+
const corruptedPath = `${path2}.corrupted.${Date.now()}`;
|
|
260
260
|
await writeFile(corruptedPath, corruptedBytes);
|
|
261
|
-
await truncate(
|
|
261
|
+
await truncate(path2, keepByteLength);
|
|
262
262
|
return { truncated_bytes: corruptedBytes.length, corrupted_path: corruptedPath };
|
|
263
263
|
}
|
|
264
264
|
function parseEventLedgerLine(line, index) {
|
|
@@ -428,6 +428,27 @@ import {
|
|
|
428
428
|
parseKnowledgeId
|
|
429
429
|
} from "@fenglimg/fabric-shared";
|
|
430
430
|
import { atomicWriteText as atomicWriteText3 } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
431
|
+
async function loadKbIdTypeMap(projectRootInput) {
|
|
432
|
+
const projectRoot = normalizeProjectRoot(projectRootInput);
|
|
433
|
+
const metaPath = join4(projectRoot, ".fabric", "agents.meta.json");
|
|
434
|
+
const meta = await readExistingMeta(metaPath);
|
|
435
|
+
const map = /* @__PURE__ */ new Map();
|
|
436
|
+
if (meta === void 0) {
|
|
437
|
+
return map;
|
|
438
|
+
}
|
|
439
|
+
for (const node of Object.values(meta.nodes)) {
|
|
440
|
+
const stableId = node.stable_id;
|
|
441
|
+
if (stableId === void 0 || !isKnowledgeStableId(stableId)) {
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
const knowledgeType = node.description?.knowledge_type;
|
|
445
|
+
if (knowledgeType === void 0) {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
map.set(stableId, knowledgeType);
|
|
449
|
+
}
|
|
450
|
+
return map;
|
|
451
|
+
}
|
|
431
452
|
async function buildKnowledgeMeta(projectRootInput) {
|
|
432
453
|
const projectRoot = normalizeProjectRoot(projectRootInput);
|
|
433
454
|
assertExistingDirectory(projectRoot);
|
|
@@ -870,8 +891,8 @@ function flattenKeys(value, keys = {}) {
|
|
|
870
891
|
}
|
|
871
892
|
return keys;
|
|
872
893
|
}
|
|
873
|
-
function toPosixPath(
|
|
874
|
-
return
|
|
894
|
+
function toPosixPath(path2) {
|
|
895
|
+
return path2.split(sep2).join("/");
|
|
875
896
|
}
|
|
876
897
|
function deriveRuleIdentity(file, source, existing) {
|
|
877
898
|
const declaredKnowledgeId = extractDeclaredKnowledgeId(source);
|
|
@@ -959,7 +980,7 @@ function extractRuleDescription(source) {
|
|
|
959
980
|
};
|
|
960
981
|
}
|
|
961
982
|
function extractRuleSections(source) {
|
|
962
|
-
const sections = Array.from(source.matchAll(
|
|
983
|
+
const sections = Array.from(source.matchAll(/^#{2,6}\s+(.+?)\s*$/gmu)).map((match) => match[1].trim()).filter((section, index, all) => section.length > 0 && all.indexOf(section) === index);
|
|
963
984
|
return sections.length > 0 ? sections : void 0;
|
|
964
985
|
}
|
|
965
986
|
function extractDescriptionFromFrontmatter(frontmatter) {
|
|
@@ -1147,11 +1168,11 @@ async function readMetaEntries(projectRoot) {
|
|
|
1147
1168
|
return map;
|
|
1148
1169
|
}
|
|
1149
1170
|
for (const node of Object.values(parsed.nodes ?? {})) {
|
|
1150
|
-
const
|
|
1171
|
+
const path2 = node.content_ref ?? node.file;
|
|
1151
1172
|
const stable_id = node.stable_id;
|
|
1152
1173
|
const content_hash = node.hash;
|
|
1153
|
-
if (
|
|
1154
|
-
map.set(
|
|
1174
|
+
if (path2 !== void 0 && stable_id !== void 0 && content_hash !== void 0) {
|
|
1175
|
+
map.set(path2, { stable_id, path: path2, content_hash });
|
|
1155
1176
|
}
|
|
1156
1177
|
}
|
|
1157
1178
|
return map;
|
|
@@ -1407,7 +1428,8 @@ async function reconcileKnowledge(projectRoot, opts) {
|
|
|
1407
1428
|
throw error;
|
|
1408
1429
|
}
|
|
1409
1430
|
}
|
|
1410
|
-
|
|
1431
|
+
const forceWriteForDescriptionHeal = trigger === "auto-heal-description";
|
|
1432
|
+
if (events.length > 0 || revisionDrift || forceWriteForDescriptionHeal) {
|
|
1411
1433
|
await writeKnowledgeMeta(projectRoot, { source: "sync_meta" });
|
|
1412
1434
|
if (events.length > 0) {
|
|
1413
1435
|
await appendRuleSyncEvents(projectRoot, events);
|
|
@@ -1417,7 +1439,7 @@ async function reconcileKnowledge(projectRoot, opts) {
|
|
|
1417
1439
|
}
|
|
1418
1440
|
const duration_ms = Date.now() - startTime;
|
|
1419
1441
|
const reconciledFiles = events.map((e) => e.path);
|
|
1420
|
-
if (trigger !== void 0 && (events.length > 0 || revisionDrift)) {
|
|
1442
|
+
if (trigger !== void 0 && (events.length > 0 || revisionDrift || forceWriteForDescriptionHeal)) {
|
|
1421
1443
|
if (trigger === "startup") {
|
|
1422
1444
|
await appendEventLedgerEvent(projectRoot, {
|
|
1423
1445
|
event_type: "meta_reconciled_on_startup",
|
|
@@ -1448,10 +1470,103 @@ async function reconcileKnowledge(projectRoot, opts) {
|
|
|
1448
1470
|
};
|
|
1449
1471
|
}
|
|
1450
1472
|
|
|
1473
|
+
// src/services/serve-lock.ts
|
|
1474
|
+
import fs from "fs";
|
|
1475
|
+
import path from "path";
|
|
1476
|
+
import { createTranslator, detectNodeLocale } from "@fenglimg/fabric-shared";
|
|
1477
|
+
import { IOFabricError as IOFabricError2 } from "@fenglimg/fabric-shared/errors";
|
|
1478
|
+
var LOCK_FILENAME = ".serve.lock";
|
|
1479
|
+
var t = createTranslator(detectNodeLocale());
|
|
1480
|
+
var ServeLockHeldError = class extends IOFabricError2 {
|
|
1481
|
+
code = "SERVE_LOCK_HELD";
|
|
1482
|
+
httpStatus = 423;
|
|
1483
|
+
};
|
|
1484
|
+
function lockPath(projectRoot) {
|
|
1485
|
+
return path.join(projectRoot, ".fabric", LOCK_FILENAME);
|
|
1486
|
+
}
|
|
1487
|
+
function isAlive(pid) {
|
|
1488
|
+
try {
|
|
1489
|
+
process.kill(pid, 0);
|
|
1490
|
+
return true;
|
|
1491
|
+
} catch (e) {
|
|
1492
|
+
const err = e;
|
|
1493
|
+
if (err.code === "ESRCH") return false;
|
|
1494
|
+
if (err.code === "EPERM") return true;
|
|
1495
|
+
throw e;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
function acquireLock(projectRoot, opts) {
|
|
1499
|
+
const p = lockPath(projectRoot);
|
|
1500
|
+
if (fs.existsSync(p)) {
|
|
1501
|
+
let state = null;
|
|
1502
|
+
try {
|
|
1503
|
+
state = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
1504
|
+
} catch {
|
|
1505
|
+
}
|
|
1506
|
+
if (state && state.pid && state.pid !== process.pid && isAlive(state.pid) && !opts?.force) {
|
|
1507
|
+
throw new ServeLockHeldError(
|
|
1508
|
+
`serve lock held by live PID ${state.pid}`,
|
|
1509
|
+
{
|
|
1510
|
+
actionHint: t("cli.serve.lock-held.action-hint", { pid: String(state.pid) }),
|
|
1511
|
+
details: state
|
|
1512
|
+
}
|
|
1513
|
+
);
|
|
1514
|
+
}
|
|
1515
|
+
if (state && state.pid && !isAlive(state.pid)) {
|
|
1516
|
+
process.stderr.write(`[serve-lock] stale lock from PID ${state.pid} \u2014 overwriting
|
|
1517
|
+
`);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
1521
|
+
fs.writeFileSync(
|
|
1522
|
+
p,
|
|
1523
|
+
JSON.stringify({ pid: process.pid, acquiredAt: Date.now(), host: process.env.HOSTNAME })
|
|
1524
|
+
);
|
|
1525
|
+
}
|
|
1526
|
+
function releaseLock(projectRoot) {
|
|
1527
|
+
const p = lockPath(projectRoot);
|
|
1528
|
+
try {
|
|
1529
|
+
if (fs.existsSync(p)) {
|
|
1530
|
+
const state = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
1531
|
+
if (state.pid === process.pid) {
|
|
1532
|
+
fs.unlinkSync(p);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
} catch {
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
function readLockState(projectRoot) {
|
|
1539
|
+
const p = lockPath(projectRoot);
|
|
1540
|
+
if (!fs.existsSync(p)) return null;
|
|
1541
|
+
try {
|
|
1542
|
+
return JSON.parse(fs.readFileSync(p, "utf8"));
|
|
1543
|
+
} catch {
|
|
1544
|
+
return null;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
function checkLockOrThrow(projectRoot, opts) {
|
|
1548
|
+
const state = readLockState(projectRoot);
|
|
1549
|
+
if (state === null) return;
|
|
1550
|
+
if (state.pid === process.pid) return;
|
|
1551
|
+
if (!isAlive(state.pid)) {
|
|
1552
|
+
process.stderr.write(`[serve-lock] stale lock from PID ${state.pid} \u2014 ignoring
|
|
1553
|
+
`);
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
if (opts?.force) return;
|
|
1557
|
+
throw new ServeLockHeldError(
|
|
1558
|
+
`serve lock held by live PID ${state.pid}`,
|
|
1559
|
+
{
|
|
1560
|
+
actionHint: t("cli.serve.lock-held.action-hint", { pid: String(state.pid) }),
|
|
1561
|
+
details: state
|
|
1562
|
+
}
|
|
1563
|
+
);
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1451
1566
|
// src/services/doctor.ts
|
|
1452
1567
|
import { execFileSync } from "child_process";
|
|
1453
1568
|
import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync2, statSync as statSync4 } from "fs";
|
|
1454
|
-
import { access, mkdir as mkdir4, readFile as readFile5, rename, writeFile as writeFile2 } from "fs/promises";
|
|
1569
|
+
import { access, mkdir as mkdir4, readFile as readFile5, rename, unlink, writeFile as writeFile2 } from "fs/promises";
|
|
1455
1570
|
import { constants } from "fs";
|
|
1456
1571
|
import { homedir as homedir3 } from "os";
|
|
1457
1572
|
import { isAbsolute as isAbsolute2, join as join6, posix, relative as nodeRelative, resolve as resolve3, sep as sep3 } from "path";
|
|
@@ -1466,7 +1581,9 @@ import {
|
|
|
1466
1581
|
BOOTSTRAP_CANONICAL,
|
|
1467
1582
|
BOOTSTRAP_MARKER_BEGIN,
|
|
1468
1583
|
BOOTSTRAP_MARKER_END,
|
|
1469
|
-
BOOTSTRAP_REGEX
|
|
1584
|
+
BOOTSTRAP_REGEX,
|
|
1585
|
+
ONBOARD_SLOT_NAMES,
|
|
1586
|
+
ONBOARD_SLOT_TOTAL
|
|
1470
1587
|
} from "@fenglimg/fabric-shared";
|
|
1471
1588
|
import { detectFramework } from "@fenglimg/fabric-shared/node";
|
|
1472
1589
|
import { atomicWriteJson as atomicWriteJson2, atomicWriteText as atomicWriteText4 } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
@@ -1600,8 +1717,10 @@ async function runDoctorReport(target) {
|
|
|
1600
1717
|
const relevancePathsDrift = inspectRelevancePathsDrift(projectRoot);
|
|
1601
1718
|
const narrowTooFew = inspectNarrowTooFew(projectRoot, lintNow);
|
|
1602
1719
|
const sessionHintsStale = inspectSessionHintsStale(projectRoot, lintNow);
|
|
1720
|
+
const staleServeLock = inspectStaleServeLock(projectRoot, lintNow);
|
|
1603
1721
|
const relevanceFieldsMissing = inspectRelevanceFieldsMissing(projectRoot);
|
|
1604
1722
|
const skillMdYamlInvalid = inspectSkillMdYamlInvalid(projectRoot);
|
|
1723
|
+
const onboardCoverage = inspectOnboardCoverage(projectRoot);
|
|
1605
1724
|
const checks = [
|
|
1606
1725
|
createBootstrapAnchorCheck(bootstrapAnchor),
|
|
1607
1726
|
// v2.0.0-rc.19 TASK-004: bootstrap marker migration check sits adjacent to
|
|
@@ -1616,7 +1735,8 @@ async function runDoctorReport(target) {
|
|
|
1616
1735
|
createKnowledgeDirMissingCheck(knowledgeDirMissing),
|
|
1617
1736
|
// v2.0.0-rc.22 TASK-006: baseline filename format. Sits adjacent to
|
|
1618
1737
|
// knowledge_dir_missing — both are knowledge-layout invariants. manual_error
|
|
1619
|
-
// kind; resolution
|
|
1738
|
+
// kind; resolution is manual file deletion (rc.23 TASK-012 (F8a) removed
|
|
1739
|
+
// the baseline-emit pipeline, so no auto-fix exists).
|
|
1620
1740
|
createBaselineFilenameFormatCheck(baselineFilenameFormat),
|
|
1621
1741
|
createForensicCheck(forensic, framework.kind, entryPoints.length),
|
|
1622
1742
|
// v2.0: removed `createInitContextCheck` — `.fabric/init-context.json`
|
|
@@ -1670,6 +1790,10 @@ async function runDoctorReport(target) {
|
|
|
1670
1790
|
createNarrowTooFewCheck(narrowTooFew),
|
|
1671
1791
|
// rc.6 TASK-021 (E3): session-hints cache hygiene (lint #27). Info kind.
|
|
1672
1792
|
createSessionHintsStaleCheck(sessionHintsStale),
|
|
1793
|
+
// rc.23 TASK-010 (e): stale .fabric/.serve.lock advisory. Info kind —
|
|
1794
|
+
// does not bump report status. `--fix` unlinks the corpse and emits
|
|
1795
|
+
// `serve_lock_cleared`.
|
|
1796
|
+
createStaleServeLockCheck(staleServeLock),
|
|
1673
1797
|
// v2.0.0-rc.9 TASK-003 (A3): relevance fields back-fill (lint #28).
|
|
1674
1798
|
// Info kind — applies to pending entries only; canonical entries get
|
|
1675
1799
|
// the fields written verbatim by fab_review.approve/modify.
|
|
@@ -1677,6 +1801,11 @@ async function runDoctorReport(target) {
|
|
|
1677
1801
|
// rc.12 lint #29: skill_md_yaml_invalid. Warning kind — surfaces
|
|
1678
1802
|
// SKILL.md frontmatter that Codex CLI silently drops at load.
|
|
1679
1803
|
createSkillMdYamlInvalidCheck(skillMdYamlInvalid),
|
|
1804
|
+
// v2.0.0-rc.23 TASK-014 (F8c): Onboard coverage advisory. Info kind.
|
|
1805
|
+
// Surfaces uncovered S5 onboard slots and recommends /fabric-archive
|
|
1806
|
+
// first-run phase. Sits adjacent to Skill markdown YAML — both are
|
|
1807
|
+
// Skill-adjacent advisories. --fix never mutates onboard state.
|
|
1808
|
+
createOnboardCoverageCheck(onboardCoverage),
|
|
1680
1809
|
createPreexistingRootFilesCheck(preexistingRootFiles)
|
|
1681
1810
|
// v2.0 / rc.2: `createLegacyClientPathCheck` removed. The schema now
|
|
1682
1811
|
// rejects retired clientPaths keys (windsurf/rooCode/geminiCLI) at Zod
|
|
@@ -1717,7 +1846,7 @@ async function runDoctorReport(target) {
|
|
|
1717
1846
|
warningCount: warnings.length,
|
|
1718
1847
|
infoCount: infos.length,
|
|
1719
1848
|
targetFiles: Object.fromEntries(
|
|
1720
|
-
TARGET_FILE_PATHS.map((
|
|
1849
|
+
TARGET_FILE_PATHS.map((path2) => [path2, existsSync4(join6(projectRoot, path2))])
|
|
1721
1850
|
)
|
|
1722
1851
|
}
|
|
1723
1852
|
};
|
|
@@ -1731,11 +1860,11 @@ async function runDoctorFix(target) {
|
|
|
1731
1860
|
)) {
|
|
1732
1861
|
const migrated = await migrateBootstrapMarkers(projectRoot);
|
|
1733
1862
|
fixed.push(findIssue(before.fixable_errors, "bootstrap_marker_migration_required"));
|
|
1734
|
-
for (const
|
|
1863
|
+
for (const path2 of migrated.paths) {
|
|
1735
1864
|
await appendEventLedgerEvent(projectRoot, {
|
|
1736
1865
|
event_type: "bootstrap_marker_migrated",
|
|
1737
|
-
path,
|
|
1738
|
-
migrated_count: migrated.countPerPath[
|
|
1866
|
+
path: path2,
|
|
1867
|
+
migrated_count: migrated.countPerPath[path2] ?? 1,
|
|
1739
1868
|
legacy_marker: "fabric:knowledge-base",
|
|
1740
1869
|
new_marker: "fabric:bootstrap",
|
|
1741
1870
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -1814,6 +1943,31 @@ async function runDoctorFix(target) {
|
|
|
1814
1943
|
await fixMcpConfigInWrongFile(projectRoot);
|
|
1815
1944
|
fixed.push(findIssue(before.fixable_errors, "mcp_config_in_wrong_file"));
|
|
1816
1945
|
}
|
|
1946
|
+
if (before.infos.some((issue) => issue.code === "stale_serve_lock")) {
|
|
1947
|
+
const lockInspection = inspectStaleServeLock(projectRoot, Date.now());
|
|
1948
|
+
if (lockInspection.present && !lockInspection.pidAlive) {
|
|
1949
|
+
const lockFilePath = join6(projectRoot, ".fabric", ".serve.lock");
|
|
1950
|
+
try {
|
|
1951
|
+
await unlink(lockFilePath);
|
|
1952
|
+
} catch (err) {
|
|
1953
|
+
const errno = err;
|
|
1954
|
+
if (errno.code !== "ENOENT") throw err;
|
|
1955
|
+
}
|
|
1956
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
1957
|
+
event_type: "serve_lock_cleared",
|
|
1958
|
+
pid: lockInspection.pid,
|
|
1959
|
+
age_ms: lockInspection.ageMs,
|
|
1960
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1961
|
+
}).catch(() => {
|
|
1962
|
+
});
|
|
1963
|
+
fixed.push({
|
|
1964
|
+
code: "stale_serve_lock",
|
|
1965
|
+
name: "Serve lock",
|
|
1966
|
+
message: `Removed stale .fabric/.serve.lock (dead PID ${lockInspection.pid}).`,
|
|
1967
|
+
path: ".fabric/.serve.lock"
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1817
1971
|
const report = await runDoctorReport(projectRoot);
|
|
1818
1972
|
return {
|
|
1819
1973
|
changed: fixed.length > 0,
|
|
@@ -2027,8 +2181,8 @@ async function applyStaleArchive(projectRoot, candidate, now) {
|
|
|
2027
2181
|
if (renameError instanceof Error && "code" in renameError && renameError.code === "EXDEV") {
|
|
2028
2182
|
const data = await readFile5(sourceAbs);
|
|
2029
2183
|
await writeFile2(destAbs, data);
|
|
2030
|
-
const { unlink } = await import("fs/promises");
|
|
2031
|
-
await
|
|
2184
|
+
const { unlink: unlink2 } = await import("fs/promises");
|
|
2185
|
+
await unlink2(sourceAbs);
|
|
2032
2186
|
} else {
|
|
2033
2187
|
throw renameError;
|
|
2034
2188
|
}
|
|
@@ -2100,8 +2254,8 @@ async function applyPendingAutoArchive(projectRoot, candidate, now) {
|
|
|
2100
2254
|
if (renameError instanceof Error && "code" in renameError && renameError.code === "EXDEV") {
|
|
2101
2255
|
const data = await readFile5(candidate.pending_path_abs);
|
|
2102
2256
|
await writeFile2(candidate.archived_to_abs, data);
|
|
2103
|
-
const { unlink } = await import("fs/promises");
|
|
2104
|
-
await
|
|
2257
|
+
const { unlink: unlink2 } = await import("fs/promises");
|
|
2258
|
+
await unlink2(candidate.pending_path_abs);
|
|
2105
2259
|
} else {
|
|
2106
2260
|
throw renameError;
|
|
2107
2261
|
}
|
|
@@ -2158,8 +2312,8 @@ async function applySessionHintsStaleCleanup(projectRoot, candidate) {
|
|
|
2158
2312
|
const detail = `deleted (${candidate.age_days}d old)`;
|
|
2159
2313
|
const absPath = join6(projectRoot, candidate.path);
|
|
2160
2314
|
try {
|
|
2161
|
-
const { unlink } = await import("fs/promises");
|
|
2162
|
-
await
|
|
2315
|
+
const { unlink: unlink2 } = await import("fs/promises");
|
|
2316
|
+
await unlink2(absPath);
|
|
2163
2317
|
return {
|
|
2164
2318
|
kind: "knowledge_session_hints_stale_cleanup",
|
|
2165
2319
|
path: candidate.path,
|
|
@@ -2213,9 +2367,9 @@ function truncateErrorMessage(error) {
|
|
|
2213
2367
|
return raw.length > 240 ? `${raw.slice(0, 237)}...` : raw;
|
|
2214
2368
|
}
|
|
2215
2369
|
async function inspectForensic(projectRoot) {
|
|
2216
|
-
const
|
|
2370
|
+
const path2 = join6(projectRoot, ".fabric", "forensic.json");
|
|
2217
2371
|
try {
|
|
2218
|
-
const parsed = forensicReportSchema.parse(JSON.parse(await readFile5(
|
|
2372
|
+
const parsed = forensicReportSchema.parse(JSON.parse(await readFile5(path2, "utf8")));
|
|
2219
2373
|
return { present: true, valid: true, report: parsed };
|
|
2220
2374
|
} catch (error) {
|
|
2221
2375
|
if (isMissingFileError(error)) {
|
|
@@ -2326,15 +2480,15 @@ function inspectContentRefs(projectRoot, meta) {
|
|
|
2326
2480
|
return { missing, invalid };
|
|
2327
2481
|
}
|
|
2328
2482
|
async function inspectEventLedger(projectRoot) {
|
|
2329
|
-
const
|
|
2330
|
-
const exists = existsSync4(
|
|
2483
|
+
const path2 = getEventLedgerPath(projectRoot);
|
|
2484
|
+
const exists = existsSync4(path2);
|
|
2331
2485
|
if (!exists) {
|
|
2332
|
-
return { exists: false, writable: false, parseable: false, hasPartialWrite: false, partialWriteByteOffset: 0, partialWriteByteLength: 0, path };
|
|
2486
|
+
return { exists: false, writable: false, parseable: false, hasPartialWrite: false, partialWriteByteOffset: 0, partialWriteByteLength: 0, path: path2 };
|
|
2333
2487
|
}
|
|
2334
2488
|
try {
|
|
2335
|
-
await access(
|
|
2489
|
+
await access(path2, constants.W_OK);
|
|
2336
2490
|
const { warnings } = await readEventLedger(projectRoot);
|
|
2337
|
-
const raw = await readFile5(
|
|
2491
|
+
const raw = await readFile5(path2, "utf8");
|
|
2338
2492
|
const invalidLine = raw.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).find((line) => !isValidJsonLine(line));
|
|
2339
2493
|
const partialWarning = warnings.find((w) => w.kind === "partial_write_at_tail");
|
|
2340
2494
|
return {
|
|
@@ -2344,7 +2498,7 @@ async function inspectEventLedger(projectRoot) {
|
|
|
2344
2498
|
hasPartialWrite: partialWarning !== void 0,
|
|
2345
2499
|
partialWriteByteOffset: partialWarning?.byte_offset ?? 0,
|
|
2346
2500
|
partialWriteByteLength: partialWarning?.byte_length ?? 0,
|
|
2347
|
-
path,
|
|
2501
|
+
path: path2,
|
|
2348
2502
|
error: invalidLine === void 0 ? void 0 : "events.jsonl contains an invalid JSON line."
|
|
2349
2503
|
};
|
|
2350
2504
|
} catch (error) {
|
|
@@ -2355,16 +2509,16 @@ async function inspectEventLedger(projectRoot) {
|
|
|
2355
2509
|
hasPartialWrite: false,
|
|
2356
2510
|
partialWriteByteOffset: 0,
|
|
2357
2511
|
partialWriteByteLength: 0,
|
|
2358
|
-
path,
|
|
2512
|
+
path: path2,
|
|
2359
2513
|
error: error instanceof Error ? error.message : String(error)
|
|
2360
2514
|
};
|
|
2361
2515
|
}
|
|
2362
2516
|
}
|
|
2363
2517
|
async function inspectKnowledgeTestIndex(projectRoot) {
|
|
2364
|
-
const
|
|
2518
|
+
const path2 = join6(projectRoot, ".fabric", ".cache", "knowledge-test.index.json");
|
|
2365
2519
|
const built = await tryBuildRuleMeta(projectRoot);
|
|
2366
2520
|
try {
|
|
2367
|
-
const index = knowledgeTestIndexSchema2.parse(JSON.parse(await readFile5(
|
|
2521
|
+
const index = knowledgeTestIndexSchema2.parse(JSON.parse(await readFile5(path2, "utf8")));
|
|
2368
2522
|
return {
|
|
2369
2523
|
present: true,
|
|
2370
2524
|
valid: true,
|
|
@@ -2590,8 +2744,8 @@ function inspectKnowledgeDirMissing(projectRoot) {
|
|
|
2590
2744
|
const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
|
|
2591
2745
|
const missingSubdirs = [];
|
|
2592
2746
|
for (const sub of KNOWLEDGE_SUBDIRS3) {
|
|
2593
|
-
const
|
|
2594
|
-
if (!existsSync4(
|
|
2747
|
+
const path2 = join6(knowledgeRoot, sub);
|
|
2748
|
+
if (!existsSync4(path2)) {
|
|
2595
2749
|
missingSubdirs.push(`.fabric/knowledge/${sub}`);
|
|
2596
2750
|
}
|
|
2597
2751
|
}
|
|
@@ -2660,7 +2814,7 @@ function createBaselineFilenameFormatCheck(inspection) {
|
|
|
2660
2814
|
"manual_error",
|
|
2661
2815
|
"lint-baseline-filename-format",
|
|
2662
2816
|
`${inspection.offenders.length} baseline knowledge file${inspection.offenders.length === 1 ? "" : "s"} use${inspection.offenders.length === 1 ? "s" : ""} the deprecated bare-slug filename format and must be migrated to \`\${id}--\${slug}.md\`. First: ${detail}.`,
|
|
2663
|
-
"
|
|
2817
|
+
"Delete the legacy bare-slug baseline file(s) manually \u2014 the baseline pipeline was removed in rc.23 and is no longer an auto-fix path."
|
|
2664
2818
|
);
|
|
2665
2819
|
}
|
|
2666
2820
|
function createKnowledgeDirMissingCheck(inspection) {
|
|
@@ -2889,7 +3043,9 @@ function collectMdFilesUnder(out, projectRoot, rootDir, relPrefix) {
|
|
|
2889
3043
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
2890
3044
|
const abs = join6(dir, entry.name);
|
|
2891
3045
|
if (entry.isDirectory()) {
|
|
2892
|
-
|
|
3046
|
+
if (entry.name !== "pending" && entry.name !== "archive") {
|
|
3047
|
+
stack.push(abs);
|
|
3048
|
+
}
|
|
2893
3049
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
2894
3050
|
const rel = posix.join(relPrefix, abs.slice(rootDir.length + 1).replace(/\\/gu, "/"));
|
|
2895
3051
|
out.add(rel);
|
|
@@ -2991,7 +3147,7 @@ function inspectCounterDesync(meta) {
|
|
|
2991
3147
|
["guideline", "GLD"],
|
|
2992
3148
|
["pitfall", "PIT"],
|
|
2993
3149
|
["process", "PRO"]
|
|
2994
|
-
].find(([
|
|
3150
|
+
].find(([t2]) => t2 === parsed.type)?.[1];
|
|
2995
3151
|
if (typeCode === void 0) {
|
|
2996
3152
|
continue;
|
|
2997
3153
|
}
|
|
@@ -3582,6 +3738,20 @@ function inspectSessionHintsStale(projectRoot, now) {
|
|
|
3582
3738
|
candidates.sort((a, b) => a.path.localeCompare(b.path));
|
|
3583
3739
|
return { candidates };
|
|
3584
3740
|
}
|
|
3741
|
+
function inspectStaleServeLock(projectRoot, now) {
|
|
3742
|
+
const state = readLockState(projectRoot);
|
|
3743
|
+
if (state === null) {
|
|
3744
|
+
return { present: false };
|
|
3745
|
+
}
|
|
3746
|
+
const ageMs = Math.max(0, now - state.acquiredAt);
|
|
3747
|
+
return {
|
|
3748
|
+
present: true,
|
|
3749
|
+
pid: state.pid,
|
|
3750
|
+
acquiredAt: state.acquiredAt,
|
|
3751
|
+
ageMs,
|
|
3752
|
+
pidAlive: isAlive(state.pid)
|
|
3753
|
+
};
|
|
3754
|
+
}
|
|
3585
3755
|
function inspectNarrowTooFew(projectRoot, now) {
|
|
3586
3756
|
let total = 0;
|
|
3587
3757
|
let narrowWithPaths = 0;
|
|
@@ -3740,6 +3910,28 @@ function createSessionHintsStaleCheck(inspection) {
|
|
|
3740
3910
|
"Run `fab doctor --apply-lint` to delete stale session-hints cache files."
|
|
3741
3911
|
);
|
|
3742
3912
|
}
|
|
3913
|
+
function createStaleServeLockCheck(inspection) {
|
|
3914
|
+
if (!inspection.present) {
|
|
3915
|
+
return okCheck("Serve lock", "No .fabric/.serve.lock present.");
|
|
3916
|
+
}
|
|
3917
|
+
if (inspection.pidAlive) {
|
|
3918
|
+
return okCheck(
|
|
3919
|
+
"Serve lock",
|
|
3920
|
+
`.fabric/.serve.lock held by live PID ${inspection.pid}.`
|
|
3921
|
+
);
|
|
3922
|
+
}
|
|
3923
|
+
const days = Math.floor(inspection.ageMs / MS_PER_DAY);
|
|
3924
|
+
const hours = Math.floor(inspection.ageMs / (60 * 60 * 1e3));
|
|
3925
|
+
const acquiredAgo = days >= 1 ? `${days} day${days === 1 ? "" : "s"} ago` : `${hours} hour${hours === 1 ? "" : "s"} ago`;
|
|
3926
|
+
return issueCheck(
|
|
3927
|
+
"Serve lock",
|
|
3928
|
+
"ok",
|
|
3929
|
+
"info",
|
|
3930
|
+
"stale_serve_lock",
|
|
3931
|
+
`[advisory] .fabric/.serve.lock holds dead PID ${inspection.pid} (acquired ${acquiredAgo}). Run \`fab doctor --fix\` to remove.`,
|
|
3932
|
+
"Run `fab doctor --fix` to remove the stale .fabric/.serve.lock."
|
|
3933
|
+
);
|
|
3934
|
+
}
|
|
3743
3935
|
function extractKnowledgeFrontmatterRelevanceScope(source) {
|
|
3744
3936
|
const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
|
|
3745
3937
|
const fm = FM_PATTERN.exec(source);
|
|
@@ -4244,6 +4436,117 @@ function createSkillMdYamlInvalidCheck(inspection) {
|
|
|
4244
4436
|
'Quote the value with double quotes (`description: "\u2026"`) or rewrite the inner `key: value` token to `key=value`.'
|
|
4245
4437
|
);
|
|
4246
4438
|
}
|
|
4439
|
+
var KNOWLEDGE_CANONICAL_TYPE_DIRS_FOR_ONBOARD = [
|
|
4440
|
+
"decisions",
|
|
4441
|
+
"pitfalls",
|
|
4442
|
+
"guidelines",
|
|
4443
|
+
"models",
|
|
4444
|
+
"processes"
|
|
4445
|
+
];
|
|
4446
|
+
function inspectOnboardCoverage(projectRoot) {
|
|
4447
|
+
const filled = {};
|
|
4448
|
+
for (const slot of ONBOARD_SLOT_NAMES) {
|
|
4449
|
+
filled[slot] = [];
|
|
4450
|
+
}
|
|
4451
|
+
const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
|
|
4452
|
+
if (existsSync4(knowledgeRoot)) {
|
|
4453
|
+
for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS_FOR_ONBOARD) {
|
|
4454
|
+
const dir = join6(knowledgeRoot, typeDir);
|
|
4455
|
+
if (!existsSync4(dir)) continue;
|
|
4456
|
+
let entries;
|
|
4457
|
+
try {
|
|
4458
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
4459
|
+
} catch {
|
|
4460
|
+
continue;
|
|
4461
|
+
}
|
|
4462
|
+
for (const entry of entries) {
|
|
4463
|
+
if (!entry.isFile()) continue;
|
|
4464
|
+
if (!entry.name.endsWith(".md")) continue;
|
|
4465
|
+
const filePath = join6(dir, entry.name);
|
|
4466
|
+
let content;
|
|
4467
|
+
try {
|
|
4468
|
+
content = readFileSync2(filePath, "utf8");
|
|
4469
|
+
} catch {
|
|
4470
|
+
continue;
|
|
4471
|
+
}
|
|
4472
|
+
const slot = readFrontmatterScalar(content, "onboard_slot");
|
|
4473
|
+
if (slot === void 0) continue;
|
|
4474
|
+
if (!ONBOARD_SLOT_NAMES.includes(slot)) continue;
|
|
4475
|
+
const stableId = readFrontmatterScalar(content, "id") ?? entry.name.replace(/\.md$/u, "");
|
|
4476
|
+
filled[slot].push(stableId);
|
|
4477
|
+
}
|
|
4478
|
+
}
|
|
4479
|
+
}
|
|
4480
|
+
for (const slot of ONBOARD_SLOT_NAMES) {
|
|
4481
|
+
filled[slot].sort();
|
|
4482
|
+
}
|
|
4483
|
+
const optedOut = readOnboardOptedOut(projectRoot);
|
|
4484
|
+
const missing = ONBOARD_SLOT_NAMES.filter((slot) => {
|
|
4485
|
+
if (filled[slot].length > 0) return false;
|
|
4486
|
+
if (optedOut.includes(slot)) return false;
|
|
4487
|
+
return true;
|
|
4488
|
+
});
|
|
4489
|
+
return { filled, missing, opted_out: optedOut };
|
|
4490
|
+
}
|
|
4491
|
+
function readOnboardOptedOut(projectRoot) {
|
|
4492
|
+
const path2 = join6(projectRoot, ".fabric", "fabric-config.json");
|
|
4493
|
+
if (!existsSync4(path2)) return [];
|
|
4494
|
+
let raw;
|
|
4495
|
+
try {
|
|
4496
|
+
raw = readFileSync2(path2, "utf8");
|
|
4497
|
+
} catch {
|
|
4498
|
+
return [];
|
|
4499
|
+
}
|
|
4500
|
+
let parsed;
|
|
4501
|
+
try {
|
|
4502
|
+
parsed = JSON.parse(raw);
|
|
4503
|
+
} catch {
|
|
4504
|
+
return [];
|
|
4505
|
+
}
|
|
4506
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4507
|
+
return [];
|
|
4508
|
+
}
|
|
4509
|
+
const list = parsed.onboard_slots_opted_out;
|
|
4510
|
+
if (!Array.isArray(list)) return [];
|
|
4511
|
+
return list.filter((v) => typeof v === "string");
|
|
4512
|
+
}
|
|
4513
|
+
function readFrontmatterScalar(content, key) {
|
|
4514
|
+
const match = /^---\n([\s\S]*?)\n---/u.exec(content);
|
|
4515
|
+
if (match === null) return void 0;
|
|
4516
|
+
const block = match[1];
|
|
4517
|
+
if (block === void 0) return void 0;
|
|
4518
|
+
for (const rawLine of block.split(/\r?\n/u)) {
|
|
4519
|
+
const line = rawLine.trim();
|
|
4520
|
+
const sep4 = line.indexOf(":");
|
|
4521
|
+
if (sep4 === -1) continue;
|
|
4522
|
+
if (line.slice(0, sep4).trim() !== key) continue;
|
|
4523
|
+
let value = line.slice(sep4 + 1).trim();
|
|
4524
|
+
if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
|
|
4525
|
+
value = value.slice(1, -1);
|
|
4526
|
+
}
|
|
4527
|
+
return value;
|
|
4528
|
+
}
|
|
4529
|
+
return void 0;
|
|
4530
|
+
}
|
|
4531
|
+
function createOnboardCoverageCheck(inspection) {
|
|
4532
|
+
const filledCount = ONBOARD_SLOT_NAMES.filter(
|
|
4533
|
+
(slot) => inspection.filled[slot].length > 0
|
|
4534
|
+
).length;
|
|
4535
|
+
if (inspection.missing.length === 0) {
|
|
4536
|
+
return okCheck(
|
|
4537
|
+
"Onboard coverage",
|
|
4538
|
+
`Onboard coverage: ${filledCount}/${ONBOARD_SLOT_TOTAL} \u2713 (opted-out: ${inspection.opted_out.length}).`
|
|
4539
|
+
);
|
|
4540
|
+
}
|
|
4541
|
+
return issueCheck(
|
|
4542
|
+
"Onboard coverage",
|
|
4543
|
+
"ok",
|
|
4544
|
+
"info",
|
|
4545
|
+
"onboard_coverage_incomplete",
|
|
4546
|
+
`Onboard slots not yet covered: [${inspection.missing.join(", ")}]. ${filledCount}/${ONBOARD_SLOT_TOTAL} filled; ${inspection.opted_out.length} opted-out.`,
|
|
4547
|
+
"Run /fabric-archive to onboard \u2014 the Skill's first-run phase will tour the project and propose pending entries for each unclaimed slot."
|
|
4548
|
+
);
|
|
4549
|
+
}
|
|
4247
4550
|
function createNarrowTooFewCheck(inspection) {
|
|
4248
4551
|
const { structural_flagged, telemetry_flagged } = inspection;
|
|
4249
4552
|
if (!structural_flagged && !telemetry_flagged) {
|
|
@@ -4652,9 +4955,9 @@ async function fixCounterDesync(projectRoot) {
|
|
|
4652
4955
|
await atomicWriteJson2(metaPath, updated, { indent: 2 });
|
|
4653
4956
|
}
|
|
4654
4957
|
async function ensureEventLedger(projectRoot) {
|
|
4655
|
-
const
|
|
4656
|
-
await ensureParentDirectory(
|
|
4657
|
-
await writeFile2(
|
|
4958
|
+
const path2 = getEventLedgerPath(projectRoot);
|
|
4959
|
+
await ensureParentDirectory(path2);
|
|
4960
|
+
await writeFile2(path2, "", { encoding: "utf8", flag: "a" });
|
|
4658
4961
|
}
|
|
4659
4962
|
var CITE_POLICY_VERSION = "2.0.0-rc.20";
|
|
4660
4963
|
async function ensureCitePolicyActivatedMarker(projectRoot) {
|
|
@@ -4681,6 +4984,48 @@ async function ensureCitePolicyActivatedMarker(projectRoot) {
|
|
|
4681
4984
|
return { marker_ts: 0, emitted_now: false };
|
|
4682
4985
|
}
|
|
4683
4986
|
}
|
|
4987
|
+
async function ensureCiteContractPolicyActivatedMarker(projectRoot) {
|
|
4988
|
+
let driftStatus;
|
|
4989
|
+
try {
|
|
4990
|
+
const inspection = await inspectL1BootstrapSnapshotDrift(projectRoot);
|
|
4991
|
+
driftStatus = inspection.status;
|
|
4992
|
+
} catch {
|
|
4993
|
+
driftStatus = "drift";
|
|
4994
|
+
}
|
|
4995
|
+
if (driftStatus !== "ok") {
|
|
4996
|
+
return { marker_ts: 0, emitted_now: false, blocked_by: "bootstrap_drift" };
|
|
4997
|
+
}
|
|
4998
|
+
let existing;
|
|
4999
|
+
try {
|
|
5000
|
+
const { events } = await readEventLedger(projectRoot, {
|
|
5001
|
+
event_type: "cite_contract_policy_activated"
|
|
5002
|
+
});
|
|
5003
|
+
if (events.length > 0) {
|
|
5004
|
+
existing = events[0];
|
|
5005
|
+
}
|
|
5006
|
+
} catch {
|
|
5007
|
+
return { marker_ts: 0, emitted_now: false, blocked_by: null };
|
|
5008
|
+
}
|
|
5009
|
+
if (existing !== void 0) {
|
|
5010
|
+
return { marker_ts: existing.ts, emitted_now: false, blocked_by: null };
|
|
5011
|
+
}
|
|
5012
|
+
try {
|
|
5013
|
+
const stored = await appendEventLedgerEvent(projectRoot, {
|
|
5014
|
+
event_type: "cite_contract_policy_activated"
|
|
5015
|
+
});
|
|
5016
|
+
return { marker_ts: stored.ts, emitted_now: true, blocked_by: null };
|
|
5017
|
+
} catch {
|
|
5018
|
+
return { marker_ts: 0, emitted_now: false, blocked_by: null };
|
|
5019
|
+
}
|
|
5020
|
+
}
|
|
5021
|
+
function parseNoneSentinel(kbLineRaw) {
|
|
5022
|
+
if (typeof kbLineRaw !== "string" || kbLineRaw.length === 0) return "unspecified";
|
|
5023
|
+
const m = kbLineRaw.match(/^KB:\s*none\b\s*(?:\[([^\]]*)\])?\s*$/i);
|
|
5024
|
+
if (m === null) return "unspecified";
|
|
5025
|
+
const inner = (m[1] ?? "").trim().toLowerCase();
|
|
5026
|
+
if (inner === "no-relevant" || inner === "not-applicable") return inner;
|
|
5027
|
+
return "unspecified";
|
|
5028
|
+
}
|
|
4684
5029
|
function categorizeCiteTag(tag) {
|
|
4685
5030
|
if (tag === "planned" || tag === "recalled" || tag === "chained-from" || tag === "none") {
|
|
4686
5031
|
return { category: tag };
|
|
@@ -4710,7 +5055,10 @@ function matchesRelevancePath(editPath, relevancePaths) {
|
|
|
4710
5055
|
return false;
|
|
4711
5056
|
}
|
|
4712
5057
|
async function runDoctorCiteCoverage(projectRoot, options) {
|
|
5058
|
+
const layerFilter = options.layer ?? "all";
|
|
4713
5059
|
const marker = await ensureCitePolicyActivatedMarker(projectRoot);
|
|
5060
|
+
const contractMarker = await ensureCiteContractPolicyActivatedMarker(projectRoot);
|
|
5061
|
+
const idTypeMap = await loadKbIdTypeMap(projectRoot);
|
|
4714
5062
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4715
5063
|
const zeroMetrics = {
|
|
4716
5064
|
edits_touched: 0,
|
|
@@ -4719,6 +5067,20 @@ async function runDoctorCiteCoverage(projectRoot, options) {
|
|
|
4719
5067
|
expected_but_missed: 0,
|
|
4720
5068
|
total_turns: 0
|
|
4721
5069
|
};
|
|
5070
|
+
const contractStatus = contractMarker.blocked_by === "bootstrap_drift" ? "skipped:bootstrap_drift" : contractMarker.marker_ts === 0 ? "awaiting_marker" : "ok";
|
|
5071
|
+
const zeroContractMetrics = {
|
|
5072
|
+
decisions_cited: 0,
|
|
5073
|
+
pitfalls_cited: 0,
|
|
5074
|
+
contract_with: 0,
|
|
5075
|
+
contract_missing: 0,
|
|
5076
|
+
hard_violated: 0,
|
|
5077
|
+
cite_id_unresolved: 0,
|
|
5078
|
+
skip_count: {}
|
|
5079
|
+
};
|
|
5080
|
+
const zeroLayerType = {
|
|
5081
|
+
team: {},
|
|
5082
|
+
personal: {}
|
|
5083
|
+
};
|
|
4722
5084
|
if (marker.marker_ts === 0) {
|
|
4723
5085
|
return {
|
|
4724
5086
|
status: "skipped",
|
|
@@ -4726,11 +5088,17 @@ async function runDoctorCiteCoverage(projectRoot, options) {
|
|
|
4726
5088
|
marker_emitted_now: false,
|
|
4727
5089
|
since_ts: options.since,
|
|
4728
5090
|
client_filter: options.client,
|
|
5091
|
+
layer_filter: layerFilter,
|
|
4729
5092
|
metrics: zeroMetrics,
|
|
5093
|
+
contract_metrics_status: contractStatus,
|
|
5094
|
+
contract_metrics: zeroContractMetrics,
|
|
5095
|
+
per_layer_type: zeroLayerType,
|
|
5096
|
+
contract_marker_ts: contractMarker.marker_ts,
|
|
4730
5097
|
generated_at: generatedAt
|
|
4731
5098
|
};
|
|
4732
5099
|
}
|
|
4733
5100
|
const effectiveSince = Math.max(marker.marker_ts, options.since);
|
|
5101
|
+
const contractEffectiveSince = contractStatus === "ok" ? Math.max(contractMarker.marker_ts, options.since) : Number.POSITIVE_INFINITY;
|
|
4734
5102
|
let ledgerEvents = [];
|
|
4735
5103
|
try {
|
|
4736
5104
|
const result = await readEventLedger(projectRoot, { since: effectiveSince });
|
|
@@ -4742,7 +5110,12 @@ async function runDoctorCiteCoverage(projectRoot, options) {
|
|
|
4742
5110
|
marker_emitted_now: marker.emitted_now,
|
|
4743
5111
|
since_ts: effectiveSince,
|
|
4744
5112
|
client_filter: options.client,
|
|
5113
|
+
layer_filter: layerFilter,
|
|
4745
5114
|
metrics: zeroMetrics,
|
|
5115
|
+
contract_metrics_status: contractStatus,
|
|
5116
|
+
contract_metrics: zeroContractMetrics,
|
|
5117
|
+
per_layer_type: zeroLayerType,
|
|
5118
|
+
contract_marker_ts: contractMarker.marker_ts,
|
|
4746
5119
|
generated_at: generatedAt
|
|
4747
5120
|
};
|
|
4748
5121
|
}
|
|
@@ -4764,7 +5137,7 @@ async function runDoctorCiteCoverage(projectRoot, options) {
|
|
|
4764
5137
|
break;
|
|
4765
5138
|
}
|
|
4766
5139
|
}
|
|
4767
|
-
const filteredTurns = options.client === "all" ? assistantTurns : assistantTurns.filter((
|
|
5140
|
+
const filteredTurns = options.client === "all" ? assistantTurns : assistantTurns.filter((t2) => t2.client === options.client);
|
|
4768
5141
|
let clientSessionIds = null;
|
|
4769
5142
|
if (options.client !== "all") {
|
|
4770
5143
|
clientSessionIds = /* @__PURE__ */ new Set();
|
|
@@ -4819,6 +5192,7 @@ async function runDoctorCiteCoverage(projectRoot, options) {
|
|
|
4819
5192
|
return false;
|
|
4820
5193
|
};
|
|
4821
5194
|
const dismissedHistogram = {};
|
|
5195
|
+
const noneHistogram = {};
|
|
4822
5196
|
const perClientAccum = /* @__PURE__ */ new Map();
|
|
4823
5197
|
const emptyMetrics = () => ({
|
|
4824
5198
|
edits_touched: 0,
|
|
@@ -4834,6 +5208,80 @@ async function runDoctorCiteCoverage(projectRoot, options) {
|
|
|
4834
5208
|
perClientAccum.set(client, existing);
|
|
4835
5209
|
};
|
|
4836
5210
|
const sessionCitedKbs = /* @__PURE__ */ new Map();
|
|
5211
|
+
const sessionEditPaths = /* @__PURE__ */ new Map();
|
|
5212
|
+
for (const edit of editEvents) {
|
|
5213
|
+
const sid = edit.session_id;
|
|
5214
|
+
if (typeof sid !== "string" || sid.length === 0) continue;
|
|
5215
|
+
const list = sessionEditPaths.get(sid) ?? [];
|
|
5216
|
+
list.push(normalizePath(edit.path));
|
|
5217
|
+
sessionEditPaths.set(sid, list);
|
|
5218
|
+
}
|
|
5219
|
+
let decisionsCited = 0;
|
|
5220
|
+
let pitfallsCited = 0;
|
|
5221
|
+
let contractWith = 0;
|
|
5222
|
+
let contractMissing = 0;
|
|
5223
|
+
let hardViolated = 0;
|
|
5224
|
+
let citeIdUnresolved = 0;
|
|
5225
|
+
const skipCount = {};
|
|
5226
|
+
const layerTypeAccum = { team: {}, personal: {} };
|
|
5227
|
+
const bumpLayerType = (citeId, type) => {
|
|
5228
|
+
const layer = citeId.startsWith("KP-") ? "personal" : citeId.startsWith("KT-") ? "team" : null;
|
|
5229
|
+
if (layer === null) return;
|
|
5230
|
+
layerTypeAccum[layer][type] = (layerTypeAccum[layer][type] ?? 0) + 1;
|
|
5231
|
+
};
|
|
5232
|
+
const passesLayerFilter = (citeId) => {
|
|
5233
|
+
if (layerFilter === "all") return true;
|
|
5234
|
+
if (layerFilter === "team") return citeId.startsWith("KT-");
|
|
5235
|
+
return citeId.startsWith("KP-");
|
|
5236
|
+
};
|
|
5237
|
+
const evaluateOperatorViolation = (sessionId, operators) => {
|
|
5238
|
+
const editPaths = typeof sessionId === "string" && sessionId.length > 0 ? sessionEditPaths.get(sessionId) ?? [] : [];
|
|
5239
|
+
for (const op of operators) {
|
|
5240
|
+
switch (op.kind) {
|
|
5241
|
+
case "edit": {
|
|
5242
|
+
let matched = false;
|
|
5243
|
+
for (const p of editPaths) {
|
|
5244
|
+
if (minimatch(p, op.target, { dot: true, matchBase: false })) {
|
|
5245
|
+
matched = true;
|
|
5246
|
+
break;
|
|
5247
|
+
}
|
|
5248
|
+
}
|
|
5249
|
+
if (!matched) return true;
|
|
5250
|
+
break;
|
|
5251
|
+
}
|
|
5252
|
+
case "not_edit": {
|
|
5253
|
+
for (const p of editPaths) {
|
|
5254
|
+
if (minimatch(p, op.target, { dot: true, matchBase: false })) {
|
|
5255
|
+
return true;
|
|
5256
|
+
}
|
|
5257
|
+
}
|
|
5258
|
+
break;
|
|
5259
|
+
}
|
|
5260
|
+
case "require": {
|
|
5261
|
+
let found = false;
|
|
5262
|
+
for (const p of editPaths) {
|
|
5263
|
+
if (p.includes(op.target)) {
|
|
5264
|
+
found = true;
|
|
5265
|
+
break;
|
|
5266
|
+
}
|
|
5267
|
+
}
|
|
5268
|
+
if (!found) return true;
|
|
5269
|
+
break;
|
|
5270
|
+
}
|
|
5271
|
+
case "forbid": {
|
|
5272
|
+
for (const p of editPaths) {
|
|
5273
|
+
if (p.includes(op.target)) {
|
|
5274
|
+
return true;
|
|
5275
|
+
}
|
|
5276
|
+
}
|
|
5277
|
+
break;
|
|
5278
|
+
}
|
|
5279
|
+
default:
|
|
5280
|
+
break;
|
|
5281
|
+
}
|
|
5282
|
+
}
|
|
5283
|
+
return false;
|
|
5284
|
+
};
|
|
4837
5285
|
let totalTurns = 0;
|
|
4838
5286
|
let qualifyingCites = 0;
|
|
4839
5287
|
let recalledUnverified = 0;
|
|
@@ -4868,7 +5316,11 @@ async function runDoctorCiteCoverage(projectRoot, options) {
|
|
|
4868
5316
|
dismissedHistogram[key] = (dismissedHistogram[key] ?? 0) + 1;
|
|
4869
5317
|
break;
|
|
4870
5318
|
}
|
|
4871
|
-
case "none":
|
|
5319
|
+
case "none": {
|
|
5320
|
+
const sentinel = parseNoneSentinel(turn.kb_line_raw);
|
|
5321
|
+
noneHistogram[sentinel] = (noneHistogram[sentinel] ?? 0) + 1;
|
|
5322
|
+
break;
|
|
5323
|
+
}
|
|
4872
5324
|
default:
|
|
4873
5325
|
break;
|
|
4874
5326
|
}
|
|
@@ -4879,6 +5331,40 @@ async function runDoctorCiteCoverage(projectRoot, options) {
|
|
|
4879
5331
|
m.recalled_unverified += 1;
|
|
4880
5332
|
});
|
|
4881
5333
|
}
|
|
5334
|
+
if (contractStatus === "ok" && turn.ts >= contractEffectiveSince) {
|
|
5335
|
+
const commitments = turn.cite_commitments ?? [];
|
|
5336
|
+
for (let i = 0; i < turn.cite_ids.length; i += 1) {
|
|
5337
|
+
const citeId = turn.cite_ids[i];
|
|
5338
|
+
if (typeof citeId !== "string" || citeId.length === 0) continue;
|
|
5339
|
+
if (!passesLayerFilter(citeId)) continue;
|
|
5340
|
+
const kbType = idTypeMap.get(citeId);
|
|
5341
|
+
if (kbType === void 0) {
|
|
5342
|
+
citeIdUnresolved += 1;
|
|
5343
|
+
bumpLayerType(citeId, "unresolved");
|
|
5344
|
+
continue;
|
|
5345
|
+
}
|
|
5346
|
+
bumpLayerType(citeId, kbType);
|
|
5347
|
+
if (kbType === "decision" || kbType === "pitfall") {
|
|
5348
|
+
if (kbType === "decision") decisionsCited += 1;
|
|
5349
|
+
else pitfallsCited += 1;
|
|
5350
|
+
const commitment = commitments[i];
|
|
5351
|
+
const operators = commitment?.operators ?? [];
|
|
5352
|
+
const skipReason = commitment?.skip_reason ?? null;
|
|
5353
|
+
if (skipReason !== null) {
|
|
5354
|
+
skipCount[skipReason] = (skipCount[skipReason] ?? 0) + 1;
|
|
5355
|
+
continue;
|
|
5356
|
+
}
|
|
5357
|
+
if (operators.length === 0) {
|
|
5358
|
+
contractMissing += 1;
|
|
5359
|
+
continue;
|
|
5360
|
+
}
|
|
5361
|
+
contractWith += 1;
|
|
5362
|
+
if (evaluateOperatorViolation(sid, operators)) {
|
|
5363
|
+
hardViolated += 1;
|
|
5364
|
+
}
|
|
5365
|
+
}
|
|
5366
|
+
}
|
|
5367
|
+
}
|
|
4882
5368
|
}
|
|
4883
5369
|
let editsTouched = 0;
|
|
4884
5370
|
let expectedButMissed = 0;
|
|
@@ -4913,18 +5399,96 @@ async function runDoctorCiteCoverage(projectRoot, options) {
|
|
|
4913
5399
|
perClient[client] = m;
|
|
4914
5400
|
}
|
|
4915
5401
|
}
|
|
5402
|
+
const contractMetrics = {
|
|
5403
|
+
decisions_cited: decisionsCited,
|
|
5404
|
+
pitfalls_cited: pitfallsCited,
|
|
5405
|
+
contract_with: contractWith,
|
|
5406
|
+
contract_missing: contractMissing,
|
|
5407
|
+
hard_violated: hardViolated,
|
|
5408
|
+
cite_id_unresolved: citeIdUnresolved,
|
|
5409
|
+
skip_count: skipCount
|
|
5410
|
+
};
|
|
4916
5411
|
return {
|
|
4917
5412
|
status: "ok",
|
|
4918
5413
|
marker_ts: marker.marker_ts,
|
|
4919
5414
|
marker_emitted_now: marker.emitted_now,
|
|
4920
5415
|
since_ts: effectiveSince,
|
|
4921
5416
|
client_filter: options.client,
|
|
5417
|
+
layer_filter: layerFilter,
|
|
4922
5418
|
metrics,
|
|
4923
5419
|
...perClient !== void 0 ? { per_client: perClient } : {},
|
|
4924
5420
|
...Object.keys(dismissedHistogram).length > 0 ? { dismissed_reason_histogram: dismissedHistogram } : {},
|
|
5421
|
+
...Object.keys(noneHistogram).length > 0 ? { none_reason_histogram: noneHistogram } : {},
|
|
5422
|
+
contract_metrics_status: contractStatus,
|
|
5423
|
+
contract_metrics: contractMetrics,
|
|
5424
|
+
per_layer_type: layerTypeAccum,
|
|
5425
|
+
contract_marker_ts: contractMarker.marker_ts,
|
|
4925
5426
|
generated_at: generatedAt
|
|
4926
5427
|
};
|
|
4927
5428
|
}
|
|
5429
|
+
async function runDoctorArchiveHistory(projectRoot, options) {
|
|
5430
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5431
|
+
const nowMs = Date.now();
|
|
5432
|
+
let events = [];
|
|
5433
|
+
try {
|
|
5434
|
+
const result = await readEventLedger(projectRoot, {
|
|
5435
|
+
event_type: "session_archive_attempted",
|
|
5436
|
+
since: options.since
|
|
5437
|
+
});
|
|
5438
|
+
events = result.events;
|
|
5439
|
+
} catch {
|
|
5440
|
+
return {
|
|
5441
|
+
entries: [],
|
|
5442
|
+
total: 0,
|
|
5443
|
+
since_ms: options.since,
|
|
5444
|
+
generated_at: generatedAt
|
|
5445
|
+
};
|
|
5446
|
+
}
|
|
5447
|
+
const mostRecentBySession = /* @__PURE__ */ new Map();
|
|
5448
|
+
for (const event of events) {
|
|
5449
|
+
if (event.event_type !== "session_archive_attempted") {
|
|
5450
|
+
continue;
|
|
5451
|
+
}
|
|
5452
|
+
const sessionId = event.session_id;
|
|
5453
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
5454
|
+
continue;
|
|
5455
|
+
}
|
|
5456
|
+
const prior = mostRecentBySession.get(sessionId);
|
|
5457
|
+
if (prior === void 0 || event.ts > prior.ts) {
|
|
5458
|
+
mostRecentBySession.set(sessionId, event);
|
|
5459
|
+
}
|
|
5460
|
+
}
|
|
5461
|
+
const entries = [];
|
|
5462
|
+
for (const [sessionId, event] of mostRecentBySession.entries()) {
|
|
5463
|
+
const ageHours = Math.max(
|
|
5464
|
+
0,
|
|
5465
|
+
Math.floor((nowMs - event.covered_through_ts) / 36e5)
|
|
5466
|
+
);
|
|
5467
|
+
entries.push({
|
|
5468
|
+
session_id_short: truncateSessionId(sessionId),
|
|
5469
|
+
last_attempted_at: new Date(event.ts).toISOString(),
|
|
5470
|
+
outcome: event.outcome,
|
|
5471
|
+
candidates_proposed: event.candidates_proposed,
|
|
5472
|
+
covered_through_ts: event.covered_through_ts,
|
|
5473
|
+
age_since_covered_hours: ageHours
|
|
5474
|
+
});
|
|
5475
|
+
}
|
|
5476
|
+
entries.sort(
|
|
5477
|
+
(a, b) => a.last_attempted_at < b.last_attempted_at ? 1 : a.last_attempted_at > b.last_attempted_at ? -1 : 0
|
|
5478
|
+
);
|
|
5479
|
+
return {
|
|
5480
|
+
entries,
|
|
5481
|
+
total: entries.length,
|
|
5482
|
+
since_ms: options.since,
|
|
5483
|
+
generated_at: generatedAt
|
|
5484
|
+
};
|
|
5485
|
+
}
|
|
5486
|
+
function truncateSessionId(sessionId) {
|
|
5487
|
+
if (sessionId.length <= 8) {
|
|
5488
|
+
return sessionId;
|
|
5489
|
+
}
|
|
5490
|
+
return `${sessionId.slice(0, 8)}...`;
|
|
5491
|
+
}
|
|
4928
5492
|
function createFixMessage(fixed, report) {
|
|
4929
5493
|
const fixedText = fixed.length === 0 ? "No deterministic doctor fixes were needed." : `Applied ${fixed.length} deterministic doctor fix${fixed.length === 1 ? "" : "es"}.`;
|
|
4930
5494
|
const manualText = report.manual_errors.length === 0 ? "No manual errors remain." : `${report.manual_errors.length} manual error${report.manual_errors.length === 1 ? "" : "s"} remain.`;
|
|
@@ -4941,8 +5505,8 @@ function isValidJsonLine(line) {
|
|
|
4941
5505
|
function normalizeTarget(targetInput) {
|
|
4942
5506
|
return isAbsolute2(targetInput) ? targetInput : resolve3(process.cwd(), targetInput);
|
|
4943
5507
|
}
|
|
4944
|
-
function normalizePath(
|
|
4945
|
-
return posix.normalize(
|
|
5508
|
+
function normalizePath(path2) {
|
|
5509
|
+
return posix.normalize(path2.split("\\").join("/"));
|
|
4946
5510
|
}
|
|
4947
5511
|
function collectEntryPoints(root) {
|
|
4948
5512
|
if (!existsSync4(root) || !statSync4(root).isDirectory()) {
|
|
@@ -5012,6 +5576,123 @@ function reduceStatus(statuses) {
|
|
|
5012
5576
|
function isMissingFileError(error) {
|
|
5013
5577
|
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
5014
5578
|
}
|
|
5579
|
+
var ENRICH_DESC_FIELDS = ["intent_clues", "tech_stack", "impact", "must_read_if"];
|
|
5580
|
+
var ENRICH_DESC_FIELD_PATTERNS = {
|
|
5581
|
+
intent_clues: /^intent_clues\s*:/mu,
|
|
5582
|
+
tech_stack: /^tech_stack\s*:/mu,
|
|
5583
|
+
impact: /^impact\s*:/mu,
|
|
5584
|
+
must_read_if: /^must_read_if\s*:/mu
|
|
5585
|
+
};
|
|
5586
|
+
async function enrichDescriptions(projectRoot, opts = {}) {
|
|
5587
|
+
const auto = opts.auto === true;
|
|
5588
|
+
const dryRun = opts.dryRun === true;
|
|
5589
|
+
const mode = auto ? "auto" : "interactive";
|
|
5590
|
+
const candidates = [];
|
|
5591
|
+
let scanned = 0;
|
|
5592
|
+
let modified = 0;
|
|
5593
|
+
let skipped = 0;
|
|
5594
|
+
for (const visit of iterateCanonicalFilenames(projectRoot)) {
|
|
5595
|
+
const layerRoot = visit.layer === "team" ? join6(projectRoot, ".fabric", "knowledge") : resolvePersonalKnowledgeRoot();
|
|
5596
|
+
const absPath = join6(layerRoot, visit.type, visit.filename);
|
|
5597
|
+
scanned += 1;
|
|
5598
|
+
let source;
|
|
5599
|
+
try {
|
|
5600
|
+
source = await readFile5(absPath, "utf8");
|
|
5601
|
+
} catch {
|
|
5602
|
+
continue;
|
|
5603
|
+
}
|
|
5604
|
+
const fmMatch = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u.exec(source);
|
|
5605
|
+
if (fmMatch === null) {
|
|
5606
|
+
candidates.push({
|
|
5607
|
+
path: visit.displayPath,
|
|
5608
|
+
missing: [...ENRICH_DESC_FIELDS],
|
|
5609
|
+
modified: false,
|
|
5610
|
+
added_fields: [],
|
|
5611
|
+
error: "frontmatter not parseable"
|
|
5612
|
+
});
|
|
5613
|
+
continue;
|
|
5614
|
+
}
|
|
5615
|
+
const block = fmMatch[1];
|
|
5616
|
+
const missing = ENRICH_DESC_FIELDS.filter(
|
|
5617
|
+
(field) => !ENRICH_DESC_FIELD_PATTERNS[field].test(block)
|
|
5618
|
+
);
|
|
5619
|
+
if (missing.length === 0) {
|
|
5620
|
+
skipped += 1;
|
|
5621
|
+
continue;
|
|
5622
|
+
}
|
|
5623
|
+
if (!auto || dryRun) {
|
|
5624
|
+
candidates.push({
|
|
5625
|
+
path: visit.displayPath,
|
|
5626
|
+
missing,
|
|
5627
|
+
modified: false,
|
|
5628
|
+
added_fields: []
|
|
5629
|
+
});
|
|
5630
|
+
continue;
|
|
5631
|
+
}
|
|
5632
|
+
const mustReadIf = synthesizeMustReadIfStub(source, visit.filename);
|
|
5633
|
+
const additions = [];
|
|
5634
|
+
for (const field of missing) {
|
|
5635
|
+
if (field === "must_read_if") {
|
|
5636
|
+
additions.push({ field, line: `must_read_if: ${yamlQuoteIfNeeded(mustReadIf)}` });
|
|
5637
|
+
} else {
|
|
5638
|
+
additions.push({ field, line: `${field}: []` });
|
|
5639
|
+
}
|
|
5640
|
+
}
|
|
5641
|
+
const trailing = block.endsWith("\n") ? "" : "\n";
|
|
5642
|
+
const replacedBlock = `${block}${trailing}${additions.map((a) => a.line).join("\n")}`;
|
|
5643
|
+
const blockStart = source.indexOf(block);
|
|
5644
|
+
if (blockStart < 0) {
|
|
5645
|
+
candidates.push({
|
|
5646
|
+
path: visit.displayPath,
|
|
5647
|
+
missing,
|
|
5648
|
+
modified: false,
|
|
5649
|
+
added_fields: [],
|
|
5650
|
+
error: "frontmatter block not located after match"
|
|
5651
|
+
});
|
|
5652
|
+
continue;
|
|
5653
|
+
}
|
|
5654
|
+
const rewritten = source.slice(0, blockStart) + replacedBlock + source.slice(blockStart + block.length);
|
|
5655
|
+
await atomicWriteText4(absPath, rewritten);
|
|
5656
|
+
modified += 1;
|
|
5657
|
+
candidates.push({
|
|
5658
|
+
path: visit.displayPath,
|
|
5659
|
+
missing,
|
|
5660
|
+
modified: true,
|
|
5661
|
+
added_fields: additions.map((a) => a.field)
|
|
5662
|
+
});
|
|
5663
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
5664
|
+
event_type: "knowledge_enriched",
|
|
5665
|
+
path: visit.displayPath,
|
|
5666
|
+
added_fields: additions.map((a) => a.field),
|
|
5667
|
+
mode,
|
|
5668
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5669
|
+
}).catch(() => {
|
|
5670
|
+
});
|
|
5671
|
+
}
|
|
5672
|
+
candidates.sort((a, b) => a.path.localeCompare(b.path));
|
|
5673
|
+
return { mode, dryRun, scanned, modified, skipped, candidates };
|
|
5674
|
+
}
|
|
5675
|
+
function synthesizeMustReadIfStub(source, filename) {
|
|
5676
|
+
const h1Match = /^#\s+(.+?)\s*$/mu.exec(source);
|
|
5677
|
+
let raw = h1Match !== null ? h1Match[1] : filename.replace(/^K[PT]-[A-Z]+-\d+--/, "").replace(/\.md$/u, "").replace(/-/g, " ");
|
|
5678
|
+
raw = raw.trim();
|
|
5679
|
+
if (raw.length === 0) {
|
|
5680
|
+
raw = "describes a knowledge invariant for this project";
|
|
5681
|
+
}
|
|
5682
|
+
if (raw.length > 120) {
|
|
5683
|
+
raw = `${raw.slice(0, 117)}...`;
|
|
5684
|
+
}
|
|
5685
|
+
return raw;
|
|
5686
|
+
}
|
|
5687
|
+
function yamlQuoteIfNeeded(value) {
|
|
5688
|
+
if (value.length === 0) {
|
|
5689
|
+
return '""';
|
|
5690
|
+
}
|
|
5691
|
+
if (/[:#"'\\[\]{},&*!|>%@`]/.test(value) || /^[\s-?]/.test(value) || /\s$/.test(value)) {
|
|
5692
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
5693
|
+
}
|
|
5694
|
+
return value;
|
|
5695
|
+
}
|
|
5015
5696
|
|
|
5016
5697
|
// src/services/load-active-meta.ts
|
|
5017
5698
|
async function loadActiveMeta(projectRoot, opts = {}) {
|
|
@@ -5168,16 +5849,16 @@ async function loadGetKnowledgeContext(projectRoot) {
|
|
|
5168
5849
|
contextCache.set("context", projectRoot, context);
|
|
5169
5850
|
return context;
|
|
5170
5851
|
}
|
|
5171
|
-
async function resolveKnowledgeForPath(projectRoot, context,
|
|
5172
|
-
const matchedNodes = matchRuleNodes(context.meta,
|
|
5852
|
+
async function resolveKnowledgeForPath(projectRoot, context, path2, options = {}) {
|
|
5853
|
+
const matchedNodes = matchRuleNodes(context.meta, path2);
|
|
5173
5854
|
const loaded = await loadMatchedRules(projectRoot, matchedNodes);
|
|
5174
5855
|
return buildKnowledgePayload(context, loaded, options);
|
|
5175
5856
|
}
|
|
5176
5857
|
function normalizeKnowledgePath(value) {
|
|
5177
5858
|
return value.replaceAll("\\", "/");
|
|
5178
5859
|
}
|
|
5179
|
-
function matchRuleNodes(meta,
|
|
5180
|
-
const requestedPath = normalizeKnowledgePath(
|
|
5860
|
+
function matchRuleNodes(meta, path2) {
|
|
5861
|
+
const requestedPath = normalizeKnowledgePath(path2);
|
|
5181
5862
|
return Object.entries(meta.nodes).filter(([, node]) => shouldLoadNodeForPath(requestedPath, node)).sort((left, right) => {
|
|
5182
5863
|
const [leftId, leftNode] = left;
|
|
5183
5864
|
const [rightId, rightNode] = right;
|
|
@@ -5321,6 +6002,7 @@ export {
|
|
|
5321
6002
|
appendEventLedgerEvent,
|
|
5322
6003
|
readEventLedger,
|
|
5323
6004
|
flushAndSyncEventLedger,
|
|
6005
|
+
loadKbIdTypeMap,
|
|
5324
6006
|
buildKnowledgeMeta,
|
|
5325
6007
|
writeKnowledgeMeta,
|
|
5326
6008
|
computeKnowledgeBasedAgentsMeta,
|
|
@@ -5336,8 +6018,15 @@ export {
|
|
|
5336
6018
|
loadActiveMetaOrStale,
|
|
5337
6019
|
getKnowledge,
|
|
5338
6020
|
normalizeKnowledgePath,
|
|
6021
|
+
ServeLockHeldError,
|
|
6022
|
+
acquireLock,
|
|
6023
|
+
releaseLock,
|
|
6024
|
+
readLockState,
|
|
6025
|
+
checkLockOrThrow,
|
|
5339
6026
|
runDoctorReport,
|
|
5340
6027
|
runDoctorFix,
|
|
5341
6028
|
runDoctorApplyLint,
|
|
5342
|
-
runDoctorCiteCoverage
|
|
6029
|
+
runDoctorCiteCoverage,
|
|
6030
|
+
runDoctorArchiveHistory,
|
|
6031
|
+
enrichDescriptions
|
|
5343
6032
|
};
|