@fenglimg/fabric-server 2.0.0-rc.15 → 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";
@@ -1892,7 +1893,7 @@ function formatPreexistingRootMessage(projectRoot) {
1892
1893
  function createFabricServer(tracker) {
1893
1894
  const server = new McpServer({
1894
1895
  name: "fabric-knowledge-server",
1895
- version: "2.0.0-rc.15"
1896
+ version: "2.0.0-rc.21"
1896
1897
  });
1897
1898
  registerPlanContext(server, tracker);
1898
1899
  registerKnowledgeSections(server, tracker);
@@ -1992,7 +1993,7 @@ function createShutdownHandler(deps) {
1992
1993
  };
1993
1994
  }
1994
1995
  async function startHttpServer(options) {
1995
- const { createFabricHttpApp } = await import("./http-FCXAUGSO.js");
1996
+ const { createFabricHttpApp } = await import("./http-JGWQGUZS.js");
1996
1997
  const { port, projectRoot, host = "127.0.0.1", authToken } = options;
1997
1998
  const app = createFabricHttpApp({ projectRoot, host, authToken });
1998
1999
  return await new Promise((resolveServer, rejectServer) => {
@@ -2050,6 +2051,7 @@ export {
2050
2051
  releaseLock,
2051
2052
  reviewKnowledge,
2052
2053
  runDoctorApplyLint,
2054
+ runDoctorCiteCoverage,
2053
2055
  runDoctorFix,
2054
2056
  runDoctorReport,
2055
2057
  stableStringify,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fenglimg/fabric-server",
3
- "version": "2.0.0-rc.15",
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.15"
16
+ "@fenglimg/fabric-shared": "2.0.0-rc.21"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@types/express": "^5.0.6",