@saasontools/strauss-kb 0.1.15 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -408,8 +408,8 @@ Worked flows:
408
408
 
409
409
  **Placement is cache economics.** `load`'s output belongs in the stable
410
410
  prefix — system prompt or first turn; `query` and `pack` results belong at
411
- the tail. `digest` is the base's content stamp: a change-notification hook
412
- and `kb_stamp` (SAA-719) compare it to detect change, not the model. A
411
+ the tail. `digest` is the base's content stamp: `kb_stamp` and the opt-in
412
+ reload hook compare it to detect change, not the model. A
413
413
  prompt cache matches a byte-for-byte prefix, so one volatile result ahead of
414
414
  a stable load prices the base at full rate thereafter. Mechanism and digest
415
415
  caveats: <https://saasontools.github.io/strauss-agent-tools/mcp-reference>.
@@ -451,6 +451,11 @@ carry no numbers:
451
451
  }
452
452
  ```
453
453
 
454
+ A pinned base can also change mid-session — a `git pull`, a sub-agent's write.
455
+ `strauss-kb stamp` reports each pinned base's content digest. Comparing it for
456
+ you is opt-in, see the
457
+ [plugin README](../../plugins/strauss-kb/README.md#opt-in-workspace-hooks).
458
+
454
459
  `sync-instructions <file>` keeps that block between
455
460
  `<!-- strauss-kb:begin/end -->` sentinels in AGENTS.md or CLAUDE.md; it is
456
461
  idempotent, and covers runtimes without a reliable post-compaction hook.
@@ -458,15 +463,16 @@ idempotent, and covers runtimes without a reliable post-compaction hook.
458
463
  What each runtime gets (configs in the
459
464
  [plugin's adapters](../../plugins/strauss-kb/adapters/)):
460
465
 
461
- | Layer | Claude Code | Codex CLI | Antigravity CLI |
462
- | ------------------------- | ------------------ | ------------------------------------------- | -------------------------- |
463
- | MCP tool descriptions | ✓ | ✓ | ✓ |
464
- | Session-start injection | SessionStart hook | SessionStart hook | PreInvocation, per turn |
465
- | Post-compact re-injection | ✓ `compact` source | ✓ client-side; instruction-only when hosted | moot — injected every turn |
466
- | File-read blocking | opt-in PreToolUse | (shell is the side door) | opt-in PreToolUse, JSON |
467
- | Manual-edit validation | opt-in PostToolUse | ✗ | |
468
- | Generated-file edit guard | opt-in PreToolUse | ✗ | ✗ |
469
- | Instruction file | CLAUDE.md | AGENTS.md | AGENTS.md + rules/ |
466
+ | Layer | Claude Code | Codex CLI | Antigravity CLI |
467
+ | ------------------------- | -------------------------------- | ------------------------------------------- | -------------------------- |
468
+ | MCP tool descriptions | ✓ | ✓ | ✓ |
469
+ | Session-start injection | SessionStart hook | SessionStart hook | PreInvocation, per turn |
470
+ | Post-compact re-injection | ✓ `compact` source | ✓ client-side; instruction-only when hosted | moot — injected every turn |
471
+ | Reload after a pull | opt-in PostToolUse, SubagentStop | opt-in PostToolUse on `shell` | moot injected every turn |
472
+ | File-read blocking | opt-in PreToolUse | ✗ (shell is the side door) | opt-in PreToolUse, JSON |
473
+ | Manual-edit validation | opt-in PostToolUse | ✗ | ✗ |
474
+ | Generated-file edit guard | opt-in PreToolUse | | ✗ |
475
+ | Instruction file | CLAUDE.md | AGENTS.md | AGENTS.md + rules/ |
470
476
 
471
477
  Never read record files directly — read through the tools; a raw read bypasses
472
478
  standing, and a superseded record reads exactly like a current one. Enforce it
@@ -1273,6 +1273,8 @@ var ErrorTypes = /* @__PURE__ */ ((ErrorTypes2) => {
1273
1273
  ErrorTypes2["KbPackBudgetExceeded"] = "KbPackBudgetExceeded";
1274
1274
  ErrorTypes2["KbRecordNotFound"] = "KbRecordNotFound";
1275
1275
  ErrorTypes2["KbSelfVerification"] = "KbSelfVerification";
1276
+ ErrorTypes2["KbStampBaselineUnreadable"] = "KbStampBaselineUnreadable";
1277
+ ErrorTypes2["KbStampDigestBaselineAmbiguous"] = "KbStampDigestBaselineAmbiguous";
1276
1278
  ErrorTypes2["KbUnknownLinkRel"] = "KbUnknownLinkRel";
1277
1279
  ErrorTypes2["KbWriteConflict"] = "KbWriteConflict";
1278
1280
  return ErrorTypes2;
@@ -1427,6 +1429,36 @@ var KbInvalidConceptIdError = class extends BaseError {
1427
1429
  });
1428
1430
  }
1429
1431
  };
1432
+ var KbStampBaselineError = class extends BaseError {
1433
+ constructor(since) {
1434
+ super({
1435
+ message: `kb: --since ${since} is neither a 64-character digest nor a readable stamp file`,
1436
+ errorType: "KbStampBaselineUnreadable" /* KbStampBaselineUnreadable */,
1437
+ code: 400,
1438
+ fault: "User" /* User */,
1439
+ retriable: false,
1440
+ reportToUser: true,
1441
+ details: { since }
1442
+ });
1443
+ this.since = since;
1444
+ }
1445
+ since;
1446
+ };
1447
+ var KbStampDigestBaselineError = class extends BaseError {
1448
+ constructor(since) {
1449
+ super({
1450
+ message: `kb: --since ${since} is a digest, which needs --bundle (one base) \u2014 a file baseline works for many`,
1451
+ errorType: "KbStampDigestBaselineAmbiguous" /* KbStampDigestBaselineAmbiguous */,
1452
+ code: 400,
1453
+ fault: "User" /* User */,
1454
+ retriable: false,
1455
+ reportToUser: true,
1456
+ details: { since }
1457
+ });
1458
+ this.since = since;
1459
+ }
1460
+ since;
1461
+ };
1430
1462
 
1431
1463
  // src/kb-pins/budgets.ts
1432
1464
  function asBudgets(value) {
@@ -3611,17 +3643,115 @@ var schemaCommand = define({
3611
3643
  run: () => Promise.resolve(kbJsonSchemas())
3612
3644
  });
3613
3645
 
3614
- // src/commands/status.ts
3646
+ // src/commands/stamp.ts
3647
+ import { readFile as readFile4 } from "fs/promises";
3615
3648
  import { z as z25 } from "zod";
3649
+ var DIGEST = /^[0-9a-f]{64}$/;
3650
+ var stampCommand = define({
3651
+ name: "stamp",
3652
+ tool: "kb_stamp",
3653
+ usage: "stamp [--bundle PATH] [--since DIGEST|FILE]",
3654
+ description: "Content stamp of a base \u2014 `load`'s digest, record counts, per-record digests \u2014 without any bodies. Takes no bundlePath to stamp every pinned base. With `since`, reports only the bases that moved, naming the changed ids when the baseline is a prior stamp; silent when nothing changed. Reads, never writes.",
3655
+ input: z25.object({
3656
+ bundlePath: z25.string().min(1).optional().describe(
3657
+ "Absolute path to one knowledge base. Omit to stamp every pinned base."
3658
+ ),
3659
+ since: z25.string().min(1).optional().describe(
3660
+ "Prior digest, or path to a prior `stamp --json`; only moved bases return, with changed ids when the baseline is a file."
3661
+ )
3662
+ }),
3663
+ fromArgv: (argv, path, _stdin, bundleExplicit) => {
3664
+ const since = argvFlag(argv, "--since");
3665
+ return {
3666
+ ...bundleExplicit ? { bundlePath: path } : {},
3667
+ ...since !== void 0 ? { since } : {}
3668
+ };
3669
+ },
3670
+ run: async ({ store }, { bundlePath: bundlePath2, since }) => {
3671
+ const targets = bundlePath2 ? [bundlePath2] : (await readMergedPins(process.cwd())).pins.map(
3672
+ (pin) => pin.absolutePath
3673
+ );
3674
+ if (since !== void 0 && DIGEST.test(since) && targets.length > 1) {
3675
+ throw new KbStampDigestBaselineError(since);
3676
+ }
3677
+ const stamps = await Promise.all(
3678
+ targets.map((target) => store.stamp(target))
3679
+ );
3680
+ if (since === void 0) {
3681
+ return stamps.map((stamp) => ({ ...stamp, changed: null }));
3682
+ }
3683
+ const baseline = await readBaseline(since);
3684
+ const reports = [];
3685
+ for (const stamp of stamps) {
3686
+ const before = baseline.byPath.get(stamp.path);
3687
+ if (baseline.digest !== null) {
3688
+ if (baseline.digest === stamp.digest) continue;
3689
+ reports.push({ ...stamp, changed: null });
3690
+ continue;
3691
+ }
3692
+ if (before && before.digest === stamp.digest) continue;
3693
+ reports.push({ ...stamp, changed: changedIds(before?.records, stamp) });
3694
+ }
3695
+ return reports;
3696
+ },
3697
+ render: (result) => result.map((report) => {
3698
+ const counts = `${report.recordCount} record(s), ${report.superseded} superseded`;
3699
+ const head = `${report.path} ${report.digest} ${counts}${report.newestAt ? ` newest ${report.newestAt}` : ""}`;
3700
+ return report.changed?.length ? `${head}
3701
+ changed: ${report.changed.join(", ")}` : head;
3702
+ }).join("\n")
3703
+ });
3704
+ function changedIds(before, stamp) {
3705
+ const now = new Map(
3706
+ stamp.records.map((record) => [record.conceptId, record.digest])
3707
+ );
3708
+ const ids = /* @__PURE__ */ new Set();
3709
+ for (const [conceptId2, digest] of now) {
3710
+ if (before?.get(conceptId2) !== digest) ids.add(conceptId2);
3711
+ }
3712
+ for (const conceptId2 of before?.keys() ?? []) {
3713
+ if (!now.has(conceptId2)) ids.add(conceptId2);
3714
+ }
3715
+ return [...ids].sort();
3716
+ }
3717
+ async function readBaseline(since) {
3718
+ if (DIGEST.test(since)) return { digest: since, byPath: /* @__PURE__ */ new Map() };
3719
+ let parsed;
3720
+ try {
3721
+ parsed = JSON.parse(await readFile4(since, "utf8"));
3722
+ } catch {
3723
+ throw new KbStampBaselineError(since);
3724
+ }
3725
+ const entries = Array.isArray(parsed) ? parsed : parsed?.stamps ?? [];
3726
+ const byPath = /* @__PURE__ */ new Map();
3727
+ for (const entry of entries) {
3728
+ if (typeof entry?.path !== "string" || typeof entry?.digest !== "string") {
3729
+ continue;
3730
+ }
3731
+ byPath.set(entry.path, {
3732
+ digest: entry.digest,
3733
+ records: new Map(
3734
+ (entry.records ?? []).map((record) => [
3735
+ record.conceptId,
3736
+ record.digest
3737
+ ])
3738
+ )
3739
+ });
3740
+ }
3741
+ return { digest: null, byPath };
3742
+ }
3743
+
3744
+ // src/commands/status.ts
3745
+ import { z as z26 } from "zod";
3616
3746
  var statusCommand = define({
3617
3747
  name: "status",
3618
3748
  tool: "kb_status",
3619
3749
  usage: "status <concept-id> <status>",
3620
3750
  description: "Move a record's status. Compare-and-swap: a concurrent change fails instead of being overwritten.",
3621
- input: z25.object({
3751
+ input: z26.object({
3622
3752
  bundlePath,
3623
3753
  conceptId,
3624
- status: z25.enum(KB_RECORD_STATUSES)
3754
+ status: z26.enum(KB_RECORD_STATUSES)
3625
3755
  }),
3626
3756
  fromArgv: (argv, path) => ({
3627
3757
  bundlePath: path,
@@ -3636,13 +3766,13 @@ var statusCommand = define({
3636
3766
  });
3637
3767
 
3638
3768
  // src/commands/supersede.ts
3639
- import { z as z26 } from "zod";
3769
+ import { z as z27 } from "zod";
3640
3770
  var supersedeCommand = define({
3641
3771
  name: "supersede",
3642
3772
  tool: "kb_supersede",
3643
3773
  usage: "supersede <concept-id> <replacement-id>",
3644
3774
  description: "Mark a record superseded by another, linked in both directions. Use instead of editing a record whose meaning changed.",
3645
- input: z26.object({ bundlePath, conceptId, replacementId: conceptId }),
3775
+ input: z27.object({ bundlePath, conceptId, replacementId: conceptId }),
3646
3776
  fromArgv: (argv, path) => ({
3647
3777
  bundlePath: path,
3648
3778
  conceptId: argv[1],
@@ -3656,16 +3786,16 @@ var supersedeCommand = define({
3656
3786
  });
3657
3787
 
3658
3788
  // src/commands/sync-instructions.ts
3659
- import { z as z27 } from "zod";
3789
+ import { z as z28 } from "zod";
3660
3790
  var syncInstructionsCommand = define({
3661
3791
  name: "sync-instructions",
3662
3792
  usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
3663
3793
  description: "CLI-only: plant the kb_context block between sentinel comments in AGENTS.md or CLAUDE.md, idempotently.",
3664
- input: z27.object({
3665
- file: z27.string().min(1).describe("The instruction file to edit in place."),
3666
- budgetTokens: z27.number().int().positive().optional(),
3667
- fullUnderTokens: z27.number().int().positive().optional(),
3668
- profile: z27.string().optional()
3794
+ input: z28.object({
3795
+ file: z28.string().min(1).describe("The instruction file to edit in place."),
3796
+ budgetTokens: z28.number().int().positive().optional(),
3797
+ fullUnderTokens: z28.number().int().positive().optional(),
3798
+ profile: z28.string().optional()
3669
3799
  }),
3670
3800
  fromArgv: (argv) => {
3671
3801
  const budget = argvFlag(argv, "--budget");
@@ -3691,17 +3821,17 @@ var syncInstructionsCommand = define({
3691
3821
  });
3692
3822
 
3693
3823
  // src/commands/trace.ts
3694
- import { z as z28 } from "zod";
3824
+ import { z as z29 } from "zod";
3695
3825
  var traceCommand = define({
3696
3826
  name: "trace",
3697
3827
  tool: "kb_trace",
3698
3828
  usage: "trace <concept-id> [edges...]",
3699
3829
  description: 'Timeline of how a position was reached, ordered by write time, following supersession, shared anchors and shared sources. Includes rejected, draft and superseded records \u2014 in a history they are the content. For "why is it like this"; kb_load answers "what holds now".',
3700
- input: z28.object({
3830
+ input: z29.object({
3701
3831
  bundlePath,
3702
3832
  conceptId,
3703
- edges: z28.array(z28.enum(TRACE_EDGES)).optional(),
3704
- depth: z28.number().int().positive().optional()
3833
+ edges: z29.array(z29.enum(TRACE_EDGES)).optional(),
3834
+ depth: z29.number().int().positive().optional()
3705
3835
  }),
3706
3836
  fromArgv: (argv, path) => ({
3707
3837
  bundlePath: path,
@@ -3723,37 +3853,37 @@ var traceCommand = define({
3723
3853
  });
3724
3854
 
3725
3855
  // src/commands/types.ts
3726
- import { z as z29 } from "zod";
3856
+ import { z as z30 } from "zod";
3727
3857
  var typesCommand = define({
3728
3858
  name: "types",
3729
3859
  tool: "kb_types",
3730
3860
  usage: "types",
3731
3861
  description: "The twelve record types with their purpose, body sections, and starting status. Read this before writing rather than guessing headings \u2014 a section the type does not define is rejected.",
3732
- input: z29.object({}),
3862
+ input: z30.object({}),
3733
3863
  fromArgv: () => ({}),
3734
3864
  run: () => Promise.resolve(RECORD_TYPES)
3735
3865
  });
3736
3866
 
3737
3867
  // src/commands/unpin.ts
3738
- import { z as z30 } from "zod";
3868
+ import { z as z31 } from "zod";
3739
3869
  var unpinCommand = define({
3740
3870
  name: "unpin",
3741
3871
  tool: "kb_unpin",
3742
3872
  usage: "unpin [bundle-path]",
3743
3873
  description: "Remove a base from every manifest layer that holds it. Reports the layers touched.",
3744
- input: z30.object({ bundlePath }),
3874
+ input: z31.object({ bundlePath }),
3745
3875
  fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
3746
3876
  run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
3747
3877
  });
3748
3878
 
3749
3879
  // src/commands/validate.ts
3750
- import { z as z31 } from "zod";
3880
+ import { z as z32 } from "zod";
3751
3881
  var validateCommand = define({
3752
3882
  name: "validate",
3753
3883
  tool: "kb_validate",
3754
3884
  usage: "validate",
3755
3885
  description: "Check pointers no single record can see: supersession links that disagree between the two records, typed causal links, and assumptions that cite sources. Each finding carries a severity: errors fail the exit code, warnings do not.",
3756
- input: z31.object({ bundlePath }),
3886
+ input: z32.object({ bundlePath }),
3757
3887
  fromArgv: (_argv, path) => ({ bundlePath: path }),
3758
3888
  run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
3759
3889
  // Warnings never fail the exit code; every other severity does.
@@ -3763,16 +3893,16 @@ var validateCommand = define({
3763
3893
  });
3764
3894
 
3765
3895
  // src/commands/verify.ts
3766
- import { z as z32 } from "zod";
3896
+ import { z as z33 } from "zod";
3767
3897
  var verifyCommand = define({
3768
3898
  name: "verify",
3769
3899
  tool: "kb_verify",
3770
3900
  usage: "verify <concept-id> --note <text>",
3771
3901
  description: "Append a verified[] event: who checked, when, and what was found. Append-only. A record's own generator is refused unless the actor is `human:`-prefixed.",
3772
- input: z32.object({
3902
+ input: z33.object({
3773
3903
  bundlePath,
3774
3904
  conceptId,
3775
- note: z32.string().refine((s) => s.trim().length > 0, {
3905
+ note: z33.string().refine((s) => s.trim().length > 0, {
3776
3906
  message: "note must say what the check found"
3777
3907
  })
3778
3908
  }),
@@ -3792,15 +3922,15 @@ var verifyCommand = define({
3792
3922
  });
3793
3923
 
3794
3924
  // src/commands/write.ts
3795
- import { z as z33 } from "zod";
3925
+ import { z as z34 } from "zod";
3796
3926
  var writeCommand = define({
3797
3927
  name: "write",
3798
3928
  tool: "kb_write",
3799
3929
  usage: "write <type> < record.json",
3800
3930
  description: "Write one record. Search first \u2014 a duplicate concept id is rejected, not overwritten; kb_types lists each type's sections. An unsourced claim is an `assumption` with assumption: true, never a vague `fact`. Conflicting records get a `risk`, `open-question`, or superseding `decision`. Prefer a new short record over overloading one. Never delete; supersede.",
3801
- input: z33.object({
3931
+ input: z34.object({
3802
3932
  bundlePath,
3803
- type: z33.enum(KB_RECORD_TYPES),
3933
+ type: z34.enum(KB_RECORD_TYPES),
3804
3934
  input: composeInputSchema
3805
3935
  }),
3806
3936
  fromArgv: async (argv, path, stdin) => ({
@@ -3824,13 +3954,13 @@ var writeCommand = define({
3824
3954
  });
3825
3955
 
3826
3956
  // src/commands/write-decision.ts
3827
- import { z as z34 } from "zod";
3957
+ import { z as z35 } from "zod";
3828
3958
  var writeDecisionCommand = define({
3829
3959
  name: "write-decision",
3830
3960
  tool: "kb_write_decision",
3831
3961
  usage: "write-decision < decision.json",
3832
3962
  description: "Write a decision, with `alternative` (what was rejected and why) and `impact` as fields. Record one when a later reader would otherwise simplify the constraint away; skip when the diff already answers it. `sources` for material read, `anchors` for code, `relatedConceptIds` for records.",
3833
- input: z34.object({ bundlePath, input: decisionInputSchema }),
3963
+ input: z35.object({ bundlePath, input: decisionInputSchema }),
3834
3964
  fromArgv: async (_argv, path, stdin) => ({
3835
3965
  bundlePath: path,
3836
3966
  input: JSON.parse(await stdin())
@@ -3870,6 +4000,7 @@ var KB_COMMANDS = [
3870
4000
  listCommand,
3871
4001
  readIndexCommand,
3872
4002
  logCommand,
4003
+ stampCommand,
3873
4004
  validateCommand,
3874
4005
  doctorCommand,
3875
4006
  schemaCommand,
@@ -3999,19 +4130,50 @@ async function loadQmd(logger) {
3999
4130
  }
4000
4131
 
4001
4132
  // src/kb-store.ts
4002
- import { createHash as createHash2 } from "crypto";
4003
4133
  import {
4004
4134
  appendFile,
4005
4135
  link,
4006
4136
  mkdir as mkdir3,
4007
4137
  readdir,
4008
- readFile as readFile4,
4138
+ readFile as readFile5,
4009
4139
  rename,
4010
4140
  unlink,
4011
4141
  writeFile as writeFile3
4012
4142
  } from "fs/promises";
4013
4143
  import { join as join5, resolve as resolve5, sep as sep3 } from "path";
4014
4144
 
4145
+ // src/kb-stamp.ts
4146
+ import { createHash as createHash2 } from "crypto";
4147
+ function sha256(contents) {
4148
+ return createHash2("sha256").update(contents).digest("hex");
4149
+ }
4150
+ function bundleStamp(records, superseded) {
4151
+ const entries = [
4152
+ ...records.map((hit) => ({
4153
+ conceptId: hit.record.conceptId,
4154
+ digest: `current:${sha256(
4155
+ stringifyMarkdownWithFrontmatter(
4156
+ hit.record.body,
4157
+ hit.record.frontmatter
4158
+ )
4159
+ )}`
4160
+ })),
4161
+ ...superseded.map((entry) => ({
4162
+ conceptId: entry.conceptId,
4163
+ digest: `superseded:${sha256(JSON.stringify(entry))}`
4164
+ }))
4165
+ ].sort((a, b) => a.conceptId < b.conceptId ? -1 : 1);
4166
+ return {
4167
+ digest: sha256(
4168
+ entries.map((entry) => `${entry.conceptId}:${entry.digest}`).join("\n")
4169
+ ),
4170
+ records: entries
4171
+ };
4172
+ }
4173
+ function bundleDigest(records, superseded) {
4174
+ return bundleStamp(records, superseded).digest;
4175
+ }
4176
+
4015
4177
  // src/kb-links/inbound.ts
4016
4178
  function inboundIndex(bundle) {
4017
4179
  const byTarget = /* @__PURE__ */ new Map();
@@ -4242,7 +4404,7 @@ var KbStore = class {
4242
4404
  const target = this.recordPath(bundlePath2, conceptId2);
4243
4405
  let raw;
4244
4406
  try {
4245
- raw = await readFile4(target, "utf8");
4407
+ raw = await readFile5(target, "utf8");
4246
4408
  } catch {
4247
4409
  return null;
4248
4410
  }
@@ -4267,7 +4429,7 @@ var KbStore = class {
4267
4429
  const records = await mapLimit(
4268
4430
  wanted,
4269
4431
  DEFAULT_IO_CONCURRENCY,
4270
- async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await readFile4(join5(root, name), "utf8"))
4432
+ async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await readFile5(join5(root, name), "utf8"))
4271
4433
  );
4272
4434
  return records.filter((record) => record !== null);
4273
4435
  }
@@ -4542,6 +4704,28 @@ ${answer}
4542
4704
  digest: bundleDigestValue
4543
4705
  };
4544
4706
  }
4707
+ /**
4708
+ * `load`'s digest without `load`'s bodies — the same records, adjudicated
4709
+ * the same way, handed back as a stamp. Skips the anchor drift pass, which
4710
+ * reads source files and only ever adds warnings: no warning reaches the
4711
+ * digest, so the value is identical to the one `load` returns.
4712
+ */
4713
+ async stamp(bundlePath2) {
4714
+ const bundle = await this.list(bundlePath2);
4715
+ const adjudicated = adjudicate(bundle, bundle, /* @__PURE__ */ new Date());
4716
+ const current = adjudicated.filter((hit) => hit.standing !== "superseded");
4717
+ const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
4718
+ const stamped = bundleStamp(current, superseded);
4719
+ const dates = bundle.map((record) => record.frontmatter.generated?.at ?? null).filter((at) => typeof at === "string").sort();
4720
+ return {
4721
+ path: bundlePath2,
4722
+ digest: stamped.digest,
4723
+ recordCount: bundle.length,
4724
+ superseded: superseded.length,
4725
+ newestAt: dates.at(-1) ?? null,
4726
+ records: stamped.records
4727
+ };
4728
+ }
4545
4729
  /** How a position was arrived at, as a timeline. See `trace.ts`. */
4546
4730
  async trace(bundlePath2, seedId, options = {}) {
4547
4731
  return trace(seedId, await this.list(bundlePath2), options);
@@ -4572,7 +4756,7 @@ ${answer}
4572
4756
  async readIndex(bundlePath2) {
4573
4757
  const root = this.root(bundlePath2);
4574
4758
  const expected = renderIndex(await this.list(bundlePath2));
4575
- const stored = await readFile4(join5(root, INDEX_FILE), "utf8").catch(
4759
+ const stored = await readFile5(join5(root, INDEX_FILE), "utf8").catch(
4576
4760
  () => null
4577
4761
  );
4578
4762
  if (indexIsStale(stored, expected)) {
@@ -4593,7 +4777,7 @@ ${answer}
4593
4777
  * knows which agent touched what. So a bad line is surfaced and left alone.
4594
4778
  */
4595
4779
  async readLog(bundlePath2) {
4596
- const raw = await readFile4(
4780
+ const raw = await readFile5(
4597
4781
  join5(this.root(bundlePath2), LOG_FILE),
4598
4782
  "utf8"
4599
4783
  ).catch(() => "");
@@ -4645,15 +4829,15 @@ ${answer}
4645
4829
  }
4646
4830
  async mutate(bundlePath2, conceptId2, change, entry, changeBody = (body) => body) {
4647
4831
  const target = this.recordPath(bundlePath2, conceptId2);
4648
- const before = await readFile4(target, "utf8").catch(() => null);
4832
+ const before = await readFile5(target, "utf8").catch(() => null);
4649
4833
  if (before === null) throw new KbRecordNotFoundError(conceptId2);
4650
4834
  const parsed = this.parse(conceptId2, before);
4651
4835
  if (!parsed) throw new KbRecordNotFoundError(conceptId2);
4652
4836
  const frontmatter = change(parsed.frontmatter);
4653
4837
  const body = changeBody(parsed.body);
4654
4838
  const contents = stringifyMarkdownWithFrontmatter(body, frontmatter);
4655
- const witness = await readFile4(target, "utf8").catch(() => null);
4656
- if (witness === null || digest(witness) !== digest(before)) {
4839
+ const witness = await readFile5(target, "utf8").catch(() => null);
4840
+ if (witness === null || sha256(witness) !== sha256(before)) {
4657
4841
  throw new KbWriteConflictError(conceptId2);
4658
4842
  }
4659
4843
  await this.publish(target, contents, true, conceptId2);
@@ -4739,7 +4923,7 @@ ${answer}
4739
4923
  try {
4740
4924
  let existing;
4741
4925
  try {
4742
- existing = await readFile4(target, "utf8");
4926
+ existing = await readFile5(target, "utf8");
4743
4927
  } catch (error) {
4744
4928
  if (error.code !== "ENOENT") throw error;
4745
4929
  existing = null;
@@ -4861,25 +5045,6 @@ function normalizeActor(id) {
4861
5045
  if (colon === -1) return id.toLowerCase();
4862
5046
  return id.slice(0, colon + 1).toLowerCase() + id.slice(colon + 1);
4863
5047
  }
4864
- function digest(contents) {
4865
- return createHash2("sha256").update(contents).digest("hex");
4866
- }
4867
- function bundleDigest(records, superseded) {
4868
- const entries = [
4869
- ...records.map(
4870
- (hit) => `${hit.record.conceptId}:current:${digest(
4871
- stringifyMarkdownWithFrontmatter(
4872
- hit.record.body,
4873
- hit.record.frontmatter
4874
- )
4875
- )}`
4876
- ),
4877
- ...superseded.map(
4878
- (entry) => `${entry.conceptId}:superseded:${digest(JSON.stringify(entry))}`
4879
- )
4880
- ].sort();
4881
- return digest(entries.join("\n"));
4882
- }
4883
5048
 
4884
5049
  // src/pack.ts
4885
5050
  var DEFAULT_PACK_HOPS = 2;
@@ -4966,7 +5131,7 @@ function typeRank(record) {
4966
5131
  }
4967
5132
 
4968
5133
  // src/version.ts
4969
- var VERSION = true ? "0.1.15" : "0.0.0-dev";
5134
+ var VERSION = true ? "0.1.16" : "0.0.0-dev";
4970
5135
 
4971
5136
  export {
4972
5137
  kbSourceSchema,
@@ -5082,4 +5247,4 @@ export {
5082
5247
  KbStore,
5083
5248
  VERSION
5084
5249
  };
5085
- //# sourceMappingURL=chunk-KNIUBCZY.js.map
5250
+ //# sourceMappingURL=chunk-H5W53NVU.js.map