@saasontools/strauss-kb 0.1.22 → 0.1.23

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
@@ -2389,8 +2389,8 @@ function selectDecisions(records) {
2389
2389
  );
2390
2390
  }
2391
2391
 
2392
- // src/commands/anchor-resolve.ts
2393
- var import_zod7 = require("zod");
2392
+ // src/commands/anchor-resolve/command.ts
2393
+ var import_zod8 = require("zod");
2394
2394
 
2395
2395
  // src/kb-errors.ts
2396
2396
  var KbRecordAlreadyExistsError = class extends BaseError {
@@ -2669,6 +2669,73 @@ var KbStampDigestBaselineError = class extends BaseError {
2669
2669
  since;
2670
2670
  };
2671
2671
 
2672
+ // src/commands/model.ts
2673
+ var import_zod5 = require("zod");
2674
+ var bundlePath = import_zod5.z.string().min(1).describe("Absolute path to the knowledge base directory.");
2675
+ var conceptId = import_zod5.z.string().min(1).describe("e.g. decision.cursor-v2");
2676
+ var TAGS = import_zod5.z.array(import_zod5.z.string().min(1)).optional().describe(
2677
+ "Keep only records carrying every one of these frontmatter tags. Matched exactly."
2678
+ );
2679
+ var REPO_ROOT = import_zod5.z.string().min(1).optional().describe(
2680
+ "Where the anchored source lives, for the drift check. Defaults to the working directory."
2681
+ );
2682
+ function define(command) {
2683
+ return command;
2684
+ }
2685
+ function argvFlag(argv, name) {
2686
+ const joined = argv.find((arg) => arg.startsWith(`${name}=`));
2687
+ if (joined !== void 0) {
2688
+ const value2 = joined.slice(name.length + 1);
2689
+ if (!value2) throw new KbMissingFlagValueError(name);
2690
+ return value2;
2691
+ }
2692
+ const at2 = argv.indexOf(name);
2693
+ if (at2 === -1) return void 0;
2694
+ const value = argv[at2 + 1];
2695
+ if (value === void 0 || value.startsWith("--")) {
2696
+ throw new KbMissingFlagValueError(name);
2697
+ }
2698
+ return value;
2699
+ }
2700
+ function argvFlags(argv, name) {
2701
+ const values = [];
2702
+ for (const [at2, arg] of argv.entries()) {
2703
+ if (arg.startsWith(`${name}=`)) {
2704
+ const value = arg.slice(name.length + 1);
2705
+ if (!value) throw new KbMissingFlagValueError(name);
2706
+ values.push(value);
2707
+ } else if (arg === name) {
2708
+ const value = argv[at2 + 1];
2709
+ if (value === void 0 || value.startsWith("--")) {
2710
+ throw new KbMissingFlagValueError(name);
2711
+ }
2712
+ values.push(value);
2713
+ }
2714
+ }
2715
+ return values;
2716
+ }
2717
+ function argvWithout(argv, ...names) {
2718
+ const kept = [];
2719
+ for (let at2 = 0; at2 < argv.length; at2 += 1) {
2720
+ const arg = argv[at2];
2721
+ if (names.some((name) => arg.startsWith(`${name}=`))) continue;
2722
+ if (names.includes(arg)) {
2723
+ at2 += 1;
2724
+ continue;
2725
+ }
2726
+ kept.push(arg);
2727
+ }
2728
+ return kept;
2729
+ }
2730
+ function argvPositional(argv, ...names) {
2731
+ return argvWithout(argv.slice(1), ...names).find(
2732
+ (arg) => !arg.startsWith("--")
2733
+ );
2734
+ }
2735
+
2736
+ // src/commands/anchor-resolve/apply.ts
2737
+ var import_zod7 = require("zod");
2738
+
2672
2739
  // src/kb-pins/budgets.ts
2673
2740
  function asBudgets(value) {
2674
2741
  if (value === null || typeof value !== "object") return {};
@@ -2730,14 +2797,14 @@ var import_node_path7 = require("path");
2730
2797
 
2731
2798
  // src/kb-pins/model.ts
2732
2799
  var import_node_path6 = require("path");
2733
- var import_zod5 = require("zod");
2800
+ var import_zod6 = require("zod");
2734
2801
  var PINS_FILE = (0, import_node_path6.join)(".strauss", "kb-pins.json");
2735
2802
  var PINS_LOCAL_FILE = (0, import_node_path6.join)(".strauss", "kb-pins.local.json");
2736
2803
  var PIN_LAYERS = ["project", "local", "user"];
2737
- var pinSchema = import_zod5.z.object({
2804
+ var pinSchema = import_zod6.z.object({
2738
2805
  /** Relative to the manifest's root, so the file is committable. */
2739
- path: import_zod5.z.string().min(1),
2740
- pinnedAt: import_zod5.z.string().min(1).optional(),
2806
+ path: import_zod6.z.string().min(1),
2807
+ pinnedAt: import_zod6.z.string().min(1).optional(),
2741
2808
  /**
2742
2809
  * How `context` renders this base. `full` preloads the whole base into
2743
2810
  * the block regardless of the full-under threshold — for a base whose
@@ -2747,7 +2814,7 @@ var pinSchema = import_zod5.z.object({
2747
2814
  * Absent: the profile's full-under threshold decides. Invalid values
2748
2815
  * degrade to absent rather than failing the manifest.
2749
2816
  */
2750
- mode: import_zod5.z.enum(["full", "index"]).optional().catch(void 0),
2817
+ mode: import_zod6.z.enum(["full", "index"]).optional().catch(void 0),
2751
2818
  /**
2752
2819
  * Context profiles this pin surfaces in (e.g. only at session-start,
2753
2820
  * not per turn). Absent: every profile. A run without a profile sees
@@ -2755,17 +2822,17 @@ var pinSchema = import_zod5.z.object({
2755
2822
  * that skill at point of use than pinned at all — pins are what every
2756
2823
  * session should see.
2757
2824
  */
2758
- profiles: import_zod5.z.array(import_zod5.z.string()).optional().catch(void 0),
2825
+ profiles: import_zod6.z.array(import_zod6.z.string()).optional().catch(void 0),
2759
2826
  /**
2760
2827
  * The base is concluded — a finished piece of research, a frozen ADR
2761
2828
  * set. Write commands against it refuse while this workspace holds the
2762
2829
  * pin, and `context` labels it read-only. Workspace policy, not base
2763
2830
  * state: the base itself stays copyable and writable elsewhere.
2764
2831
  */
2765
- frozen: import_zod5.z.boolean().optional().catch(void 0)
2832
+ frozen: import_zod6.z.boolean().optional().catch(void 0)
2766
2833
  }).passthrough();
2767
- var pinsManifestSchema = import_zod5.z.object({
2768
- pins: import_zod5.z.array(pinSchema).default([]),
2834
+ var pinsManifestSchema = import_zod6.z.object({
2835
+ pins: import_zod6.z.array(pinSchema).default([]),
2769
2836
  /**
2770
2837
  * Per-repo budgets for the `context` command, keyed by profile —
2771
2838
  * `"session-start"`, `"compact"`, `"turn"`, or `"default"` for all of
@@ -2774,7 +2841,7 @@ var pinsManifestSchema = import_zod5.z.object({
2774
2841
  * the index at every session start. `contextProfileBudgets` does the
2775
2842
  * tolerant read.
2776
2843
  */
2777
- context: import_zod5.z.unknown().optional()
2844
+ context: import_zod6.z.unknown().optional()
2778
2845
  }).passthrough();
2779
2846
 
2780
2847
  // src/kb-pins/layers.ts
@@ -2967,90 +3034,269 @@ async function unpinBase(workspaceDir, bundlePath2) {
2967
3034
  };
2968
3035
  }
2969
3036
 
2970
- // src/commands/model.ts
2971
- var import_zod6 = require("zod");
2972
- var bundlePath = import_zod6.z.string().min(1).describe("Absolute path to the knowledge base directory.");
2973
- var conceptId = import_zod6.z.string().min(1).describe("e.g. decision.cursor-v2");
2974
- var TAGS = import_zod6.z.array(import_zod6.z.string().min(1)).optional().describe(
2975
- "Keep only records carrying every one of these frontmatter tags. Matched exactly."
2976
- );
2977
- var REPO_ROOT = import_zod6.z.string().min(1).optional().describe(
2978
- "Where the anchored source lives, for the drift check. Defaults to the working directory."
2979
- );
2980
- function define(command) {
2981
- return command;
2982
- }
2983
- function argvFlag(argv, name) {
2984
- const joined = argv.find((arg) => arg.startsWith(`${name}=`));
2985
- if (joined !== void 0) {
2986
- const value2 = joined.slice(name.length + 1);
2987
- if (!value2) throw new KbMissingFlagValueError(name);
2988
- return value2;
2989
- }
2990
- const at2 = argv.indexOf(name);
2991
- if (at2 === -1) return void 0;
2992
- const value = argv[at2 + 1];
2993
- if (value === void 0 || value.startsWith("--")) {
2994
- throw new KbMissingFlagValueError(name);
3037
+ // src/commands/anchor-resolve/apply.ts
3038
+ async function baseFrozen(cwd, bundlePath2) {
3039
+ try {
3040
+ await assertBaseNotFrozen(cwd, bundlePath2);
3041
+ return false;
3042
+ } catch (caught) {
3043
+ if (!(caught instanceof KbBaseFrozenError)) throw caught;
3044
+ return true;
2995
3045
  }
2996
- return value;
2997
3046
  }
2998
- function argvFlags(argv, name) {
2999
- const values = [];
3000
- for (const [at2, arg] of argv.entries()) {
3001
- if (arg.startsWith(`${name}=`)) {
3002
- const value = arg.slice(name.length + 1);
3003
- if (!value) throw new KbMissingFlagValueError(name);
3004
- values.push(value);
3005
- } else if (arg === name) {
3006
- const value = argv[at2 + 1];
3007
- if (value === void 0 || value.startsWith("--")) {
3008
- throw new KbMissingFlagValueError(name);
3047
+ async function applyPlan(plans, target) {
3048
+ if (!plans.some((plan) => plan.write)) {
3049
+ return { results: plans.map((plan) => plan.finding) };
3050
+ }
3051
+ let failure = target.frozen ? "frozen" : void 0;
3052
+ let error;
3053
+ if (!failure) {
3054
+ try {
3055
+ await target.store.updateAnchors(
3056
+ target.bundlePath,
3057
+ target.conceptId,
3058
+ plans.map((plan) => plan.anchor),
3059
+ target.actor
3060
+ );
3061
+ } catch (caught) {
3062
+ if (caught instanceof BaseError || caught instanceof import_zod7.z.ZodError) {
3063
+ throw caught;
3009
3064
  }
3010
- values.push(value);
3065
+ failure = "write-failed";
3066
+ error = clamp(caught instanceof Error ? caught.message : String(caught));
3011
3067
  }
3012
3068
  }
3013
- return values;
3069
+ return {
3070
+ results: plans.map((plan) => settle(plan, failure)),
3071
+ ...error ? { error } : {}
3072
+ };
3014
3073
  }
3015
- function argvWithout(argv, ...names) {
3016
- const kept = [];
3017
- for (let at2 = 0; at2 < argv.length; at2 += 1) {
3018
- const arg = argv[at2];
3019
- if (names.some((name) => arg.startsWith(`${name}=`))) continue;
3020
- if (names.includes(arg)) {
3021
- at2 += 1;
3074
+ function settle(plan, failure) {
3075
+ if (!plan.write) return plan.finding;
3076
+ if (failure) {
3077
+ return { ...plan.finding, outcome: "failed", outcomeReason: failure };
3078
+ }
3079
+ if (plan.write === "refresh") return plan.finding;
3080
+ return plan.write === "stamp" ? { ...plan.finding, state: "stamped", outcome: "applied" } : { ...plan.finding, outcome: "applied", rebaselined: true };
3081
+ }
3082
+ function clamp(message) {
3083
+ const line = message.split("\n")[0] ?? "";
3084
+ return line.length > 200 ? `${line.slice(0, 199)}\u2026` : line;
3085
+ }
3086
+
3087
+ // src/commands/anchor-resolve/sources.ts
3088
+ async function readSources(anchors, root, offline) {
3089
+ const origin = new LazyOrigin(root);
3090
+ if (anchors.some((anchor) => anchor.repo)) await origin.prime();
3091
+ const foreign = new Map(
3092
+ anchors.map((anchor) => [anchor, origin.isForeign(anchor)])
3093
+ );
3094
+ const local = anchors.filter(
3095
+ (anchor) => !foreign.get(anchor) && anchor.side !== "old"
3096
+ );
3097
+ const committed = anchors.filter(
3098
+ (anchor) => !foreign.get(anchor) && anchor.side === "old"
3099
+ );
3100
+ const remote = anchors.filter((anchor) => foreign.get(anchor));
3101
+ const reads = await readAnchorFiles(
3102
+ local.map((anchor) => anchor.file),
3103
+ anchorFileReader(root)
3104
+ );
3105
+ const atRef = await readCommitted(root, committed);
3106
+ const blobs = await readRemoteAnchors(remote.flatMap(remoteWants), {
3107
+ offline
3108
+ });
3109
+ const sources = /* @__PURE__ */ new Map();
3110
+ for (const anchor of local) {
3111
+ const read = reads.get(anchor.file);
3112
+ sources.set(
3113
+ anchor,
3114
+ read.ok ? { ok: true, source: read.source } : { ok: false, reason: read.reason }
3115
+ );
3116
+ }
3117
+ for (const anchor of committed) {
3118
+ const read = atRef.get(atRefKey(anchor));
3119
+ sources.set(
3120
+ anchor,
3121
+ read.ok ? { ok: true, source: read.source } : { ok: false, reason: read.reason }
3122
+ );
3123
+ }
3124
+ for (const anchor of remote) {
3125
+ const repo = anchor.repo;
3126
+ const key2 = normalizeRepoUrl(repo);
3127
+ const atDefault = blobs.get(wantKey(key2, void 0, anchor.file));
3128
+ const primary = anchor.ref ? blobs.get(wantKey(key2, anchor.ref, anchor.file)) : atDefault;
3129
+ if (!primary?.ok) {
3130
+ sources.set(anchor, {
3131
+ ok: false,
3132
+ reason: primary?.ok === false ? primary.reason : "remote-unreachable",
3133
+ repo
3134
+ });
3022
3135
  continue;
3023
3136
  }
3024
- kept.push(arg);
3137
+ sources.set(anchor, {
3138
+ ok: true,
3139
+ source: primary.source,
3140
+ repo,
3141
+ ...anchor.ref && atDefault?.ok ? { head: atDefault.source } : {}
3142
+ });
3025
3143
  }
3026
- return kept;
3144
+ return sources;
3027
3145
  }
3028
- function argvPositional(argv, ...names) {
3029
- return argvWithout(argv.slice(1), ...names).find(
3030
- (arg) => !arg.startsWith("--")
3146
+
3147
+ // src/commands/anchor-resolve/plan.ts
3148
+ async function planAnchors(anchors, options) {
3149
+ const { root, offline, rebaseline, restamp, check, frozen, now } = options;
3150
+ const sources = await readSources(anchors, root, offline);
3151
+ const resolvers = defaultAnchorResolvers({ offline });
3152
+ await prepareResolvers(
3153
+ resolvers,
3154
+ anchors.map((anchor) => anchor.file)
3031
3155
  );
3156
+ const plans = [];
3157
+ for (const anchor of anchors) {
3158
+ const base2 = {
3159
+ file: anchor.file,
3160
+ ...anchor.symbol ? { symbol: anchor.symbol } : {},
3161
+ ...anchor.side === "old" ? { side: "old" } : {},
3162
+ // Carried onto unresolved findings too: an anchor that once hashed
3163
+ // and now resolves to nothing is a broken anchor, and the exit code
3164
+ // has to be able to tell it from one nobody ever stamped.
3165
+ ...anchor.hash ? { storedHash: anchor.hash } : {}
3166
+ };
3167
+ const source = sources.get(anchor);
3168
+ if (source.repo) base2.repo = source.repo;
3169
+ if (!source.ok) {
3170
+ plans.push({
3171
+ finding: { ...base2, state: "unresolved", reason: source.reason },
3172
+ anchor
3173
+ });
3174
+ continue;
3175
+ }
3176
+ const outcome = resolveAnchorSpan(source.source, anchor, resolvers);
3177
+ if (!outcome.ok) {
3178
+ plans.push({
3179
+ finding: { ...base2, state: "unresolved", reason: outcome.reason },
3180
+ anchor
3181
+ });
3182
+ continue;
3183
+ }
3184
+ const resolved = outcome.span;
3185
+ const producedBy = outcome.resolver;
3186
+ const { hash: currentHash, kind } = anchorHashOf(anchor, outcome);
3187
+ const currentLines = resolved.endLine - resolved.startLine + 1;
3188
+ const stampedKind = outcome.normalized ? "ast" : "raw";
3189
+ const stampedHash = outcome.normalized ? anchorHashOf({ ...anchor, hash: void 0 }, outcome).hash : currentHash;
3190
+ const stamped = {
3191
+ ...anchor,
3192
+ hash: stampedHash,
3193
+ hash_kind: stampedKind,
3194
+ lines: currentLines,
3195
+ resolved_at: now(),
3196
+ ...producedBy ? { resolver: producedBy } : {}
3197
+ };
3198
+ const pinned = anchor.ref !== void 0 && source.repo !== void 0;
3199
+ if (!anchor.hash) {
3200
+ plans.push({
3201
+ finding: {
3202
+ ...base2,
3203
+ state: "unstamped",
3204
+ currentHash: stampedHash,
3205
+ hashKind: stampedKind,
3206
+ ...producedBy ? { resolver: producedBy } : {}
3207
+ },
3208
+ anchor: check ? anchor : stamped,
3209
+ ...check ? {} : { write: "stamp" }
3210
+ });
3211
+ continue;
3212
+ }
3213
+ if (anchor.hash !== currentHash) {
3214
+ plans.push({
3215
+ finding: {
3216
+ ...base2,
3217
+ state: "drifted",
3218
+ currentHash,
3219
+ hashKind: kind,
3220
+ diffSize: lineDelta(anchor, currentLines),
3221
+ ...producedBy ? { resolver: producedBy } : {},
3222
+ // A regex-stamped anchor re-read by tree-sitter drifts because the
3223
+ // resolver changed, not because the code did.
3224
+ ...resolverChanged(source.source, anchor, producedBy) ? { reason: "resolver-changed" } : {},
3225
+ ...pinned ? { remoteState: "drifted-from-ref" } : {}
3226
+ },
3227
+ anchor: rebaseline && !check ? stamped : anchor,
3228
+ ...rebaseline && !check ? { write: "rebaseline" } : {}
3229
+ });
3230
+ continue;
3231
+ }
3232
+ const onDefault = pinned ? headHash(source, anchor, resolvers) : void 0;
3233
+ if (onDefault && onDefault.hash !== anchor.hash) {
3234
+ plans.push({
3235
+ finding: {
3236
+ ...base2,
3237
+ state: "drifted",
3238
+ currentHash: onDefault.hash,
3239
+ diffSize: lineDelta(anchor, onDefault.lines),
3240
+ remoteState: "drifted-on-default",
3241
+ ...rebaseline ? {
3242
+ outcome: "skipped",
3243
+ outcomeReason: "pinned-ref"
3244
+ } : {}
3245
+ },
3246
+ anchor
3247
+ });
3248
+ continue;
3249
+ }
3250
+ const backfill = anchor.resolved_at === void 0 && !frozen;
3251
+ const refresh = !check && (restamp || backfill);
3252
+ plans.push({
3253
+ finding: {
3254
+ ...base2,
3255
+ state: "match",
3256
+ currentHash,
3257
+ hashKind: kind,
3258
+ ...producedBy ? { resolver: producedBy } : {},
3259
+ ...pinned ? { remoteState: "matches-ref" } : {}
3260
+ },
3261
+ anchor: refresh ? { ...anchor, resolved_at: now() } : anchor,
3262
+ ...refresh ? { write: "refresh" } : {}
3263
+ });
3264
+ }
3265
+ return plans;
3266
+ }
3267
+ function lineDelta(anchor, current) {
3268
+ return anchor.lines === void 0 ? null : Math.abs(current - anchor.lines);
3269
+ }
3270
+ function headHash(source, anchor, resolvers) {
3271
+ if (source.head === void 0) return void 0;
3272
+ const outcome = resolveAnchorSpan(source.head, anchor, resolvers);
3273
+ if (!outcome.ok) return void 0;
3274
+ return {
3275
+ hash: hashAnchorText(outcome.span.text),
3276
+ lines: outcome.span.endLine - outcome.span.startLine + 1
3277
+ };
3032
3278
  }
3033
3279
 
3034
- // src/commands/anchor-resolve.ts
3280
+ // src/commands/anchor-resolve/command.ts
3035
3281
  var anchorResolveCommand = define({
3036
3282
  name: "anchor-resolve",
3037
3283
  tool: "kb_anchor_resolve",
3038
3284
  usage: "anchor-resolve <concept-id> [--repo-root <path>] [--offline] [--rebaseline] [--restamp] [--check]",
3039
- description: "Resolve a record's anchors: stamp a hash onto anchors that lack one, report drift where the code moved. An anchor naming another repository is read from that remote through a bare cache; --offline uses the cache only. Never writes verified[]; a judgment is kb_verify. Exits non-zero on drift.",
3040
- input: import_zod7.z.object({
3285
+ description: "Resolve a record's anchors: stamp a hash onto anchors that lack one, report drift where the code moved. An anchor naming another repository is read from that remote through a bare cache; --offline uses the cache only. Never writes verified[]; a judgment is kb_verify. Each result says what it compared and whether the write applied.",
3286
+ input: import_zod8.z.object({
3041
3287
  bundlePath,
3042
3288
  conceptId,
3043
- repoRoot: import_zod7.z.string().min(1).optional(),
3044
- offline: import_zod7.z.boolean().optional().describe(
3289
+ repoRoot: import_zod8.z.string().min(1).optional(),
3290
+ offline: import_zod8.z.boolean().optional().describe(
3045
3291
  "Resolve foreign anchors from the local repo cache only, never fetching."
3046
3292
  ),
3047
- rebaseline: import_zod7.z.boolean().optional().describe(
3293
+ rebaseline: import_zod8.z.boolean().optional().describe(
3048
3294
  "Accept the current code as the new baseline for anchors that drifted."
3049
3295
  ),
3050
- restamp: import_zod7.z.boolean().optional().describe(
3296
+ restamp: import_zod8.z.boolean().optional().describe(
3051
3297
  "Refresh `resolved_at` on anchors that already match. Off by default, so a green run writes nothing."
3052
3298
  ),
3053
- check: import_zod7.z.boolean().optional().describe(
3299
+ check: import_zod8.z.boolean().optional().describe(
3054
3300
  "Resolve and report only: no hash, no `resolved_at`, no log entry."
3055
3301
  )
3056
3302
  }),
@@ -3089,246 +3335,87 @@ var anchorResolveCommand = define({
3089
3335
  note: "record has no anchors"
3090
3336
  };
3091
3337
  }
3092
- const results = [];
3093
- const updated = [];
3094
- let dirty = false;
3095
- const sources = await readSources(anchors, root, offline === true);
3096
- const resolvers = defaultAnchorResolvers({ offline: offline === true });
3097
- await prepareResolvers(
3098
- resolvers,
3099
- anchors.map((anchor) => anchor.file)
3100
- );
3101
- for (const anchor of anchors) {
3102
- const base2 = {
3103
- file: anchor.file,
3104
- ...anchor.symbol ? { symbol: anchor.symbol } : {},
3105
- ...anchor.side === "old" ? { side: "old" } : {},
3106
- // Carried onto unresolved findings too: an anchor that once hashed
3107
- // and now resolves to nothing is a broken anchor, and the exit code
3108
- // has to be able to tell it from one nobody ever stamped.
3109
- ...anchor.hash ? { storedHash: anchor.hash } : {}
3110
- };
3111
- const source = sources.get(anchor);
3112
- if (source.repo) base2.repo = source.repo;
3113
- if (!source.ok) {
3114
- results.push({ ...base2, state: "unresolved", reason: source.reason });
3115
- updated.push(anchor);
3116
- continue;
3117
- }
3118
- const outcome = resolveAnchorSpan(source.source, anchor, resolvers);
3119
- if (!outcome.ok) {
3120
- results.push({
3121
- ...base2,
3122
- state: "unresolved",
3123
- reason: outcome.reason
3124
- });
3125
- updated.push(anchor);
3126
- continue;
3127
- }
3128
- const resolved = outcome.span;
3129
- const producedBy = outcome.resolver;
3130
- const { hash: currentHash, kind } = anchorHashOf(anchor, outcome);
3131
- const currentLines = resolved.endLine - resolved.startLine + 1;
3132
- const stampedKind = outcome.normalized ? "ast" : "raw";
3133
- const stampedHash = outcome.normalized ? anchorHashOf({ ...anchor, hash: void 0 }, outcome).hash : currentHash;
3134
- const stamped = {
3135
- ...anchor,
3136
- hash: stampedHash,
3137
- hash_kind: stampedKind,
3138
- lines: currentLines,
3139
- resolved_at: now(),
3140
- ...producedBy ? { resolver: producedBy } : {}
3141
- };
3142
- const pinned = anchor.ref !== void 0 && source.repo !== void 0;
3143
- if (!anchor.hash) {
3144
- results.push({
3145
- ...base2,
3146
- state: check ? "unstamped" : "stamped",
3147
- currentHash: stampedHash,
3148
- hashKind: stampedKind,
3149
- ...producedBy ? { resolver: producedBy } : {}
3150
- });
3151
- updated.push(stamped);
3152
- dirty = true;
3153
- continue;
3154
- }
3155
- if (anchor.hash !== currentHash) {
3156
- results.push({
3157
- ...base2,
3158
- state: "drifted",
3159
- currentHash,
3160
- hashKind: kind,
3161
- diffSize: lineDelta(anchor, currentLines),
3162
- ...producedBy ? { resolver: producedBy } : {},
3163
- // A regex-stamped anchor re-read by tree-sitter drifts because the
3164
- // resolver changed, not because the code did.
3165
- ...resolverChanged(source.source, anchor, producedBy) ? { reason: "resolver-changed" } : {},
3166
- ...pinned ? { remoteState: "drifted-from-ref" } : {},
3167
- ...rebaseline ? { rebaselined: true } : {}
3168
- });
3169
- updated.push(rebaseline ? stamped : anchor);
3170
- if (rebaseline) dirty = true;
3171
- continue;
3172
- }
3173
- const onDefault = pinned ? headHash(source, anchor, resolvers) : void 0;
3174
- if (onDefault && onDefault.hash !== anchor.hash) {
3175
- results.push({
3176
- ...base2,
3177
- state: "drifted",
3178
- currentHash: onDefault.hash,
3179
- diffSize: lineDelta(anchor, onDefault.lines),
3180
- remoteState: "drifted-on-default"
3181
- });
3182
- updated.push(anchor);
3183
- continue;
3184
- }
3185
- results.push({
3186
- ...base2,
3187
- state: "match",
3188
- currentHash,
3189
- hashKind: kind,
3190
- ...producedBy ? { resolver: producedBy } : {},
3191
- ...pinned ? { remoteState: "matches-ref" } : {}
3192
- });
3193
- const refresh = restamp || anchor.resolved_at === void 0;
3194
- updated.push(refresh ? { ...anchor, resolved_at: now() } : anchor);
3195
- if (refresh) dirty = true;
3196
- }
3197
- let frozen = false;
3198
- if (dirty && !check) {
3199
- try {
3200
- await assertBaseNotFrozen(process.cwd(), path);
3201
- } catch (error) {
3202
- if (!(error instanceof KbBaseFrozenError)) throw error;
3203
- frozen = true;
3204
- }
3205
- if (!frozen) await store.updateAnchors(path, id, updated, actor);
3206
- }
3207
- const frozenNote = frozen ? { frozen: true, note: "base is frozen: nothing was stamped" } : {};
3338
+ const frozen = check ? false : await baseFrozen(process.cwd(), path);
3339
+ const plans = await planAnchors(anchors, {
3340
+ root,
3341
+ offline: offline === true,
3342
+ rebaseline: rebaseline === true,
3343
+ restamp: restamp === true,
3344
+ check: check === true,
3345
+ frozen,
3346
+ now
3347
+ });
3348
+ const applied = await applyPlan(plans, {
3349
+ store,
3350
+ actor,
3351
+ bundlePath: path,
3352
+ conceptId: id,
3353
+ frozen
3354
+ });
3355
+ const results = applied.results;
3356
+ const refused = frozen && plans.some((plan) => plan.write);
3208
3357
  const hints = grammarHints();
3209
3358
  const hintNote = hints.length ? { hints } : {};
3210
3359
  const unreachable = results.filter(
3211
3360
  (entry) => isUncheckedReason(entry.reason)
3212
3361
  ).length;
3213
3362
  const matches3 = results.filter((entry) => entry.state === "match").length;
3214
- const note = `${matches3}/${results.length - unreachable} anchors match, ${unreachable} unreachable`;
3363
+ const note = [
3364
+ unreachable ? `${matches3}/${results.length - unreachable} anchors match, ${unreachable} unreachable` : "",
3365
+ refused ? "base is frozen: nothing was stamped" : "",
3366
+ applied.error ? `nothing was written: ${applied.error}` : ""
3367
+ ].filter(Boolean).join("; ");
3215
3368
  return {
3216
3369
  conceptId: id,
3217
3370
  results,
3218
- ...unreachable ? { note } : {},
3219
- ...frozenNote,
3371
+ ...note ? { note } : {},
3372
+ ...refused ? { frozen: true } : {},
3220
3373
  ...hintNote
3221
3374
  };
3222
3375
  },
3376
+ // Drift is a finding until a write settles it: a rebaseline the base took is
3377
+ // the answer to the drift it reports, while one refused, skipped, or never
3378
+ // asked for leaves the gate exactly what it was meant to catch.
3379
+ //
3223
3380
  // A stored hash that no longer resolves is a broken anchor, not an absence:
3224
- // the file was deleted or the symbol renamed, and exiting zero on it would
3225
- // let the one edit that destroys an anchor pass the gate that exists to
3226
- // catch it. An anchor nobody ever stamped is still just unstamped, and one
3227
- // whose remote nothing could reach was never checked — failing CI on either
3228
- // would gate on work this command did not do.
3381
+ // the file was deleted or the symbol renamed. An anchor nobody ever stamped
3382
+ // is still just unstamped, and one whose remote nothing could reach was
3383
+ // never checked failing CI on either would gate on work this command did
3384
+ // not do.
3229
3385
  failsWhen: (result) => result.results.some(
3230
- (entry) => entry.state === "drifted" || entry.state === "unresolved" && entry.storedHash !== void 0 && !isUncheckedReason(entry.reason)
3386
+ (entry) => entry.outcome === "failed" || entry.outcome === "skipped" || entry.state === "drifted" && entry.outcome !== "applied" || entry.state === "unresolved" && entry.storedHash !== void 0 && !isUncheckedReason(entry.reason)
3231
3387
  )
3232
3388
  });
3233
- function lineDelta(anchor, current) {
3234
- return anchor.lines === void 0 ? null : Math.abs(current - anchor.lines);
3235
- }
3236
- function headHash(source, anchor, resolvers) {
3237
- if (source.head === void 0) return void 0;
3238
- const outcome = resolveAnchorSpan(source.head, anchor, resolvers);
3239
- if (!outcome.ok) return void 0;
3240
- return {
3241
- hash: hashAnchorText(outcome.span.text),
3242
- lines: outcome.span.endLine - outcome.span.startLine + 1
3243
- };
3244
- }
3245
- async function readSources(anchors, root, offline) {
3246
- const origin = new LazyOrigin(root);
3247
- if (anchors.some((anchor) => anchor.repo)) await origin.prime();
3248
- const foreign = new Map(
3249
- anchors.map((anchor) => [anchor, origin.isForeign(anchor)])
3250
- );
3251
- const local = anchors.filter(
3252
- (anchor) => !foreign.get(anchor) && anchor.side !== "old"
3253
- );
3254
- const committed = anchors.filter(
3255
- (anchor) => !foreign.get(anchor) && anchor.side === "old"
3256
- );
3257
- const remote = anchors.filter((anchor) => foreign.get(anchor));
3258
- const reads = await readAnchorFiles(
3259
- local.map((anchor) => anchor.file),
3260
- anchorFileReader(root)
3261
- );
3262
- const atRef = await readCommitted(root, committed);
3263
- const blobs = await readRemoteAnchors(remote.flatMap(remoteWants), {
3264
- offline
3265
- });
3266
- const sources = /* @__PURE__ */ new Map();
3267
- for (const anchor of local) {
3268
- const read = reads.get(anchor.file);
3269
- sources.set(
3270
- anchor,
3271
- read.ok ? { ok: true, source: read.source } : { ok: false, reason: read.reason }
3272
- );
3273
- }
3274
- for (const anchor of committed) {
3275
- const read = atRef.get(atRefKey(anchor));
3276
- sources.set(
3277
- anchor,
3278
- read.ok ? { ok: true, source: read.source } : { ok: false, reason: read.reason }
3279
- );
3280
- }
3281
- for (const anchor of remote) {
3282
- const repo = anchor.repo;
3283
- const key2 = normalizeRepoUrl(repo);
3284
- const atDefault = blobs.get(wantKey(key2, void 0, anchor.file));
3285
- const primary = anchor.ref ? blobs.get(wantKey(key2, anchor.ref, anchor.file)) : atDefault;
3286
- if (!primary?.ok) {
3287
- sources.set(anchor, {
3288
- ok: false,
3289
- reason: primary?.ok === false ? primary.reason : "remote-unreachable",
3290
- repo
3291
- });
3292
- continue;
3293
- }
3294
- sources.set(anchor, {
3295
- ok: true,
3296
- source: primary.source,
3297
- repo,
3298
- ...anchor.ref && atDefault?.ok ? { head: atDefault.source } : {}
3299
- });
3300
- }
3301
- return sources;
3302
- }
3303
3389
 
3304
3390
  // src/commands/anchor-set/model.ts
3305
- var import_zod8 = require("zod");
3306
- var anchorSetInputSchema = import_zod8.z.object({
3307
- reason: import_zod8.z.string().refine((text) => text.trim().length > 0, {
3391
+ var import_zod9 = require("zod");
3392
+ var anchorSetInputSchema = import_zod9.z.object({
3393
+ reason: import_zod9.z.string().refine((text) => text.trim().length > 0, {
3308
3394
  message: "reason must say what was reviewed"
3309
3395
  }).describe(
3310
3396
  "What the reviewer read that makes these the right pointers. Recorded in the log."
3311
3397
  ),
3312
- anchors: import_zod8.z.array(kbAnchorWriteSchema).min(1).describe(
3398
+ anchors: import_zod9.z.array(kbAnchorWriteSchema).min(1).describe(
3313
3399
  "The complete new anchor set. Carry an anchor's hash forward to keep drift visible until the new code is read."
3314
3400
  )
3315
3401
  }).strict();
3316
- var anchorSetCommandInput = import_zod8.z.object({
3402
+ var anchorSetCommandInput = import_zod9.z.object({
3317
3403
  bundlePath,
3318
3404
  conceptId,
3319
3405
  input: anchorSetInputSchema,
3320
- resolve: import_zod8.z.boolean().optional().describe(
3406
+ resolve: import_zod9.z.boolean().optional().describe(
3321
3407
  "Also resolve and stamp every anchor against the current code, as anchor-resolve --rebaseline does."
3322
3408
  ),
3323
- repoRoot: import_zod8.z.string().min(1).optional().describe(
3409
+ repoRoot: import_zod9.z.string().min(1).optional().describe(
3324
3410
  "Where the anchored source lives, for resolve. Defaults to the working directory."
3325
3411
  ),
3326
- offline: import_zod8.z.boolean().optional().describe("With resolve, read foreign anchors from the repo cache only.")
3412
+ offline: import_zod9.z.boolean().optional().describe("With resolve, read foreign anchors from the repo cache only.")
3327
3413
  });
3328
3414
 
3329
3415
  // src/commands/anchor-set/command.ts
3330
3416
  var NOTE = "pointers only: nothing was resolved or verified. Run anchor-resolve to check the new pointers, --rebaseline to accept the code, or pass resolve to do both here.";
3331
3417
  var STAMPED_NOTE = "pointers set and stamped against the current code. Not verification: run verify separately if someone reviewed it.";
3418
+ var INCOMPLETE_NOTE = "pointers set, but not every anchor was stamped: see each resolved entry's state and outcome.";
3332
3419
  var anchorSetCommand = define({
3333
3420
  name: "anchor-set",
3334
3421
  tool: "kb_anchor_set",
@@ -3385,33 +3472,39 @@ var anchorSetCommand = define({
3385
3472
  })
3386
3473
  );
3387
3474
  const after = await store.read(path, id);
3475
+ const stamped = resolved.results.every(
3476
+ (entry) => entry.state === "match" || entry.outcome === "applied"
3477
+ );
3388
3478
  return {
3389
3479
  conceptId: id,
3390
3480
  reason: input.reason,
3391
3481
  changes,
3392
3482
  anchors: after?.frontmatter.strauss_anchors ?? [],
3393
- baseline: "stamped",
3483
+ baseline: stamped ? "stamped" : "incomplete",
3394
3484
  resolved: resolved.results,
3395
- note: STAMPED_NOTE
3485
+ note: stamped ? STAMPED_NOTE : INCOMPLETE_NOTE
3396
3486
  };
3397
3487
  },
3398
3488
  // With `resolve`, a pointer that names nothing is a failed set, not a
3399
- // finding to read later. A remote nothing could reach was never checked, so
3400
- // it does not fail the same line anchor-resolve draws.
3401
- failsWhen: (result) => (result.resolved ?? []).some((entry) => {
3402
- const { state, reason } = entry;
3403
- return state === "unresolved" && !isUncheckedReason(reason);
3404
- })
3489
+ // finding to read later, and a stamp that did not land fails as it does in
3490
+ // anchor-resolve. A remote nothing could reach was never checked, so it does
3491
+ // not fail the same line anchor-resolve draws.
3492
+ failsWhen: (result, input) => {
3493
+ const resolved = result.resolved ?? [];
3494
+ return resolved.some(
3495
+ (entry) => entry.state === "unresolved" && !isUncheckedReason(entry.reason)
3496
+ ) || anchorResolveCommand.failsWhen?.({ results: resolved }, input) === true;
3497
+ }
3405
3498
  });
3406
3499
 
3407
3500
  // src/commands/answer.ts
3408
- var import_zod9 = require("zod");
3501
+ var import_zod10 = require("zod");
3409
3502
  var answerCommand = define({
3410
3503
  name: "answer",
3411
3504
  tool: "kb_answer",
3412
3505
  usage: "answer <concept-id> <answer...>",
3413
3506
  description: "Resolve an open question: set status, stamp who and when, append an Answer section. If the answer overturns a decision or assumption, supersede that record explicitly.",
3414
- input: import_zod9.z.object({ bundlePath, conceptId, answer: import_zod9.z.string().min(1) }),
3507
+ input: import_zod10.z.object({ bundlePath, conceptId, answer: import_zod10.z.string().min(1) }),
3415
3508
  fromArgv: (argv, path) => ({
3416
3509
  bundlePath: path,
3417
3510
  conceptId: argv[1],
@@ -3425,19 +3518,19 @@ var answerCommand = define({
3425
3518
  });
3426
3519
 
3427
3520
  // src/commands/backlinks.ts
3428
- var import_zod10 = require("zod");
3521
+ var import_zod11 = require("zod");
3429
3522
  var backlinksCommand = define({
3430
3523
  name: "backlinks",
3431
3524
  tool: "kb_backlinks",
3432
3525
  usage: "backlinks <concept-id>",
3433
3526
  description: "Who points at this record: every inbound typed causal link (`strauss_links`), one hop, every rel including `related_to`, each with its rel and the standing of the record that made it. Use it when you need the exact edges \u2014 reviewing or renaming a record.",
3434
- input: import_zod10.z.object({ bundlePath, conceptId }),
3527
+ input: import_zod11.z.object({ bundlePath, conceptId }),
3435
3528
  fromArgv: (argv, path) => ({ bundlePath: path, conceptId: argv[1] }),
3436
3529
  run: async ({ store }, { bundlePath: path, conceptId: id }) => store.backlinks(path, id)
3437
3530
  });
3438
3531
 
3439
3532
  // src/commands/catalog.ts
3440
- var import_zod11 = require("zod");
3533
+ var import_zod12 = require("zod");
3441
3534
 
3442
3535
  // src/adjudicate.ts
3443
3536
  var STANDING = {
@@ -3616,9 +3709,9 @@ var catalogCommand = define({
3616
3709
  tool: "kb_catalog",
3617
3710
  usage: "catalog [type] [--tag T]...",
3618
3711
  description: "Lists every record as one line \u2014 concept id, type, title, standing, and a stale flag \u2014 at roughly thirty tokens each. Pick this over kb_load once kb_load refuses: kb_catalog never refuses. Superseded records show only their replacement; fetch bodies with kb_load, kb_pack, kb_query, or kb_trace.",
3619
- input: import_zod11.z.object({
3712
+ input: import_zod12.z.object({
3620
3713
  bundlePath,
3621
- type: import_zod11.z.enum(KB_RECORD_TYPES).optional(),
3714
+ type: import_zod12.z.enum(KB_RECORD_TYPES).optional(),
3622
3715
  tags: TAGS
3623
3716
  }),
3624
3717
  fromArgv: (argv, path) => {
@@ -3690,7 +3783,7 @@ function count(value, noun) {
3690
3783
  var import_node_buffer = require("buffer");
3691
3784
  var import_promises7 = require("fs/promises");
3692
3785
  var import_node_path10 = require("path");
3693
- var import_zod14 = require("zod");
3786
+ var import_zod15 = require("zod");
3694
3787
 
3695
3788
  // src/match-diff.ts
3696
3789
  function matchToDiff(files, records, options = {}) {
@@ -4294,7 +4387,7 @@ function claimOf(record) {
4294
4387
  }
4295
4388
 
4296
4389
  // src/commands/match/command.ts
4297
- var import_zod13 = require("zod");
4390
+ var import_zod14 = require("zod");
4298
4391
 
4299
4392
  // src/commands/match/errors.ts
4300
4393
  var KbMatchInputError = class extends BaseError {
@@ -4314,21 +4407,21 @@ var KbMatchInputError = class extends BaseError {
4314
4407
  };
4315
4408
 
4316
4409
  // src/commands/match/model.ts
4317
- var import_zod12 = require("zod");
4318
- var diffHunkSchema = import_zod12.z.object({
4319
- startLine: import_zod12.z.number().int().positive(),
4320
- endLine: import_zod12.z.number().int().positive(),
4321
- side: import_zod12.z.enum(["old", "new"]).optional()
4410
+ var import_zod13 = require("zod");
4411
+ var diffHunkSchema = import_zod13.z.object({
4412
+ startLine: import_zod13.z.number().int().positive(),
4413
+ endLine: import_zod13.z.number().int().positive(),
4414
+ side: import_zod13.z.enum(["old", "new"]).optional()
4322
4415
  }).passthrough();
4323
- var diffFileSchema = import_zod12.z.object({
4324
- filePath: import_zod12.z.string().min(1).describe("Repo-relative, spelled the way anchors are."),
4325
- hunks: import_zod12.z.array(diffHunkSchema)
4416
+ var diffFileSchema = import_zod13.z.object({
4417
+ filePath: import_zod13.z.string().min(1).describe("Repo-relative, spelled the way anchors are."),
4418
+ hunks: import_zod13.z.array(diffHunkSchema)
4326
4419
  });
4327
- var symbolRangeSchema = import_zod12.z.object({
4328
- file: import_zod12.z.string().min(1),
4329
- symbol: import_zod12.z.string().min(1),
4330
- startLine: import_zod12.z.number().int().positive(),
4331
- endLine: import_zod12.z.number().int().positive()
4420
+ var symbolRangeSchema = import_zod13.z.object({
4421
+ file: import_zod13.z.string().min(1),
4422
+ symbol: import_zod13.z.string().min(1),
4423
+ startLine: import_zod13.z.number().int().positive(),
4424
+ endLine: import_zod13.z.number().int().positive()
4332
4425
  });
4333
4426
 
4334
4427
  // src/commands/match/parse-unified-diff.ts
@@ -4577,17 +4670,17 @@ var matchCommand = define({
4577
4670
  tool: "kb_match",
4578
4671
  usage: "match --git <base>..<head> | --stdin [--repo-root <path>] [--offline] [--include-non-current]",
4579
4672
  description: "Which records sit on each changed hunk: the anchored records per file range, current first, each with its standing and the anchor that matched. kb_load hands over a whole base; this narrows a diff. Symbol ranges resolve from repoRoot when omitted; non-current records need includeNonCurrent.",
4580
- input: import_zod13.z.object({
4673
+ input: import_zod14.z.object({
4581
4674
  bundlePath,
4582
- files: import_zod13.z.array(diffFileSchema).describe("The changed files, each with its post-change line ranges."),
4583
- symbolRanges: import_zod13.z.array(symbolRangeSchema).optional().describe(
4675
+ files: import_zod14.z.array(diffFileSchema).describe("The changed files, each with its post-change line ranges."),
4676
+ symbolRanges: import_zod14.z.array(symbolRangeSchema).optional().describe(
4584
4677
  "Symbol spans the caller already has. Resolved from repoRoot when omitted."
4585
4678
  ),
4586
4679
  repoRoot: REPO_ROOT,
4587
- offline: import_zod13.z.boolean().optional().describe(
4680
+ offline: import_zod14.z.boolean().optional().describe(
4588
4681
  "Resolve symbol ranges from what is already on disk, never fetching a grammar."
4589
4682
  ),
4590
- includeNonCurrent: import_zod13.z.boolean().optional().describe(
4683
+ includeNonCurrent: import_zod14.z.boolean().optional().describe(
4591
4684
  "Return superseded, rejected and unsettled records too, each carrying its standing."
4592
4685
  )
4593
4686
  }),
@@ -4691,22 +4784,22 @@ function project(match, ranges, all) {
4691
4784
 
4692
4785
  // src/commands/classify.ts
4693
4786
  var classifyFileSchema = diffFileSchema.extend({
4694
- hunks: import_zod14.z.array(
4695
- diffHunkSchema.extend({ lines: import_zod14.z.array(import_zod14.z.string()).optional() })
4787
+ hunks: import_zod15.z.array(
4788
+ diffHunkSchema.extend({ lines: import_zod15.z.array(import_zod15.z.string()).optional() })
4696
4789
  ),
4697
- renamedFrom: import_zod14.z.string().min(1).optional().describe("Where `git diff -M` says the path came from."),
4698
- similarity: import_zod14.z.number().min(0).max(100).optional()
4790
+ renamedFrom: import_zod15.z.string().min(1).optional().describe("Where `git diff -M` says the path came from."),
4791
+ similarity: import_zod15.z.number().min(0).max(100).optional()
4699
4792
  });
4700
4793
  var classifyCommand = define({
4701
4794
  name: "classify",
4702
4795
  tool: "kb_classify",
4703
4796
  usage: "classify --git <base>..<head> | --stdin [--repo-root <path>] [--offline]",
4704
4797
  description: "What kind of change each file carries: test, config, ci, docs, lockfile, generated, boilerplate, rename or source, with the rule that decided it. Derived from the diff and never stored; a `review:generated`, `review:boilerplate` or `review:move` fact anchored on a file overrides the heuristic. kb_match says what sits on a hunk; this says whether to read it.",
4705
- input: import_zod14.z.object({
4798
+ input: import_zod15.z.object({
4706
4799
  bundlePath,
4707
- files: import_zod14.z.array(classifyFileSchema).describe("The changed files, each with its line ranges."),
4800
+ files: import_zod15.z.array(classifyFileSchema).describe("The changed files, each with its line ranges."),
4708
4801
  repoRoot: REPO_ROOT,
4709
- offline: import_zod14.z.boolean().optional().describe(
4802
+ offline: import_zod15.z.boolean().optional().describe(
4710
4803
  "Resolve symbol ranges from what is already on disk, never fetching a grammar."
4711
4804
  )
4712
4805
  }),
@@ -4817,7 +4910,7 @@ function renderClassify(result) {
4817
4910
  }
4818
4911
 
4819
4912
  // src/commands/context.ts
4820
- var import_zod15 = require("zod");
4913
+ var import_zod16 = require("zod");
4821
4914
 
4822
4915
  // src/kb-context.ts
4823
4916
  var import_promises8 = require("fs/promises");
@@ -5088,23 +5181,23 @@ var contextCommand = define({
5088
5181
  tool: "kb_context",
5089
5182
  usage: "context [--profile NAME] [--budget N] [--full-under N] [--exclude-tag T]... [--format json] [--event NAME]",
5090
5183
  description: "Index block of pinned bases (ids, titles, standing) for injection at context birth. Takes no bundlePath \u2014 reads the workspace pin manifests. Empty when nothing is pinned; refuses over budget rather than truncating. Budget precedence: flags, then the manifest `context[profile]` over `context.default`, then the built-in profile, then package defaults.",
5091
- input: import_zod15.z.object({
5092
- budgetTokens: import_zod15.z.number().int().positive().optional().describe(
5184
+ input: import_zod16.z.object({
5185
+ budgetTokens: import_zod16.z.number().int().positive().optional().describe(
5093
5186
  "Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
5094
5187
  ),
5095
- fullUnderTokens: import_zod15.z.number().int().positive().optional().describe(
5188
+ fullUnderTokens: import_zod16.z.number().int().positive().optional().describe(
5096
5189
  "Per-base rendering threshold, applied before the budget: a base whose complete load fits under this arrives as full records instead of index lines, and the whole block still answers to budgetTokens. Off by default \u2014 index-only is the safe default at a context birth, because injected bodies outlive the qualifiers on them; the session-start profile opts tiny bases in at 1500."
5097
5190
  ),
5098
- profile: import_zod15.z.string().optional().describe(
5191
+ profile: import_zod16.z.string().optional().describe(
5099
5192
  "Named budget set: built-ins are session-start (full-under 1500), compact and turn (budget 2500); the manifests' `context` tables override per repo. Unknown names fall through to defaults rather than failing."
5100
5193
  ),
5101
- excludeTags: import_zod15.z.array(import_zod15.z.string().min(1)).optional().describe(
5194
+ excludeTags: import_zod16.z.array(import_zod16.z.string().min(1)).optional().describe(
5102
5195
  "Frontmatter tags whose records stay out of the block. The base stays pinned and stays readable by tool; resolved like the budgets."
5103
5196
  ),
5104
- format: import_zod15.z.enum(["markdown", "json"]).optional().describe(
5197
+ format: import_zod16.z.enum(["markdown", "json"]).optional().describe(
5105
5198
  "CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
5106
5199
  ),
5107
- event: import_zod15.z.string().optional().describe(
5200
+ event: import_zod16.z.string().optional().describe(
5108
5201
  "hookEventName stamped into the JSON envelope. Only meaningful with format=json."
5109
5202
  )
5110
5203
  }),
@@ -5143,7 +5236,7 @@ var contextCommand = define({
5143
5236
  });
5144
5237
 
5145
5238
  // src/commands/doctor.ts
5146
- var import_zod17 = require("zod");
5239
+ var import_zod18 = require("zod");
5147
5240
 
5148
5241
  // src/kb-edges.ts
5149
5242
  var KB_EDGE_KINDS = [
@@ -5659,17 +5752,17 @@ function ageInDays(record, now) {
5659
5752
  }
5660
5753
 
5661
5754
  // src/commands/reassess.ts
5662
- var import_zod16 = require("zod");
5755
+ var import_zod17 = require("zod");
5663
5756
  var reassessCommand = define({
5664
5757
  name: "reassess",
5665
5758
  tool: "kb_reassess",
5666
5759
  usage: "reassess <concept-id> [--repo-root <path>] [--with-diff]",
5667
5760
  description: "One drifted record, as something to judge: its claim, each anchor's drift class, the old-vs-new span diff, and the records that depend on it. Formatting-only drift is dropped. Empty when there is nothing to reassess. Writes: relocates moved anchors, keeping their hash; never verifies, supersedes, or changes standing.",
5668
- input: import_zod16.z.object({
5761
+ input: import_zod17.z.object({
5669
5762
  bundlePath,
5670
5763
  conceptId,
5671
5764
  repoRoot: REPO_ROOT,
5672
- withDiff: import_zod16.z.boolean().optional().describe(
5765
+ withDiff: import_zod17.z.boolean().optional().describe(
5673
5766
  "Recover each anchor's committed span and render the diff. Reads git history."
5674
5767
  )
5675
5768
  }),
@@ -5814,13 +5907,13 @@ function at(file, symbol) {
5814
5907
  }
5815
5908
 
5816
5909
  // src/commands/doctor.ts
5817
- var days = (what, fallback) => import_zod17.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
5910
+ var days = (what, fallback) => import_zod18.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
5818
5911
  var doctorCommand = define({
5819
5912
  name: "doctor",
5820
5913
  tool: "kb_doctor",
5821
5914
  usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict] [--drifted [--with-diff]]",
5822
5915
  description: "Read-only health sweep: expired, expiring, unverified, aging, orphaned, broken-supersession, superseded-but-cited, drifted and unchecked anchors. Every group is reported even when empty; nothing is written or re-stamped. `drifted` narrows it to a reassessment packet per drifted record, `with_diff` adding each anchor's old-vs-new span.",
5823
- input: import_zod17.z.object({
5916
+ input: import_zod18.z.object({
5824
5917
  bundlePath,
5825
5918
  repoRoot: REPO_ROOT,
5826
5919
  expiringDays: days(
@@ -5835,16 +5928,16 @@ var doctorCommand = define({
5835
5928
  "How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
5836
5929
  DEFAULT_AGING_DAYS
5837
5930
  ),
5838
- offline: import_zod17.z.boolean().optional().describe(
5931
+ offline: import_zod18.z.boolean().optional().describe(
5839
5932
  "Read foreign anchors from the local repo cache only, never fetching."
5840
5933
  ),
5841
- strict: import_zod17.z.boolean().optional().describe(
5934
+ strict: import_zod18.z.boolean().optional().describe(
5842
5935
  "Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
5843
5936
  ),
5844
- drifted: import_zod17.z.boolean().optional().describe(
5937
+ drifted: import_zod18.z.boolean().optional().describe(
5845
5938
  "Report only drift, as a reassessment packet per record: claim, per-anchor class, and what depends on it."
5846
5939
  ),
5847
- withDiff: import_zod17.z.boolean().optional().describe(
5940
+ withDiff: import_zod18.z.boolean().optional().describe(
5848
5941
  "With `drifted`: recover each anchor's committed span and render the old-vs-new diff. Reads git history."
5849
5942
  )
5850
5943
  }),
@@ -6021,7 +6114,7 @@ function renderPackets(result) {
6021
6114
  // src/commands/export.ts
6022
6115
  var import_promises9 = require("fs/promises");
6023
6116
  var import_node_path11 = require("path");
6024
- var import_zod18 = require("zod");
6117
+ var import_zod19 = require("zod");
6025
6118
  var NUMBERED = /^(\d{4})-(.+)\.md$/;
6026
6119
  var MARKER = "<!-- strauss-kb export: ";
6027
6120
  var exportCommand = define({
@@ -6029,10 +6122,10 @@ var exportCommand = define({
6029
6122
  tool: "kb_export",
6030
6123
  usage: "export --format madr --to <dir>",
6031
6124
  description: "Write the base's decisions out as numbered MADR files, one per decision, for a repository that keeps ADRs of its own. Numbering is by slug, so a re-run rewrites its own files in place. A superseded decision is exported with what replaced it.",
6032
- input: import_zod18.z.object({
6125
+ input: import_zod19.z.object({
6033
6126
  bundlePath,
6034
- format: import_zod18.z.enum(["madr"]).describe("Output layout. `madr` is the only one so far."),
6035
- to: import_zod18.z.string().min(1).describe("Directory the ADR files are written into.")
6127
+ format: import_zod19.z.enum(["madr"]).describe("Output layout. `madr` is the only one so far."),
6128
+ to: import_zod19.z.string().min(1).describe("Directory the ADR files are written into.")
6036
6129
  }),
6037
6130
  fromArgv: (argv, path) => ({
6038
6131
  bundlePath: path,
@@ -6154,19 +6247,19 @@ function bodySections(body) {
6154
6247
  }
6155
6248
 
6156
6249
  // src/commands/impact.ts
6157
- var import_zod19 = require("zod");
6250
+ var import_zod20 = require("zod");
6158
6251
  var impactCommand = define({
6159
6252
  name: "impact",
6160
6253
  tool: "kb_impact",
6161
6254
  usage: "impact <concept-id> [--depth N] [--rels a,b]",
6162
6255
  description: "What breaks if this record changes: its transitive set of dependants, each with its standing. Each rel declares which of its ends depends on the other, and the walk follows each rel in its own direction. Naming `related_to` or an unknown rel in `rels` is an error. kb_backlinks gives one flat hop.",
6163
- input: import_zod19.z.object({
6256
+ input: import_zod20.z.object({
6164
6257
  bundlePath,
6165
6258
  conceptId,
6166
- depth: import_zod19.z.number().int().positive().optional().describe(
6259
+ depth: import_zod20.z.number().int().positive().optional().describe(
6167
6260
  "Hops out from the record. Unbounded when omitted; a walk this cuts reports truncated: true."
6168
6261
  ),
6169
- rels: import_zod19.z.array(import_zod19.z.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
6262
+ rels: import_zod20.z.array(import_zod20.z.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
6170
6263
  "Narrow which rels the walk follows. Defaults to every rel that carries a dependence \u2014 all but related_to."
6171
6264
  )
6172
6265
  }),
@@ -6187,15 +6280,15 @@ var impactCommand = define({
6187
6280
  });
6188
6281
 
6189
6282
  // src/commands/list.ts
6190
- var import_zod20 = require("zod");
6283
+ var import_zod21 = require("zod");
6191
6284
  var listCommand = define({
6192
6285
  name: "list",
6193
6286
  tool: "kb_list",
6194
6287
  usage: "list [type] [--tag T]...",
6195
6288
  description: "Every record, optionally one type or tag. For enumerating; use kb_query for a question.",
6196
- input: import_zod20.z.object({
6289
+ input: import_zod21.z.object({
6197
6290
  bundlePath,
6198
- type: import_zod20.z.enum(KB_RECORD_TYPES).optional(),
6291
+ type: import_zod21.z.enum(KB_RECORD_TYPES).optional(),
6199
6292
  tags: TAGS
6200
6293
  }),
6201
6294
  fromArgv: (argv, path) => {
@@ -6219,17 +6312,17 @@ var listCommand = define({
6219
6312
  });
6220
6313
 
6221
6314
  // src/commands/load.ts
6222
- var import_zod21 = require("zod");
6315
+ var import_zod22 = require("zod");
6223
6316
  var loadCommand = define({
6224
6317
  name: "load",
6225
6318
  tool: "kb_load",
6226
6319
  usage: "load [type] [--budget N | --all] [--repo-root PATH]",
6227
6320
  description: "Load the whole base, each record with its standing \u2014 call it first, at the point of use, since compaction drops it. Superseded records arrive as stubs; kb_trace has the history. Over budget it refuses: kb_catalog, then kb_pack, or narrow with `type`; `all` bypasses. Never read record files directly \u2014 only kb_* tools resolve supersession. `digest` stamps the base's content, so hooks know when to reload.",
6228
- input: import_zod21.z.object({
6321
+ input: import_zod22.z.object({
6229
6322
  bundlePath,
6230
- type: import_zod21.z.enum(KB_RECORD_TYPES).optional(),
6231
- budgetTokens: import_zod21.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
6232
- all: import_zod21.z.boolean().optional().describe(
6323
+ type: import_zod22.z.enum(KB_RECORD_TYPES).optional(),
6324
+ budgetTokens: import_zod22.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
6325
+ all: import_zod22.z.boolean().optional().describe(
6233
6326
  "Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
6234
6327
  ),
6235
6328
  repoRoot: REPO_ROOT
@@ -6271,25 +6364,25 @@ var loadCommand = define({
6271
6364
  });
6272
6365
 
6273
6366
  // src/commands/log.ts
6274
- var import_zod22 = require("zod");
6367
+ var import_zod23 = require("zod");
6275
6368
  var logCommand = define({
6276
6369
  name: "log",
6277
6370
  tool: "kb_log",
6278
6371
  usage: "log",
6279
6372
  description: "Who touched what, and when. Append-only; malformed lines are reported, never repaired.",
6280
- input: import_zod22.z.object({ bundlePath }),
6373
+ input: import_zod23.z.object({ bundlePath }),
6281
6374
  fromArgv: (_argv, path) => ({ bundlePath: path }),
6282
6375
  run: ({ store }, { bundlePath: path }) => store.readLog(path)
6283
6376
  });
6284
6377
 
6285
6378
  // src/commands/no-decision.ts
6286
- var import_zod23 = require("zod");
6379
+ var import_zod24 = require("zod");
6287
6380
  var noDecisionCommand = define({
6288
6381
  name: "no-decision",
6289
6382
  tool: "kb_no_decision",
6290
6383
  usage: "no-decision <reason...>",
6291
6384
  description: "Record in one sentence that a piece of work had nothing to decide. Idempotent.",
6292
- input: import_zod23.z.object({ bundlePath, reason: import_zod23.z.string().min(1) }),
6385
+ input: import_zod24.z.object({ bundlePath, reason: import_zod24.z.string().min(1) }),
6293
6386
  fromArgv: (argv, path) => ({
6294
6387
  bundlePath: path,
6295
6388
  reason: argv.slice(1).join(" ").trim()
@@ -6306,20 +6399,20 @@ var noDecisionCommand = define({
6306
6399
  });
6307
6400
 
6308
6401
  // src/commands/pack.ts
6309
- var import_zod24 = require("zod");
6402
+ var import_zod25 = require("zod");
6310
6403
  var packCommand = define({
6311
6404
  name: "pack",
6312
6405
  tool: "kb_pack",
6313
6406
  usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
6314
6407
  description: "Bounded neighbourhood around one record: within `hops`, ranked, cut to `maxNodes`, with every cut record named under Excluded. Use when the base is over kb_load's budget and the work centres on a record you can name. Refuses over budget rather than truncating. Everything below the header is byte-stable across runs. Resolves supersession like kb_load.",
6315
- input: import_zod24.z.object({
6408
+ input: import_zod25.z.object({
6316
6409
  bundlePath,
6317
6410
  conceptId,
6318
- hops: import_zod24.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
6319
- maxNodes: import_zod24.z.number().int().positive().optional().describe(
6411
+ hops: import_zod25.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
6412
+ maxNodes: import_zod25.z.number().int().positive().optional().describe(
6320
6413
  "How many records the pack may hold, root included. Defaults to 20."
6321
6414
  ),
6322
- budgetTokens: import_zod24.z.number().int().positive().optional().describe(
6415
+ budgetTokens: import_zod25.z.number().int().positive().optional().describe(
6323
6416
  "Approximate token ceiling over what is actually emitted. Defaults to 25000."
6324
6417
  )
6325
6418
  }),
@@ -6406,22 +6499,22 @@ function warningLabel(warning) {
6406
6499
  }
6407
6500
 
6408
6501
  // src/commands/pin.ts
6409
- var import_zod25 = require("zod");
6502
+ var import_zod26 = require("zod");
6410
6503
  var pinCommand = define({
6411
6504
  name: "pin",
6412
6505
  tool: "kb_pin",
6413
6506
  usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
6414
6507
  description: "Pin a base into a workspace manifest so kb_context surfaces it. Layers, nearest wins: project `.strauss/kb-pins.json` (default), `--local` (personal, gitignored), `--user` (`~/.strauss`). Idempotent; `--mode full|index`, `--profiles`, `--frozen`/`--unfreeze` update only those fields. A path with no records pins with a warning. Never touches the base itself.",
6415
- input: import_zod25.z.object({
6508
+ input: import_zod26.z.object({
6416
6509
  bundlePath,
6417
- mode: import_zod25.z.enum(["full", "index"]).optional().describe(
6510
+ mode: import_zod26.z.enum(["full", "index"]).optional().describe(
6418
6511
  "full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
6419
6512
  ),
6420
- profiles: import_zod25.z.array(import_zod25.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
6421
- layer: import_zod25.z.enum(["project", "local", "user"]).optional().describe(
6513
+ profiles: import_zod26.z.array(import_zod26.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
6514
+ layer: import_zod26.z.enum(["project", "local", "user"]).optional().describe(
6422
6515
  "Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
6423
6516
  ),
6424
- frozen: import_zod25.z.boolean().optional().describe(
6517
+ frozen: import_zod26.z.boolean().optional().describe(
6425
6518
  "true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
6426
6519
  )
6427
6520
  }),
@@ -6450,13 +6543,13 @@ var pinCommand = define({
6450
6543
  });
6451
6544
 
6452
6545
  // src/commands/pins.ts
6453
- var import_zod26 = require("zod");
6546
+ var import_zod27 = require("zod");
6454
6547
  var pinsCommand = define({
6455
6548
  name: "pins",
6456
6549
  tool: "kb_pins",
6457
6550
  usage: "pins",
6458
6551
  description: "Every pinned base across the manifest layers, with its layer and whether it resolves to records. Takes no bundlePath.",
6459
- input: import_zod26.z.object({}),
6552
+ input: import_zod27.z.object({}),
6460
6553
  fromArgv: () => ({}),
6461
6554
  run: ({ store }) => listPins(store, process.cwd())
6462
6555
  });
@@ -6727,16 +6820,16 @@ function recordType(conceptId2) {
6727
6820
  }
6728
6821
 
6729
6822
  // src/commands/promote/model.ts
6730
- var import_zod27 = require("zod");
6731
- var promoteInputSchema = import_zod27.z.object({
6823
+ var import_zod28 = require("zod");
6824
+ var promoteInputSchema = import_zod28.z.object({
6732
6825
  bundlePath,
6733
- conceptIds: import_zod27.z.array(conceptId).max(64).optional().describe("Records to copy into the target base. Omit with `list`."),
6734
- to: import_zod27.z.string().min(1).optional().describe("Absolute path to the base being promoted into."),
6735
- source: import_zod27.z.string().min(1).optional().describe(
6826
+ conceptIds: import_zod28.z.array(conceptId).max(64).optional().describe("Records to copy into the target base. Omit with `list`."),
6827
+ to: import_zod28.z.string().min(1).optional().describe("Absolute path to the base being promoted into."),
6828
+ source: import_zod28.z.string().min(1).optional().describe(
6736
6829
  "Where the promotion came from, usually the pull request URL. Recorded on each copy as a source."
6737
6830
  ),
6738
- force: import_zod27.z.boolean().optional().describe("Overwrite a record the target base already holds."),
6739
- list: import_zod27.z.boolean().optional().describe("List the source base's candidates instead of promoting.")
6831
+ force: import_zod28.z.boolean().optional().describe("Overwrite a record the target base already holds."),
6832
+ list: import_zod28.z.boolean().optional().describe("List the source base's candidates instead of promoting.")
6740
6833
  }).refine((input) => input.list === true || input.to !== void 0, {
6741
6834
  message: "promote needs a target base \u2014 pass --to <bundle>, or --list",
6742
6835
  path: ["to"]
@@ -6873,17 +6966,17 @@ function renderPromote(result) {
6873
6966
  }
6874
6967
 
6875
6968
  // src/commands/query.ts
6876
- var import_zod28 = require("zod");
6969
+ var import_zod29 = require("zod");
6877
6970
  var queryCommand = define({
6878
6971
  name: "query",
6879
6972
  tool: "kb_query",
6880
6973
  usage: "query <text...> [--tag T]... [--repo-root PATH]",
6881
6974
  description: "Search; every hit carries its standing. Flagged, never filtered: a superseded hit returns with its replacement, a rejected one is marked. Prefer kb_load when the base fits its budget \u2014 a full read beats search. Results are volatile: place them at the tail, not the cached prefix. Never read record files directly.",
6882
- input: import_zod28.z.object({
6975
+ input: import_zod29.z.object({
6883
6976
  bundlePath,
6884
- text: import_zod28.z.string().optional(),
6885
- type: import_zod28.z.enum(KB_RECORD_TYPES).optional(),
6886
- includeNonCurrent: import_zod28.z.boolean().optional(),
6977
+ text: import_zod29.z.string().optional(),
6978
+ type: import_zod29.z.enum(KB_RECORD_TYPES).optional(),
6979
+ includeNonCurrent: import_zod29.z.boolean().optional(),
6887
6980
  tags: TAGS,
6888
6981
  repoRoot: REPO_ROOT
6889
6982
  }),
@@ -6917,32 +7010,32 @@ var queryCommand = define({
6917
7010
  });
6918
7011
 
6919
7012
  // src/commands/read-index.ts
6920
- var import_zod29 = require("zod");
7013
+ var import_zod30 = require("zod");
6921
7014
  var readIndexCommand = define({
6922
7015
  name: "index",
6923
7016
  tool: "kb_index",
6924
7017
  usage: "index",
6925
7018
  description: "The index \u2014 title, type, status, description per record \u2014 rebuilt if stale. Cheapest re-orientation after compaction: call it (or kb_context) first, then kb_load or fetch by id.",
6926
- input: import_zod29.z.object({ bundlePath }),
7019
+ input: import_zod30.z.object({ bundlePath }),
6927
7020
  fromArgv: (_argv, path) => ({ bundlePath: path }),
6928
7021
  run: ({ store }, { bundlePath: path }) => store.readIndex(path)
6929
7022
  });
6930
7023
 
6931
7024
  // src/commands/schema.ts
6932
- var import_zod32 = require("zod");
7025
+ var import_zod33 = require("zod");
6933
7026
 
6934
7027
  // src/json-schema.ts
6935
- var import_zod31 = require("zod");
7028
+ var import_zod32 = require("zod");
6936
7029
 
6937
7030
  // src/kb-log.ts
6938
- var import_zod30 = require("zod");
7031
+ var import_zod31 = require("zod");
6939
7032
  var LOG_FILE = "log.jsonl";
6940
- var kbLogAnchorChangeSchema = import_zod30.z.object({
6941
- op: import_zod30.z.enum(["move", "add", "drop"]),
7033
+ var kbLogAnchorChangeSchema = import_zod31.z.object({
7034
+ op: import_zod31.z.enum(["move", "add", "drop"]),
6942
7035
  from: kbAnchorLocatorSchema.optional(),
6943
7036
  to: kbAnchorLocatorSchema.optional()
6944
7037
  }).strict();
6945
- var kbLogEntryFields = import_zod30.z.object({
7038
+ var kbLogEntryFields = import_zod31.z.object({
6946
7039
  // Validated, not just `min(1)`: `at` is a sort key (see `parseLog`
6947
7040
  // below), and a value that isn't actually chronological — a Unix
6948
7041
  // timestamp, a human-typed date, garbage — would sort wrong without
@@ -6951,23 +7044,23 @@ var kbLogEntryFields = import_zod30.z.object({
6951
7044
  // and rejects everything else, including a non-`Z` offset — so a
6952
7045
  // malformed `at` is reported the same way a malformed line already is,
6953
7046
  // rather than silently sorting into the wrong place.
6954
- at: import_zod30.z.iso.datetime(),
6955
- by: import_zod30.z.string().min(1),
6956
- operation: import_zod30.z.string().min(1),
6957
- conceptId: import_zod30.z.string().min(1),
7047
+ at: import_zod31.z.iso.datetime(),
7048
+ by: import_zod31.z.string().min(1),
7049
+ operation: import_zod31.z.string().min(1),
7050
+ conceptId: import_zod31.z.string().min(1),
6958
7051
  /**
6959
7052
  * The operation's other end, where it has one: a second concept id for
6960
7053
  * supersession, the other base's path for promotion.
6961
7054
  */
6962
- target: import_zod30.z.string().min(1).optional(),
7055
+ target: import_zod31.z.string().min(1).optional(),
6963
7056
  /**
6964
7057
  * Why the operation was performed, where the operation demands one.
6965
7058
  * `anchor-set` does: a pointer moved by a reader is only auditable if
6966
7059
  * the reading is recorded beside it.
6967
7060
  */
6968
- reason: import_zod30.z.string().min(1).optional(),
7061
+ reason: import_zod31.z.string().min(1).optional(),
6969
7062
  /** What `anchor-set` changed, derived from the record before and after. */
6970
- anchors: import_zod30.z.array(kbLogAnchorChangeSchema).optional()
7063
+ anchors: import_zod31.z.array(kbLogAnchorChangeSchema).optional()
6971
7064
  });
6972
7065
  var kbLogEntrySchema = kbLogEntryFields.passthrough();
6973
7066
  var kbLogEntryWriteSchema = kbLogEntryFields.strict();
@@ -7013,11 +7106,11 @@ function parseLog(raw) {
7013
7106
  // src/json-schema.ts
7014
7107
  function kbJsonSchemas() {
7015
7108
  return {
7016
- recordFrontmatter: import_zod31.z.toJSONSchema(kbRecordFrontmatterSchema, {
7109
+ recordFrontmatter: import_zod32.z.toJSONSchema(kbRecordFrontmatterSchema, {
7017
7110
  io: "input"
7018
7111
  }),
7019
- composeInput: import_zod31.z.toJSONSchema(composeInputSchema, { io: "input" }),
7020
- logEntry: import_zod31.z.toJSONSchema(kbLogEntrySchema, { io: "input" })
7112
+ composeInput: import_zod32.z.toJSONSchema(composeInputSchema, { io: "input" }),
7113
+ logEntry: import_zod32.z.toJSONSchema(kbLogEntrySchema, { io: "input" })
7021
7114
  };
7022
7115
  }
7023
7116
 
@@ -7027,25 +7120,25 @@ var schemaCommand = define({
7027
7120
  tool: "kb_schema",
7028
7121
  usage: "schema",
7029
7122
  description: "JSON Schema for frontmatter, write input, and log entries, generated from the enforcing code.",
7030
- input: import_zod32.z.object({}),
7123
+ input: import_zod33.z.object({}),
7031
7124
  fromArgv: () => ({}),
7032
7125
  run: () => Promise.resolve(kbJsonSchemas())
7033
7126
  });
7034
7127
 
7035
7128
  // src/commands/stamp.ts
7036
7129
  var import_promises10 = require("fs/promises");
7037
- var import_zod33 = require("zod");
7130
+ var import_zod34 = require("zod");
7038
7131
  var DIGEST = /^[0-9a-f]{64}$/;
7039
7132
  var stampCommand = define({
7040
7133
  name: "stamp",
7041
7134
  tool: "kb_stamp",
7042
7135
  usage: "stamp [--bundle PATH] [--since DIGEST|FILE]",
7043
7136
  description: "Content stamp of a base \u2014 `load`'s digest, record counts, per-record digests, how many records have drifted anchors \u2014 without any bodies. Takes no bundlePath to stamp every pinned base. With `since`, reports only the bases that moved, naming the changed ids. Reads, never writes.",
7044
- input: import_zod33.z.object({
7045
- bundlePath: import_zod33.z.string().min(1).optional().describe(
7137
+ input: import_zod34.z.object({
7138
+ bundlePath: import_zod34.z.string().min(1).optional().describe(
7046
7139
  "Absolute path to one knowledge base. Omit to stamp every pinned base."
7047
7140
  ),
7048
- since: import_zod33.z.string().min(1).optional().describe(
7141
+ since: import_zod34.z.string().min(1).optional().describe(
7049
7142
  "Prior digest, or path to a prior `stamp --json`; only moved bases return, with changed ids when the baseline is a file."
7050
7143
  )
7051
7144
  }),
@@ -7131,16 +7224,16 @@ async function readBaseline(since) {
7131
7224
  }
7132
7225
 
7133
7226
  // src/commands/status.ts
7134
- var import_zod34 = require("zod");
7227
+ var import_zod35 = require("zod");
7135
7228
  var statusCommand = define({
7136
7229
  name: "status",
7137
7230
  tool: "kb_status",
7138
7231
  usage: "status <concept-id> <status>",
7139
7232
  description: "Move a record's status. Compare-and-swap: a concurrent change fails instead of being overwritten.",
7140
- input: import_zod34.z.object({
7233
+ input: import_zod35.z.object({
7141
7234
  bundlePath,
7142
7235
  conceptId,
7143
- status: import_zod34.z.enum(KB_RECORD_STATUSES)
7236
+ status: import_zod35.z.enum(KB_RECORD_STATUSES)
7144
7237
  }),
7145
7238
  fromArgv: (argv, path) => ({
7146
7239
  bundlePath: path,
@@ -7155,13 +7248,13 @@ var statusCommand = define({
7155
7248
  });
7156
7249
 
7157
7250
  // src/commands/supersede.ts
7158
- var import_zod35 = require("zod");
7251
+ var import_zod36 = require("zod");
7159
7252
  var supersedeCommand = define({
7160
7253
  name: "supersede",
7161
7254
  tool: "kb_supersede",
7162
7255
  usage: "supersede <concept-id> <replacement-id>",
7163
7256
  description: "Mark a record superseded by another, linked in both directions. Use instead of editing a record whose meaning changed.",
7164
- input: import_zod35.z.object({ bundlePath, conceptId, replacementId: conceptId }),
7257
+ input: import_zod36.z.object({ bundlePath, conceptId, replacementId: conceptId }),
7165
7258
  fromArgv: (argv, path) => ({
7166
7259
  bundlePath: path,
7167
7260
  conceptId: argv[1],
@@ -7175,7 +7268,7 @@ var supersedeCommand = define({
7175
7268
  });
7176
7269
 
7177
7270
  // src/commands/sweep.ts
7178
- var import_zod36 = require("zod");
7271
+ var import_zod37 = require("zod");
7179
7272
  var TERMINAL = [
7180
7273
  "resolved",
7181
7274
  "rejected",
@@ -7186,15 +7279,15 @@ var sweepCommand = define({
7186
7279
  tool: "kb_sweep",
7187
7280
  usage: "sweep --tag <tag> --terminal [--dry-run]",
7188
7281
  description: "Delete tagged records that are resolved, rejected or superseded. Refuses without --tag, keeps any record a surviving record still points at, and logs each deletion.",
7189
- input: import_zod36.z.object({
7282
+ input: import_zod37.z.object({
7190
7283
  bundlePath,
7191
- tag: import_zod36.z.string({ error: "sweep needs --tag: it never sweeps a whole base" }).min(1).describe("Only records carrying this tag are considered."),
7192
- terminal: import_zod36.z.literal(true, {
7284
+ tag: import_zod37.z.string({ error: "sweep needs --tag: it never sweeps a whole base" }).min(1).describe("Only records carrying this tag are considered."),
7285
+ terminal: import_zod37.z.literal(true, {
7193
7286
  error: "sweep needs --terminal: it deletes only settled records"
7194
7287
  }).describe(
7195
7288
  "Required. Names the only scope sweep deletes: resolved, rejected and superseded records."
7196
7289
  ),
7197
- dryRun: import_zod36.z.boolean().optional().describe("Report what would go, and delete nothing.")
7290
+ dryRun: import_zod37.z.boolean().optional().describe("Report what would go, and delete nothing.")
7198
7291
  }),
7199
7292
  fromArgv: (argv, path) => ({
7200
7293
  bundlePath: path,
@@ -7311,16 +7404,16 @@ function renderSweep(result) {
7311
7404
  }
7312
7405
 
7313
7406
  // src/commands/sync-instructions.ts
7314
- var import_zod37 = require("zod");
7407
+ var import_zod38 = require("zod");
7315
7408
  var syncInstructionsCommand = define({
7316
7409
  name: "sync-instructions",
7317
7410
  usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
7318
7411
  description: "CLI-only: plant the kb_context block between sentinel comments in AGENTS.md or CLAUDE.md, idempotently.",
7319
- input: import_zod37.z.object({
7320
- file: import_zod37.z.string().min(1).describe("The instruction file to edit in place."),
7321
- budgetTokens: import_zod37.z.number().int().positive().optional(),
7322
- fullUnderTokens: import_zod37.z.number().int().positive().optional(),
7323
- profile: import_zod37.z.string().optional()
7412
+ input: import_zod38.z.object({
7413
+ file: import_zod38.z.string().min(1).describe("The instruction file to edit in place."),
7414
+ budgetTokens: import_zod38.z.number().int().positive().optional(),
7415
+ fullUnderTokens: import_zod38.z.number().int().positive().optional(),
7416
+ profile: import_zod38.z.string().optional()
7324
7417
  }),
7325
7418
  fromArgv: (argv) => {
7326
7419
  const budget = argvFlag(argv, "--budget");
@@ -7346,7 +7439,7 @@ var syncInstructionsCommand = define({
7346
7439
  });
7347
7440
 
7348
7441
  // src/commands/trace.ts
7349
- var import_zod38 = require("zod");
7442
+ var import_zod39 = require("zod");
7350
7443
 
7351
7444
  // src/trace.ts
7352
7445
  var TRACE_EDGES = [
@@ -7402,11 +7495,11 @@ var traceCommand = define({
7402
7495
  tool: "kb_trace",
7403
7496
  usage: "trace <concept-id> [edges...]",
7404
7497
  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".',
7405
- input: import_zod38.z.object({
7498
+ input: import_zod39.z.object({
7406
7499
  bundlePath,
7407
7500
  conceptId,
7408
- edges: import_zod38.z.array(import_zod38.z.enum(TRACE_EDGES)).optional(),
7409
- depth: import_zod38.z.number().int().positive().optional()
7501
+ edges: import_zod39.z.array(import_zod39.z.enum(TRACE_EDGES)).optional(),
7502
+ depth: import_zod39.z.number().int().positive().optional()
7410
7503
  }),
7411
7504
  fromArgv: (argv, path) => ({
7412
7505
  bundlePath: path,
@@ -7428,37 +7521,37 @@ var traceCommand = define({
7428
7521
  });
7429
7522
 
7430
7523
  // src/commands/types.ts
7431
- var import_zod39 = require("zod");
7524
+ var import_zod40 = require("zod");
7432
7525
  var typesCommand = define({
7433
7526
  name: "types",
7434
7527
  tool: "kb_types",
7435
7528
  usage: "types",
7436
7529
  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.",
7437
- input: import_zod39.z.object({}),
7530
+ input: import_zod40.z.object({}),
7438
7531
  fromArgv: () => ({}),
7439
7532
  run: () => Promise.resolve(RECORD_TYPES)
7440
7533
  });
7441
7534
 
7442
7535
  // src/commands/unpin.ts
7443
- var import_zod40 = require("zod");
7536
+ var import_zod41 = require("zod");
7444
7537
  var unpinCommand = define({
7445
7538
  name: "unpin",
7446
7539
  tool: "kb_unpin",
7447
7540
  usage: "unpin [bundle-path]",
7448
7541
  description: "Remove a base from every manifest layer that holds it. Reports the layers touched.",
7449
- input: import_zod40.z.object({ bundlePath }),
7542
+ input: import_zod41.z.object({ bundlePath }),
7450
7543
  fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
7451
7544
  run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
7452
7545
  });
7453
7546
 
7454
7547
  // src/commands/validate.ts
7455
- var import_zod41 = require("zod");
7548
+ var import_zod42 = require("zod");
7456
7549
  var validateCommand = define({
7457
7550
  name: "validate",
7458
7551
  tool: "kb_validate",
7459
7552
  usage: "validate",
7460
7553
  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.",
7461
- input: import_zod41.z.object({ bundlePath }),
7554
+ input: import_zod42.z.object({ bundlePath }),
7462
7555
  fromArgv: (_argv, path) => ({ bundlePath: path }),
7463
7556
  run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
7464
7557
  // Warnings never fail the exit code; every other severity does.
@@ -7468,16 +7561,16 @@ var validateCommand = define({
7468
7561
  });
7469
7562
 
7470
7563
  // src/commands/verify.ts
7471
- var import_zod42 = require("zod");
7564
+ var import_zod43 = require("zod");
7472
7565
  var verifyCommand = define({
7473
7566
  name: "verify",
7474
7567
  tool: "kb_verify",
7475
7568
  usage: "verify <concept-id> --note <text>",
7476
7569
  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.",
7477
- input: import_zod42.z.object({
7570
+ input: import_zod43.z.object({
7478
7571
  bundlePath,
7479
7572
  conceptId,
7480
- note: import_zod42.z.string().refine((s) => s.trim().length > 0, {
7573
+ note: import_zod43.z.string().refine((s) => s.trim().length > 0, {
7481
7574
  message: "note must say what the check found"
7482
7575
  })
7483
7576
  }),
@@ -7497,15 +7590,15 @@ var verifyCommand = define({
7497
7590
  });
7498
7591
 
7499
7592
  // src/commands/write.ts
7500
- var import_zod43 = require("zod");
7593
+ var import_zod44 = require("zod");
7501
7594
  var writeCommand = define({
7502
7595
  name: "write",
7503
7596
  tool: "kb_write",
7504
7597
  usage: "write <type> < record.json",
7505
7598
  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.",
7506
- input: import_zod43.z.object({
7599
+ input: import_zod44.z.object({
7507
7600
  bundlePath,
7508
- type: import_zod43.z.enum(KB_RECORD_TYPES),
7601
+ type: import_zod44.z.enum(KB_RECORD_TYPES),
7509
7602
  input: composeInputSchema
7510
7603
  }),
7511
7604
  fromArgv: async (argv, path, stdin) => ({
@@ -7529,13 +7622,13 @@ var writeCommand = define({
7529
7622
  });
7530
7623
 
7531
7624
  // src/commands/write-decision.ts
7532
- var import_zod44 = require("zod");
7625
+ var import_zod45 = require("zod");
7533
7626
  var writeDecisionCommand = define({
7534
7627
  name: "write-decision",
7535
7628
  tool: "kb_write_decision",
7536
7629
  usage: "write-decision < decision.json",
7537
7630
  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.",
7538
- input: import_zod44.z.object({ bundlePath, input: decisionInputSchema }),
7631
+ input: import_zod45.z.object({ bundlePath, input: decisionInputSchema }),
7539
7632
  fromArgv: async (_argv, path, stdin) => ({
7540
7633
  bundlePath: path,
7541
7634
  input: JSON.parse(await stdin())
@@ -8698,7 +8791,7 @@ function assertActor(actor, { named = false } = {}) {
8698
8791
  }
8699
8792
 
8700
8793
  // src/version.ts
8701
- var VERSION = true ? "0.1.22" : "0.0.0-dev";
8794
+ var VERSION = true ? "0.1.23" : "0.0.0-dev";
8702
8795
 
8703
8796
  // src/cli.ts
8704
8797
  async function runKbCli(argv) {