@tangle-network/agent-knowledge 6.1.11 → 6.2.0

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.
@@ -1208,6 +1208,358 @@ function addSourceOverlapEdges(pages, edges) {
1208
1208
  }
1209
1209
  }
1210
1210
  //#endregion
1211
+ //#region src/claim-ledger.ts
1212
+ /**
1213
+ * The algebra of a research claim ledger: claim identity, and how two ledgers
1214
+ * that accumulated evidence for the same goal combine into one.
1215
+ *
1216
+ * This lives apart from `research-driving-driver.ts` because it is no longer
1217
+ * that driver's private business. A ledger is a durable record now, and a
1218
+ * durable record addressed by id is a record two writers can reach: two rounds
1219
+ * of one run resuming from disk, or two workers researching one goal in
1220
+ * parallel. `putClaimLedger` writes the whole record, so the second writer's
1221
+ * write erases the first writer's claims — the ledger persists and the
1222
+ * knowledge still does not compound. Combining is therefore part of what the
1223
+ * record MEANS, and it belongs next to the record rather than inside one
1224
+ * consumer of it.
1225
+ *
1226
+ * Every combination here is monotone: support only grows, contradiction edges
1227
+ * only accumulate, `contested` only latches on, `firstSeenRound` only moves
1228
+ * earlier. That is what makes it safe to apply twice — a retried write cannot
1229
+ * produce a different ledger than a single write did.
1230
+ */
1231
+ /**
1232
+ * Claim identity = sha256 of the normalized claim text, so the same assertion
1233
+ * discovered independently by two workers is ONE claim with two supporting
1234
+ * sources rather than two claims with one each — which is the difference
1235
+ * between corroborated and unsupported.
1236
+ */
1237
+ function claimId(text) {
1238
+ return `c_${sha256(normalizeClaimText(text)).slice(0, 16)}`;
1239
+ }
1240
+ /** Stable identity for one extracted claim/source/contradiction observation. */
1241
+ function claimEvidenceId(claim) {
1242
+ return `e_${sha256(JSON.stringify([
1243
+ claim.claimId,
1244
+ claim.sourceUri,
1245
+ claim.contradictsClaimId ?? null
1246
+ ])).slice(0, 16)}`;
1247
+ }
1248
+ /** Case-, whitespace-, and stylistic-punctuation-insensitive claim identity form. */
1249
+ function normalizeClaimText(text) {
1250
+ return text.normalize("NFKC").toLowerCase().replace(/<=|≤/gu, " symbol_less_than_or_equal ").replace(/>=|≥/gu, " symbol_greater_than_or_equal ").replace(/!=|≠/gu, " symbol_not_equal ").replace(/==|=/gu, " symbol_equal ").replace(/±/gu, " symbol_plus_or_minus ").replace(/[≈~]/gu, " symbol_approximately ").replace(/<|←|⇐/gu, " symbol_less_or_left ").replace(/>|→|⇒/gu, " symbol_greater_or_right ").replace(/[+]/gu, " symbol_plus_or_positive ").replace(/[-−]/gu, " symbol_minus_or_negative ").replace(/[^\p{L}\p{N}\s]+/gu, " ").replace(/\s+/g, " ").trim();
1251
+ }
1252
+ /**
1253
+ * The canonical host a source uri counts as, which is what makes two sources
1254
+ * INDEPENDENT: corroboration is "distinct hosts", so this function is the rule
1255
+ * that decides whether a claim is confirmed or merely repeated. Exported so a
1256
+ * consumer building a `ResearchClaimRecord` cannot answer it a different way — a
1257
+ * consumer that counted raw uris would report two pages of one site as
1258
+ * independent confirmation.
1259
+ */
1260
+ function claimSourceHost(uri) {
1261
+ try {
1262
+ return new URL(uri.trim()).hostname.toLowerCase().replace(/^www\./, "");
1263
+ } catch {
1264
+ return uri.trim().toLowerCase();
1265
+ }
1266
+ }
1267
+ /** Stable identity for one deep question. */
1268
+ function deepQuestionId(kind, text) {
1269
+ return `q_${sha256(`${kind}:${text}`).slice(0, 16)}`;
1270
+ }
1271
+ /**
1272
+ * Refuse a claim record whose identity or source count disagrees with its evidence.
1273
+ *
1274
+ * `supportingHosts` is used as the independent-source count, so accepting hosts
1275
+ * that cannot be derived from `supportingUris` would let a malformed record
1276
+ * manufacture corroboration. Canonical ordering also makes equal records have
1277
+ * equal bytes regardless of which process assembled them.
1278
+ */
1279
+ function assertTrackedClaimIntegrity(claim) {
1280
+ if (claim.id !== claimId(claim.text)) throw new Error(`claim '${claim.id}' does not match its text-derived identity`);
1281
+ if (claim.text !== claim.text.trim()) throw new Error(`claim '${claim.id}' text must not have surrounding whitespace`);
1282
+ assertSortedUnique(`claim '${claim.id}' supportingUris`, claim.supportingUris);
1283
+ assertSortedUnique(`claim '${claim.id}' supportingHosts`, claim.supportingHosts);
1284
+ assertSortedUnique(`claim '${claim.id}' contradicts`, claim.contradicts);
1285
+ const expectedHosts = [...new Set(claim.supportingUris.map(claimSourceHost).filter(Boolean))].sort();
1286
+ if (!sameStrings(claim.supportingHosts, expectedHosts)) throw new Error(`claim '${claim.id}' supportingHosts must equal the hosts derived from supportingUris`);
1287
+ if (claim.contradicts.includes(claim.id)) throw new Error(`claim '${claim.id}' cannot contradict itself`);
1288
+ if (claim.contradicts.length > 0 && !claim.contested) throw new Error(`claim '${claim.id}' with a contradiction must be contested`);
1289
+ }
1290
+ /** Refuse a deep question whose stable identity or set fields are malformed. */
1291
+ function assertDeepQuestionIntegrity(question) {
1292
+ if (question.id !== deepQuestionId(question.kind, question.text)) throw new Error(`question '${question.id}' does not match its kind-and-text identity`);
1293
+ if (question.text !== question.text.trim()) throw new Error(`question '${question.id}' text must not have surrounding whitespace`);
1294
+ assertSortedUnique(`question '${question.id}' claimIds`, question.claimIds);
1295
+ }
1296
+ /** Refuse an extracted observation whose identity or content is malformed. */
1297
+ function assertResearchClaimEvidenceIntegrity(evidence) {
1298
+ if (evidence.claimId !== claimId(evidence.text)) throw new Error(`claim evidence '${evidence.id}' does not match its text-derived claim identity`);
1299
+ if (evidence.id !== claimEvidenceId(evidence)) throw new Error(`claim evidence '${evidence.id}' does not match its content-derived identity`);
1300
+ if (evidence.text !== evidence.text.trim()) throw new Error(`claim evidence '${evidence.id}' text must not have surrounding whitespace`);
1301
+ if (evidence.contradictsClaimId === evidence.claimId) throw new Error(`claim evidence '${evidence.id}' cannot contradict its own claim`);
1302
+ }
1303
+ /** Refuse a ledger that is not one canonical, internally consistent record. */
1304
+ function assertResearchClaimLedgerIntegrity(ledger) {
1305
+ if (ledger.preparedRounds !== void 0 && ledger.preparedRounds <= ledger.rounds) throw new Error(`claim ledger '${ledger.id}' preparedRounds must be greater than completed rounds`);
1306
+ assertSortedUnique(`claim ledger '${ledger.id}' claimEvidence`, ledger.claimEvidence.map((evidence) => evidence.id));
1307
+ assertSortedUnique(`claim ledger '${ledger.id}' registeredSourceUris`, ledger.registeredSourceUris);
1308
+ assertSortedUnique(`claim ledger '${ledger.id}' claims`, ledger.claims.map((claim) => claim.id));
1309
+ assertSortedUnique(`claim ledger '${ledger.id}' questions`, ledger.questions.map((question) => question.id));
1310
+ for (const evidence of ledger.claimEvidence) assertResearchClaimEvidenceIntegrity(evidence);
1311
+ const registeredSourceUris = new Set(ledger.registeredSourceUris);
1312
+ for (const claim of ledger.claims) {
1313
+ assertTrackedClaimIntegrity(claim);
1314
+ for (const sourceUri of claim.supportingUris) if (!registeredSourceUris.has(sourceUri)) throw new Error(`claim '${claim.id}' counts source '${sourceUri}' before its registration is confirmed`);
1315
+ }
1316
+ const claimsById = new Map(ledger.claims.map((claim) => [claim.id, claim]));
1317
+ const claimIds = new Set(claimsById.keys());
1318
+ for (const evidence of ledger.claimEvidence) {
1319
+ if (!registeredSourceUris.has(evidence.sourceUri)) continue;
1320
+ const claim = claimsById.get(evidence.claimId);
1321
+ if (!claim?.supportingUris.includes(evidence.sourceUri)) throw new Error(`registered claim evidence '${evidence.id}' must be materialized in its claim`);
1322
+ if (evidence.contradictsClaimId !== void 0 && claimsById.has(evidence.contradictsClaimId) && !claim.contradicts.includes(evidence.contradictsClaimId)) throw new Error(`registered claim evidence '${evidence.id}' must materialize its contradiction`);
1323
+ }
1324
+ for (const question of ledger.questions) {
1325
+ assertDeepQuestionIntegrity(question);
1326
+ for (const claimId of question.claimIds) if (!claimIds.has(claimId)) throw new Error(`question '${question.id}' references claim '${claimId}' outside its ledger`);
1327
+ }
1328
+ }
1329
+ /** A ledger with nothing in it yet. */
1330
+ function emptyClaimLedger(id, goal) {
1331
+ return {
1332
+ id,
1333
+ ...goal === void 0 ? {} : { goal },
1334
+ updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
1335
+ rounds: 0,
1336
+ claimEvidence: [],
1337
+ registeredSourceUris: [],
1338
+ claims: [],
1339
+ questions: []
1340
+ };
1341
+ }
1342
+ /**
1343
+ * Turn only evidence backed by a confirmed source registration into support.
1344
+ *
1345
+ * This is a monotone closure: it never removes claims or evidence, and running
1346
+ * it twice is a no-op. Keeping it in the ledger algebra means a source-confirming
1347
+ * writer and an evidence-producing writer can arrive in either order.
1348
+ */
1349
+ function materializeRegisteredClaimEvidence(ledger) {
1350
+ const registered = new Set(ledger.registeredSourceUris);
1351
+ const claims = new Map(ledger.claims.map((claim) => [claim.id, claim]));
1352
+ for (const evidence of ledger.claimEvidence) {
1353
+ if (!registered.has(evidence.sourceUri)) continue;
1354
+ const host = claimSourceHost(evidence.sourceUri);
1355
+ const observed = {
1356
+ id: evidence.claimId,
1357
+ text: evidence.text,
1358
+ supportingHosts: host ? [host] : [],
1359
+ supportingUris: [evidence.sourceUri],
1360
+ contradicts: [],
1361
+ contested: false,
1362
+ firstSeenRound: evidence.firstSeenRound
1363
+ };
1364
+ const existing = claims.get(observed.id);
1365
+ claims.set(observed.id, existing ? mergeTrackedClaims(existing, observed) : observed);
1366
+ }
1367
+ for (const evidence of ledger.claimEvidence) {
1368
+ const otherId = evidence.contradictsClaimId;
1369
+ if (!registered.has(evidence.sourceUri) || !otherId || !claims.has(otherId)) continue;
1370
+ const claim = claims.get(evidence.claimId);
1371
+ if (!claim) continue;
1372
+ claims.set(claim.id, {
1373
+ ...claim,
1374
+ contradicts: union(claim.contradicts, [otherId]),
1375
+ contested: true
1376
+ });
1377
+ }
1378
+ return linkClaimContradictions({
1379
+ ...ledger,
1380
+ claims: [...claims.values()].sort((left, right) => left.id.localeCompare(right.id))
1381
+ });
1382
+ }
1383
+ /**
1384
+ * Raised when two ledgers that accumulated evidence for DIFFERENT goals are
1385
+ * combined. Merging them would pool two questions' evidence into one
1386
+ * corroboration count, which reports a claim as independently confirmed when
1387
+ * nobody confirmed it — strictly worse than losing the ledger, so this refuses.
1388
+ */
1389
+ var ClaimLedgerGoalConflictError = class extends Error {
1390
+ ledgerId;
1391
+ existingGoal;
1392
+ incomingGoal;
1393
+ constructor(ledgerId, existingGoal, incomingGoal) {
1394
+ super(`claim ledger '${ledgerId}' accumulated evidence for goal '${existingGoal}' and cannot be merged with evidence for '${incomingGoal}'`);
1395
+ this.ledgerId = ledgerId;
1396
+ this.existingGoal = existingGoal;
1397
+ this.incomingGoal = incomingGoal;
1398
+ this.name = "ClaimLedgerGoalConflictError";
1399
+ }
1400
+ };
1401
+ /**
1402
+ * Combine two records of the same claim.
1403
+ *
1404
+ * Union on every support collection, because a claim asserted by hosts {a} in
1405
+ * one writer and {b} in another is asserted by two independent hosts and the
1406
+ * whole completion oracle turns on that count. `contested` is OR — one writer
1407
+ * seeing a contradiction is enough for the claim to be contested, and no later
1408
+ * writer that simply did not see it may clear the flag.
1409
+ */
1410
+ function mergeTrackedClaims(base, incoming) {
1411
+ assertTrackedClaimIntegrity(base);
1412
+ assertTrackedClaimIntegrity(incoming);
1413
+ if (base.id !== incoming.id) throw new Error(`cannot merge claim '${base.id}' with a different claim '${incoming.id}'`);
1414
+ const text = incoming.firstSeenRound < base.firstSeenRound ? incoming.text : incoming.firstSeenRound > base.firstSeenRound ? base.text : incoming.text < base.text ? incoming.text : base.text;
1415
+ return {
1416
+ id: base.id,
1417
+ text,
1418
+ supportingHosts: union(base.supportingHosts, incoming.supportingHosts),
1419
+ supportingUris: union(base.supportingUris, incoming.supportingUris),
1420
+ contradicts: union(base.contradicts, incoming.contradicts),
1421
+ contested: base.contested || incoming.contested,
1422
+ firstSeenRound: Math.min(base.firstSeenRound, incoming.firstSeenRound)
1423
+ };
1424
+ }
1425
+ /**
1426
+ * Combine two ledgers for the same run.
1427
+ *
1428
+ * `addressed` on a question is OR for the same reason `contested` is: a writer
1429
+ * that answered a question has answered it, and a writer that never saw the
1430
+ * answer must not reopen it. Everything else is a union or an extreme, so this
1431
+ * is associative and idempotent — merge order cannot change the result and a
1432
+ * replayed merge is a no-op.
1433
+ */
1434
+ function mergeClaimLedgers(base, incoming) {
1435
+ assertResearchClaimLedgerIntegrity(base);
1436
+ assertResearchClaimLedgerIntegrity(incoming);
1437
+ if (base.id !== incoming.id) throw new Error(`cannot merge claim ledger '${base.id}' with a different ledger '${incoming.id}'`);
1438
+ if (base.goal !== void 0 && incoming.goal !== void 0 && base.goal !== incoming.goal) throw new ClaimLedgerGoalConflictError(base.id, base.goal, incoming.goal);
1439
+ const goal = base.goal ?? incoming.goal;
1440
+ const claimEvidence = new Map(base.claimEvidence.map((evidence) => [evidence.id, evidence]));
1441
+ for (const evidence of incoming.claimEvidence) {
1442
+ const existing = claimEvidence.get(evidence.id);
1443
+ claimEvidence.set(evidence.id, existing ? mergeClaimEvidence(existing, evidence) : evidence);
1444
+ }
1445
+ const claims = new Map(base.claims.map((claim) => [claim.id, claim]));
1446
+ for (const claim of incoming.claims) {
1447
+ const existing = claims.get(claim.id);
1448
+ claims.set(claim.id, existing ? mergeTrackedClaims(existing, claim) : claim);
1449
+ }
1450
+ const questions = new Map(base.questions.map((question) => [question.id, question]));
1451
+ for (const question of incoming.questions) {
1452
+ const existing = questions.get(question.id);
1453
+ if (existing && (existing.kind !== question.kind || existing.text !== question.text)) throw new Error(`question '${question.id}' has conflicting immutable content`);
1454
+ questions.set(question.id, existing ? {
1455
+ ...existing,
1456
+ claimIds: union(existing.claimIds, question.claimIds),
1457
+ addressed: existing.addressed || question.addressed,
1458
+ raisedRound: Math.min(existing.raisedRound, question.raisedRound)
1459
+ } : question);
1460
+ }
1461
+ const rounds = Math.max(base.rounds, incoming.rounds);
1462
+ const preparedRounds = Math.max(base.preparedRounds ?? base.rounds, incoming.preparedRounds ?? incoming.rounds);
1463
+ return materializeRegisteredClaimEvidence({
1464
+ id: base.id,
1465
+ ...goal === void 0 ? {} : { goal },
1466
+ updatedAt: incoming.updatedAt.localeCompare(base.updatedAt) > 0 ? incoming.updatedAt : base.updatedAt,
1467
+ rounds,
1468
+ ...preparedRounds > rounds ? { preparedRounds } : {},
1469
+ claimEvidence: [...claimEvidence.values()].sort((left, right) => left.id.localeCompare(right.id)),
1470
+ registeredSourceUris: union(base.registeredSourceUris, incoming.registeredSourceUris),
1471
+ claims: [...claims.values()].sort((a, b) => a.id.localeCompare(b.id)),
1472
+ questions: [...questions.values()].sort((a, b) => a.id.localeCompare(b.id))
1473
+ });
1474
+ }
1475
+ function mergeClaimEvidence(base, incoming) {
1476
+ assertResearchClaimEvidenceIntegrity(base);
1477
+ assertResearchClaimEvidenceIntegrity(incoming);
1478
+ if (base.id !== incoming.id || base.claimId !== incoming.claimId || base.sourceUri !== incoming.sourceUri || base.contradictsClaimId !== incoming.contradictsClaimId) throw new Error(`claim evidence '${base.id}' has conflicting immutable content`);
1479
+ const text = incoming.firstSeenRound < base.firstSeenRound ? incoming.text : incoming.firstSeenRound > base.firstSeenRound ? base.text : incoming.text < base.text ? incoming.text : base.text;
1480
+ return {
1481
+ ...base,
1482
+ text,
1483
+ firstSeenRound: Math.min(base.firstSeenRound, incoming.firstSeenRound)
1484
+ };
1485
+ }
1486
+ /**
1487
+ * Make every contradiction edge symmetric and mark both ends contested.
1488
+ *
1489
+ * A contradiction is a property of a PAIR, and a writer only ever sees one side
1490
+ * of it: the worker that found the refuting source records "X contradicts Y" and
1491
+ * knows nothing about Y's record. Left one-sided, Y reads as an uncontested
1492
+ * claim, and the completion oracle would settle a question two sources disagree
1493
+ * about. `createResearchDrivingDriver` does this pairwise as it records; this is
1494
+ * the same rule stated over a whole ledger, for writers that assemble one from
1495
+ * events rather than from a live loop.
1496
+ *
1497
+ * Idempotent and monotone like every other rule here: edges only appear and
1498
+ * `contested` only latches on, so applying it twice changes nothing. An edge
1499
+ * pointing at a claim this ledger does not hold is KEPT — the other side may
1500
+ * arrive from another writer later, and discarding evidence of disagreement
1501
+ * because the counterpart has not shown up yet is the failure this prevents.
1502
+ */
1503
+ function linkClaimContradictions(ledger) {
1504
+ const inbound = /* @__PURE__ */ new Map();
1505
+ for (const claim of ledger.claims) for (const other of claim.contradicts) {
1506
+ if (other === claim.id) continue;
1507
+ const edges = inbound.get(other);
1508
+ if (edges) edges.push(claim.id);
1509
+ else inbound.set(other, [claim.id]);
1510
+ }
1511
+ return {
1512
+ ...ledger,
1513
+ claims: ledger.claims.map((claim) => {
1514
+ const contradicts = union(claim.contradicts.filter((other) => other !== claim.id), inbound.get(claim.id) ?? []);
1515
+ return {
1516
+ ...claim,
1517
+ contradicts,
1518
+ contested: claim.contested || contradicts.length > 0
1519
+ };
1520
+ }).sort((a, b) => a.id.localeCompare(b.id))
1521
+ };
1522
+ }
1523
+ /**
1524
+ * Set union, SORTED.
1525
+ *
1526
+ * Sorted because these collections are sets and merging must be commutative:
1527
+ * arrival order is not part of what the ledger says, so two writers arriving in
1528
+ * either order have to produce identical bytes. Preserving first-seen order
1529
+ * instead made `merge(a, b)` and `merge(b, a)` differ, which the order-
1530
+ * independence test caught — and a non-commutative merge under a filesystem
1531
+ * lock means the record depends on scheduling.
1532
+ */
1533
+ function union(base, incoming) {
1534
+ return [.../* @__PURE__ */ new Set([...base, ...incoming])].sort();
1535
+ }
1536
+ function assertSortedUnique(label, values) {
1537
+ for (let index = 1; index < values.length; index += 1) if ((values[index - 1] ?? "") >= (values[index] ?? "")) throw new Error(`${label} must be sorted and contain no duplicates`);
1538
+ }
1539
+ function sameStrings(left, right) {
1540
+ return left.length === right.length && left.every((value, index) => value === right[index]);
1541
+ }
1542
+ //#endregion
1543
+ //#region src/types.ts
1544
+ /**
1545
+ * The event vocabulary, as a value so the runtime schema is DERIVED from it
1546
+ * rather than restated. A restated copy in `schemas.ts` drifted: it omitted
1547
+ * `research.iteration`, which is the only event `runVerifiedResearchLoop`
1548
+ * produces, so every attempt to store one would have been rejected. Nothing
1549
+ * caught it because nothing ever stored an event. Add a type here and the
1550
+ * schema accepts it in the same edit.
1551
+ */
1552
+ const KNOWLEDGE_EVENT_TYPES = [
1553
+ "source.added",
1554
+ "proposal.applied",
1555
+ "index.built",
1556
+ "lint.run",
1557
+ "research.iteration",
1558
+ "optimization.run",
1559
+ "release.promoted",
1560
+ "release.rejected"
1561
+ ];
1562
+ //#endregion
1211
1563
  //#region src/schemas.ts
1212
1564
  const SourceAnchorSchema = z.object({
1213
1565
  id: z.string().min(1),
@@ -1271,20 +1623,71 @@ const KnowledgeIndexSchema = z.object({
1271
1623
  });
1272
1624
  const KnowledgeEventSchema = z.object({
1273
1625
  id: z.string().min(1),
1274
- type: z.enum([
1275
- "source.added",
1276
- "proposal.applied",
1277
- "index.built",
1278
- "lint.run",
1279
- "optimization.run",
1280
- "release.promoted",
1281
- "release.rejected"
1282
- ]),
1626
+ type: z.enum(KNOWLEDGE_EVENT_TYPES),
1283
1627
  createdAt: z.string().min(1),
1284
1628
  actor: z.string().optional(),
1285
1629
  target: z.string().optional(),
1286
1630
  metadata: z.record(z.string(), z.unknown()).optional()
1287
1631
  });
1632
+ const DeepQuestionSchema = z.object({
1633
+ kind: z.enum([
1634
+ "comparative",
1635
+ "mechanism",
1636
+ "gap",
1637
+ "contradiction"
1638
+ ]),
1639
+ text: z.string().min(1),
1640
+ id: z.string().min(1),
1641
+ claimIds: z.array(z.string().min(1)),
1642
+ addressed: z.boolean(),
1643
+ raisedRound: z.number().int().nonnegative()
1644
+ }).strict().superRefine((question, context) => {
1645
+ reportIntegrityError(context, () => assertDeepQuestionIntegrity(question));
1646
+ });
1647
+ const ResearchClaimRecordSchema = z.object({
1648
+ id: z.string().min(1),
1649
+ text: z.string().min(1),
1650
+ supportingHosts: z.array(z.string().min(1)),
1651
+ supportingUris: z.array(z.string().min(1)),
1652
+ contradicts: z.array(z.string().min(1)),
1653
+ contested: z.boolean(),
1654
+ firstSeenRound: z.number().int().nonnegative()
1655
+ }).strict().superRefine((claim, context) => {
1656
+ reportIntegrityError(context, () => assertTrackedClaimIntegrity(claim));
1657
+ });
1658
+ const ResearchClaimEvidenceSchema = z.object({
1659
+ id: z.string().min(1),
1660
+ claimId: z.string().min(1),
1661
+ text: z.string().min(1),
1662
+ sourceUri: z.string().min(1),
1663
+ contradictsClaimId: z.string().min(1).optional(),
1664
+ firstSeenRound: z.number().int().nonnegative()
1665
+ }).strict().superRefine((evidence, context) => {
1666
+ reportIntegrityError(context, () => assertResearchClaimEvidenceIntegrity(evidence));
1667
+ });
1668
+ const ResearchClaimLedgerSchema = z.object({
1669
+ id: z.string().min(1),
1670
+ goal: z.string().trim().min(1).optional(),
1671
+ updatedAt: z.iso.datetime(),
1672
+ rounds: z.number().int().nonnegative(),
1673
+ preparedRounds: z.number().int().nonnegative().optional(),
1674
+ claimEvidence: z.array(ResearchClaimEvidenceSchema),
1675
+ registeredSourceUris: z.array(z.string().min(1)),
1676
+ claims: z.array(ResearchClaimRecordSchema),
1677
+ questions: z.array(DeepQuestionSchema)
1678
+ }).strict().superRefine((ledger, context) => {
1679
+ reportIntegrityError(context, () => assertResearchClaimLedgerIntegrity(ledger));
1680
+ });
1681
+ function reportIntegrityError(context, check) {
1682
+ try {
1683
+ check();
1684
+ } catch (error) {
1685
+ context.addIssue({
1686
+ code: "custom",
1687
+ message: error instanceof Error ? error.message : String(error)
1688
+ });
1689
+ }
1690
+ }
1288
1691
  const KnowledgeBaseCandidateSchema = z.object({
1289
1692
  id: z.string().min(1),
1290
1693
  units: z.array(z.object({
@@ -1327,6 +1730,263 @@ const KnowledgeBaseCandidateSchema = z.object({
1327
1730
  metadata: z.record(z.string(), z.unknown()).optional()
1328
1731
  });
1329
1732
  //#endregion
1733
+ //#region src/kb-store.ts
1734
+ /**
1735
+ * Where a filesystem knowledge base keeps its machine-written records, relative
1736
+ * to the knowledge-base root.
1737
+ *
1738
+ * This is the ONE location. `FileSystemKbStore` used to write `<dir>/index.json`
1739
+ * while `writeKnowledgeIndex` wrote `<root>/.agent-knowledge/index.json` — two
1740
+ * index writers, two files, and only the second one reachable, so a store that
1741
+ * had just been written to reported an empty knowledge base. Both now go through
1742
+ * `FileSystemKbStore`, anchored on the knowledge-base root, which is also the
1743
+ * directory `withKnowledgeMutation` locks: one file, one lock domain.
1744
+ */
1745
+ const KB_STORE_DIR = ".agent-knowledge";
1746
+ const KB_INDEX_PATH = `${KB_STORE_DIR}/index.json`;
1747
+ const KB_EVENTS_PATH = `${KB_STORE_DIR}/events.json`;
1748
+ const KB_CLAIM_LEDGER_DIR = `${KB_STORE_DIR}/claim-ledgers`;
1749
+ /**
1750
+ * A claim-ledger id is used as a filename, so it is restricted to one safe path
1751
+ * segment. Rejecting rather than sanitising is deliberate: a sanitised id maps
1752
+ * two different runs onto one file and silently merges their belief state.
1753
+ */
1754
+ const LEDGER_ID_PATTERN = /^[A-Za-z0-9_-][A-Za-z0-9._-]*$/;
1755
+ function assertClaimLedgerId(id) {
1756
+ if (!LEDGER_ID_PATTERN.test(id) || id === "." || id === "..") throw new Error(`claim ledger id must match ${LEDGER_ID_PATTERN} and cannot be a path segment: ${id}`);
1757
+ return id;
1758
+ }
1759
+ var MemoryKbStore = class {
1760
+ sources = /* @__PURE__ */ new Map();
1761
+ pages = /* @__PURE__ */ new Map();
1762
+ events = [];
1763
+ claimLedgers = /* @__PURE__ */ new Map();
1764
+ index = null;
1765
+ async putSource(source) {
1766
+ this.sources.set(source.id, clone(source));
1767
+ }
1768
+ async getSource(id) {
1769
+ return clone(this.sources.get(id) ?? null);
1770
+ }
1771
+ async listSources() {
1772
+ return [...this.sources.values()].map(clone);
1773
+ }
1774
+ async putPage(page) {
1775
+ this.pages.set(page.id, clone(page));
1776
+ }
1777
+ async getPage(idOrPath) {
1778
+ return clone(this.pages.get(idOrPath) ?? [...this.pages.values()].find((page) => page.path === idOrPath) ?? null);
1779
+ }
1780
+ async listPages() {
1781
+ return [...this.pages.values()].map(clone);
1782
+ }
1783
+ async putIndex(index) {
1784
+ this.index = clone(index);
1785
+ }
1786
+ async getIndex() {
1787
+ if (this.index) return clone(this.index);
1788
+ const pages = await this.listPages();
1789
+ const sources = await this.listSources();
1790
+ return {
1791
+ root: "memory",
1792
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1793
+ sources,
1794
+ pages,
1795
+ graph: buildKnowledgeGraph(pages)
1796
+ };
1797
+ }
1798
+ async putEvent(event) {
1799
+ this.events.push(clone(event));
1800
+ }
1801
+ async listEvents(query = {}) {
1802
+ let out = this.events;
1803
+ if (query.type) out = out.filter((event) => event.type === query.type);
1804
+ if (query.target) out = out.filter((event) => event.target === query.target);
1805
+ out = [...out].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
1806
+ return out.slice(-(query.limit ?? out.length)).map(clone);
1807
+ }
1808
+ async putClaimLedger(ledger) {
1809
+ const parsed = ResearchClaimLedgerSchema.parse(ledger);
1810
+ this.claimLedgers.set(assertClaimLedgerId(parsed.id), clone(parsed));
1811
+ }
1812
+ async getClaimLedger(id) {
1813
+ return clone(this.claimLedgers.get(assertClaimLedgerId(id)) ?? null);
1814
+ }
1815
+ async listClaimLedgers() {
1816
+ return [...this.claimLedgers.values()].map(clone).sort((a, b) => a.id.localeCompare(b.id));
1817
+ }
1818
+ async mergeClaimLedger(id, merge) {
1819
+ const key = assertClaimLedgerId(id);
1820
+ const next = assertMergedLedgerId(key, merge(clone(this.claimLedgers.get(key) ?? null)));
1821
+ await this.putClaimLedger(next);
1822
+ return clone(next);
1823
+ }
1824
+ };
1825
+ const knowledgeEventsSchema = z.array(KnowledgeEventSchema);
1826
+ var FileSystemKbStore = class {
1827
+ root;
1828
+ indexPath;
1829
+ eventsPath;
1830
+ claimLedgerDir;
1831
+ /**
1832
+ * A string retains the published direct-directory contract.
1833
+ * The object form explicitly selects a knowledge-base root and the canonical
1834
+ * `.agent-knowledge/` layout. A string naming that exact canonical directory
1835
+ * keeps its file paths but shares the root form's lock domain.
1836
+ */
1837
+ constructor(input) {
1838
+ const directDirectory = typeof input === "string" ? resolve(input) : void 0;
1839
+ const aliasesCanonicalDirectory = directDirectory !== void 0 && basename(directDirectory) === ".agent-knowledge";
1840
+ this.root = aliasesCanonicalDirectory ? dirname(directDirectory) : typeof input === "string" ? input : input.root;
1841
+ const canonicalLayout = typeof input !== "string" || aliasesCanonicalDirectory;
1842
+ this.indexPath = canonicalLayout ? KB_INDEX_PATH : "index.json";
1843
+ this.eventsPath = canonicalLayout ? KB_EVENTS_PATH : "events.json";
1844
+ this.claimLedgerDir = canonicalLayout ? KB_CLAIM_LEDGER_DIR : "claim-ledgers";
1845
+ }
1846
+ async putSource(source) {
1847
+ const parsed = SourceRecordSchema.parse(source);
1848
+ await this.updateIndex((index) => ({
1849
+ ...index,
1850
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1851
+ sources: [parsed, ...index.sources.filter((entry) => entry.id !== parsed.id)]
1852
+ }));
1853
+ }
1854
+ async getSource(id) {
1855
+ return withKnowledgeRead(this.root, async () => {
1856
+ return clone((await this.readIndex())?.sources.find((source) => source.id === id) ?? null);
1857
+ });
1858
+ }
1859
+ async listSources() {
1860
+ return withKnowledgeRead(this.root, async () => clone((await this.readIndex())?.sources ?? []));
1861
+ }
1862
+ async putPage(page) {
1863
+ const parsed = KnowledgePageSchema.parse(page);
1864
+ await this.updateIndex((index) => {
1865
+ const pages = [parsed, ...index.pages.filter((entry) => entry.id !== parsed.id)];
1866
+ return {
1867
+ ...index,
1868
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1869
+ pages,
1870
+ graph: buildKnowledgeGraph(pages)
1871
+ };
1872
+ });
1873
+ }
1874
+ async getPage(idOrPath) {
1875
+ return withKnowledgeRead(this.root, async () => {
1876
+ return clone((await this.readIndex())?.pages.find((page) => page.id === idOrPath || page.path === idOrPath) ?? null);
1877
+ });
1878
+ }
1879
+ async listPages() {
1880
+ return withKnowledgeRead(this.root, async () => clone((await this.readIndex())?.pages ?? []));
1881
+ }
1882
+ async putIndex(index) {
1883
+ const parsed = KnowledgeIndexSchema.parse(index);
1884
+ await withKnowledgeMutation(this.root, () => writeJsonDurableWithinRoot(this.root, this.indexPath, parsed));
1885
+ }
1886
+ async getIndex() {
1887
+ return withKnowledgeRead(this.root, () => this.readIndex());
1888
+ }
1889
+ async putEvent(event) {
1890
+ const parsed = KnowledgeEventSchema.parse(event);
1891
+ await withKnowledgeMutation(this.root, async () => {
1892
+ const next = [...(await this.readEvents()).filter((entry) => entry.id !== parsed.id), parsed].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
1893
+ await writeJsonDurableWithinRoot(this.root, this.eventsPath, next);
1894
+ });
1895
+ }
1896
+ async listEvents(query = {}) {
1897
+ return withKnowledgeRead(this.root, async () => {
1898
+ let events = await this.readEvents();
1899
+ if (query.type) events = events.filter((event) => event.type === query.type);
1900
+ if (query.target) events = events.filter((event) => event.target === query.target);
1901
+ return clone(events.slice(-(query.limit ?? events.length)));
1902
+ });
1903
+ }
1904
+ async putClaimLedger(ledger) {
1905
+ const parsed = ResearchClaimLedgerSchema.parse(ledger);
1906
+ const path = this.claimLedgerPath(parsed.id);
1907
+ await withKnowledgeMutation(this.root, () => writeJsonDurableWithinRoot(this.root, path, parsed));
1908
+ }
1909
+ async getClaimLedger(id) {
1910
+ const path = this.claimLedgerPath(id);
1911
+ return withKnowledgeRead(this.root, () => readJsonFile(this.root, path, ResearchClaimLedgerSchema));
1912
+ }
1913
+ async listClaimLedgers() {
1914
+ return withKnowledgeRead(this.root, async () => {
1915
+ let files;
1916
+ try {
1917
+ files = await listRegularFilesWithinRoot(this.root, this.claimLedgerDir);
1918
+ } catch (error) {
1919
+ if (isMissingFile(error)) return [];
1920
+ throw error;
1921
+ }
1922
+ const ledgers = [];
1923
+ for (const file of files) {
1924
+ if (!file.path.endsWith(".json")) continue;
1925
+ ledgers.push(ResearchClaimLedgerSchema.parse(JSON.parse(file.bytes.toString("utf8"))));
1926
+ }
1927
+ return ledgers.sort((a, b) => a.id.localeCompare(b.id));
1928
+ });
1929
+ }
1930
+ async mergeClaimLedger(id, merge) {
1931
+ const key = assertClaimLedgerId(id);
1932
+ return withKnowledgeMutation(this.root, async () => {
1933
+ const current = await readJsonFile(this.root, this.claimLedgerPath(key), ResearchClaimLedgerSchema);
1934
+ const next = assertMergedLedgerId(key, merge(current));
1935
+ await this.putClaimLedger(next);
1936
+ return next;
1937
+ });
1938
+ }
1939
+ async updateIndex(change) {
1940
+ await withKnowledgeMutation(this.root, async () => {
1941
+ const current = await this.readIndex() ?? emptyIndex(this.root);
1942
+ const next = KnowledgeIndexSchema.parse(change(current));
1943
+ await writeJsonDurableWithinRoot(this.root, this.indexPath, next);
1944
+ });
1945
+ }
1946
+ async readIndex() {
1947
+ return readJsonFile(this.root, this.indexPath, KnowledgeIndexSchema);
1948
+ }
1949
+ async readEvents() {
1950
+ return await readJsonFile(this.root, this.eventsPath, knowledgeEventsSchema) ?? [];
1951
+ }
1952
+ claimLedgerPath(id) {
1953
+ return `${this.claimLedgerDir}/${assertClaimLedgerId(id)}.json`;
1954
+ }
1955
+ };
1956
+ /**
1957
+ * A merge that returns a ledger under a different id would write that ledger to
1958
+ * the file the caller asked to merge, giving one file two identities. Refuse
1959
+ * rather than trust the merge function to be well behaved.
1960
+ */
1961
+ function assertMergedLedgerId(id, merged) {
1962
+ if (merged.id !== id) throw new Error(`merge of claim ledger '${id}' returned a ledger with id '${merged.id}'`);
1963
+ return merged;
1964
+ }
1965
+ function emptyIndex(root) {
1966
+ return {
1967
+ root,
1968
+ generatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
1969
+ sources: [],
1970
+ pages: [],
1971
+ graph: {
1972
+ nodes: [],
1973
+ edges: []
1974
+ }
1975
+ };
1976
+ }
1977
+ async function readJsonFile(root, relativePath, schema) {
1978
+ try {
1979
+ const file = await readRegularFileWithinRoot(root, relativePath);
1980
+ return schema.parse(JSON.parse(file.bytes.toString("utf8")));
1981
+ } catch (error) {
1982
+ if (error?.code === "ENOENT") return null;
1983
+ throw error;
1984
+ }
1985
+ }
1986
+ function clone(value) {
1987
+ return value == null ? value : JSON.parse(JSON.stringify(value));
1988
+ }
1989
+ //#endregion
1330
1990
  //#region src/sources.ts
1331
1991
  const sourceRegistrySchema = z.object({
1332
1992
  generatedAt: z.string().min(1),
@@ -1552,10 +2212,19 @@ async function buildKnowledgeIndexUnlocked(root) {
1552
2212
  graph: buildKnowledgeGraph(pages)
1553
2213
  };
1554
2214
  }
2215
+ /**
2216
+ * Build the index from the knowledge tree and store it.
2217
+ *
2218
+ * The write goes through `FileSystemKbStore` rather than straight to disk: this
2219
+ * function and the store used to write two different index files in two
2220
+ * different places, so a knowledge base could hold two disagreeing indexes and
2221
+ * a store-based reader saw none of the indexer's work. One writer now, and it
2222
+ * validates through `KnowledgeIndexSchema` on the way out.
2223
+ */
1555
2224
  async function writeKnowledgeIndex(root) {
1556
2225
  return withKnowledgeMutation(root, async () => {
1557
2226
  const index = await buildKnowledgeIndexUnlocked(root);
1558
- await writeJsonDurableWithinRoot(root, ".agent-knowledge/index.json", index);
2227
+ await new FileSystemKbStore({ root }).putIndex(index);
1559
2228
  return index;
1560
2229
  });
1561
2230
  }
@@ -1865,6 +2534,6 @@ function explainKnowledgeTarget(index, target) {
1865
2534
  };
1866
2535
  }
1867
2536
  //#endregion
1868
- export { writeFileDurableWithinRoot as $, loadKnowledgePages as A, withKnowledgeMutation as B, SourceAnchorSchema as C, initKnowledgeBase as D, SCAFFOLD_PAGE_BASENAMES as E, formatFrontmatter as F, knowledgeFileTransactionPlanHash as G, applyKnowledgeFileTransaction as H, parseFrontmatter as I, isMissingFile as J, prepareKnowledgeFileTransaction as K, acquireDurableFileLock as L, WIKILINK_REGEX as M, extractWikilinks as N, isScaffoldPath as O, normalizeLinkTarget as P, withSafeDirectory as Q, inspectPendingKnowledgeMutation as R, KnowledgePageSchema as S, buildKnowledgeGraph as T, assertKnowledgeMutationPath as U, withKnowledgeRead as V, finishKnowledgeFileTransaction as W, readRegularFileWithinRoot as X, listRegularFilesWithinRoot as Y, renameDurable as Z, KnowledgeBaseCandidateSchema as _, applyKnowledgeWriteBlocksFile as a, KnowledgeGraphNodeSchema as b, validateKnowledgeIndex as c, writeKnowledgeIndex as d, writeJsonDurableWithinRoot as et, addSourcePath as f, writeSourceRegistry as g, sourceRegistryPath as h, applyKnowledgeWriteBlocks as i, writeJson as j, layoutFor as k, lintKnowledgeIndex as l, loadSourceRegistry as m, inspectKnowledgeIndex as n, textSourceAdapter as nt, isSafeKnowledgePath as o, addSourceText as p, rollbackKnowledgeFileTransaction as q, stringMetadata as r, parseKnowledgeWriteBlocks as s, explainKnowledgeTarget as t, mediaTypeFor$1 as tt, buildKnowledgeIndex as u, KnowledgeEventSchema as v, SourceRecordSchema as w, KnowledgeIndexSchema as x, KnowledgeGraphEdgeSchema as y, recoverPendingKnowledgeMutation as z };
2537
+ export { SCAFFOLD_PAGE_BASENAMES as $, KnowledgePageSchema as A, withSafeDirectory as At, assertResearchClaimLedgerIntegrity as B, assertClaimLedgerId as C, listRegularFilesWithinRoot as Ct, KnowledgeGraphEdgeSchema as D, renameDurable as Dt, KnowledgeEventSchema as E, removeDurable as Et, SourceRecordSchema as F, mediaTypeFor$1 as Ft, deepQuestionId as G, claimEvidenceId as H, KNOWLEDGE_EVENT_TYPES as I, textSourceAdapter as It, materializeRegisteredClaimEvidence as J, emptyClaimLedger as K, ClaimLedgerGoalConflictError as L, ResearchClaimLedgerSchema as M, writeFileDurableWithinRoot as Mt, ResearchClaimRecordSchema as N, writeJsonDurable as Nt, KnowledgeGraphNodeSchema as O, syncDirectory as Ot, SourceAnchorSchema as P, writeJsonDurableWithinRoot as Pt, buildKnowledgeGraph as Q, assertDeepQuestionIntegrity as R, MemoryKbStore as S, isMissingFile as St, KnowledgeBaseCandidateSchema as T, readRegularFileWithinRoot as Tt, claimId as U, assertTrackedClaimIntegrity as V, claimSourceHost as W, mergeTrackedClaims as X, mergeClaimLedgers as Y, normalizeClaimText as Z, FileSystemKbStore as _, finishKnowledgeFileTransaction as _t, applyKnowledgeWriteBlocksFile as a, WIKILINK_REGEX as at, KB_INDEX_PATH as b, rollbackKnowledgeFileTransaction as bt, validateKnowledgeIndex as c, formatFrontmatter as ct, writeKnowledgeIndex as d, inspectPendingKnowledgeMutation as dt, initKnowledgeBase as et, addSourcePath as f, recoverPendingKnowledgeMutation as ft, writeSourceRegistry as g, assertKnowledgeMutationPath as gt, sourceRegistryPath as h, applyKnowledgeFileTransaction as ht, applyKnowledgeWriteBlocks as i, writeJson as it, ResearchClaimEvidenceSchema as j, writeFileDurable as jt, KnowledgeIndexSchema as k, withSafeDescendant as kt, lintKnowledgeIndex as l, parseFrontmatter as lt, loadSourceRegistry as m, withKnowledgeRead as mt, inspectKnowledgeIndex as n, layoutFor as nt, isSafeKnowledgePath as o, extractWikilinks as ot, addSourceText as p, withKnowledgeMutation as pt, linkClaimContradictions as q, stringMetadata as r, loadKnowledgePages as rt, parseKnowledgeWriteBlocks as s, normalizeLinkTarget as st, explainKnowledgeTarget as t, isScaffoldPath as tt, buildKnowledgeIndex as u, acquireDurableFileLock as ut, KB_CLAIM_LEDGER_DIR as v, knowledgeFileTransactionPlanHash as vt, DeepQuestionSchema as w, readRegularFileNoFollow as wt, KB_STORE_DIR as x, isKernelAnchoredPath as xt, KB_EVENTS_PATH as y, prepareKnowledgeFileTransaction as yt, assertResearchClaimEvidenceIntegrity as z };
1869
2538
 
1870
- //# sourceMappingURL=inspect-DAXpFyrs.js.map
2539
+ //# sourceMappingURL=inspect-LUMkas6A.js.map