@fenglimg/fabric-server 2.0.0-rc.13 → 2.0.0-rc.21

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.
@@ -1281,7 +1281,12 @@ import {
1281
1281
  AgentsMetaCountersSchema,
1282
1282
  forensicReportSchema,
1283
1283
  parseKnowledgeId as parseKnowledgeId2,
1284
- knowledgeTestIndexSchema as knowledgeTestIndexSchema2
1284
+ knowledgeTestIndexSchema as knowledgeTestIndexSchema2,
1285
+ LEGACY_KB_REGEX,
1286
+ BOOTSTRAP_CANONICAL,
1287
+ BOOTSTRAP_MARKER_BEGIN,
1288
+ BOOTSTRAP_MARKER_END,
1289
+ BOOTSTRAP_REGEX
1285
1290
  } from "@fenglimg/fabric-shared";
1286
1291
  import { detectFramework } from "@fenglimg/fabric-shared/node";
1287
1292
  import { atomicWriteJson as atomicWriteJson2, atomicWriteText as atomicWriteText3 } from "@fenglimg/fabric-shared/node/atomic-write";
@@ -1342,7 +1347,13 @@ var TARGET_FILE_PATHS = [
1342
1347
  ".fabric/agents.meta.json",
1343
1348
  ".fabric/.cache/knowledge-test.index.json",
1344
1349
  ".fabric/events.jsonl",
1345
- ".fabric/knowledge"
1350
+ ".fabric/knowledge",
1351
+ // v2.0.0-rc.19 bootstrap-consolidation TASK-005: L1 canonical snapshot
1352
+ // (.fabric/AGENTS.md) and optional project-rules concat source
1353
+ // (.fabric/project-rules.md). Surfaced in summary.targetFiles so --json
1354
+ // consumers can confirm L1 presence at a glance.
1355
+ ".fabric/AGENTS.md",
1356
+ ".fabric/project-rules.md"
1346
1357
  ];
1347
1358
  async function runDoctorReport(target) {
1348
1359
  const projectRoot = normalizeTarget(target);
@@ -1352,12 +1363,24 @@ async function runDoctorReport(target) {
1352
1363
  forensic,
1353
1364
  meta,
1354
1365
  eventLedger,
1355
- knowledgeTestIndex
1366
+ knowledgeTestIndex,
1367
+ bootstrapMarkerMigration,
1368
+ l1BootstrapSnapshotDrift,
1369
+ l2ManagedBlockDrift
1356
1370
  ] = await Promise.all([
1357
1371
  inspectForensic(projectRoot),
1358
1372
  inspectMeta(projectRoot),
1359
1373
  inspectEventLedger(projectRoot),
1360
- inspectKnowledgeTestIndex(projectRoot)
1374
+ inspectKnowledgeTestIndex(projectRoot),
1375
+ // v2.0.0-rc.19 TASK-004: one-time fabric:knowledge-base → fabric:bootstrap
1376
+ // marker migration scan. Inspect runs in this Promise.all block to keep
1377
+ // performance parity with the other I/O-bound inspections.
1378
+ inspectBootstrapMarkerMigration(projectRoot),
1379
+ // v2.0.0-rc.19 TASK-005: L1 + L2 byte-level drift detection. Both are
1380
+ // I/O-bound (small file reads + buffer compare) so they live in the same
1381
+ // Promise.all block as the other bootstrap inspections.
1382
+ inspectL1BootstrapSnapshotDrift(projectRoot),
1383
+ inspectL2ManagedBlockDrift(projectRoot)
1361
1384
  ]);
1362
1385
  const mcpConfigInWrongFile = inspectMcpConfigInWrongFile(projectRoot);
1363
1386
  const metaManuallyDiverged = await inspectMetaManuallyDiverged(projectRoot);
@@ -1385,6 +1408,15 @@ async function runDoctorReport(target) {
1385
1408
  const skillMdYamlInvalid = inspectSkillMdYamlInvalid(projectRoot);
1386
1409
  const checks = [
1387
1410
  createBootstrapAnchorCheck(bootstrapAnchor),
1411
+ // v2.0.0-rc.19 TASK-004: bootstrap marker migration check sits adjacent to
1412
+ // the anchor check — both are bootstrap-file invariants. fixable_error
1413
+ // when any of the four target paths still carries the legacy marker.
1414
+ createBootstrapMarkerMigrationCheck(bootstrapMarkerMigration),
1415
+ // v2.0.0-rc.19 TASK-005: L1 + L2 byte-level drift detection sit immediately
1416
+ // after the marker migration check. Order: anchor existence → migration →
1417
+ // L1 (canonical ↔ snapshot) → L2 (snapshot+rules ↔ three-end blocks).
1418
+ createL1BootstrapSnapshotDriftCheck(l1BootstrapSnapshotDrift),
1419
+ createL2ManagedBlockDriftCheck(l2ManagedBlockDrift),
1388
1420
  createKnowledgeDirMissingCheck(knowledgeDirMissing),
1389
1421
  createForensicCheck(forensic, framework.kind, entryPoints.length),
1390
1422
  // v2.0: removed `createInitContextCheck` — `.fabric/init-context.json`
@@ -1494,6 +1526,33 @@ async function runDoctorFix(target) {
1494
1526
  const projectRoot = normalizeTarget(target);
1495
1527
  const before = await runDoctorReport(projectRoot);
1496
1528
  const fixed = [];
1529
+ if (before.fixable_errors.some(
1530
+ (issue) => issue.code === "bootstrap_marker_migration_required"
1531
+ )) {
1532
+ const migrated = await migrateBootstrapMarkers(projectRoot);
1533
+ fixed.push(findIssue(before.fixable_errors, "bootstrap_marker_migration_required"));
1534
+ for (const path of migrated.paths) {
1535
+ await appendEventLedgerEvent(projectRoot, {
1536
+ event_type: "bootstrap_marker_migrated",
1537
+ path,
1538
+ migrated_count: migrated.countPerPath[path] ?? 1,
1539
+ legacy_marker: "fabric:knowledge-base",
1540
+ new_marker: "fabric:bootstrap",
1541
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1542
+ }).catch(() => {
1543
+ });
1544
+ }
1545
+ }
1546
+ if (before.fixable_errors.some((issue) => issue.code === "bootstrap_snapshot_drift")) {
1547
+ const snapshotPath = join5(projectRoot, ".fabric", "AGENTS.md");
1548
+ await ensureParentDirectory(snapshotPath);
1549
+ await atomicWriteText3(snapshotPath, BOOTSTRAP_CANONICAL);
1550
+ fixed.push(findIssue(before.fixable_errors, "bootstrap_snapshot_drift"));
1551
+ }
1552
+ if (before.fixable_errors.some((issue) => issue.code === "managed_block_drift")) {
1553
+ await rewriteThreeEndManagedBlocks(projectRoot);
1554
+ fixed.push(findIssue(before.fixable_errors, "managed_block_drift"));
1555
+ }
1497
1556
  if (before.fixable_errors.some((issue) => issue.code === "knowledge_dir_missing")) {
1498
1557
  await ensureKnowledgeSubdirs(projectRoot);
1499
1558
  fixed.push(findIssue(before.fixable_errors, "knowledge_dir_missing"));
@@ -2124,6 +2183,186 @@ function inspectBootstrapAnchor(projectRoot) {
2124
2183
  hasClaudeMd: existsSync4(join5(projectRoot, "CLAUDE.md"))
2125
2184
  };
2126
2185
  }
2186
+ var BOOTSTRAP_MARKER_MIGRATION_TARGETS = [
2187
+ "CLAUDE.md",
2188
+ "AGENTS.md",
2189
+ ".cursor/rules",
2190
+ ".cursor/rules/fabric-bootstrap.mdc"
2191
+ ];
2192
+ async function inspectBootstrapMarkerMigration(target) {
2193
+ const filesNeedingMigration = [];
2194
+ for (const rel of BOOTSTRAP_MARKER_MIGRATION_TARGETS) {
2195
+ const abs = join5(target, rel);
2196
+ if (!existsSync4(abs)) {
2197
+ continue;
2198
+ }
2199
+ let content;
2200
+ try {
2201
+ content = await readFile5(abs, "utf8");
2202
+ } catch {
2203
+ continue;
2204
+ }
2205
+ if (LEGACY_KB_REGEX.test(content)) {
2206
+ filesNeedingMigration.push(abs);
2207
+ }
2208
+ }
2209
+ return { filesNeedingMigration };
2210
+ }
2211
+ function createBootstrapMarkerMigrationCheck(inspection) {
2212
+ if (inspection.filesNeedingMigration.length === 0) {
2213
+ return okCheck(
2214
+ "Bootstrap marker migration",
2215
+ "No legacy fabric:knowledge-base markers detected in bootstrap target files."
2216
+ );
2217
+ }
2218
+ const list = inspection.filesNeedingMigration.join(", ");
2219
+ return issueCheck(
2220
+ "Bootstrap marker migration",
2221
+ "error",
2222
+ "fixable_error",
2223
+ "bootstrap_marker_migration_required",
2224
+ `${inspection.filesNeedingMigration.length} file${inspection.filesNeedingMigration.length === 1 ? "" : "s"} still carry the legacy fabric:knowledge-base bootstrap marker: ${list}.`,
2225
+ "Run `fab doctor --fix` to migrate to fabric:bootstrap marker"
2226
+ );
2227
+ }
2228
+ async function inspectL1BootstrapSnapshotDrift(target) {
2229
+ const abs = join5(target, ".fabric", "AGENTS.md");
2230
+ if (!existsSync4(abs)) {
2231
+ return { status: "missing", canonical: BOOTSTRAP_CANONICAL, onDisk: null };
2232
+ }
2233
+ let onDisk;
2234
+ try {
2235
+ onDisk = await readFile5(abs, "utf8");
2236
+ } catch {
2237
+ return { status: "missing", canonical: BOOTSTRAP_CANONICAL, onDisk: null };
2238
+ }
2239
+ if (onDisk === BOOTSTRAP_CANONICAL) {
2240
+ return { status: "ok", canonical: BOOTSTRAP_CANONICAL, onDisk };
2241
+ }
2242
+ return { status: "drift", canonical: BOOTSTRAP_CANONICAL, onDisk };
2243
+ }
2244
+ function createL1BootstrapSnapshotDriftCheck(inspection) {
2245
+ if (inspection.status === "drift") {
2246
+ return issueCheck(
2247
+ "Bootstrap snapshot drift",
2248
+ "error",
2249
+ "fixable_error",
2250
+ "bootstrap_snapshot_drift",
2251
+ ".fabric/AGENTS.md content diverges byte-for-byte from BOOTSTRAP_CANONICAL.",
2252
+ "Run `fab doctor --fix` to restore canonical bootstrap snapshot"
2253
+ );
2254
+ }
2255
+ return okCheck(
2256
+ "Bootstrap snapshot drift",
2257
+ inspection.status === "ok" ? ".fabric/AGENTS.md byte-equals BOOTSTRAP_CANONICAL." : ".fabric/AGENTS.md absent \u2014 delegated to bootstrap_anchor_missing."
2258
+ );
2259
+ }
2260
+ async function inspectL2ManagedBlockDrift(target) {
2261
+ const snapshotPath = join5(target, ".fabric", "AGENTS.md");
2262
+ if (!existsSync4(snapshotPath)) {
2263
+ return { status: "ok", drifted: [] };
2264
+ }
2265
+ let snapshot;
2266
+ try {
2267
+ snapshot = await readFile5(snapshotPath, "utf8");
2268
+ } catch {
2269
+ return { status: "ok", drifted: [] };
2270
+ }
2271
+ const projectRulesPath = join5(target, ".fabric", "project-rules.md");
2272
+ let expectedBody = snapshot;
2273
+ if (existsSync4(projectRulesPath)) {
2274
+ try {
2275
+ const projectRules = await readFile5(projectRulesPath, "utf8");
2276
+ expectedBody = `${snapshot}
2277
+ ---
2278
+ ${projectRules}`;
2279
+ } catch {
2280
+ }
2281
+ }
2282
+ const drifted = [];
2283
+ let anyManagedBlockFound = false;
2284
+ const blockTargets = [
2285
+ join5(target, "AGENTS.md"),
2286
+ join5(target, ".cursor", "rules", "fabric-bootstrap.mdc")
2287
+ ];
2288
+ for (const abs of blockTargets) {
2289
+ if (!existsSync4(abs)) {
2290
+ continue;
2291
+ }
2292
+ let content;
2293
+ try {
2294
+ content = await readFile5(abs, "utf8");
2295
+ } catch {
2296
+ continue;
2297
+ }
2298
+ if (!BOOTSTRAP_REGEX.test(content) && LEGACY_KB_REGEX.test(content)) {
2299
+ continue;
2300
+ }
2301
+ const match = content.match(BOOTSTRAP_REGEX);
2302
+ if (match === null) {
2303
+ continue;
2304
+ }
2305
+ anyManagedBlockFound = true;
2306
+ const region = match[0];
2307
+ const beginIdx = region.indexOf(BOOTSTRAP_MARKER_BEGIN);
2308
+ const bodyStart = beginIdx + BOOTSTRAP_MARKER_BEGIN.length;
2309
+ const endIdx = region.indexOf(BOOTSTRAP_MARKER_END, bodyStart);
2310
+ if (bodyStart < 0 || endIdx < 0) {
2311
+ continue;
2312
+ }
2313
+ let body = region.slice(bodyStart, endIdx);
2314
+ if (body.startsWith("\n")) body = body.slice(1);
2315
+ if (body.endsWith("\n")) body = body.slice(0, -1);
2316
+ if (body !== expectedBody) {
2317
+ drifted.push({ path: abs, expected: expectedBody, actual: body });
2318
+ }
2319
+ }
2320
+ const claudeMdPath = join5(target, "CLAUDE.md");
2321
+ if (existsSync4(claudeMdPath)) {
2322
+ let claudeContent;
2323
+ try {
2324
+ claudeContent = await readFile5(claudeMdPath, "utf8");
2325
+ if (!BOOTSTRAP_REGEX.test(claudeContent) && LEGACY_KB_REGEX.test(claudeContent)) {
2326
+ } else {
2327
+ anyManagedBlockFound = true;
2328
+ const lines = claudeContent.split(/\r?\n/u);
2329
+ const hasAtImport = lines.some((line) => line.trim() === "@.fabric/AGENTS.md");
2330
+ if (!hasAtImport) {
2331
+ drifted.push({
2332
+ path: claudeMdPath,
2333
+ expected: "@.fabric/AGENTS.md",
2334
+ actual: "(line missing)"
2335
+ });
2336
+ }
2337
+ }
2338
+ } catch {
2339
+ }
2340
+ }
2341
+ if (!anyManagedBlockFound) {
2342
+ return { status: "no-managed-block", drifted: [] };
2343
+ }
2344
+ if (drifted.length === 0) {
2345
+ return { status: "ok", drifted: [] };
2346
+ }
2347
+ return { status: "drift", drifted };
2348
+ }
2349
+ function createL2ManagedBlockDriftCheck(inspection) {
2350
+ if (inspection.status === "drift") {
2351
+ const list = inspection.drifted.map((d) => d.path).join(", ");
2352
+ return issueCheck(
2353
+ "Managed block drift",
2354
+ "error",
2355
+ "fixable_error",
2356
+ "managed_block_drift",
2357
+ `${inspection.drifted.length} three-end managed block${inspection.drifted.length === 1 ? "" : "s"} diverge from expected body (snapshot + optional project-rules concat): ${list}.`,
2358
+ "Run `fab doctor --fix` to restore three-end managed blocks from canonical"
2359
+ );
2360
+ }
2361
+ return okCheck(
2362
+ "Managed block drift",
2363
+ inspection.status === "ok" ? "Three-end managed blocks byte-equal expectedBody." : "No three-end managed blocks detected \u2014 propagation pending or legacy-marker state."
2364
+ );
2365
+ }
2127
2366
  function createBootstrapAnchorCheck(inspection) {
2128
2367
  if (!inspection.hasAgentsMd && !inspection.hasClaudeMd) {
2129
2368
  return issueCheck(
@@ -3953,6 +4192,125 @@ function createIndexDriftCheck(inspection) {
3953
4192
  "Run `fab doctor --apply-lint` (rc.4 TASK-003) to bump agents.meta.json counters to max_observed + 1."
3954
4193
  );
3955
4194
  }
4195
+ async function migrateBootstrapMarkers(projectRoot) {
4196
+ const paths = [];
4197
+ const countPerPath = {};
4198
+ for (const rel of BOOTSTRAP_MARKER_MIGRATION_TARGETS) {
4199
+ const abs = join5(projectRoot, rel);
4200
+ if (!existsSync4(abs)) {
4201
+ continue;
4202
+ }
4203
+ let original;
4204
+ try {
4205
+ original = await readFile5(abs, "utf8");
4206
+ } catch {
4207
+ continue;
4208
+ }
4209
+ const beginMatches = original.match(/<!-- fabric:knowledge-base:begin -->/g);
4210
+ const endMatches = original.match(/<!-- fabric:knowledge-base:end -->/g);
4211
+ const replacedCount = (beginMatches?.length ?? 0) + (endMatches?.length ?? 0);
4212
+ if (replacedCount === 0) {
4213
+ continue;
4214
+ }
4215
+ const rewritten = original.replace(/<!-- fabric:knowledge-base:begin -->/g, BOOTSTRAP_MARKER_BEGIN).replace(/<!-- fabric:knowledge-base:end -->/g, BOOTSTRAP_MARKER_END);
4216
+ if (rewritten === original) {
4217
+ continue;
4218
+ }
4219
+ await atomicWriteText3(abs, rewritten);
4220
+ paths.push(abs);
4221
+ countPerPath[abs] = replacedCount;
4222
+ }
4223
+ return { paths, countPerPath };
4224
+ }
4225
+ async function rewriteThreeEndManagedBlocks(projectRoot) {
4226
+ const snapshotPath = join5(projectRoot, ".fabric", "AGENTS.md");
4227
+ if (!existsSync4(snapshotPath)) {
4228
+ return;
4229
+ }
4230
+ let snapshot;
4231
+ try {
4232
+ snapshot = await readFile5(snapshotPath, "utf8");
4233
+ } catch {
4234
+ return;
4235
+ }
4236
+ const projectRulesPath = join5(projectRoot, ".fabric", "project-rules.md");
4237
+ const hasProjectRules = existsSync4(projectRulesPath);
4238
+ let expectedBody = snapshot;
4239
+ if (hasProjectRules) {
4240
+ try {
4241
+ const projectRules = await readFile5(projectRulesPath, "utf8");
4242
+ expectedBody = `${snapshot}
4243
+ ---
4244
+ ${projectRules}`;
4245
+ } catch {
4246
+ }
4247
+ }
4248
+ const managedBlock = `${BOOTSTRAP_MARKER_BEGIN}
4249
+ ${expectedBody}
4250
+ ${BOOTSTRAP_MARKER_END}`;
4251
+ const blockTargets = [
4252
+ join5(projectRoot, "AGENTS.md"),
4253
+ join5(projectRoot, ".cursor", "rules", "fabric-bootstrap.mdc")
4254
+ ];
4255
+ for (const abs of blockTargets) {
4256
+ if (!existsSync4(abs)) {
4257
+ continue;
4258
+ }
4259
+ let existing;
4260
+ try {
4261
+ existing = await readFile5(abs, "utf8");
4262
+ } catch {
4263
+ continue;
4264
+ }
4265
+ let next;
4266
+ const match = existing.match(BOOTSTRAP_REGEX);
4267
+ if (match !== null) {
4268
+ const before = existing.slice(0, match.index ?? 0);
4269
+ const after = existing.slice((match.index ?? 0) + match[0].length);
4270
+ const stripped = `${before}${after.replace(/^\r?\n/, "")}`;
4271
+ const trailingNewline = stripped.length === 0 || stripped.endsWith("\n") ? "" : "\n";
4272
+ next = `${stripped}${trailingNewline}
4273
+ ${managedBlock}
4274
+ `;
4275
+ } else {
4276
+ const trailingNewline = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
4277
+ next = `${existing}${trailingNewline}
4278
+ ${managedBlock}
4279
+ `;
4280
+ }
4281
+ if (next === existing) {
4282
+ continue;
4283
+ }
4284
+ await atomicWriteText3(abs, next);
4285
+ }
4286
+ const claudeMdPath = join5(projectRoot, "CLAUDE.md");
4287
+ if (existsSync4(claudeMdPath)) {
4288
+ let claudeContent;
4289
+ try {
4290
+ claudeContent = await readFile5(claudeMdPath, "utf8");
4291
+ } catch {
4292
+ return;
4293
+ }
4294
+ const lines = claudeContent.split(/\r?\n/u);
4295
+ let updated = claudeContent;
4296
+ const ensureLine = (line) => {
4297
+ if (lines.some((existingLine) => existingLine.trim() === line)) {
4298
+ return;
4299
+ }
4300
+ const trailingNewline = updated.length === 0 || updated.endsWith("\n") ? "" : "\n";
4301
+ updated = `${updated}${trailingNewline}${line}
4302
+ `;
4303
+ lines.push(line);
4304
+ };
4305
+ ensureLine("@.fabric/AGENTS.md");
4306
+ if (hasProjectRules) {
4307
+ ensureLine("@.fabric/project-rules.md");
4308
+ }
4309
+ if (updated !== claudeContent) {
4310
+ await atomicWriteText3(claudeMdPath, updated);
4311
+ }
4312
+ }
4313
+ }
3956
4314
  async function fixMcpConfigInWrongFile(projectRoot) {
3957
4315
  const settingsPath = join5(projectRoot, ".claude", "settings.json");
3958
4316
  if (!existsSync4(settingsPath)) {
@@ -4026,6 +4384,275 @@ async function ensureEventLedger(projectRoot) {
4026
4384
  await ensureParentDirectory(path);
4027
4385
  await writeFile2(path, "", { encoding: "utf8", flag: "a" });
4028
4386
  }
4387
+ var CITE_POLICY_VERSION = "2.0.0-rc.20";
4388
+ async function ensureCitePolicyActivatedMarker(projectRoot) {
4389
+ let existing;
4390
+ try {
4391
+ const { events } = await readEventLedger(projectRoot, { event_type: "cite_policy_activated" });
4392
+ if (events.length > 0) {
4393
+ existing = events[0];
4394
+ }
4395
+ } catch {
4396
+ return { marker_ts: 0, emitted_now: false };
4397
+ }
4398
+ if (existing !== void 0) {
4399
+ return { marker_ts: existing.ts, emitted_now: false };
4400
+ }
4401
+ try {
4402
+ const stored = await appendEventLedgerEvent(projectRoot, {
4403
+ event_type: "cite_policy_activated",
4404
+ policy_version: CITE_POLICY_VERSION,
4405
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4406
+ });
4407
+ return { marker_ts: stored.ts, emitted_now: true };
4408
+ } catch {
4409
+ return { marker_ts: 0, emitted_now: false };
4410
+ }
4411
+ }
4412
+ function categorizeCiteTag(tag) {
4413
+ if (tag === "planned" || tag === "recalled" || tag === "chained-from" || tag === "none") {
4414
+ return { category: tag };
4415
+ }
4416
+ if (tag === "dismissed") {
4417
+ return { category: "dismissed", reason: "unspecified" };
4418
+ }
4419
+ if (tag.startsWith("dismissed:")) {
4420
+ const remainder = tag.slice("dismissed:".length);
4421
+ if (remainder.startsWith("other:")) {
4422
+ return { category: "dismissed", reason: remainder.slice("other:".length) || "other" };
4423
+ }
4424
+ return { category: "dismissed", reason: remainder || "unspecified" };
4425
+ }
4426
+ return { category: "none" };
4427
+ }
4428
+ function matchesRelevancePath(editPath, relevancePaths) {
4429
+ if (relevancePaths.length === 0) {
4430
+ return false;
4431
+ }
4432
+ const normalized = normalizePath(editPath);
4433
+ for (const glob of relevancePaths) {
4434
+ if (minimatch(normalized, glob, { dot: true, matchBase: false })) {
4435
+ return true;
4436
+ }
4437
+ }
4438
+ return false;
4439
+ }
4440
+ async function runDoctorCiteCoverage(projectRoot, options) {
4441
+ const marker = await ensureCitePolicyActivatedMarker(projectRoot);
4442
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
4443
+ const zeroMetrics = {
4444
+ edits_touched: 0,
4445
+ qualifying_cites: 0,
4446
+ recalled_unverified: 0,
4447
+ expected_but_missed: 0,
4448
+ total_turns: 0
4449
+ };
4450
+ if (marker.marker_ts === 0) {
4451
+ return {
4452
+ status: "skipped",
4453
+ marker_ts: 0,
4454
+ marker_emitted_now: false,
4455
+ since_ts: options.since,
4456
+ client_filter: options.client,
4457
+ metrics: zeroMetrics,
4458
+ generated_at: generatedAt
4459
+ };
4460
+ }
4461
+ const effectiveSince = Math.max(marker.marker_ts, options.since);
4462
+ let ledgerEvents = [];
4463
+ try {
4464
+ const result = await readEventLedger(projectRoot, { since: effectiveSince });
4465
+ ledgerEvents = result.events;
4466
+ } catch {
4467
+ return {
4468
+ status: "ok",
4469
+ marker_ts: marker.marker_ts,
4470
+ marker_emitted_now: marker.emitted_now,
4471
+ since_ts: effectiveSince,
4472
+ client_filter: options.client,
4473
+ metrics: zeroMetrics,
4474
+ generated_at: generatedAt
4475
+ };
4476
+ }
4477
+ const assistantTurns = [];
4478
+ const editEvents = [];
4479
+ const fetchEvents = [];
4480
+ for (const event of ledgerEvents) {
4481
+ switch (event.event_type) {
4482
+ case "assistant_turn_observed":
4483
+ assistantTurns.push(event);
4484
+ break;
4485
+ case "edit_intent_checked":
4486
+ editEvents.push(event);
4487
+ break;
4488
+ case "knowledge_sections_fetched":
4489
+ fetchEvents.push(event);
4490
+ break;
4491
+ default:
4492
+ break;
4493
+ }
4494
+ }
4495
+ const filteredTurns = options.client === "all" ? assistantTurns : assistantTurns.filter((t) => t.client === options.client);
4496
+ let clientSessionIds = null;
4497
+ if (options.client !== "all") {
4498
+ clientSessionIds = /* @__PURE__ */ new Set();
4499
+ for (const turn of assistantTurns) {
4500
+ if (turn.client === options.client) {
4501
+ const sid = turn.session_id;
4502
+ if (typeof sid === "string" && sid.length > 0) {
4503
+ clientSessionIds.add(sid);
4504
+ }
4505
+ }
4506
+ }
4507
+ }
4508
+ const kbIndex = /* @__PURE__ */ new Map();
4509
+ try {
4510
+ const meta = await readAgentsMeta(projectRoot);
4511
+ for (const node of Object.values(meta.nodes)) {
4512
+ const stableId = node.stable_id;
4513
+ if (typeof stableId !== "string" || stableId.length === 0) continue;
4514
+ const description = node.description;
4515
+ if (description === void 0) continue;
4516
+ const paths = description.relevance_paths ?? [];
4517
+ const scope = description.relevance_scope ?? "broad";
4518
+ kbIndex.set(stableId, {
4519
+ relevance_paths: paths,
4520
+ // A broad entry with no paths is the safe default. A narrow entry must
4521
+ // carry at least one path; an empty-paths narrow is treated as broad.
4522
+ relevance_scope: scope === "narrow" && paths.length > 0 ? "narrow" : "broad"
4523
+ });
4524
+ }
4525
+ } catch {
4526
+ }
4527
+ const fetchesBySession = /* @__PURE__ */ new Map();
4528
+ for (const fetch of fetchEvents) {
4529
+ const sid = fetch.session_id;
4530
+ if (typeof sid !== "string" || sid.length === 0) continue;
4531
+ const list = fetchesBySession.get(sid) ?? [];
4532
+ list.push(fetch.ts);
4533
+ fetchesBySession.set(sid, list);
4534
+ }
4535
+ for (const list of fetchesBySession.values()) {
4536
+ list.sort((a, b) => a - b);
4537
+ }
4538
+ const RECALL_WINDOW_MS = 6e4;
4539
+ const isRecallVerified = (turn) => {
4540
+ const sid = turn.session_id;
4541
+ if (typeof sid !== "string" || sid.length === 0) return false;
4542
+ const fetches = fetchesBySession.get(sid);
4543
+ if (fetches === void 0 || fetches.length === 0) return false;
4544
+ for (const ft of fetches) {
4545
+ if (Math.abs(ft - turn.ts) <= RECALL_WINDOW_MS) return true;
4546
+ }
4547
+ return false;
4548
+ };
4549
+ const dismissedHistogram = {};
4550
+ const perClientAccum = /* @__PURE__ */ new Map();
4551
+ const emptyMetrics = () => ({
4552
+ edits_touched: 0,
4553
+ qualifying_cites: 0,
4554
+ recalled_unverified: 0,
4555
+ expected_but_missed: 0,
4556
+ total_turns: 0
4557
+ });
4558
+ const bumpClient = (client, mut) => {
4559
+ if (typeof client !== "string" || client.length === 0) return;
4560
+ const existing = perClientAccum.get(client) ?? emptyMetrics();
4561
+ mut(existing);
4562
+ perClientAccum.set(client, existing);
4563
+ };
4564
+ const sessionCitedKbs = /* @__PURE__ */ new Map();
4565
+ let totalTurns = 0;
4566
+ let qualifyingCites = 0;
4567
+ let recalledUnverified = 0;
4568
+ for (const turn of filteredTurns) {
4569
+ totalTurns += 1;
4570
+ bumpClient(turn.client, (m) => {
4571
+ m.total_turns += 1;
4572
+ });
4573
+ const sid = turn.session_id;
4574
+ if (typeof sid === "string" && sid.length > 0) {
4575
+ const set = sessionCitedKbs.get(sid) ?? /* @__PURE__ */ new Set();
4576
+ for (const id of turn.cite_ids) {
4577
+ set.add(id);
4578
+ }
4579
+ sessionCitedKbs.set(sid, set);
4580
+ }
4581
+ let turnHadRecalled = false;
4582
+ for (const tag of turn.cite_tags) {
4583
+ const { category, reason } = categorizeCiteTag(tag);
4584
+ switch (category) {
4585
+ case "planned":
4586
+ case "recalled":
4587
+ case "chained-from":
4588
+ qualifyingCites += 1;
4589
+ bumpClient(turn.client, (m) => {
4590
+ m.qualifying_cites += 1;
4591
+ });
4592
+ if (category === "recalled") turnHadRecalled = true;
4593
+ break;
4594
+ case "dismissed": {
4595
+ const key = reason ?? "unspecified";
4596
+ dismissedHistogram[key] = (dismissedHistogram[key] ?? 0) + 1;
4597
+ break;
4598
+ }
4599
+ case "none":
4600
+ default:
4601
+ break;
4602
+ }
4603
+ }
4604
+ if (turnHadRecalled && !isRecallVerified(turn)) {
4605
+ recalledUnverified += 1;
4606
+ bumpClient(turn.client, (m) => {
4607
+ m.recalled_unverified += 1;
4608
+ });
4609
+ }
4610
+ }
4611
+ let editsTouched = 0;
4612
+ let expectedButMissed = 0;
4613
+ for (const edit of editEvents) {
4614
+ const sid = edit.session_id;
4615
+ if (clientSessionIds !== null) {
4616
+ if (typeof sid !== "string" || sid.length === 0) continue;
4617
+ if (!clientSessionIds.has(sid)) continue;
4618
+ }
4619
+ editsTouched += 1;
4620
+ if (typeof sid !== "string" || sid.length === 0) continue;
4621
+ const citedSet = sessionCitedKbs.get(sid) ?? /* @__PURE__ */ new Set();
4622
+ for (const [kbId, kb] of kbIndex) {
4623
+ if (kb.relevance_scope !== "narrow") continue;
4624
+ if (!matchesRelevancePath(edit.path, kb.relevance_paths)) continue;
4625
+ if (!citedSet.has(kbId)) {
4626
+ expectedButMissed += 1;
4627
+ }
4628
+ }
4629
+ }
4630
+ const metrics = {
4631
+ edits_touched: editsTouched,
4632
+ qualifying_cites: qualifyingCites,
4633
+ recalled_unverified: recalledUnverified,
4634
+ expected_but_missed: expectedButMissed,
4635
+ total_turns: totalTurns
4636
+ };
4637
+ let perClient;
4638
+ if (options.client === "all" && perClientAccum.size > 0) {
4639
+ perClient = {};
4640
+ for (const [client, m] of perClientAccum) {
4641
+ perClient[client] = m;
4642
+ }
4643
+ }
4644
+ return {
4645
+ status: "ok",
4646
+ marker_ts: marker.marker_ts,
4647
+ marker_emitted_now: marker.emitted_now,
4648
+ since_ts: effectiveSince,
4649
+ client_filter: options.client,
4650
+ metrics,
4651
+ ...perClient !== void 0 ? { per_client: perClient } : {},
4652
+ ...Object.keys(dismissedHistogram).length > 0 ? { dismissed_reason_histogram: dismissedHistogram } : {},
4653
+ generated_at: generatedAt
4654
+ };
4655
+ }
4029
4656
  function createFixMessage(fixed, report) {
4030
4657
  const fixedText = fixed.length === 0 ? "No deterministic doctor fixes were needed." : `Applied ${fixed.length} deterministic doctor fix${fixed.length === 1 ? "" : "es"}.`;
4031
4658
  const manualText = report.manual_errors.length === 0 ? "No manual errors remain." : `${report.manual_errors.length} manual error${report.manual_errors.length === 1 ? "" : "s"} remain.`;
@@ -4332,5 +4959,6 @@ export {
4332
4959
  normalizeKnowledgePath,
4333
4960
  runDoctorReport,
4334
4961
  runDoctorFix,
4335
- runDoctorApplyLint
4962
+ runDoctorApplyLint,
4963
+ runDoctorCiteCoverage
4336
4964
  };
@@ -14,7 +14,7 @@ import {
14
14
  readEventLedger,
15
15
  runDoctorReport,
16
16
  sha256
17
- } from "./chunk-3ZBYYPXQ.js";
17
+ } from "./chunk-7R6MFA7Y.js";
18
18
 
19
19
  // src/http.ts
20
20
  import { randomUUID as randomUUID2 } from "crypto";
package/dist/index.d.ts CHANGED
@@ -97,6 +97,27 @@ type DoctorApplyLintReport = {
97
97
  declare function runDoctorReport(target: string): Promise<DoctorReport>;
98
98
  declare function runDoctorFix(target: string): Promise<DoctorFixReport>;
99
99
  declare function runDoctorApplyLint(target: string): Promise<DoctorApplyLintReport>;
100
+ type CiteCoverageReport = {
101
+ status: "ok" | "skipped";
102
+ marker_ts: number;
103
+ marker_emitted_now: boolean;
104
+ since_ts: number;
105
+ client_filter: "cc" | "codex" | "cursor" | "all";
106
+ metrics: {
107
+ edits_touched: number;
108
+ qualifying_cites: number;
109
+ recalled_unverified: number;
110
+ expected_but_missed: number;
111
+ total_turns: number;
112
+ };
113
+ per_client?: Record<string, Partial<CiteCoverageReport["metrics"]>>;
114
+ dismissed_reason_histogram?: Record<string, number>;
115
+ generated_at: string;
116
+ };
117
+ declare function runDoctorCiteCoverage(projectRoot: string, options: {
118
+ since: number;
119
+ client: "cc" | "codex" | "cursor" | "all";
120
+ }): Promise<CiteCoverageReport>;
100
121
 
101
122
  type KnowledgeMetaBuildSource = "doctor_fix" | "sync_meta";
102
123
  type KnowledgeMetaBuildResult = {
@@ -382,4 +403,4 @@ declare function startHttpServer(options: {
382
403
  authToken?: string;
383
404
  }): Promise<Server>;
384
405
 
385
- export { AGENTS_MD_RESOURCE_URI, type AcquireOptions, type DoctorApplyLintMutation, type DoctorApplyLintMutationKind, type DoctorApplyLintReport, type DoctorFixReport, type DoctorIssue, type DoctorReport, EVENT_LEDGER_PATH, type InFlightTracker, KnowledgeIdAllocator, type KnowledgeMetaBuildResult, type KnowledgeMetaBuildSource, type KnowledgeSyncLedgerEvent, type KnowledgeSyncOptions, type KnowledgeSyncReport, LEDGER_PATH, LEGACY_LEDGER_PATH, type LedgerEvent, type LockState, type PlanContextInput, type PlanContextResult, type ReconcileKnowledgeOptions, type RequirementProfile, type SelectionTokenState, ServeLockHeldError, type ShutdownHandlerDeps, type StructuredWarning, type WriteKnowledgeMetaOptions, acquireLock, appendEventLedgerEvent, buildKnowledgeMeta, checkLockOrThrow, computeKnowledgeBasedAgentsMeta, computeKnowledgeTestIndex, createFabricServer, createInFlightTracker, createShutdownHandler, deriveKnowledgeMetaLayer, deriveKnowledgeMetaTopologyType, ensureKnowledgeFresh, extractKnowledge, flushAndSyncEventLedger, formatPreexistingRootMessage, getEventLedgerPath, getLedgerPath, getLegacyLedgerPath, isSameKnowledgeTestIndex, planContext, readLockState, readSelectionToken, reconcileKnowledge, releaseLock, reviewKnowledge, runDoctorApplyLint, runDoctorFix, runDoctorReport, stableStringify, startHttpServer, startStdioServer, writeKnowledgeMeta };
406
+ export { AGENTS_MD_RESOURCE_URI, type AcquireOptions, type CiteCoverageReport, type DoctorApplyLintMutation, type DoctorApplyLintMutationKind, type DoctorApplyLintReport, type DoctorFixReport, type DoctorIssue, type DoctorReport, EVENT_LEDGER_PATH, type InFlightTracker, KnowledgeIdAllocator, type KnowledgeMetaBuildResult, type KnowledgeMetaBuildSource, type KnowledgeSyncLedgerEvent, type KnowledgeSyncOptions, type KnowledgeSyncReport, LEDGER_PATH, LEGACY_LEDGER_PATH, type LedgerEvent, type LockState, type PlanContextInput, type PlanContextResult, type ReconcileKnowledgeOptions, type RequirementProfile, type SelectionTokenState, ServeLockHeldError, type ShutdownHandlerDeps, type StructuredWarning, type WriteKnowledgeMetaOptions, acquireLock, appendEventLedgerEvent, buildKnowledgeMeta, checkLockOrThrow, computeKnowledgeBasedAgentsMeta, computeKnowledgeTestIndex, createFabricServer, createInFlightTracker, createShutdownHandler, deriveKnowledgeMetaLayer, deriveKnowledgeMetaTopologyType, ensureKnowledgeFresh, extractKnowledge, flushAndSyncEventLedger, formatPreexistingRootMessage, getEventLedgerPath, getLedgerPath, getLegacyLedgerPath, isSameKnowledgeTestIndex, planContext, readLockState, readSelectionToken, reconcileKnowledge, releaseLock, reviewKnowledge, runDoctorApplyLint, runDoctorCiteCoverage, runDoctorFix, runDoctorReport, stableStringify, startHttpServer, startStdioServer, writeKnowledgeMeta };
package/dist/index.js CHANGED
@@ -21,12 +21,13 @@ import {
21
21
  reconcileKnowledge,
22
22
  resolveProjectRoot,
23
23
  runDoctorApplyLint,
24
+ runDoctorCiteCoverage,
24
25
  runDoctorFix,
25
26
  runDoctorReport,
26
27
  sha256,
27
28
  stableStringify,
28
29
  writeKnowledgeMeta
29
- } from "./chunk-3ZBYYPXQ.js";
30
+ } from "./chunk-7R6MFA7Y.js";
30
31
 
31
32
  // src/index.ts
32
33
  import { existsSync as existsSync4 } from "fs";
@@ -318,9 +319,9 @@ ${renderEvidenceBlock(newSummary, newRecentPaths)}
318
319
  const pathSection = /Recent paths:\s*\n([\s\S]*?)(?:\n\s*Notes:|$)/u.exec(block);
319
320
  if (pathSection !== null) {
320
321
  for (const rawLine of (pathSection[1] ?? "").split(/\r?\n/u)) {
321
- const t = rawLine.trim();
322
- if (t.startsWith("- ")) {
323
- existingPaths.push(t.slice(2).trim());
322
+ const t2 = rawLine.trim();
323
+ if (t2.startsWith("- ")) {
324
+ existingPaths.push(t2.slice(2).trim());
324
325
  }
325
326
  }
326
327
  }
@@ -329,16 +330,16 @@ ${renderEvidenceBlock(newSummary, newRecentPaths)}
329
330
  const bulletLines = [];
330
331
  let prose = [];
331
332
  for (const rawLine of noteBody.split(/\r?\n/u)) {
332
- const t = rawLine.trim();
333
- if (t.length === 0) continue;
334
- if (t.startsWith("- ")) {
333
+ const t2 = rawLine.trim();
334
+ if (t2.length === 0) continue;
335
+ if (t2.startsWith("- ")) {
335
336
  if (prose.length > 0) {
336
337
  existingNotes.push(prose.join(" ").trim());
337
338
  prose = [];
338
339
  }
339
- bulletLines.push(t.slice(2).trim());
340
+ bulletLines.push(t2.slice(2).trim());
340
341
  } else {
341
- prose.push(t);
342
+ prose.push(t2);
342
343
  }
343
344
  }
344
345
  if (prose.length > 0) existingNotes.push(prose.join(" ").trim());
@@ -934,7 +935,7 @@ async function listPending(projectRoot, filters) {
934
935
  }
935
936
  if (filters?.tags !== void 0 && filters.tags.length > 0) {
936
937
  const itemTags = fm.tags ?? [];
937
- const hasAll = filters.tags.every((t) => itemTags.includes(t));
938
+ const hasAll = filters.tags.every((t2) => itemTags.includes(t2));
938
939
  if (!hasAll) continue;
939
940
  }
940
941
  if (filters?.created_after !== void 0) {
@@ -1234,7 +1235,7 @@ async function searchEntries(projectRoot, query, filters) {
1234
1235
  }
1235
1236
  if (filters?.tags !== void 0 && filters.tags.length > 0) {
1236
1237
  const itemTags = fm.tags ?? [];
1237
- const hasAll = filters.tags.every((t) => itemTags.includes(t));
1238
+ const hasAll = filters.tags.every((t2) => itemTags.includes(t2));
1238
1239
  if (!hasAll) continue;
1239
1240
  }
1240
1241
  if (filters?.created_after !== void 0) {
@@ -1781,8 +1782,10 @@ function registerKnowledgeSections(server, tracker) {
1781
1782
  // src/services/serve-lock.ts
1782
1783
  import fs from "fs";
1783
1784
  import path from "path";
1785
+ import { createTranslator, detectNodeLocale } from "@fenglimg/fabric-shared";
1784
1786
  import { IOFabricError } from "@fenglimg/fabric-shared/errors";
1785
1787
  var LOCK_FILENAME = ".serve.lock";
1788
+ var t = createTranslator(detectNodeLocale());
1786
1789
  var ServeLockHeldError = class extends IOFabricError {
1787
1790
  code = "SERVE_LOCK_HELD";
1788
1791
  httpStatus = 423;
@@ -1813,7 +1816,7 @@ function acquireLock(projectRoot, opts) {
1813
1816
  throw new ServeLockHeldError(
1814
1817
  `serve lock held by live PID ${state.pid}`,
1815
1818
  {
1816
- actionHint: `Stop the other process (PID ${state.pid}) or run with --force to override`,
1819
+ actionHint: t("cli.serve.lock-held.action-hint", { pid: String(state.pid) }),
1817
1820
  details: state
1818
1821
  }
1819
1822
  );
@@ -1863,7 +1866,7 @@ function checkLockOrThrow(projectRoot, opts) {
1863
1866
  throw new ServeLockHeldError(
1864
1867
  `serve lock held by live PID ${state.pid}`,
1865
1868
  {
1866
- actionHint: `Stop the other serve process (PID ${state.pid}) before running this command, or pass --force to override`,
1869
+ actionHint: t("cli.serve.lock-held.action-hint", { pid: String(state.pid) }),
1867
1870
  details: state
1868
1871
  }
1869
1872
  );
@@ -1890,7 +1893,7 @@ function formatPreexistingRootMessage(projectRoot) {
1890
1893
  function createFabricServer(tracker) {
1891
1894
  const server = new McpServer({
1892
1895
  name: "fabric-knowledge-server",
1893
- version: "2.0.0-rc.13"
1896
+ version: "2.0.0-rc.21"
1894
1897
  });
1895
1898
  registerPlanContext(server, tracker);
1896
1899
  registerKnowledgeSections(server, tracker);
@@ -1990,7 +1993,7 @@ function createShutdownHandler(deps) {
1990
1993
  };
1991
1994
  }
1992
1995
  async function startHttpServer(options) {
1993
- const { createFabricHttpApp } = await import("./http-B3UKRP5Y.js");
1996
+ const { createFabricHttpApp } = await import("./http-JGWQGUZS.js");
1994
1997
  const { port, projectRoot, host = "127.0.0.1", authToken } = options;
1995
1998
  const app = createFabricHttpApp({ projectRoot, host, authToken });
1996
1999
  return await new Promise((resolveServer, rejectServer) => {
@@ -2048,6 +2051,7 @@ export {
2048
2051
  releaseLock,
2049
2052
  reviewKnowledge,
2050
2053
  runDoctorApplyLint,
2054
+ runDoctorCiteCoverage,
2051
2055
  runDoctorFix,
2052
2056
  runDoctorReport,
2053
2057
  stableStringify,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fenglimg/fabric-server",
3
- "version": "2.0.0-rc.13",
3
+ "version": "2.0.0-rc.21",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -13,7 +13,7 @@
13
13
  "express": "^5.2.1",
14
14
  "minimatch": "^10.0.1",
15
15
  "zod": "^3.25.0",
16
- "@fenglimg/fabric-shared": "2.0.0-rc.13"
16
+ "@fenglimg/fabric-shared": "2.0.0-rc.21"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@types/express": "^5.0.6",