@fenglimg/fabric-server 2.0.0-rc.22 → 2.0.0-rc.23
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-IRB77C6E.js} +476 -57
- package/dist/{http-FF5NZCJK.js → http-ZBV6YUHD.js} +1 -1
- package/dist/index.d.ts +35 -5
- package/dist/index.js +208 -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) {
|
|
@@ -870,8 +870,8 @@ function flattenKeys(value, keys = {}) {
|
|
|
870
870
|
}
|
|
871
871
|
return keys;
|
|
872
872
|
}
|
|
873
|
-
function toPosixPath(
|
|
874
|
-
return
|
|
873
|
+
function toPosixPath(path2) {
|
|
874
|
+
return path2.split(sep2).join("/");
|
|
875
875
|
}
|
|
876
876
|
function deriveRuleIdentity(file, source, existing) {
|
|
877
877
|
const declaredKnowledgeId = extractDeclaredKnowledgeId(source);
|
|
@@ -959,7 +959,7 @@ function extractRuleDescription(source) {
|
|
|
959
959
|
};
|
|
960
960
|
}
|
|
961
961
|
function extractRuleSections(source) {
|
|
962
|
-
const sections = Array.from(source.matchAll(
|
|
962
|
+
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
963
|
return sections.length > 0 ? sections : void 0;
|
|
964
964
|
}
|
|
965
965
|
function extractDescriptionFromFrontmatter(frontmatter) {
|
|
@@ -1147,11 +1147,11 @@ async function readMetaEntries(projectRoot) {
|
|
|
1147
1147
|
return map;
|
|
1148
1148
|
}
|
|
1149
1149
|
for (const node of Object.values(parsed.nodes ?? {})) {
|
|
1150
|
-
const
|
|
1150
|
+
const path2 = node.content_ref ?? node.file;
|
|
1151
1151
|
const stable_id = node.stable_id;
|
|
1152
1152
|
const content_hash = node.hash;
|
|
1153
|
-
if (
|
|
1154
|
-
map.set(
|
|
1153
|
+
if (path2 !== void 0 && stable_id !== void 0 && content_hash !== void 0) {
|
|
1154
|
+
map.set(path2, { stable_id, path: path2, content_hash });
|
|
1155
1155
|
}
|
|
1156
1156
|
}
|
|
1157
1157
|
return map;
|
|
@@ -1407,7 +1407,8 @@ async function reconcileKnowledge(projectRoot, opts) {
|
|
|
1407
1407
|
throw error;
|
|
1408
1408
|
}
|
|
1409
1409
|
}
|
|
1410
|
-
|
|
1410
|
+
const forceWriteForDescriptionHeal = trigger === "auto-heal-description";
|
|
1411
|
+
if (events.length > 0 || revisionDrift || forceWriteForDescriptionHeal) {
|
|
1411
1412
|
await writeKnowledgeMeta(projectRoot, { source: "sync_meta" });
|
|
1412
1413
|
if (events.length > 0) {
|
|
1413
1414
|
await appendRuleSyncEvents(projectRoot, events);
|
|
@@ -1417,7 +1418,7 @@ async function reconcileKnowledge(projectRoot, opts) {
|
|
|
1417
1418
|
}
|
|
1418
1419
|
const duration_ms = Date.now() - startTime;
|
|
1419
1420
|
const reconciledFiles = events.map((e) => e.path);
|
|
1420
|
-
if (trigger !== void 0 && (events.length > 0 || revisionDrift)) {
|
|
1421
|
+
if (trigger !== void 0 && (events.length > 0 || revisionDrift || forceWriteForDescriptionHeal)) {
|
|
1421
1422
|
if (trigger === "startup") {
|
|
1422
1423
|
await appendEventLedgerEvent(projectRoot, {
|
|
1423
1424
|
event_type: "meta_reconciled_on_startup",
|
|
@@ -1448,10 +1449,103 @@ async function reconcileKnowledge(projectRoot, opts) {
|
|
|
1448
1449
|
};
|
|
1449
1450
|
}
|
|
1450
1451
|
|
|
1452
|
+
// src/services/serve-lock.ts
|
|
1453
|
+
import fs from "fs";
|
|
1454
|
+
import path from "path";
|
|
1455
|
+
import { createTranslator, detectNodeLocale } from "@fenglimg/fabric-shared";
|
|
1456
|
+
import { IOFabricError as IOFabricError2 } from "@fenglimg/fabric-shared/errors";
|
|
1457
|
+
var LOCK_FILENAME = ".serve.lock";
|
|
1458
|
+
var t = createTranslator(detectNodeLocale());
|
|
1459
|
+
var ServeLockHeldError = class extends IOFabricError2 {
|
|
1460
|
+
code = "SERVE_LOCK_HELD";
|
|
1461
|
+
httpStatus = 423;
|
|
1462
|
+
};
|
|
1463
|
+
function lockPath(projectRoot) {
|
|
1464
|
+
return path.join(projectRoot, ".fabric", LOCK_FILENAME);
|
|
1465
|
+
}
|
|
1466
|
+
function isAlive(pid) {
|
|
1467
|
+
try {
|
|
1468
|
+
process.kill(pid, 0);
|
|
1469
|
+
return true;
|
|
1470
|
+
} catch (e) {
|
|
1471
|
+
const err = e;
|
|
1472
|
+
if (err.code === "ESRCH") return false;
|
|
1473
|
+
if (err.code === "EPERM") return true;
|
|
1474
|
+
throw e;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
function acquireLock(projectRoot, opts) {
|
|
1478
|
+
const p = lockPath(projectRoot);
|
|
1479
|
+
if (fs.existsSync(p)) {
|
|
1480
|
+
let state = null;
|
|
1481
|
+
try {
|
|
1482
|
+
state = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
1483
|
+
} catch {
|
|
1484
|
+
}
|
|
1485
|
+
if (state && state.pid && state.pid !== process.pid && isAlive(state.pid) && !opts?.force) {
|
|
1486
|
+
throw new ServeLockHeldError(
|
|
1487
|
+
`serve lock held by live PID ${state.pid}`,
|
|
1488
|
+
{
|
|
1489
|
+
actionHint: t("cli.serve.lock-held.action-hint", { pid: String(state.pid) }),
|
|
1490
|
+
details: state
|
|
1491
|
+
}
|
|
1492
|
+
);
|
|
1493
|
+
}
|
|
1494
|
+
if (state && state.pid && !isAlive(state.pid)) {
|
|
1495
|
+
process.stderr.write(`[serve-lock] stale lock from PID ${state.pid} \u2014 overwriting
|
|
1496
|
+
`);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
1500
|
+
fs.writeFileSync(
|
|
1501
|
+
p,
|
|
1502
|
+
JSON.stringify({ pid: process.pid, acquiredAt: Date.now(), host: process.env.HOSTNAME })
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
function releaseLock(projectRoot) {
|
|
1506
|
+
const p = lockPath(projectRoot);
|
|
1507
|
+
try {
|
|
1508
|
+
if (fs.existsSync(p)) {
|
|
1509
|
+
const state = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
1510
|
+
if (state.pid === process.pid) {
|
|
1511
|
+
fs.unlinkSync(p);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
} catch {
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
function readLockState(projectRoot) {
|
|
1518
|
+
const p = lockPath(projectRoot);
|
|
1519
|
+
if (!fs.existsSync(p)) return null;
|
|
1520
|
+
try {
|
|
1521
|
+
return JSON.parse(fs.readFileSync(p, "utf8"));
|
|
1522
|
+
} catch {
|
|
1523
|
+
return null;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
function checkLockOrThrow(projectRoot, opts) {
|
|
1527
|
+
const state = readLockState(projectRoot);
|
|
1528
|
+
if (state === null) return;
|
|
1529
|
+
if (state.pid === process.pid) return;
|
|
1530
|
+
if (!isAlive(state.pid)) {
|
|
1531
|
+
process.stderr.write(`[serve-lock] stale lock from PID ${state.pid} \u2014 ignoring
|
|
1532
|
+
`);
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
if (opts?.force) return;
|
|
1536
|
+
throw new ServeLockHeldError(
|
|
1537
|
+
`serve lock held by live PID ${state.pid}`,
|
|
1538
|
+
{
|
|
1539
|
+
actionHint: t("cli.serve.lock-held.action-hint", { pid: String(state.pid) }),
|
|
1540
|
+
details: state
|
|
1541
|
+
}
|
|
1542
|
+
);
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1451
1545
|
// src/services/doctor.ts
|
|
1452
1546
|
import { execFileSync } from "child_process";
|
|
1453
1547
|
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";
|
|
1548
|
+
import { access, mkdir as mkdir4, readFile as readFile5, rename, unlink, writeFile as writeFile2 } from "fs/promises";
|
|
1455
1549
|
import { constants } from "fs";
|
|
1456
1550
|
import { homedir as homedir3 } from "os";
|
|
1457
1551
|
import { isAbsolute as isAbsolute2, join as join6, posix, relative as nodeRelative, resolve as resolve3, sep as sep3 } from "path";
|
|
@@ -1466,7 +1560,9 @@ import {
|
|
|
1466
1560
|
BOOTSTRAP_CANONICAL,
|
|
1467
1561
|
BOOTSTRAP_MARKER_BEGIN,
|
|
1468
1562
|
BOOTSTRAP_MARKER_END,
|
|
1469
|
-
BOOTSTRAP_REGEX
|
|
1563
|
+
BOOTSTRAP_REGEX,
|
|
1564
|
+
ONBOARD_SLOT_NAMES,
|
|
1565
|
+
ONBOARD_SLOT_TOTAL
|
|
1470
1566
|
} from "@fenglimg/fabric-shared";
|
|
1471
1567
|
import { detectFramework } from "@fenglimg/fabric-shared/node";
|
|
1472
1568
|
import { atomicWriteJson as atomicWriteJson2, atomicWriteText as atomicWriteText4 } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
@@ -1600,8 +1696,10 @@ async function runDoctorReport(target) {
|
|
|
1600
1696
|
const relevancePathsDrift = inspectRelevancePathsDrift(projectRoot);
|
|
1601
1697
|
const narrowTooFew = inspectNarrowTooFew(projectRoot, lintNow);
|
|
1602
1698
|
const sessionHintsStale = inspectSessionHintsStale(projectRoot, lintNow);
|
|
1699
|
+
const staleServeLock = inspectStaleServeLock(projectRoot, lintNow);
|
|
1603
1700
|
const relevanceFieldsMissing = inspectRelevanceFieldsMissing(projectRoot);
|
|
1604
1701
|
const skillMdYamlInvalid = inspectSkillMdYamlInvalid(projectRoot);
|
|
1702
|
+
const onboardCoverage = inspectOnboardCoverage(projectRoot);
|
|
1605
1703
|
const checks = [
|
|
1606
1704
|
createBootstrapAnchorCheck(bootstrapAnchor),
|
|
1607
1705
|
// v2.0.0-rc.19 TASK-004: bootstrap marker migration check sits adjacent to
|
|
@@ -1616,7 +1714,8 @@ async function runDoctorReport(target) {
|
|
|
1616
1714
|
createKnowledgeDirMissingCheck(knowledgeDirMissing),
|
|
1617
1715
|
// v2.0.0-rc.22 TASK-006: baseline filename format. Sits adjacent to
|
|
1618
1716
|
// knowledge_dir_missing — both are knowledge-layout invariants. manual_error
|
|
1619
|
-
// kind; resolution
|
|
1717
|
+
// kind; resolution is manual file deletion (rc.23 TASK-012 (F8a) removed
|
|
1718
|
+
// the baseline-emit pipeline, so no auto-fix exists).
|
|
1620
1719
|
createBaselineFilenameFormatCheck(baselineFilenameFormat),
|
|
1621
1720
|
createForensicCheck(forensic, framework.kind, entryPoints.length),
|
|
1622
1721
|
// v2.0: removed `createInitContextCheck` — `.fabric/init-context.json`
|
|
@@ -1670,6 +1769,10 @@ async function runDoctorReport(target) {
|
|
|
1670
1769
|
createNarrowTooFewCheck(narrowTooFew),
|
|
1671
1770
|
// rc.6 TASK-021 (E3): session-hints cache hygiene (lint #27). Info kind.
|
|
1672
1771
|
createSessionHintsStaleCheck(sessionHintsStale),
|
|
1772
|
+
// rc.23 TASK-010 (e): stale .fabric/.serve.lock advisory. Info kind —
|
|
1773
|
+
// does not bump report status. `--fix` unlinks the corpse and emits
|
|
1774
|
+
// `serve_lock_cleared`.
|
|
1775
|
+
createStaleServeLockCheck(staleServeLock),
|
|
1673
1776
|
// v2.0.0-rc.9 TASK-003 (A3): relevance fields back-fill (lint #28).
|
|
1674
1777
|
// Info kind — applies to pending entries only; canonical entries get
|
|
1675
1778
|
// the fields written verbatim by fab_review.approve/modify.
|
|
@@ -1677,6 +1780,11 @@ async function runDoctorReport(target) {
|
|
|
1677
1780
|
// rc.12 lint #29: skill_md_yaml_invalid. Warning kind — surfaces
|
|
1678
1781
|
// SKILL.md frontmatter that Codex CLI silently drops at load.
|
|
1679
1782
|
createSkillMdYamlInvalidCheck(skillMdYamlInvalid),
|
|
1783
|
+
// v2.0.0-rc.23 TASK-014 (F8c): Onboard coverage advisory. Info kind.
|
|
1784
|
+
// Surfaces uncovered S5 onboard slots and recommends /fabric-archive
|
|
1785
|
+
// first-run phase. Sits adjacent to Skill markdown YAML — both are
|
|
1786
|
+
// Skill-adjacent advisories. --fix never mutates onboard state.
|
|
1787
|
+
createOnboardCoverageCheck(onboardCoverage),
|
|
1680
1788
|
createPreexistingRootFilesCheck(preexistingRootFiles)
|
|
1681
1789
|
// v2.0 / rc.2: `createLegacyClientPathCheck` removed. The schema now
|
|
1682
1790
|
// rejects retired clientPaths keys (windsurf/rooCode/geminiCLI) at Zod
|
|
@@ -1717,7 +1825,7 @@ async function runDoctorReport(target) {
|
|
|
1717
1825
|
warningCount: warnings.length,
|
|
1718
1826
|
infoCount: infos.length,
|
|
1719
1827
|
targetFiles: Object.fromEntries(
|
|
1720
|
-
TARGET_FILE_PATHS.map((
|
|
1828
|
+
TARGET_FILE_PATHS.map((path2) => [path2, existsSync4(join6(projectRoot, path2))])
|
|
1721
1829
|
)
|
|
1722
1830
|
}
|
|
1723
1831
|
};
|
|
@@ -1731,11 +1839,11 @@ async function runDoctorFix(target) {
|
|
|
1731
1839
|
)) {
|
|
1732
1840
|
const migrated = await migrateBootstrapMarkers(projectRoot);
|
|
1733
1841
|
fixed.push(findIssue(before.fixable_errors, "bootstrap_marker_migration_required"));
|
|
1734
|
-
for (const
|
|
1842
|
+
for (const path2 of migrated.paths) {
|
|
1735
1843
|
await appendEventLedgerEvent(projectRoot, {
|
|
1736
1844
|
event_type: "bootstrap_marker_migrated",
|
|
1737
|
-
path,
|
|
1738
|
-
migrated_count: migrated.countPerPath[
|
|
1845
|
+
path: path2,
|
|
1846
|
+
migrated_count: migrated.countPerPath[path2] ?? 1,
|
|
1739
1847
|
legacy_marker: "fabric:knowledge-base",
|
|
1740
1848
|
new_marker: "fabric:bootstrap",
|
|
1741
1849
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -1814,6 +1922,31 @@ async function runDoctorFix(target) {
|
|
|
1814
1922
|
await fixMcpConfigInWrongFile(projectRoot);
|
|
1815
1923
|
fixed.push(findIssue(before.fixable_errors, "mcp_config_in_wrong_file"));
|
|
1816
1924
|
}
|
|
1925
|
+
if (before.infos.some((issue) => issue.code === "stale_serve_lock")) {
|
|
1926
|
+
const lockInspection = inspectStaleServeLock(projectRoot, Date.now());
|
|
1927
|
+
if (lockInspection.present && !lockInspection.pidAlive) {
|
|
1928
|
+
const lockFilePath = join6(projectRoot, ".fabric", ".serve.lock");
|
|
1929
|
+
try {
|
|
1930
|
+
await unlink(lockFilePath);
|
|
1931
|
+
} catch (err) {
|
|
1932
|
+
const errno = err;
|
|
1933
|
+
if (errno.code !== "ENOENT") throw err;
|
|
1934
|
+
}
|
|
1935
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
1936
|
+
event_type: "serve_lock_cleared",
|
|
1937
|
+
pid: lockInspection.pid,
|
|
1938
|
+
age_ms: lockInspection.ageMs,
|
|
1939
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1940
|
+
}).catch(() => {
|
|
1941
|
+
});
|
|
1942
|
+
fixed.push({
|
|
1943
|
+
code: "stale_serve_lock",
|
|
1944
|
+
name: "Serve lock",
|
|
1945
|
+
message: `Removed stale .fabric/.serve.lock (dead PID ${lockInspection.pid}).`,
|
|
1946
|
+
path: ".fabric/.serve.lock"
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1817
1950
|
const report = await runDoctorReport(projectRoot);
|
|
1818
1951
|
return {
|
|
1819
1952
|
changed: fixed.length > 0,
|
|
@@ -2027,8 +2160,8 @@ async function applyStaleArchive(projectRoot, candidate, now) {
|
|
|
2027
2160
|
if (renameError instanceof Error && "code" in renameError && renameError.code === "EXDEV") {
|
|
2028
2161
|
const data = await readFile5(sourceAbs);
|
|
2029
2162
|
await writeFile2(destAbs, data);
|
|
2030
|
-
const { unlink } = await import("fs/promises");
|
|
2031
|
-
await
|
|
2163
|
+
const { unlink: unlink2 } = await import("fs/promises");
|
|
2164
|
+
await unlink2(sourceAbs);
|
|
2032
2165
|
} else {
|
|
2033
2166
|
throw renameError;
|
|
2034
2167
|
}
|
|
@@ -2100,8 +2233,8 @@ async function applyPendingAutoArchive(projectRoot, candidate, now) {
|
|
|
2100
2233
|
if (renameError instanceof Error && "code" in renameError && renameError.code === "EXDEV") {
|
|
2101
2234
|
const data = await readFile5(candidate.pending_path_abs);
|
|
2102
2235
|
await writeFile2(candidate.archived_to_abs, data);
|
|
2103
|
-
const { unlink } = await import("fs/promises");
|
|
2104
|
-
await
|
|
2236
|
+
const { unlink: unlink2 } = await import("fs/promises");
|
|
2237
|
+
await unlink2(candidate.pending_path_abs);
|
|
2105
2238
|
} else {
|
|
2106
2239
|
throw renameError;
|
|
2107
2240
|
}
|
|
@@ -2158,8 +2291,8 @@ async function applySessionHintsStaleCleanup(projectRoot, candidate) {
|
|
|
2158
2291
|
const detail = `deleted (${candidate.age_days}d old)`;
|
|
2159
2292
|
const absPath = join6(projectRoot, candidate.path);
|
|
2160
2293
|
try {
|
|
2161
|
-
const { unlink } = await import("fs/promises");
|
|
2162
|
-
await
|
|
2294
|
+
const { unlink: unlink2 } = await import("fs/promises");
|
|
2295
|
+
await unlink2(absPath);
|
|
2163
2296
|
return {
|
|
2164
2297
|
kind: "knowledge_session_hints_stale_cleanup",
|
|
2165
2298
|
path: candidate.path,
|
|
@@ -2213,9 +2346,9 @@ function truncateErrorMessage(error) {
|
|
|
2213
2346
|
return raw.length > 240 ? `${raw.slice(0, 237)}...` : raw;
|
|
2214
2347
|
}
|
|
2215
2348
|
async function inspectForensic(projectRoot) {
|
|
2216
|
-
const
|
|
2349
|
+
const path2 = join6(projectRoot, ".fabric", "forensic.json");
|
|
2217
2350
|
try {
|
|
2218
|
-
const parsed = forensicReportSchema.parse(JSON.parse(await readFile5(
|
|
2351
|
+
const parsed = forensicReportSchema.parse(JSON.parse(await readFile5(path2, "utf8")));
|
|
2219
2352
|
return { present: true, valid: true, report: parsed };
|
|
2220
2353
|
} catch (error) {
|
|
2221
2354
|
if (isMissingFileError(error)) {
|
|
@@ -2326,15 +2459,15 @@ function inspectContentRefs(projectRoot, meta) {
|
|
|
2326
2459
|
return { missing, invalid };
|
|
2327
2460
|
}
|
|
2328
2461
|
async function inspectEventLedger(projectRoot) {
|
|
2329
|
-
const
|
|
2330
|
-
const exists = existsSync4(
|
|
2462
|
+
const path2 = getEventLedgerPath(projectRoot);
|
|
2463
|
+
const exists = existsSync4(path2);
|
|
2331
2464
|
if (!exists) {
|
|
2332
|
-
return { exists: false, writable: false, parseable: false, hasPartialWrite: false, partialWriteByteOffset: 0, partialWriteByteLength: 0, path };
|
|
2465
|
+
return { exists: false, writable: false, parseable: false, hasPartialWrite: false, partialWriteByteOffset: 0, partialWriteByteLength: 0, path: path2 };
|
|
2333
2466
|
}
|
|
2334
2467
|
try {
|
|
2335
|
-
await access(
|
|
2468
|
+
await access(path2, constants.W_OK);
|
|
2336
2469
|
const { warnings } = await readEventLedger(projectRoot);
|
|
2337
|
-
const raw = await readFile5(
|
|
2470
|
+
const raw = await readFile5(path2, "utf8");
|
|
2338
2471
|
const invalidLine = raw.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).find((line) => !isValidJsonLine(line));
|
|
2339
2472
|
const partialWarning = warnings.find((w) => w.kind === "partial_write_at_tail");
|
|
2340
2473
|
return {
|
|
@@ -2344,7 +2477,7 @@ async function inspectEventLedger(projectRoot) {
|
|
|
2344
2477
|
hasPartialWrite: partialWarning !== void 0,
|
|
2345
2478
|
partialWriteByteOffset: partialWarning?.byte_offset ?? 0,
|
|
2346
2479
|
partialWriteByteLength: partialWarning?.byte_length ?? 0,
|
|
2347
|
-
path,
|
|
2480
|
+
path: path2,
|
|
2348
2481
|
error: invalidLine === void 0 ? void 0 : "events.jsonl contains an invalid JSON line."
|
|
2349
2482
|
};
|
|
2350
2483
|
} catch (error) {
|
|
@@ -2355,16 +2488,16 @@ async function inspectEventLedger(projectRoot) {
|
|
|
2355
2488
|
hasPartialWrite: false,
|
|
2356
2489
|
partialWriteByteOffset: 0,
|
|
2357
2490
|
partialWriteByteLength: 0,
|
|
2358
|
-
path,
|
|
2491
|
+
path: path2,
|
|
2359
2492
|
error: error instanceof Error ? error.message : String(error)
|
|
2360
2493
|
};
|
|
2361
2494
|
}
|
|
2362
2495
|
}
|
|
2363
2496
|
async function inspectKnowledgeTestIndex(projectRoot) {
|
|
2364
|
-
const
|
|
2497
|
+
const path2 = join6(projectRoot, ".fabric", ".cache", "knowledge-test.index.json");
|
|
2365
2498
|
const built = await tryBuildRuleMeta(projectRoot);
|
|
2366
2499
|
try {
|
|
2367
|
-
const index = knowledgeTestIndexSchema2.parse(JSON.parse(await readFile5(
|
|
2500
|
+
const index = knowledgeTestIndexSchema2.parse(JSON.parse(await readFile5(path2, "utf8")));
|
|
2368
2501
|
return {
|
|
2369
2502
|
present: true,
|
|
2370
2503
|
valid: true,
|
|
@@ -2590,8 +2723,8 @@ function inspectKnowledgeDirMissing(projectRoot) {
|
|
|
2590
2723
|
const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
|
|
2591
2724
|
const missingSubdirs = [];
|
|
2592
2725
|
for (const sub of KNOWLEDGE_SUBDIRS3) {
|
|
2593
|
-
const
|
|
2594
|
-
if (!existsSync4(
|
|
2726
|
+
const path2 = join6(knowledgeRoot, sub);
|
|
2727
|
+
if (!existsSync4(path2)) {
|
|
2595
2728
|
missingSubdirs.push(`.fabric/knowledge/${sub}`);
|
|
2596
2729
|
}
|
|
2597
2730
|
}
|
|
@@ -2660,7 +2793,7 @@ function createBaselineFilenameFormatCheck(inspection) {
|
|
|
2660
2793
|
"manual_error",
|
|
2661
2794
|
"lint-baseline-filename-format",
|
|
2662
2795
|
`${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
|
-
"
|
|
2796
|
+
"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
2797
|
);
|
|
2665
2798
|
}
|
|
2666
2799
|
function createKnowledgeDirMissingCheck(inspection) {
|
|
@@ -2889,7 +3022,9 @@ function collectMdFilesUnder(out, projectRoot, rootDir, relPrefix) {
|
|
|
2889
3022
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
2890
3023
|
const abs = join6(dir, entry.name);
|
|
2891
3024
|
if (entry.isDirectory()) {
|
|
2892
|
-
|
|
3025
|
+
if (entry.name !== "pending" && entry.name !== "archive") {
|
|
3026
|
+
stack.push(abs);
|
|
3027
|
+
}
|
|
2893
3028
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
2894
3029
|
const rel = posix.join(relPrefix, abs.slice(rootDir.length + 1).replace(/\\/gu, "/"));
|
|
2895
3030
|
out.add(rel);
|
|
@@ -2991,7 +3126,7 @@ function inspectCounterDesync(meta) {
|
|
|
2991
3126
|
["guideline", "GLD"],
|
|
2992
3127
|
["pitfall", "PIT"],
|
|
2993
3128
|
["process", "PRO"]
|
|
2994
|
-
].find(([
|
|
3129
|
+
].find(([t2]) => t2 === parsed.type)?.[1];
|
|
2995
3130
|
if (typeCode === void 0) {
|
|
2996
3131
|
continue;
|
|
2997
3132
|
}
|
|
@@ -3582,6 +3717,20 @@ function inspectSessionHintsStale(projectRoot, now) {
|
|
|
3582
3717
|
candidates.sort((a, b) => a.path.localeCompare(b.path));
|
|
3583
3718
|
return { candidates };
|
|
3584
3719
|
}
|
|
3720
|
+
function inspectStaleServeLock(projectRoot, now) {
|
|
3721
|
+
const state = readLockState(projectRoot);
|
|
3722
|
+
if (state === null) {
|
|
3723
|
+
return { present: false };
|
|
3724
|
+
}
|
|
3725
|
+
const ageMs = Math.max(0, now - state.acquiredAt);
|
|
3726
|
+
return {
|
|
3727
|
+
present: true,
|
|
3728
|
+
pid: state.pid,
|
|
3729
|
+
acquiredAt: state.acquiredAt,
|
|
3730
|
+
ageMs,
|
|
3731
|
+
pidAlive: isAlive(state.pid)
|
|
3732
|
+
};
|
|
3733
|
+
}
|
|
3585
3734
|
function inspectNarrowTooFew(projectRoot, now) {
|
|
3586
3735
|
let total = 0;
|
|
3587
3736
|
let narrowWithPaths = 0;
|
|
@@ -3740,6 +3889,28 @@ function createSessionHintsStaleCheck(inspection) {
|
|
|
3740
3889
|
"Run `fab doctor --apply-lint` to delete stale session-hints cache files."
|
|
3741
3890
|
);
|
|
3742
3891
|
}
|
|
3892
|
+
function createStaleServeLockCheck(inspection) {
|
|
3893
|
+
if (!inspection.present) {
|
|
3894
|
+
return okCheck("Serve lock", "No .fabric/.serve.lock present.");
|
|
3895
|
+
}
|
|
3896
|
+
if (inspection.pidAlive) {
|
|
3897
|
+
return okCheck(
|
|
3898
|
+
"Serve lock",
|
|
3899
|
+
`.fabric/.serve.lock held by live PID ${inspection.pid}.`
|
|
3900
|
+
);
|
|
3901
|
+
}
|
|
3902
|
+
const days = Math.floor(inspection.ageMs / MS_PER_DAY);
|
|
3903
|
+
const hours = Math.floor(inspection.ageMs / (60 * 60 * 1e3));
|
|
3904
|
+
const acquiredAgo = days >= 1 ? `${days} day${days === 1 ? "" : "s"} ago` : `${hours} hour${hours === 1 ? "" : "s"} ago`;
|
|
3905
|
+
return issueCheck(
|
|
3906
|
+
"Serve lock",
|
|
3907
|
+
"ok",
|
|
3908
|
+
"info",
|
|
3909
|
+
"stale_serve_lock",
|
|
3910
|
+
`[advisory] .fabric/.serve.lock holds dead PID ${inspection.pid} (acquired ${acquiredAgo}). Run \`fab doctor --fix\` to remove.`,
|
|
3911
|
+
"Run `fab doctor --fix` to remove the stale .fabric/.serve.lock."
|
|
3912
|
+
);
|
|
3913
|
+
}
|
|
3743
3914
|
function extractKnowledgeFrontmatterRelevanceScope(source) {
|
|
3744
3915
|
const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
|
|
3745
3916
|
const fm = FM_PATTERN.exec(source);
|
|
@@ -4244,6 +4415,117 @@ function createSkillMdYamlInvalidCheck(inspection) {
|
|
|
4244
4415
|
'Quote the value with double quotes (`description: "\u2026"`) or rewrite the inner `key: value` token to `key=value`.'
|
|
4245
4416
|
);
|
|
4246
4417
|
}
|
|
4418
|
+
var KNOWLEDGE_CANONICAL_TYPE_DIRS_FOR_ONBOARD = [
|
|
4419
|
+
"decisions",
|
|
4420
|
+
"pitfalls",
|
|
4421
|
+
"guidelines",
|
|
4422
|
+
"models",
|
|
4423
|
+
"processes"
|
|
4424
|
+
];
|
|
4425
|
+
function inspectOnboardCoverage(projectRoot) {
|
|
4426
|
+
const filled = {};
|
|
4427
|
+
for (const slot of ONBOARD_SLOT_NAMES) {
|
|
4428
|
+
filled[slot] = [];
|
|
4429
|
+
}
|
|
4430
|
+
const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
|
|
4431
|
+
if (existsSync4(knowledgeRoot)) {
|
|
4432
|
+
for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS_FOR_ONBOARD) {
|
|
4433
|
+
const dir = join6(knowledgeRoot, typeDir);
|
|
4434
|
+
if (!existsSync4(dir)) continue;
|
|
4435
|
+
let entries;
|
|
4436
|
+
try {
|
|
4437
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
4438
|
+
} catch {
|
|
4439
|
+
continue;
|
|
4440
|
+
}
|
|
4441
|
+
for (const entry of entries) {
|
|
4442
|
+
if (!entry.isFile()) continue;
|
|
4443
|
+
if (!entry.name.endsWith(".md")) continue;
|
|
4444
|
+
const filePath = join6(dir, entry.name);
|
|
4445
|
+
let content;
|
|
4446
|
+
try {
|
|
4447
|
+
content = readFileSync2(filePath, "utf8");
|
|
4448
|
+
} catch {
|
|
4449
|
+
continue;
|
|
4450
|
+
}
|
|
4451
|
+
const slot = readFrontmatterScalar(content, "onboard_slot");
|
|
4452
|
+
if (slot === void 0) continue;
|
|
4453
|
+
if (!ONBOARD_SLOT_NAMES.includes(slot)) continue;
|
|
4454
|
+
const stableId = readFrontmatterScalar(content, "id") ?? entry.name.replace(/\.md$/u, "");
|
|
4455
|
+
filled[slot].push(stableId);
|
|
4456
|
+
}
|
|
4457
|
+
}
|
|
4458
|
+
}
|
|
4459
|
+
for (const slot of ONBOARD_SLOT_NAMES) {
|
|
4460
|
+
filled[slot].sort();
|
|
4461
|
+
}
|
|
4462
|
+
const optedOut = readOnboardOptedOut(projectRoot);
|
|
4463
|
+
const missing = ONBOARD_SLOT_NAMES.filter((slot) => {
|
|
4464
|
+
if (filled[slot].length > 0) return false;
|
|
4465
|
+
if (optedOut.includes(slot)) return false;
|
|
4466
|
+
return true;
|
|
4467
|
+
});
|
|
4468
|
+
return { filled, missing, opted_out: optedOut };
|
|
4469
|
+
}
|
|
4470
|
+
function readOnboardOptedOut(projectRoot) {
|
|
4471
|
+
const path2 = join6(projectRoot, ".fabric", "fabric-config.json");
|
|
4472
|
+
if (!existsSync4(path2)) return [];
|
|
4473
|
+
let raw;
|
|
4474
|
+
try {
|
|
4475
|
+
raw = readFileSync2(path2, "utf8");
|
|
4476
|
+
} catch {
|
|
4477
|
+
return [];
|
|
4478
|
+
}
|
|
4479
|
+
let parsed;
|
|
4480
|
+
try {
|
|
4481
|
+
parsed = JSON.parse(raw);
|
|
4482
|
+
} catch {
|
|
4483
|
+
return [];
|
|
4484
|
+
}
|
|
4485
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4486
|
+
return [];
|
|
4487
|
+
}
|
|
4488
|
+
const list = parsed.onboard_slots_opted_out;
|
|
4489
|
+
if (!Array.isArray(list)) return [];
|
|
4490
|
+
return list.filter((v) => typeof v === "string");
|
|
4491
|
+
}
|
|
4492
|
+
function readFrontmatterScalar(content, key) {
|
|
4493
|
+
const match = /^---\n([\s\S]*?)\n---/u.exec(content);
|
|
4494
|
+
if (match === null) return void 0;
|
|
4495
|
+
const block = match[1];
|
|
4496
|
+
if (block === void 0) return void 0;
|
|
4497
|
+
for (const rawLine of block.split(/\r?\n/u)) {
|
|
4498
|
+
const line = rawLine.trim();
|
|
4499
|
+
const sep4 = line.indexOf(":");
|
|
4500
|
+
if (sep4 === -1) continue;
|
|
4501
|
+
if (line.slice(0, sep4).trim() !== key) continue;
|
|
4502
|
+
let value = line.slice(sep4 + 1).trim();
|
|
4503
|
+
if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
|
|
4504
|
+
value = value.slice(1, -1);
|
|
4505
|
+
}
|
|
4506
|
+
return value;
|
|
4507
|
+
}
|
|
4508
|
+
return void 0;
|
|
4509
|
+
}
|
|
4510
|
+
function createOnboardCoverageCheck(inspection) {
|
|
4511
|
+
const filledCount = ONBOARD_SLOT_NAMES.filter(
|
|
4512
|
+
(slot) => inspection.filled[slot].length > 0
|
|
4513
|
+
).length;
|
|
4514
|
+
if (inspection.missing.length === 0) {
|
|
4515
|
+
return okCheck(
|
|
4516
|
+
"Onboard coverage",
|
|
4517
|
+
`Onboard coverage: ${filledCount}/${ONBOARD_SLOT_TOTAL} \u2713 (opted-out: ${inspection.opted_out.length}).`
|
|
4518
|
+
);
|
|
4519
|
+
}
|
|
4520
|
+
return issueCheck(
|
|
4521
|
+
"Onboard coverage",
|
|
4522
|
+
"ok",
|
|
4523
|
+
"info",
|
|
4524
|
+
"onboard_coverage_incomplete",
|
|
4525
|
+
`Onboard slots not yet covered: [${inspection.missing.join(", ")}]. ${filledCount}/${ONBOARD_SLOT_TOTAL} filled; ${inspection.opted_out.length} opted-out.`,
|
|
4526
|
+
"Run /fabric-archive to onboard \u2014 the Skill's first-run phase will tour the project and propose pending entries for each unclaimed slot."
|
|
4527
|
+
);
|
|
4528
|
+
}
|
|
4247
4529
|
function createNarrowTooFewCheck(inspection) {
|
|
4248
4530
|
const { structural_flagged, telemetry_flagged } = inspection;
|
|
4249
4531
|
if (!structural_flagged && !telemetry_flagged) {
|
|
@@ -4652,9 +4934,9 @@ async function fixCounterDesync(projectRoot) {
|
|
|
4652
4934
|
await atomicWriteJson2(metaPath, updated, { indent: 2 });
|
|
4653
4935
|
}
|
|
4654
4936
|
async function ensureEventLedger(projectRoot) {
|
|
4655
|
-
const
|
|
4656
|
-
await ensureParentDirectory(
|
|
4657
|
-
await writeFile2(
|
|
4937
|
+
const path2 = getEventLedgerPath(projectRoot);
|
|
4938
|
+
await ensureParentDirectory(path2);
|
|
4939
|
+
await writeFile2(path2, "", { encoding: "utf8", flag: "a" });
|
|
4658
4940
|
}
|
|
4659
4941
|
var CITE_POLICY_VERSION = "2.0.0-rc.20";
|
|
4660
4942
|
async function ensureCitePolicyActivatedMarker(projectRoot) {
|
|
@@ -4681,6 +4963,14 @@ async function ensureCitePolicyActivatedMarker(projectRoot) {
|
|
|
4681
4963
|
return { marker_ts: 0, emitted_now: false };
|
|
4682
4964
|
}
|
|
4683
4965
|
}
|
|
4966
|
+
function parseNoneSentinel(kbLineRaw) {
|
|
4967
|
+
if (typeof kbLineRaw !== "string" || kbLineRaw.length === 0) return "unspecified";
|
|
4968
|
+
const m = kbLineRaw.match(/^KB:\s*none\b\s*(?:\[([^\]]*)\])?\s*$/i);
|
|
4969
|
+
if (m === null) return "unspecified";
|
|
4970
|
+
const inner = (m[1] ?? "").trim().toLowerCase();
|
|
4971
|
+
if (inner === "no-relevant" || inner === "not-applicable") return inner;
|
|
4972
|
+
return "unspecified";
|
|
4973
|
+
}
|
|
4684
4974
|
function categorizeCiteTag(tag) {
|
|
4685
4975
|
if (tag === "planned" || tag === "recalled" || tag === "chained-from" || tag === "none") {
|
|
4686
4976
|
return { category: tag };
|
|
@@ -4764,7 +5054,7 @@ async function runDoctorCiteCoverage(projectRoot, options) {
|
|
|
4764
5054
|
break;
|
|
4765
5055
|
}
|
|
4766
5056
|
}
|
|
4767
|
-
const filteredTurns = options.client === "all" ? assistantTurns : assistantTurns.filter((
|
|
5057
|
+
const filteredTurns = options.client === "all" ? assistantTurns : assistantTurns.filter((t2) => t2.client === options.client);
|
|
4768
5058
|
let clientSessionIds = null;
|
|
4769
5059
|
if (options.client !== "all") {
|
|
4770
5060
|
clientSessionIds = /* @__PURE__ */ new Set();
|
|
@@ -4819,6 +5109,7 @@ async function runDoctorCiteCoverage(projectRoot, options) {
|
|
|
4819
5109
|
return false;
|
|
4820
5110
|
};
|
|
4821
5111
|
const dismissedHistogram = {};
|
|
5112
|
+
const noneHistogram = {};
|
|
4822
5113
|
const perClientAccum = /* @__PURE__ */ new Map();
|
|
4823
5114
|
const emptyMetrics = () => ({
|
|
4824
5115
|
edits_touched: 0,
|
|
@@ -4868,7 +5159,11 @@ async function runDoctorCiteCoverage(projectRoot, options) {
|
|
|
4868
5159
|
dismissedHistogram[key] = (dismissedHistogram[key] ?? 0) + 1;
|
|
4869
5160
|
break;
|
|
4870
5161
|
}
|
|
4871
|
-
case "none":
|
|
5162
|
+
case "none": {
|
|
5163
|
+
const sentinel = parseNoneSentinel(turn.kb_line_raw);
|
|
5164
|
+
noneHistogram[sentinel] = (noneHistogram[sentinel] ?? 0) + 1;
|
|
5165
|
+
break;
|
|
5166
|
+
}
|
|
4872
5167
|
default:
|
|
4873
5168
|
break;
|
|
4874
5169
|
}
|
|
@@ -4922,6 +5217,7 @@ async function runDoctorCiteCoverage(projectRoot, options) {
|
|
|
4922
5217
|
metrics,
|
|
4923
5218
|
...perClient !== void 0 ? { per_client: perClient } : {},
|
|
4924
5219
|
...Object.keys(dismissedHistogram).length > 0 ? { dismissed_reason_histogram: dismissedHistogram } : {},
|
|
5220
|
+
...Object.keys(noneHistogram).length > 0 ? { none_reason_histogram: noneHistogram } : {},
|
|
4925
5221
|
generated_at: generatedAt
|
|
4926
5222
|
};
|
|
4927
5223
|
}
|
|
@@ -4941,8 +5237,8 @@ function isValidJsonLine(line) {
|
|
|
4941
5237
|
function normalizeTarget(targetInput) {
|
|
4942
5238
|
return isAbsolute2(targetInput) ? targetInput : resolve3(process.cwd(), targetInput);
|
|
4943
5239
|
}
|
|
4944
|
-
function normalizePath(
|
|
4945
|
-
return posix.normalize(
|
|
5240
|
+
function normalizePath(path2) {
|
|
5241
|
+
return posix.normalize(path2.split("\\").join("/"));
|
|
4946
5242
|
}
|
|
4947
5243
|
function collectEntryPoints(root) {
|
|
4948
5244
|
if (!existsSync4(root) || !statSync4(root).isDirectory()) {
|
|
@@ -5012,6 +5308,123 @@ function reduceStatus(statuses) {
|
|
|
5012
5308
|
function isMissingFileError(error) {
|
|
5013
5309
|
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
5014
5310
|
}
|
|
5311
|
+
var ENRICH_DESC_FIELDS = ["intent_clues", "tech_stack", "impact", "must_read_if"];
|
|
5312
|
+
var ENRICH_DESC_FIELD_PATTERNS = {
|
|
5313
|
+
intent_clues: /^intent_clues\s*:/mu,
|
|
5314
|
+
tech_stack: /^tech_stack\s*:/mu,
|
|
5315
|
+
impact: /^impact\s*:/mu,
|
|
5316
|
+
must_read_if: /^must_read_if\s*:/mu
|
|
5317
|
+
};
|
|
5318
|
+
async function enrichDescriptions(projectRoot, opts = {}) {
|
|
5319
|
+
const auto = opts.auto === true;
|
|
5320
|
+
const dryRun = opts.dryRun === true;
|
|
5321
|
+
const mode = auto ? "auto" : "interactive";
|
|
5322
|
+
const candidates = [];
|
|
5323
|
+
let scanned = 0;
|
|
5324
|
+
let modified = 0;
|
|
5325
|
+
let skipped = 0;
|
|
5326
|
+
for (const visit of iterateCanonicalFilenames(projectRoot)) {
|
|
5327
|
+
const layerRoot = visit.layer === "team" ? join6(projectRoot, ".fabric", "knowledge") : resolvePersonalKnowledgeRoot();
|
|
5328
|
+
const absPath = join6(layerRoot, visit.type, visit.filename);
|
|
5329
|
+
scanned += 1;
|
|
5330
|
+
let source;
|
|
5331
|
+
try {
|
|
5332
|
+
source = await readFile5(absPath, "utf8");
|
|
5333
|
+
} catch {
|
|
5334
|
+
continue;
|
|
5335
|
+
}
|
|
5336
|
+
const fmMatch = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u.exec(source);
|
|
5337
|
+
if (fmMatch === null) {
|
|
5338
|
+
candidates.push({
|
|
5339
|
+
path: visit.displayPath,
|
|
5340
|
+
missing: [...ENRICH_DESC_FIELDS],
|
|
5341
|
+
modified: false,
|
|
5342
|
+
added_fields: [],
|
|
5343
|
+
error: "frontmatter not parseable"
|
|
5344
|
+
});
|
|
5345
|
+
continue;
|
|
5346
|
+
}
|
|
5347
|
+
const block = fmMatch[1];
|
|
5348
|
+
const missing = ENRICH_DESC_FIELDS.filter(
|
|
5349
|
+
(field) => !ENRICH_DESC_FIELD_PATTERNS[field].test(block)
|
|
5350
|
+
);
|
|
5351
|
+
if (missing.length === 0) {
|
|
5352
|
+
skipped += 1;
|
|
5353
|
+
continue;
|
|
5354
|
+
}
|
|
5355
|
+
if (!auto || dryRun) {
|
|
5356
|
+
candidates.push({
|
|
5357
|
+
path: visit.displayPath,
|
|
5358
|
+
missing,
|
|
5359
|
+
modified: false,
|
|
5360
|
+
added_fields: []
|
|
5361
|
+
});
|
|
5362
|
+
continue;
|
|
5363
|
+
}
|
|
5364
|
+
const mustReadIf = synthesizeMustReadIfStub(source, visit.filename);
|
|
5365
|
+
const additions = [];
|
|
5366
|
+
for (const field of missing) {
|
|
5367
|
+
if (field === "must_read_if") {
|
|
5368
|
+
additions.push({ field, line: `must_read_if: ${yamlQuoteIfNeeded(mustReadIf)}` });
|
|
5369
|
+
} else {
|
|
5370
|
+
additions.push({ field, line: `${field}: []` });
|
|
5371
|
+
}
|
|
5372
|
+
}
|
|
5373
|
+
const trailing = block.endsWith("\n") ? "" : "\n";
|
|
5374
|
+
const replacedBlock = `${block}${trailing}${additions.map((a) => a.line).join("\n")}`;
|
|
5375
|
+
const blockStart = source.indexOf(block);
|
|
5376
|
+
if (blockStart < 0) {
|
|
5377
|
+
candidates.push({
|
|
5378
|
+
path: visit.displayPath,
|
|
5379
|
+
missing,
|
|
5380
|
+
modified: false,
|
|
5381
|
+
added_fields: [],
|
|
5382
|
+
error: "frontmatter block not located after match"
|
|
5383
|
+
});
|
|
5384
|
+
continue;
|
|
5385
|
+
}
|
|
5386
|
+
const rewritten = source.slice(0, blockStart) + replacedBlock + source.slice(blockStart + block.length);
|
|
5387
|
+
await atomicWriteText4(absPath, rewritten);
|
|
5388
|
+
modified += 1;
|
|
5389
|
+
candidates.push({
|
|
5390
|
+
path: visit.displayPath,
|
|
5391
|
+
missing,
|
|
5392
|
+
modified: true,
|
|
5393
|
+
added_fields: additions.map((a) => a.field)
|
|
5394
|
+
});
|
|
5395
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
5396
|
+
event_type: "knowledge_enriched",
|
|
5397
|
+
path: visit.displayPath,
|
|
5398
|
+
added_fields: additions.map((a) => a.field),
|
|
5399
|
+
mode,
|
|
5400
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5401
|
+
}).catch(() => {
|
|
5402
|
+
});
|
|
5403
|
+
}
|
|
5404
|
+
candidates.sort((a, b) => a.path.localeCompare(b.path));
|
|
5405
|
+
return { mode, dryRun, scanned, modified, skipped, candidates };
|
|
5406
|
+
}
|
|
5407
|
+
function synthesizeMustReadIfStub(source, filename) {
|
|
5408
|
+
const h1Match = /^#\s+(.+?)\s*$/mu.exec(source);
|
|
5409
|
+
let raw = h1Match !== null ? h1Match[1] : filename.replace(/^K[PT]-[A-Z]+-\d+--/, "").replace(/\.md$/u, "").replace(/-/g, " ");
|
|
5410
|
+
raw = raw.trim();
|
|
5411
|
+
if (raw.length === 0) {
|
|
5412
|
+
raw = "describes a knowledge invariant for this project";
|
|
5413
|
+
}
|
|
5414
|
+
if (raw.length > 120) {
|
|
5415
|
+
raw = `${raw.slice(0, 117)}...`;
|
|
5416
|
+
}
|
|
5417
|
+
return raw;
|
|
5418
|
+
}
|
|
5419
|
+
function yamlQuoteIfNeeded(value) {
|
|
5420
|
+
if (value.length === 0) {
|
|
5421
|
+
return '""';
|
|
5422
|
+
}
|
|
5423
|
+
if (/[:#"'\\[\]{},&*!|>%@`]/.test(value) || /^[\s-?]/.test(value) || /\s$/.test(value)) {
|
|
5424
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
5425
|
+
}
|
|
5426
|
+
return value;
|
|
5427
|
+
}
|
|
5015
5428
|
|
|
5016
5429
|
// src/services/load-active-meta.ts
|
|
5017
5430
|
async function loadActiveMeta(projectRoot, opts = {}) {
|
|
@@ -5168,16 +5581,16 @@ async function loadGetKnowledgeContext(projectRoot) {
|
|
|
5168
5581
|
contextCache.set("context", projectRoot, context);
|
|
5169
5582
|
return context;
|
|
5170
5583
|
}
|
|
5171
|
-
async function resolveKnowledgeForPath(projectRoot, context,
|
|
5172
|
-
const matchedNodes = matchRuleNodes(context.meta,
|
|
5584
|
+
async function resolveKnowledgeForPath(projectRoot, context, path2, options = {}) {
|
|
5585
|
+
const matchedNodes = matchRuleNodes(context.meta, path2);
|
|
5173
5586
|
const loaded = await loadMatchedRules(projectRoot, matchedNodes);
|
|
5174
5587
|
return buildKnowledgePayload(context, loaded, options);
|
|
5175
5588
|
}
|
|
5176
5589
|
function normalizeKnowledgePath(value) {
|
|
5177
5590
|
return value.replaceAll("\\", "/");
|
|
5178
5591
|
}
|
|
5179
|
-
function matchRuleNodes(meta,
|
|
5180
|
-
const requestedPath = normalizeKnowledgePath(
|
|
5592
|
+
function matchRuleNodes(meta, path2) {
|
|
5593
|
+
const requestedPath = normalizeKnowledgePath(path2);
|
|
5181
5594
|
return Object.entries(meta.nodes).filter(([, node]) => shouldLoadNodeForPath(requestedPath, node)).sort((left, right) => {
|
|
5182
5595
|
const [leftId, leftNode] = left;
|
|
5183
5596
|
const [rightId, rightNode] = right;
|
|
@@ -5336,8 +5749,14 @@ export {
|
|
|
5336
5749
|
loadActiveMetaOrStale,
|
|
5337
5750
|
getKnowledge,
|
|
5338
5751
|
normalizeKnowledgePath,
|
|
5752
|
+
ServeLockHeldError,
|
|
5753
|
+
acquireLock,
|
|
5754
|
+
releaseLock,
|
|
5755
|
+
readLockState,
|
|
5756
|
+
checkLockOrThrow,
|
|
5339
5757
|
runDoctorReport,
|
|
5340
5758
|
runDoctorFix,
|
|
5341
5759
|
runDoctorApplyLint,
|
|
5342
|
-
runDoctorCiteCoverage
|
|
5760
|
+
runDoctorCiteCoverage,
|
|
5761
|
+
enrichDescriptions
|
|
5343
5762
|
};
|