@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/dist/cli-main.cjs CHANGED
@@ -1439,6 +1439,36 @@ var KbInvalidConceptIdError = class extends BaseError {
1439
1439
  });
1440
1440
  }
1441
1441
  };
1442
+ var KbStampBaselineError = class extends BaseError {
1443
+ constructor(since) {
1444
+ super({
1445
+ message: `kb: --since ${since} is neither a 64-character digest nor a readable stamp file`,
1446
+ errorType: "KbStampBaselineUnreadable" /* KbStampBaselineUnreadable */,
1447
+ code: 400,
1448
+ fault: "User" /* User */,
1449
+ retriable: false,
1450
+ reportToUser: true,
1451
+ details: { since }
1452
+ });
1453
+ this.since = since;
1454
+ }
1455
+ since;
1456
+ };
1457
+ var KbStampDigestBaselineError = class extends BaseError {
1458
+ constructor(since) {
1459
+ super({
1460
+ message: `kb: --since ${since} is a digest, which needs --bundle (one base) \u2014 a file baseline works for many`,
1461
+ errorType: "KbStampDigestBaselineAmbiguous" /* KbStampDigestBaselineAmbiguous */,
1462
+ code: 400,
1463
+ fault: "User" /* User */,
1464
+ retriable: false,
1465
+ reportToUser: true,
1466
+ details: { since }
1467
+ });
1468
+ this.since = since;
1469
+ }
1470
+ since;
1471
+ };
1442
1472
 
1443
1473
  // src/kb-pins/budgets.ts
1444
1474
  function asBudgets(value) {
@@ -3577,17 +3607,115 @@ var schemaCommand = define({
3577
3607
  run: () => Promise.resolve(kbJsonSchemas())
3578
3608
  });
3579
3609
 
3580
- // src/commands/status.ts
3610
+ // src/commands/stamp.ts
3611
+ var import_promises5 = require("fs/promises");
3581
3612
  var import_zod25 = require("zod");
3613
+ var DIGEST = /^[0-9a-f]{64}$/;
3614
+ var stampCommand = define({
3615
+ name: "stamp",
3616
+ tool: "kb_stamp",
3617
+ usage: "stamp [--bundle PATH] [--since DIGEST|FILE]",
3618
+ 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.",
3619
+ input: import_zod25.z.object({
3620
+ bundlePath: import_zod25.z.string().min(1).optional().describe(
3621
+ "Absolute path to one knowledge base. Omit to stamp every pinned base."
3622
+ ),
3623
+ since: import_zod25.z.string().min(1).optional().describe(
3624
+ "Prior digest, or path to a prior `stamp --json`; only moved bases return, with changed ids when the baseline is a file."
3625
+ )
3626
+ }),
3627
+ fromArgv: (argv, path, _stdin, bundleExplicit) => {
3628
+ const since = argvFlag(argv, "--since");
3629
+ return {
3630
+ ...bundleExplicit ? { bundlePath: path } : {},
3631
+ ...since !== void 0 ? { since } : {}
3632
+ };
3633
+ },
3634
+ run: async ({ store }, { bundlePath: bundlePath2, since }) => {
3635
+ const targets = bundlePath2 ? [bundlePath2] : (await readMergedPins(process.cwd())).pins.map(
3636
+ (pin) => pin.absolutePath
3637
+ );
3638
+ if (since !== void 0 && DIGEST.test(since) && targets.length > 1) {
3639
+ throw new KbStampDigestBaselineError(since);
3640
+ }
3641
+ const stamps = await Promise.all(
3642
+ targets.map((target) => store.stamp(target))
3643
+ );
3644
+ if (since === void 0) {
3645
+ return stamps.map((stamp) => ({ ...stamp, changed: null }));
3646
+ }
3647
+ const baseline = await readBaseline(since);
3648
+ const reports = [];
3649
+ for (const stamp of stamps) {
3650
+ const before = baseline.byPath.get(stamp.path);
3651
+ if (baseline.digest !== null) {
3652
+ if (baseline.digest === stamp.digest) continue;
3653
+ reports.push({ ...stamp, changed: null });
3654
+ continue;
3655
+ }
3656
+ if (before && before.digest === stamp.digest) continue;
3657
+ reports.push({ ...stamp, changed: changedIds(before?.records, stamp) });
3658
+ }
3659
+ return reports;
3660
+ },
3661
+ render: (result) => result.map((report) => {
3662
+ const counts = `${report.recordCount} record(s), ${report.superseded} superseded`;
3663
+ const head = `${report.path} ${report.digest} ${counts}${report.newestAt ? ` newest ${report.newestAt}` : ""}`;
3664
+ return report.changed?.length ? `${head}
3665
+ changed: ${report.changed.join(", ")}` : head;
3666
+ }).join("\n")
3667
+ });
3668
+ function changedIds(before, stamp) {
3669
+ const now = new Map(
3670
+ stamp.records.map((record) => [record.conceptId, record.digest])
3671
+ );
3672
+ const ids = /* @__PURE__ */ new Set();
3673
+ for (const [conceptId2, digest] of now) {
3674
+ if (before?.get(conceptId2) !== digest) ids.add(conceptId2);
3675
+ }
3676
+ for (const conceptId2 of before?.keys() ?? []) {
3677
+ if (!now.has(conceptId2)) ids.add(conceptId2);
3678
+ }
3679
+ return [...ids].sort();
3680
+ }
3681
+ async function readBaseline(since) {
3682
+ if (DIGEST.test(since)) return { digest: since, byPath: /* @__PURE__ */ new Map() };
3683
+ let parsed;
3684
+ try {
3685
+ parsed = JSON.parse(await (0, import_promises5.readFile)(since, "utf8"));
3686
+ } catch {
3687
+ throw new KbStampBaselineError(since);
3688
+ }
3689
+ const entries = Array.isArray(parsed) ? parsed : parsed?.stamps ?? [];
3690
+ const byPath = /* @__PURE__ */ new Map();
3691
+ for (const entry of entries) {
3692
+ if (typeof entry?.path !== "string" || typeof entry?.digest !== "string") {
3693
+ continue;
3694
+ }
3695
+ byPath.set(entry.path, {
3696
+ digest: entry.digest,
3697
+ records: new Map(
3698
+ (entry.records ?? []).map((record) => [
3699
+ record.conceptId,
3700
+ record.digest
3701
+ ])
3702
+ )
3703
+ });
3704
+ }
3705
+ return { digest: null, byPath };
3706
+ }
3707
+
3708
+ // src/commands/status.ts
3709
+ var import_zod26 = require("zod");
3582
3710
  var statusCommand = define({
3583
3711
  name: "status",
3584
3712
  tool: "kb_status",
3585
3713
  usage: "status <concept-id> <status>",
3586
3714
  description: "Move a record's status. Compare-and-swap: a concurrent change fails instead of being overwritten.",
3587
- input: import_zod25.z.object({
3715
+ input: import_zod26.z.object({
3588
3716
  bundlePath,
3589
3717
  conceptId,
3590
- status: import_zod25.z.enum(KB_RECORD_STATUSES)
3718
+ status: import_zod26.z.enum(KB_RECORD_STATUSES)
3591
3719
  }),
3592
3720
  fromArgv: (argv, path) => ({
3593
3721
  bundlePath: path,
@@ -3602,13 +3730,13 @@ var statusCommand = define({
3602
3730
  });
3603
3731
 
3604
3732
  // src/commands/supersede.ts
3605
- var import_zod26 = require("zod");
3733
+ var import_zod27 = require("zod");
3606
3734
  var supersedeCommand = define({
3607
3735
  name: "supersede",
3608
3736
  tool: "kb_supersede",
3609
3737
  usage: "supersede <concept-id> <replacement-id>",
3610
3738
  description: "Mark a record superseded by another, linked in both directions. Use instead of editing a record whose meaning changed.",
3611
- input: import_zod26.z.object({ bundlePath, conceptId, replacementId: conceptId }),
3739
+ input: import_zod27.z.object({ bundlePath, conceptId, replacementId: conceptId }),
3612
3740
  fromArgv: (argv, path) => ({
3613
3741
  bundlePath: path,
3614
3742
  conceptId: argv[1],
@@ -3622,16 +3750,16 @@ var supersedeCommand = define({
3622
3750
  });
3623
3751
 
3624
3752
  // src/commands/sync-instructions.ts
3625
- var import_zod27 = require("zod");
3753
+ var import_zod28 = require("zod");
3626
3754
  var syncInstructionsCommand = define({
3627
3755
  name: "sync-instructions",
3628
3756
  usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
3629
3757
  description: "CLI-only: plant the kb_context block between sentinel comments in AGENTS.md or CLAUDE.md, idempotently.",
3630
- input: import_zod27.z.object({
3631
- file: import_zod27.z.string().min(1).describe("The instruction file to edit in place."),
3632
- budgetTokens: import_zod27.z.number().int().positive().optional(),
3633
- fullUnderTokens: import_zod27.z.number().int().positive().optional(),
3634
- profile: import_zod27.z.string().optional()
3758
+ input: import_zod28.z.object({
3759
+ file: import_zod28.z.string().min(1).describe("The instruction file to edit in place."),
3760
+ budgetTokens: import_zod28.z.number().int().positive().optional(),
3761
+ fullUnderTokens: import_zod28.z.number().int().positive().optional(),
3762
+ profile: import_zod28.z.string().optional()
3635
3763
  }),
3636
3764
  fromArgv: (argv) => {
3637
3765
  const budget = argvFlag(argv, "--budget");
@@ -3657,7 +3785,7 @@ var syncInstructionsCommand = define({
3657
3785
  });
3658
3786
 
3659
3787
  // src/commands/trace.ts
3660
- var import_zod28 = require("zod");
3788
+ var import_zod29 = require("zod");
3661
3789
 
3662
3790
  // src/trace.ts
3663
3791
  var TRACE_EDGES = [
@@ -3713,11 +3841,11 @@ var traceCommand = define({
3713
3841
  tool: "kb_trace",
3714
3842
  usage: "trace <concept-id> [edges...]",
3715
3843
  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".',
3716
- input: import_zod28.z.object({
3844
+ input: import_zod29.z.object({
3717
3845
  bundlePath,
3718
3846
  conceptId,
3719
- edges: import_zod28.z.array(import_zod28.z.enum(TRACE_EDGES)).optional(),
3720
- depth: import_zod28.z.number().int().positive().optional()
3847
+ edges: import_zod29.z.array(import_zod29.z.enum(TRACE_EDGES)).optional(),
3848
+ depth: import_zod29.z.number().int().positive().optional()
3721
3849
  }),
3722
3850
  fromArgv: (argv, path) => ({
3723
3851
  bundlePath: path,
@@ -3739,37 +3867,37 @@ var traceCommand = define({
3739
3867
  });
3740
3868
 
3741
3869
  // src/commands/types.ts
3742
- var import_zod29 = require("zod");
3870
+ var import_zod30 = require("zod");
3743
3871
  var typesCommand = define({
3744
3872
  name: "types",
3745
3873
  tool: "kb_types",
3746
3874
  usage: "types",
3747
3875
  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.",
3748
- input: import_zod29.z.object({}),
3876
+ input: import_zod30.z.object({}),
3749
3877
  fromArgv: () => ({}),
3750
3878
  run: () => Promise.resolve(RECORD_TYPES)
3751
3879
  });
3752
3880
 
3753
3881
  // src/commands/unpin.ts
3754
- var import_zod30 = require("zod");
3882
+ var import_zod31 = require("zod");
3755
3883
  var unpinCommand = define({
3756
3884
  name: "unpin",
3757
3885
  tool: "kb_unpin",
3758
3886
  usage: "unpin [bundle-path]",
3759
3887
  description: "Remove a base from every manifest layer that holds it. Reports the layers touched.",
3760
- input: import_zod30.z.object({ bundlePath }),
3888
+ input: import_zod31.z.object({ bundlePath }),
3761
3889
  fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
3762
3890
  run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
3763
3891
  });
3764
3892
 
3765
3893
  // src/commands/validate.ts
3766
- var import_zod31 = require("zod");
3894
+ var import_zod32 = require("zod");
3767
3895
  var validateCommand = define({
3768
3896
  name: "validate",
3769
3897
  tool: "kb_validate",
3770
3898
  usage: "validate",
3771
3899
  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.",
3772
- input: import_zod31.z.object({ bundlePath }),
3900
+ input: import_zod32.z.object({ bundlePath }),
3773
3901
  fromArgv: (_argv, path) => ({ bundlePath: path }),
3774
3902
  run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
3775
3903
  // Warnings never fail the exit code; every other severity does.
@@ -3779,16 +3907,16 @@ var validateCommand = define({
3779
3907
  });
3780
3908
 
3781
3909
  // src/commands/verify.ts
3782
- var import_zod32 = require("zod");
3910
+ var import_zod33 = require("zod");
3783
3911
  var verifyCommand = define({
3784
3912
  name: "verify",
3785
3913
  tool: "kb_verify",
3786
3914
  usage: "verify <concept-id> --note <text>",
3787
3915
  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.",
3788
- input: import_zod32.z.object({
3916
+ input: import_zod33.z.object({
3789
3917
  bundlePath,
3790
3918
  conceptId,
3791
- note: import_zod32.z.string().refine((s) => s.trim().length > 0, {
3919
+ note: import_zod33.z.string().refine((s) => s.trim().length > 0, {
3792
3920
  message: "note must say what the check found"
3793
3921
  })
3794
3922
  }),
@@ -3808,15 +3936,15 @@ var verifyCommand = define({
3808
3936
  });
3809
3937
 
3810
3938
  // src/commands/write.ts
3811
- var import_zod33 = require("zod");
3939
+ var import_zod34 = require("zod");
3812
3940
  var writeCommand = define({
3813
3941
  name: "write",
3814
3942
  tool: "kb_write",
3815
3943
  usage: "write <type> < record.json",
3816
3944
  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.",
3817
- input: import_zod33.z.object({
3945
+ input: import_zod34.z.object({
3818
3946
  bundlePath,
3819
- type: import_zod33.z.enum(KB_RECORD_TYPES),
3947
+ type: import_zod34.z.enum(KB_RECORD_TYPES),
3820
3948
  input: composeInputSchema
3821
3949
  }),
3822
3950
  fromArgv: async (argv, path, stdin) => ({
@@ -3840,13 +3968,13 @@ var writeCommand = define({
3840
3968
  });
3841
3969
 
3842
3970
  // src/commands/write-decision.ts
3843
- var import_zod34 = require("zod");
3971
+ var import_zod35 = require("zod");
3844
3972
  var writeDecisionCommand = define({
3845
3973
  name: "write-decision",
3846
3974
  tool: "kb_write_decision",
3847
3975
  usage: "write-decision < decision.json",
3848
3976
  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.",
3849
- input: import_zod34.z.object({ bundlePath, input: decisionInputSchema }),
3977
+ input: import_zod35.z.object({ bundlePath, input: decisionInputSchema }),
3850
3978
  fromArgv: async (_argv, path, stdin) => ({
3851
3979
  bundlePath: path,
3852
3980
  input: JSON.parse(await stdin())
@@ -3886,6 +4014,7 @@ var KB_COMMANDS = [
3886
4014
  listCommand,
3887
4015
  readIndexCommand,
3888
4016
  logCommand,
4017
+ stampCommand,
3889
4018
  validateCommand,
3890
4019
  doctorCommand,
3891
4020
  schemaCommand,
@@ -3901,8 +4030,7 @@ var KB_COMMANDS_BY_NAME = new Map(
3901
4030
  );
3902
4031
 
3903
4032
  // src/kb-store.ts
3904
- var import_node_crypto2 = require("crypto");
3905
- var import_promises6 = require("fs/promises");
4033
+ var import_promises7 = require("fs/promises");
3906
4034
  var import_node_path8 = require("path");
3907
4035
 
3908
4036
  // src/markdown.ts
@@ -3930,8 +4058,40 @@ function parseMarkdownWithFrontmatter(text, schema) {
3930
4058
  };
3931
4059
  }
3932
4060
 
4061
+ // src/kb-stamp.ts
4062
+ var import_node_crypto2 = require("crypto");
4063
+ function sha256(contents) {
4064
+ return (0, import_node_crypto2.createHash)("sha256").update(contents).digest("hex");
4065
+ }
4066
+ function bundleStamp(records, superseded) {
4067
+ const entries = [
4068
+ ...records.map((hit) => ({
4069
+ conceptId: hit.record.conceptId,
4070
+ digest: `current:${sha256(
4071
+ stringifyMarkdownWithFrontmatter(
4072
+ hit.record.body,
4073
+ hit.record.frontmatter
4074
+ )
4075
+ )}`
4076
+ })),
4077
+ ...superseded.map((entry) => ({
4078
+ conceptId: entry.conceptId,
4079
+ digest: `superseded:${sha256(JSON.stringify(entry))}`
4080
+ }))
4081
+ ].sort((a, b) => a.conceptId < b.conceptId ? -1 : 1);
4082
+ return {
4083
+ digest: sha256(
4084
+ entries.map((entry) => `${entry.conceptId}:${entry.digest}`).join("\n")
4085
+ ),
4086
+ records: entries
4087
+ };
4088
+ }
4089
+ function bundleDigest(records, superseded) {
4090
+ return bundleStamp(records, superseded).digest;
4091
+ }
4092
+
3933
4093
  // src/search-index.ts
3934
- var import_promises5 = require("fs/promises");
4094
+ var import_promises6 = require("fs/promises");
3935
4095
  var import_node_path7 = require("path");
3936
4096
  var SEARCH_INDEX_FILE = ".index.sqlite";
3937
4097
  var COLLECTION = "kb";
@@ -3976,7 +4136,7 @@ async function searchBase(bundlePath2, query, options = {}) {
3976
4136
  }
3977
4137
  }
3978
4138
  async function isStale(bundlePath2) {
3979
- const indexAt = await (0, import_promises5.stat)((0, import_node_path7.join)(bundlePath2, SEARCH_INDEX_FILE)).then((s) => s.mtimeMs).catch(() => 0);
4139
+ const indexAt = await (0, import_promises6.stat)((0, import_node_path7.join)(bundlePath2, SEARCH_INDEX_FILE)).then((s) => s.mtimeMs).catch(() => 0);
3980
4140
  if (!indexAt) return true;
3981
4141
  const { readdir: readdir2 } = await import("fs/promises");
3982
4142
  const names = (await readdir2(bundlePath2).catch(() => [])).filter(
@@ -3985,7 +4145,7 @@ async function isStale(bundlePath2) {
3985
4145
  let stale = false;
3986
4146
  await mapLimit(names, DEFAULT_IO_CONCURRENCY, async (name) => {
3987
4147
  if (stale) return;
3988
- const at = await (0, import_promises5.stat)((0, import_node_path7.join)(bundlePath2, name)).then((s) => s.mtimeMs).catch(() => 0);
4148
+ const at = await (0, import_promises6.stat)((0, import_node_path7.join)(bundlePath2, name)).then((s) => s.mtimeMs).catch(() => 0);
3989
4149
  if (at > indexAt) stale = true;
3990
4150
  });
3991
4151
  return stale;
@@ -4294,7 +4454,7 @@ var KbStore = class {
4294
4454
  const conceptId2 = `${input.type}.${input.slug}`;
4295
4455
  const root = this.root(bundlePath2);
4296
4456
  const target = this.recordPath(bundlePath2, conceptId2);
4297
- await (0, import_promises6.mkdir)(root, { recursive: true });
4457
+ await (0, import_promises7.mkdir)(root, { recursive: true });
4298
4458
  await this.publish(
4299
4459
  target,
4300
4460
  stringifyMarkdownWithFrontmatter(input.body, frontmatter),
@@ -4333,7 +4493,7 @@ var KbStore = class {
4333
4493
  const target = this.recordPath(bundlePath2, conceptId2);
4334
4494
  let raw;
4335
4495
  try {
4336
- raw = await (0, import_promises6.readFile)(target, "utf8");
4496
+ raw = await (0, import_promises7.readFile)(target, "utf8");
4337
4497
  } catch {
4338
4498
  return null;
4339
4499
  }
@@ -4350,7 +4510,7 @@ var KbStore = class {
4350
4510
  const root = this.root(bundlePath2);
4351
4511
  let names;
4352
4512
  try {
4353
- names = await (0, import_promises6.readdir)(root);
4513
+ names = await (0, import_promises7.readdir)(root);
4354
4514
  } catch {
4355
4515
  return [];
4356
4516
  }
@@ -4358,7 +4518,7 @@ var KbStore = class {
4358
4518
  const records = await mapLimit(
4359
4519
  wanted,
4360
4520
  DEFAULT_IO_CONCURRENCY,
4361
- async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await (0, import_promises6.readFile)((0, import_node_path8.join)(root, name), "utf8"))
4521
+ async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await (0, import_promises7.readFile)((0, import_node_path8.join)(root, name), "utf8"))
4362
4522
  );
4363
4523
  return records.filter((record) => record !== null);
4364
4524
  }
@@ -4633,6 +4793,28 @@ ${answer}
4633
4793
  digest: bundleDigestValue
4634
4794
  };
4635
4795
  }
4796
+ /**
4797
+ * `load`'s digest without `load`'s bodies — the same records, adjudicated
4798
+ * the same way, handed back as a stamp. Skips the anchor drift pass, which
4799
+ * reads source files and only ever adds warnings: no warning reaches the
4800
+ * digest, so the value is identical to the one `load` returns.
4801
+ */
4802
+ async stamp(bundlePath2) {
4803
+ const bundle = await this.list(bundlePath2);
4804
+ const adjudicated = adjudicate(bundle, bundle, /* @__PURE__ */ new Date());
4805
+ const current = adjudicated.filter((hit) => hit.standing !== "superseded");
4806
+ const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
4807
+ const stamped = bundleStamp(current, superseded);
4808
+ const dates = bundle.map((record) => record.frontmatter.generated?.at ?? null).filter((at) => typeof at === "string").sort();
4809
+ return {
4810
+ path: bundlePath2,
4811
+ digest: stamped.digest,
4812
+ recordCount: bundle.length,
4813
+ superseded: superseded.length,
4814
+ newestAt: dates.at(-1) ?? null,
4815
+ records: stamped.records
4816
+ };
4817
+ }
4636
4818
  /** How a position was arrived at, as a timeline. See `trace.ts`. */
4637
4819
  async trace(bundlePath2, seedId, options = {}) {
4638
4820
  return trace(seedId, await this.list(bundlePath2), options);
@@ -4663,7 +4845,7 @@ ${answer}
4663
4845
  async readIndex(bundlePath2) {
4664
4846
  const root = this.root(bundlePath2);
4665
4847
  const expected = renderIndex(await this.list(bundlePath2));
4666
- const stored = await (0, import_promises6.readFile)((0, import_node_path8.join)(root, INDEX_FILE), "utf8").catch(
4848
+ const stored = await (0, import_promises7.readFile)((0, import_node_path8.join)(root, INDEX_FILE), "utf8").catch(
4667
4849
  () => null
4668
4850
  );
4669
4851
  if (indexIsStale(stored, expected)) {
@@ -4684,7 +4866,7 @@ ${answer}
4684
4866
  * knows which agent touched what. So a bad line is surfaced and left alone.
4685
4867
  */
4686
4868
  async readLog(bundlePath2) {
4687
- const raw = await (0, import_promises6.readFile)(
4869
+ const raw = await (0, import_promises7.readFile)(
4688
4870
  (0, import_node_path8.join)(this.root(bundlePath2), LOG_FILE),
4689
4871
  "utf8"
4690
4872
  ).catch(() => "");
@@ -4736,15 +4918,15 @@ ${answer}
4736
4918
  }
4737
4919
  async mutate(bundlePath2, conceptId2, change, entry, changeBody = (body) => body) {
4738
4920
  const target = this.recordPath(bundlePath2, conceptId2);
4739
- const before = await (0, import_promises6.readFile)(target, "utf8").catch(() => null);
4921
+ const before = await (0, import_promises7.readFile)(target, "utf8").catch(() => null);
4740
4922
  if (before === null) throw new KbRecordNotFoundError(conceptId2);
4741
4923
  const parsed = this.parse(conceptId2, before);
4742
4924
  if (!parsed) throw new KbRecordNotFoundError(conceptId2);
4743
4925
  const frontmatter = change(parsed.frontmatter);
4744
4926
  const body = changeBody(parsed.body);
4745
4927
  const contents = stringifyMarkdownWithFrontmatter(body, frontmatter);
4746
- const witness = await (0, import_promises6.readFile)(target, "utf8").catch(() => null);
4747
- if (witness === null || digest(witness) !== digest(before)) {
4928
+ const witness = await (0, import_promises7.readFile)(target, "utf8").catch(() => null);
4929
+ if (witness === null || sha256(witness) !== sha256(before)) {
4748
4930
  throw new KbWriteConflictError(conceptId2);
4749
4931
  }
4750
4932
  await this.publish(target, contents, true, conceptId2);
@@ -4769,20 +4951,20 @@ ${answer}
4769
4951
  */
4770
4952
  async publish(target, contents, overwrite, conceptId2) {
4771
4953
  const staging = `${target}.${process.pid}.tmp`;
4772
- await (0, import_promises6.writeFile)(staging, contents, "utf8");
4954
+ await (0, import_promises7.writeFile)(staging, contents, "utf8");
4773
4955
  try {
4774
4956
  if (overwrite) {
4775
- await (0, import_promises6.rename)(staging, target);
4957
+ await (0, import_promises7.rename)(staging, target);
4776
4958
  return;
4777
4959
  }
4778
- await (0, import_promises6.link)(staging, target);
4960
+ await (0, import_promises7.link)(staging, target);
4779
4961
  } catch (error) {
4780
4962
  if (error.code === "EEXIST") {
4781
4963
  throw new KbRecordAlreadyExistsError(conceptId2);
4782
4964
  }
4783
4965
  throw error;
4784
4966
  } finally {
4785
- await (0, import_promises6.unlink)(staging).catch(() => void 0);
4967
+ await (0, import_promises7.unlink)(staging).catch(() => void 0);
4786
4968
  }
4787
4969
  }
4788
4970
  /**
@@ -4830,14 +5012,14 @@ ${answer}
4830
5012
  try {
4831
5013
  let existing;
4832
5014
  try {
4833
- existing = await (0, import_promises6.readFile)(target, "utf8");
5015
+ existing = await (0, import_promises7.readFile)(target, "utf8");
4834
5016
  } catch (error) {
4835
5017
  if (error.code !== "ENOENT") throw error;
4836
5018
  existing = null;
4837
5019
  }
4838
5020
  if (existing === null) {
4839
5021
  try {
4840
- await (0, import_promises6.writeFile)(target, appendUnionMergeLine(""), {
5022
+ await (0, import_promises7.writeFile)(target, appendUnionMergeLine(""), {
4841
5023
  encoding: "utf8",
4842
5024
  flag: "wx"
4843
5025
  });
@@ -4858,7 +5040,7 @@ ${answer}
4858
5040
  return;
4859
5041
  }
4860
5042
  if (!hasMergeDeclaration(existing)) {
4861
- await (0, import_promises6.appendFile)(target, appendUnionMergeLine(existing), "utf8");
5043
+ await (0, import_promises7.appendFile)(target, appendUnionMergeLine(existing), "utf8");
4862
5044
  this.logger.info?.({
4863
5045
  operation: "kb.gitattributes.ensure",
4864
5046
  bundlePath: root,
@@ -4877,7 +5059,7 @@ ${answer}
4877
5059
  async record(root, entry) {
4878
5060
  await this.ensureGitattributes(root);
4879
5061
  const line = renderLogEntry({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
4880
- await (0, import_promises6.appendFile)((0, import_node_path8.join)(root, LOG_FILE), line, "utf8").catch((error) => {
5062
+ await (0, import_promises7.appendFile)((0, import_node_path8.join)(root, LOG_FILE), line, "utf8").catch((error) => {
4881
5063
  this.logger.warn?.({
4882
5064
  operation: "kb.log.append",
4883
5065
  outcome: "failed",
@@ -4952,33 +5134,18 @@ function normalizeActor(id) {
4952
5134
  if (colon === -1) return id.toLowerCase();
4953
5135
  return id.slice(0, colon + 1).toLowerCase() + id.slice(colon + 1);
4954
5136
  }
4955
- function digest(contents) {
4956
- return (0, import_node_crypto2.createHash)("sha256").update(contents).digest("hex");
4957
- }
4958
- function bundleDigest(records, superseded) {
4959
- const entries = [
4960
- ...records.map(
4961
- (hit) => `${hit.record.conceptId}:current:${digest(
4962
- stringifyMarkdownWithFrontmatter(
4963
- hit.record.body,
4964
- hit.record.frontmatter
4965
- )
4966
- )}`
4967
- ),
4968
- ...superseded.map(
4969
- (entry) => `${entry.conceptId}:superseded:${digest(JSON.stringify(entry))}`
4970
- )
4971
- ].sort();
4972
- return digest(entries.join("\n"));
4973
- }
4974
5137
 
4975
5138
  // src/version.ts
4976
- var VERSION = true ? "0.1.15" : "0.0.0-dev";
5139
+ var VERSION = true ? "0.1.16" : "0.0.0-dev";
4977
5140
 
4978
5141
  // src/cli.ts
4979
5142
  async function runKbCli(argv) {
4980
5143
  const { flags, literal } = takeLiteral(argv);
4981
- const { bundle, rest: withFlags } = takeBundle(flags);
5144
+ const {
5145
+ bundle,
5146
+ explicit: bundleExplicit,
5147
+ rest: withFlags
5148
+ } = takeBundle(flags);
4982
5149
  const name = withFlags[0] ?? "";
4983
5150
  if (!name || name === "-h" || name === "--help") {
4984
5151
  process.stdout.write(usage());
@@ -4999,7 +5166,7 @@ async function runKbCli(argv) {
4999
5166
  ...json ? withFlags.filter((argument) => argument !== "--json") : withFlags,
5000
5167
  ...literal
5001
5168
  ];
5002
- const raw = await command.fromArgv(rest, bundle, readStdin);
5169
+ const raw = await command.fromArgv(rest, bundle, readStdin, bundleExplicit);
5003
5170
  const parsed = command.input.safeParse(raw);
5004
5171
  if (!parsed.success) {
5005
5172
  die(
@@ -5021,6 +5188,7 @@ async function runKbCli(argv) {
5021
5188
  if (command.failsWhen?.(result, parsed.data)) process.exitCode = 1;
5022
5189
  if (result === "") return;
5023
5190
  const text = command.render && !json ? command.render(result) : typeof result === "string" ? result : JSON.stringify(result, null, 2);
5191
+ if (text === "") return;
5024
5192
  process.stdout.write(text.endsWith("\n") ? text : `${text}
5025
5193
  `);
5026
5194
  }
@@ -5032,11 +5200,15 @@ function takeLiteral(argv) {
5032
5200
  function takeBundle(argv) {
5033
5201
  const at = argv.indexOf("--bundle");
5034
5202
  if (at === -1) {
5035
- return { bundle: (0, import_node_path9.join)(process.cwd(), KB_DIR), rest: argv };
5203
+ return { bundle: (0, import_node_path9.join)(process.cwd(), KB_DIR), explicit: false, rest: argv };
5036
5204
  }
5037
5205
  const bundle = argv[at + 1];
5038
5206
  if (!bundle) die("--bundle requires a path");
5039
- return { bundle, rest: [...argv.slice(0, at), ...argv.slice(at + 2)] };
5207
+ return {
5208
+ bundle,
5209
+ explicit: true,
5210
+ rest: [...argv.slice(0, at), ...argv.slice(at + 2)]
5211
+ };
5040
5212
  }
5041
5213
  function readStdin() {
5042
5214
  return new Promise((resolve6, reject) => {