@rayul/kordis 0.15.1 → 0.15.2

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/index.js CHANGED
@@ -1159,12 +1159,17 @@ var init_api = __esm({
1159
1159
  */
1160
1160
  content: z11.string().min(50).max(1e5),
1161
1161
  /**
1162
- * Inline overrides for frontmatter fields. Merged into parsed frontmatter
1163
- * server-side; existing frontmatter wins if both present.
1162
+ * Accepted and ignored. core-api stores the title, description and type it
1163
+ * reads from the frontmatter of `content`, which must carry all three; these
1164
+ * three fields change nothing (`kordis push --type` sends `type` here).
1164
1165
  */
1165
1166
  title: z11.string().min(1).max(200).optional(),
1166
1167
  description: z11.string().min(1).max(2048).optional(),
1167
1168
  type: HarnessTypeSchema.optional(),
1169
+ /**
1170
+ * A new package is published under this slug instead of the one derived from
1171
+ * the frontmatter title. An update keeps the slug of the package it updates.
1172
+ */
1168
1173
  slug: SlugSchema.optional(),
1169
1174
  /**
1170
1175
  * When updating, the target full_slug. Omitted for new packages (server
@@ -2443,13 +2448,17 @@ var init_sections = __esm({
2443
2448
 
2444
2449
  // ../../packages/memory-engine/src/schema.ts
2445
2450
  import { z as z22 } from "zod";
2446
- var MappingModeSchema, ScopeSourceSchema, SCOPE_LIMIT, TELEMETRY_ERROR_KINDS, TELEMETRY_MESSAGE_LIMIT, TelemetryErrorPayloadSchema2, ProvenanceSchema, MemoryStatusSchema, ClaimConfidenceSchema, MemoryKindSchema, ClaimSchema, MemorySchema, RelationTypeSchema, RelationSourceSchema, RelationSchema;
2451
+ function isGlobalScope(scope) {
2452
+ return scope != null && scope.trim().toLowerCase() === GLOBAL_SCOPE;
2453
+ }
2454
+ var MappingModeSchema, ScopeSourceSchema, SCOPE_LIMIT, GLOBAL_SCOPE, TELEMETRY_ERROR_KINDS, TELEMETRY_MESSAGE_LIMIT, TelemetryErrorPayloadSchema2, ProvenanceSchema, MemoryStatusSchema, ClaimConfidenceSchema, MemoryKindSchema, ClaimSchema, MemorySchema, RelationTypeSchema, RelationSourceSchema, RelationSchema;
2447
2455
  var init_schema = __esm({
2448
2456
  "../../packages/memory-engine/src/schema.ts"() {
2449
2457
  "use strict";
2450
2458
  MappingModeSchema = z22.enum(["explicit", "heuristic"]);
2451
2459
  ScopeSourceSchema = z22.enum(["explicit", "path", "none"]);
2452
2460
  SCOPE_LIMIT = 128;
2461
+ GLOBAL_SCOPE = "global";
2453
2462
  TELEMETRY_ERROR_KINDS = [
2454
2463
  "network_unreachable",
2455
2464
  "server_error",
@@ -2529,6 +2538,26 @@ var init_schema = __esm({
2529
2538
  scope: z22.string().min(1).nullable(),
2530
2539
  /** Id of the claim that is current for this memory, when one is declared. */
2531
2540
  current_claim: z22.string().min(1).nullable(),
2541
+ /**
2542
+ * The end date the NOTE declares, for the notes that state no claim.
2543
+ *
2544
+ * `valid_until` lived only on `Claim`, and the mapper mints a claim only for
2545
+ * a note carrying a `claim:` key — which `remember` has never written. So on
2546
+ * a real brain most notes could not carry an end date that anything read:
2547
+ * sampled six binding rules on a production brain and four had no claim at
2548
+ * all, the two that did having been through `revise`. A date in the
2549
+ * frontmatter of the other four was simply ignored.
2550
+ *
2551
+ * Optional rather than nullable, like `kind` and `binding` above and for the
2552
+ * same reason: a note written before this existed must map to exactly the
2553
+ * `Memory` it always did, or the regeneration test's byte comparison rewrites
2554
+ * every cache in existence on upgrade.
2555
+ *
2556
+ * The claim's copy WINS where both exist — it is the more specific statement,
2557
+ * and a note whose conclusion carries its own dates is saying something about
2558
+ * that conclusion rather than about the file.
2559
+ */
2560
+ valid_until: z22.string().min(1).optional(),
2532
2561
  status: MemoryStatusSchema,
2533
2562
  /**
2534
2563
  * Whether this memory binds what an agent does, rather than informing it.
@@ -2610,6 +2639,104 @@ var init_schema = __esm({
2610
2639
  }
2611
2640
  });
2612
2641
 
2642
+ // ../../packages/memory-engine/src/layout.ts
2643
+ var OBSERVATIONS_DIR, DERIVED_DIR, DERIVED_BLUEPRINT;
2644
+ var init_layout = __esm({
2645
+ "../../packages/memory-engine/src/layout.ts"() {
2646
+ "use strict";
2647
+ OBSERVATIONS_DIR = "_observations";
2648
+ DERIVED_DIR = "_derived";
2649
+ DERIVED_BLUEPRINT = "_blueprint.json";
2650
+ }
2651
+ });
2652
+
2653
+ // ../../packages/memory-engine/src/rule-budget.ts
2654
+ function ruleLane(scope, project) {
2655
+ if (scope == null || scope.trim() === "" || isGlobalScope(scope)) return "global";
2656
+ if (project === null) return "elsewhere";
2657
+ return scope.trim().toLowerCase() === project.trim().toLowerCase() ? "project" : "elsewhere";
2658
+ }
2659
+ function declaredScopeOf(memory) {
2660
+ return memory.provenance.scopeSource === "explicit" ? memory.scope : null;
2661
+ }
2662
+ function splitRuleBudget(ranked, scopeOf, project, budget = DEFAULT_RULE_BUDGET) {
2663
+ const globalLane = [];
2664
+ const projectLane = [];
2665
+ const elsewhere = [];
2666
+ for (const item of ranked) {
2667
+ const lane = ruleLane(scopeOf(item), project);
2668
+ if (lane === "global") globalLane.push(item);
2669
+ else if (lane === "project") projectLane.push(item);
2670
+ else elsewhere.push(item);
2671
+ }
2672
+ const total = budget.global + budget.project;
2673
+ const globalRoom = Math.max(budget.global, total - Math.min(projectLane.length, budget.project));
2674
+ const projectRoom = Math.max(budget.project, total - Math.min(globalLane.length, budget.global));
2675
+ return {
2676
+ global: globalLane.slice(0, globalRoom),
2677
+ project: projectLane.slice(0, projectRoom),
2678
+ elsewhere,
2679
+ // Everything either budget COULD have shown, so "5 of 12" is honest about
2680
+ // the cap without counting another project's rules as things withheld.
2681
+ eligible: globalLane.length + projectLane.length
2682
+ };
2683
+ }
2684
+ var DEFAULT_RULE_BUDGET;
2685
+ var init_rule_budget = __esm({
2686
+ "../../packages/memory-engine/src/rule-budget.ts"() {
2687
+ "use strict";
2688
+ init_schema();
2689
+ DEFAULT_RULE_BUDGET = { global: 5, project: 5 };
2690
+ }
2691
+ });
2692
+
2693
+ // ../../packages/memory-engine/src/rule-text.ts
2694
+ function ruleTextOf(entry) {
2695
+ const intro = (entry.chunks ?? []).find((chunk) => chunk.label === null)?.text.trim();
2696
+ if (intro !== void 0 && intro !== "") return intro;
2697
+ return entry.memory.summary ?? entry.memory.title;
2698
+ }
2699
+ function ruleWidthFor(count) {
2700
+ if (count <= 0) return RULE_WIDTH_MIN;
2701
+ const share = Math.floor(RULE_BLOCK_BUDGET / count);
2702
+ return Math.min(RULE_WIDTH_MAX, Math.max(RULE_WIDTH_MIN, share));
2703
+ }
2704
+ function rulePassage(text, max) {
2705
+ const flat = text.replace(/\s+/g, " ").trim();
2706
+ const chars = [...flat];
2707
+ if (chars.length <= max) return flat;
2708
+ if (max <= 1) return "\u2026";
2709
+ const budget = max - 1;
2710
+ if (budget < 2) return `${chars.slice(0, budget).join("")}\u2026`;
2711
+ const tailBudget = Math.min(budget - 1, Math.max(1, Math.floor(budget * 0.6)));
2712
+ const tail = [...trailingSentences(flat, tailBudget)];
2713
+ const head = chars.slice(0, budget - tail.length).join("").trimEnd();
2714
+ return `${head}\u2026${tail.join("")}`;
2715
+ }
2716
+ function trailingSentences(flat, budget) {
2717
+ const starts = [...flat.matchAll(/[.!?。!?](?=\s)/g)].map(
2718
+ (match) => match.index + match[0].length
2719
+ );
2720
+ let best = null;
2721
+ for (let i = starts.length - 1; i >= 0; i -= 1) {
2722
+ const candidate = flat.slice(starts[i]).trim();
2723
+ if ([...candidate].length > budget) break;
2724
+ if (candidate !== "") best = candidate;
2725
+ }
2726
+ if (best !== null) return best;
2727
+ return [...flat].slice(-budget).join("");
2728
+ }
2729
+ var RULE_WIRE_WIDTH, RULE_WIDTH_MIN, RULE_WIDTH_MAX, RULE_BLOCK_BUDGET;
2730
+ var init_rule_text = __esm({
2731
+ "../../packages/memory-engine/src/rule-text.ts"() {
2732
+ "use strict";
2733
+ RULE_WIRE_WIDTH = 600;
2734
+ RULE_WIDTH_MIN = 200;
2735
+ RULE_WIDTH_MAX = 600;
2736
+ RULE_BLOCK_BUDGET = RULE_WIDTH_MIN * 10;
2737
+ }
2738
+ });
2739
+
2613
2740
  // ../../packages/memory-engine/src/boundary.ts
2614
2741
  function normalizeForMatch(value) {
2615
2742
  return value.normalize("NFC").replace(ZERO_WIDTH, "").replace(WHITESPACE_RUN, " ").trim();
@@ -2753,8 +2880,8 @@ function scopeFromPath(path2) {
2753
2880
  return parts.length >= 2 ? parts[parts.length - 2] ?? null : null;
2754
2881
  }
2755
2882
  function titleFromPath(path2) {
2756
- const base = path2.split("/").pop() ?? path2;
2757
- return base.replace(/\.[^.]+$/, "") || base;
2883
+ const base2 = path2.split("/").pop() ?? path2;
2884
+ return base2.replace(/\.[^.]+$/, "") || base2;
2758
2885
  }
2759
2886
  function h1Of(intro) {
2760
2887
  for (const line of intro.split("\n")) {
@@ -2868,8 +2995,8 @@ function mapNoteToMemory(input) {
2868
2995
  const ns = explicit ? nsRaw : {};
2869
2996
  const id = asString(ns["id"]) ?? path2;
2870
2997
  const title = asString(ns["title"]) ?? asString(frontmatter?.["title"]) ?? h1Of(outline.intro) ?? titleFromPath(path2);
2871
- const declaredScope = asString(ns["scope"]);
2872
- const scope = declaredScope ?? scopeFromPath(path2);
2998
+ const declaredScope2 = asString(ns["scope"]);
2999
+ const scope = declaredScope2 ?? scopeFromPath(path2);
2873
3000
  const summary = asString(ns["summary"]) ?? outline.description ?? firstParagraph(outline.intro) ?? firstSectionParagraph(outline.sections) ?? null;
2874
3001
  const memory = {
2875
3002
  id,
@@ -2913,11 +3040,17 @@ function mapNoteToMemory(input) {
2913
3040
  aliases: asStringArray(ns["aliases"]),
2914
3041
  entities: asStringArray(ns["entities"]),
2915
3042
  updated_at: asString(ns["updated_at"]) ?? asString(frontmatter?.["updated_at"]),
3043
+ // Carried onto the memory as well as onto the claim below, because a note
3044
+ // that states no claim gets no claim object at all — and `remember` has
3045
+ // never written a `claim:` key, so that is most of a real brain. Written
3046
+ // only when present, so a note without one maps byte-identically to what
3047
+ // it always did.
3048
+ ...asString(ns["valid_until"]) === null ? {} : { valid_until: asString(ns["valid_until"]) },
2916
3049
  provenance: {
2917
3050
  sourcePath: path2,
2918
3051
  sourceRevision: revision,
2919
3052
  mapping: explicit ? "explicit" : "heuristic",
2920
- scopeSource: declaredScope !== null ? "explicit" : scope === null ? "none" : "path"
3053
+ scopeSource: declaredScope2 !== null ? "explicit" : scope === null ? "none" : "path"
2921
3054
  }
2922
3055
  };
2923
3056
  const claims = [];
@@ -2943,22 +3076,6 @@ function mapNoteToMemory(input) {
2943
3076
  issues
2944
3077
  };
2945
3078
  }
2946
- function isUntouchedBase(path2, content) {
2947
- if (!/^01_BASE\//.test(path2)) return false;
2948
- const body = content.replace(/^---\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/, "");
2949
- const lines = body.split(/\r?\n/).map((l) => l.trimEnd());
2950
- let seenHeading = false;
2951
- for (const line of lines) {
2952
- if (line.trim() === "") continue;
2953
- if (!seenHeading && line.startsWith("# ")) {
2954
- seenHeading = true;
2955
- continue;
2956
- }
2957
- if (/^-\s*$/.test(line)) continue;
2958
- if (/^[-*+]\s+\S/.test(line) || /^#{1,6}\s/.test(line)) return false;
2959
- }
2960
- return seenHeading;
2961
- }
2962
3079
  var NAMESPACE, isRecord, asString, asStringArray, AUTHORABLE;
2963
3080
  var init_markdown_mapping = __esm({
2964
3081
  "../../packages/memory-engine/src/markdown-mapping.ts"() {
@@ -3048,7 +3165,7 @@ var init_cache_io = __esm({
3048
3165
  "../../packages/memory-engine/src/view/cache-io.ts"() {
3049
3166
  "use strict";
3050
3167
  init_schema();
3051
- SCHEMA_VERSION = 6;
3168
+ SCHEMA_VERSION = 7;
3052
3169
  POLICY_VERSION = 1;
3053
3170
  CacheEntrySchema = z23.object({
3054
3171
  path: z23.string().min(1),
@@ -3663,6 +3780,12 @@ function terms(text) {
3663
3780
  (match) => splitMixedScript(match[0])
3664
3781
  );
3665
3782
  }
3783
+ function contentTerms(text) {
3784
+ const all = terms(text);
3785
+ if (all.some((t) => HANGUL.test(t))) return all;
3786
+ const content = all.filter((t) => !LATIN_STOPWORDS.has(t));
3787
+ return content.length > 0 ? content : all;
3788
+ }
3666
3789
  function indexTerms(text) {
3667
3790
  return [...text.normalize("NFC").toLowerCase().matchAll(TOKEN)].flatMap((match) => {
3668
3791
  const token = match[0];
@@ -3735,8 +3858,9 @@ function fieldScore(queryTerms, field, weight, allowLeadingOneSyllable = false)
3735
3858
  return score;
3736
3859
  }
3737
3860
  function searchMemories(memories, query) {
3738
- const q = terms(query);
3861
+ const q = contentTerms(query);
3739
3862
  if (q.length === 0) return [];
3863
+ const scale = terms(query).length;
3740
3864
  const scored = [];
3741
3865
  for (const memory of memories) {
3742
3866
  let score = fieldScore(q, memory.title, WEIGHTS.title, true);
@@ -3744,13 +3868,13 @@ function searchMemories(memories, query) {
3744
3868
  for (const entity of memory.entities) score += fieldScore(q, entity, WEIGHTS.entity);
3745
3869
  score += fieldScore(q, memory.summary, WEIGHTS.summary);
3746
3870
  score += fieldScore(q, memory.scope, WEIGHTS.scope);
3747
- if (score > 0) scored.push({ memory, score: score / q.length });
3871
+ if (score > 0) scored.push({ memory, score: score / scale });
3748
3872
  }
3749
3873
  return scored.sort(
3750
3874
  (a, b) => b.score !== a.score ? b.score - a.score : a.memory.id < b.memory.id ? -1 : 1
3751
3875
  );
3752
3876
  }
3753
- var WEIGHTS, TOKEN, COMPONENT_SEPARATOR, HAS_COMPONENT_SEPARATOR, LATIN_HANGUL_BOUNDARY, SUBSTRING_MIN, PARTICLES, HANGUL, ONE_SYLLABLE_TOPIC_TERMS;
3877
+ var WEIGHTS, TOKEN, COMPONENT_SEPARATOR, HAS_COMPONENT_SEPARATOR, LATIN_HANGUL_BOUNDARY, LATIN_STOPWORDS, SUBSTRING_MIN, PARTICLES, HANGUL, ONE_SYLLABLE_TOPIC_TERMS;
3754
3878
  var init_search = __esm({
3755
3879
  "../../packages/memory-engine/src/recall/search.ts"() {
3756
3880
  "use strict";
@@ -3759,6 +3883,120 @@ var init_search = __esm({
3759
3883
  COMPONENT_SEPARATOR = /[._/-]+/u;
3760
3884
  HAS_COMPONENT_SEPARATOR = /[._/-]/u;
3761
3885
  LATIN_HANGUL_BOUNDARY = new RegExp("(?<=\\p{Script=Latin})(?=\\p{Script=Hangul})|(?<=\\p{Script=Hangul})(?=\\p{Script=Latin})", "u");
3886
+ LATIN_STOPWORDS = /* @__PURE__ */ new Set([
3887
+ // pronouns and possessives
3888
+ "i",
3889
+ "me",
3890
+ "my",
3891
+ "mine",
3892
+ "myself",
3893
+ "we",
3894
+ "us",
3895
+ "our",
3896
+ "ours",
3897
+ "ourselves",
3898
+ "you",
3899
+ "your",
3900
+ "yours",
3901
+ "yourself",
3902
+ "yourselves",
3903
+ "he",
3904
+ "him",
3905
+ "his",
3906
+ "himself",
3907
+ "she",
3908
+ "her",
3909
+ "hers",
3910
+ "herself",
3911
+ "it",
3912
+ "its",
3913
+ "itself",
3914
+ "they",
3915
+ "them",
3916
+ "their",
3917
+ "theirs",
3918
+ "themselves",
3919
+ // determiners
3920
+ "a",
3921
+ "an",
3922
+ "the",
3923
+ "this",
3924
+ "that",
3925
+ "these",
3926
+ "those",
3927
+ "some",
3928
+ "any",
3929
+ "each",
3930
+ "every",
3931
+ // auxiliaries and modals
3932
+ "am",
3933
+ "is",
3934
+ "are",
3935
+ "was",
3936
+ "were",
3937
+ "be",
3938
+ "been",
3939
+ "being",
3940
+ "do",
3941
+ "does",
3942
+ "did",
3943
+ "have",
3944
+ "has",
3945
+ "had",
3946
+ "will",
3947
+ "would",
3948
+ "shall",
3949
+ "should",
3950
+ "can",
3951
+ "could",
3952
+ "may",
3953
+ "might",
3954
+ "must",
3955
+ // question words
3956
+ "what",
3957
+ "which",
3958
+ "who",
3959
+ "whom",
3960
+ "whose",
3961
+ "when",
3962
+ "where",
3963
+ "why",
3964
+ "how",
3965
+ // prepositions
3966
+ "about",
3967
+ "above",
3968
+ "after",
3969
+ "at",
3970
+ "before",
3971
+ "by",
3972
+ "for",
3973
+ "from",
3974
+ "in",
3975
+ "into",
3976
+ "of",
3977
+ "off",
3978
+ "on",
3979
+ "onto",
3980
+ "over",
3981
+ "to",
3982
+ "under",
3983
+ "with",
3984
+ "without",
3985
+ "through",
3986
+ "during",
3987
+ // conjunctions and particles
3988
+ "and",
3989
+ "but",
3990
+ "or",
3991
+ "nor",
3992
+ "so",
3993
+ "if",
3994
+ "then",
3995
+ "than",
3996
+ "as",
3997
+ "there",
3998
+ "here"
3999
+ ]);
3762
4000
  SUBSTRING_MIN = 2;
3763
4001
  PARTICLES = [
3764
4002
  "\uC774\uB77C\uB3C4",
@@ -3909,7 +4147,7 @@ function freshnessOf(memory, claims, now) {
3909
4147
  if (memory.status === "superseded") return "superseded";
3910
4148
  if (memory.status === "retired") return "retired";
3911
4149
  const current = claims.find((c2) => c2.id === memory.current_claim) ?? claims[0];
3912
- const until = current?.valid_until;
4150
+ const until = current?.valid_until ?? memory.valid_until ?? null;
3913
4151
  if (until != null) {
3914
4152
  const t = Date.parse(until);
3915
4153
  if (Number.isFinite(t) && t < now.getTime()) return "expired";
@@ -3939,7 +4177,23 @@ function recall(entries, query, opts = {}) {
3939
4177
  (e) => (e.chunks ?? []).map((c2) => ({ memoryId: e.memory.id, label: c2.label, text: c2.text }))
3940
4178
  )
3941
4179
  ).search(queryShape.lexical);
3942
- const semanticHits = depth === "exact" ? [] : opts.semantic ?? [];
4180
+ const offeredSemantic = depth === "exact" ? [] : opts.semantic ?? [];
4181
+ const reachedByWords = /* @__PURE__ */ new Set([
4182
+ ...cardHits.map((hit) => hit.memory.id),
4183
+ ...chunkHits.map((hit) => hit.memoryId)
4184
+ ]);
4185
+ const floor = opts.meaningFloor;
4186
+ const bestSimilarity = offeredSemantic.reduce(
4187
+ (best, hit) => Math.max(best, Math.min(1, Math.max(0, hit.similarity))),
4188
+ 0
4189
+ );
4190
+ const semanticHits = floor === void 0 ? offeredSemantic : offeredSemantic.filter((hit) => {
4191
+ if (reachedByWords.has(hit.id)) return true;
4192
+ const s = Math.min(1, Math.max(0, hit.similarity));
4193
+ if (floor.absolute !== void 0 && s < floor.absolute) return false;
4194
+ if (floor.gap !== void 0 && bestSimilarity - s > floor.gap) return false;
4195
+ return true;
4196
+ });
3943
4197
  const scoreById = /* @__PURE__ */ new Map();
3944
4198
  for (const hit of cardHits) scoreById.set(hit.memory.id, hit.score);
3945
4199
  for (const hit of chunkHits) {
@@ -3949,6 +4203,14 @@ function recall(entries, query, opts = {}) {
3949
4203
  const similarity = Math.min(1, Math.max(0, hit.similarity));
3950
4204
  scoreById.set(hit.id, (scoreById.get(hit.id) ?? 0) + similarity * SEMANTIC_WEIGHT);
3951
4205
  }
4206
+ const wordIds = reachedByWords;
4207
+ const meaningIds = new Set(semanticHits.map((hit) => hit.id));
4208
+ const matchedOf = (id) => {
4209
+ if (opts.semantic === void 0 || depth === "exact") return void 0;
4210
+ const words = wordIds.has(id);
4211
+ const meaning = meaningIds.has(id);
4212
+ return words && meaning ? "both" : words ? "words" : meaning ? "meaning" : void 0;
4213
+ };
3952
4214
  const memoryById = new Map(allMemories.map((m) => [m.id, m]));
3953
4215
  const hits = [...scoreById.entries()].flatMap(([id, score]) => {
3954
4216
  const memory = memoryById.get(id);
@@ -4049,7 +4311,11 @@ function recall(entries, query, opts = {}) {
4049
4311
  type: memory.type,
4050
4312
  // The handle IS the progressive part. No claim text here, by
4051
4313
  // construction rather than by remembering to strip it.
4052
- detail_handle: `recall://${memory.id}`
4314
+ detail_handle: `recall://${memory.id}`,
4315
+ ...(() => {
4316
+ const matched = matchedOf(memory.id);
4317
+ return matched === void 0 ? {} : { matched };
4318
+ })()
4053
4319
  };
4054
4320
  }
4055
4321
  }
@@ -4080,7 +4346,7 @@ function resolveDetail(entries, handle) {
4080
4346
  const entry = entries.find((e) => e.memory.id === id);
4081
4347
  return entry ? { memory: entry.memory, claims: entry.claims, relations: entry.relations } : null;
4082
4348
  }
4083
- var RECALL_DEPTHS, RecallOptionError, SEMANTIC_CANDIDATES, DEFAULTS, BODY_WEIGHT, SEMANTIC_WEIGHT, BINDING_BOOST, KIND_BOOST, DECISION_NOUNS, DECISION_CONTEXT, DECISION_FILLERS, CURRENCY_RANK, WIDEST_ESTIMATE;
4349
+ var MEANING_ONLY_NOTE, RECALL_DEPTHS, RecallOptionError, SEMANTIC_CANDIDATES, DEFAULTS, BODY_WEIGHT, SEMANTIC_WEIGHT, BINDING_BOOST, KIND_BOOST, DECISION_NOUNS, DECISION_CONTEXT, DECISION_FILLERS, CURRENCY_RANK, WIDEST_ESTIMATE;
4084
4350
  var init_planner = __esm({
4085
4351
  "../../packages/memory-engine/src/recall/planner.ts"() {
4086
4352
  "use strict";
@@ -4089,6 +4355,7 @@ var init_planner = __esm({
4089
4355
  init_conflicts();
4090
4356
  init_chunks();
4091
4357
  init_search();
4358
+ MEANING_ONLY_NOTE = "matched by meaning only \u2014 none of the question\u2019s words are in this note";
4092
4359
  RECALL_DEPTHS = ["exact", "standard"];
4093
4360
  RecallOptionError = class extends Error {
4094
4361
  hint;
@@ -4175,18 +4442,164 @@ var init_planner = __esm({
4175
4442
  }
4176
4443
  });
4177
4444
 
4445
+ // ../../packages/memory-engine/src/note-body.ts
4446
+ function bodyOf(content) {
4447
+ const stripped = content.startsWith(BOM) ? content.slice(BOM.length) : content;
4448
+ const fence2 = FENCE.exec(stripped);
4449
+ return fence2 ? stripped.slice(fence2[0].length) : stripped;
4450
+ }
4451
+ var BOM, FENCE;
4452
+ var init_note_body = __esm({
4453
+ "../../packages/memory-engine/src/note-body.ts"() {
4454
+ "use strict";
4455
+ BOM = String.fromCharCode(65279);
4456
+ FENCE = /^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/;
4457
+ }
4458
+ });
4459
+
4460
+ // ../../packages/memory-engine/src/base-templates.ts
4461
+ function base(id, title, summary, body) {
4462
+ return [
4463
+ "---",
4464
+ "memgineering:",
4465
+ ` id: ${id}`,
4466
+ ` title: ${title}`,
4467
+ ` summary: ${summary}`,
4468
+ " scope: base",
4469
+ "---",
4470
+ "",
4471
+ `# ${title}`,
4472
+ "",
4473
+ body,
4474
+ ""
4475
+ ].join("\n");
4476
+ }
4477
+ function ownEntry(record, key2) {
4478
+ return Object.hasOwn(record, key2) ? record[key2] : void 0;
4479
+ }
4480
+ function wordingsFor(path2) {
4481
+ const current = ownEntry(BASE_TEMPLATES, path2);
4482
+ const retired = ownEntry(RETIRED_BASE_TEMPLATES, path2) ?? [];
4483
+ return current === void 0 ? retired : [current, ...retired];
4484
+ }
4485
+ function isExactUntouchedBase(path2, content) {
4486
+ const saved = asSaved(content);
4487
+ return wordingsFor(path2).some((t) => asSaved(t) === saved);
4488
+ }
4489
+ function isTemplateBody(path2, content) {
4490
+ const lines = bodyLines(content).join("\n");
4491
+ return wordingsFor(path2).some((t) => bodyLines(t).join("\n") === lines);
4492
+ }
4493
+ function isUnwrittenBody(path2, content) {
4494
+ if (isTemplateBody(path2, content)) return true;
4495
+ const [first, ...rest] = bodyLines(content);
4496
+ if (first === void 0) return false;
4497
+ const headings = wordingsFor(path2).map((t) => bodyLines(t)[0]);
4498
+ return headings.includes(first) && rest.every((line) => /^-\s*$/.test(line));
4499
+ }
4500
+ var BASE_TEMPLATES, RETIRED_BASE_TEMPLATES, BOM2, asSaved, bodyLines;
4501
+ var init_base_templates = __esm({
4502
+ "../../packages/memory-engine/src/base-templates.ts"() {
4503
+ "use strict";
4504
+ init_note_body();
4505
+ BASE_TEMPLATES = {
4506
+ "01_BASE/CRITICAL_FACTS.md": base(
4507
+ "critical-facts",
4508
+ "Critical facts",
4509
+ "The few things that must never be wrong.",
4510
+ [
4511
+ "Keep this short enough to read every session. Anything that can be",
4512
+ "looked up when needed belongs in a note instead.",
4513
+ "",
4514
+ "- "
4515
+ ].join("\n")
4516
+ ),
4517
+ "01_BASE/USER.md": base(
4518
+ "user",
4519
+ "About me",
4520
+ "Who I am and what context I work in.",
4521
+ [
4522
+ "Background, role, what you are working on, what an agent should assume",
4523
+ "you already know.",
4524
+ "",
4525
+ "- "
4526
+ ].join("\n")
4527
+ ),
4528
+ "01_BASE/PREFERENCES.md": base(
4529
+ "preferences",
4530
+ "Preferences",
4531
+ "How I like answers, code and decisions handled.",
4532
+ ["Style, verbosity, how much to check in, how you like code written.", "", "- "].join("\n")
4533
+ ),
4534
+ "01_BASE/BOUNDARIES.md": base(
4535
+ "boundaries",
4536
+ "Boundaries",
4537
+ "What an agent must not do without asking me first.",
4538
+ [
4539
+ "Privacy, spending, anything irreversible, anything that leaves this",
4540
+ "machine. Be specific \u2014 a boundary that has to be interpreted is one",
4541
+ "that will be interpreted generously.",
4542
+ "",
4543
+ "Storage boundaries belong here too: topics you never want written into",
4544
+ "this brain \u2014 health, beliefs, relationships, anything you name. An",
4545
+ "agent checks this file before it records anything.",
4546
+ "",
4547
+ "- "
4548
+ ].join("\n")
4549
+ ),
4550
+ "01_BASE/TOOLING.md": base(
4551
+ "tooling",
4552
+ "Tooling",
4553
+ "The tools, machines and commands I actually use.",
4554
+ [
4555
+ "Editors, languages, deploy commands, where things run. Enough that an",
4556
+ "agent does not have to guess or discover it again each time.",
4557
+ "",
4558
+ "- "
4559
+ ].join("\n")
4560
+ )
4561
+ };
4562
+ RETIRED_BASE_TEMPLATES = {
4563
+ // Retired 0.11.1, when the prompt grew a storage-boundary invitation.
4564
+ "01_BASE/BOUNDARIES.md": [
4565
+ [
4566
+ "---",
4567
+ "memgineering:",
4568
+ " id: boundaries",
4569
+ " title: Boundaries",
4570
+ " summary: What an agent must not do without asking me first.",
4571
+ " scope: base",
4572
+ "---",
4573
+ "",
4574
+ "# Boundaries",
4575
+ "",
4576
+ "Privacy, spending, anything irreversible, anything that leaves this",
4577
+ "machine. Be specific \u2014 a boundary that has to be interpreted is one",
4578
+ "that will be interpreted generously.",
4579
+ "",
4580
+ "- ",
4581
+ ""
4582
+ ].join("\n")
4583
+ ]
4584
+ };
4585
+ BOM2 = String.fromCharCode(65279);
4586
+ asSaved = (text) => (text.startsWith(BOM2) ? text.slice(BOM2.length) : text).replace(/\r\n/g, "\n").trimEnd();
4587
+ bodyLines = (content) => bodyOf(content).split("\n").map((line) => line.trimEnd()).filter((line) => line.trim() !== "");
4588
+ }
4589
+ });
4590
+
4178
4591
  // ../../packages/memory-engine/src/recall/semantic.ts
4179
- function withoutUntouchedBase(hits, contentById) {
4592
+ function withoutUntouchedBase(hits, contentById, isTemplate = isExactUntouchedBase) {
4180
4593
  return hits.filter((hit) => {
4181
4594
  const note = contentById.get(hit.id);
4182
- return note === void 0 ? true : !isUntouchedBase(note.path, note.content);
4595
+ return note === void 0 ? true : !isTemplate(note.path, note.content);
4183
4596
  });
4184
4597
  }
4185
4598
  var SEMANTIC_FIELDS;
4186
4599
  var init_semantic = __esm({
4187
4600
  "../../packages/memory-engine/src/recall/semantic.ts"() {
4188
4601
  "use strict";
4189
- init_markdown_mapping();
4602
+ init_base_templates();
4190
4603
  SEMANTIC_FIELDS = {
4191
4604
  /** `used` or `skipped`. Always present on a server that has this at all. */
4192
4605
  state: "semantic",
@@ -4200,6 +4613,103 @@ var init_semantic = __esm({
4200
4613
  }
4201
4614
  });
4202
4615
 
4616
+ // ../../packages/memory-engine/src/recall/unfilled.ts
4617
+ function markUnfilled(cards, contentById, slots, inConflict = /* @__PURE__ */ new Set()) {
4618
+ const kept = [];
4619
+ const withheld = [];
4620
+ let templates = 0;
4621
+ for (const card of cards) {
4622
+ const note = contentById.get(card.id);
4623
+ const unfilled = note !== void 0 && isExactUntouchedBase(note.path, note.content);
4624
+ if (unfilled && slots !== void 0 && !inConflict.has(card.id) && ++templates > slots) {
4625
+ withheld.push(card.id);
4626
+ continue;
4627
+ }
4628
+ kept.push({ ...card, unfilled });
4629
+ }
4630
+ return { cards: kept, withheld };
4631
+ }
4632
+ var UNFILLED_FIELDS;
4633
+ var init_unfilled = __esm({
4634
+ "../../packages/memory-engine/src/recall/unfilled.ts"() {
4635
+ "use strict";
4636
+ init_base_templates();
4637
+ UNFILLED_FIELDS = {
4638
+ /** On every card from a server that has this: still the starter file, nothing written in it. */
4639
+ card: "unfilled",
4640
+ /**
4641
+ * In the request: how many such cards the caller will take. Absent means no
4642
+ * cap, so a client older than the field gets the whole ranking it always got —
4643
+ * the same boundary `budget` draws: the client states the capability.
4644
+ */
4645
+ slots: "template_slots",
4646
+ /** In the response, only when the cap left something out: how many. */
4647
+ withheld: "templates_withheld"
4648
+ };
4649
+ }
4650
+ });
4651
+
4652
+ // ../../packages/memory-engine/src/setup-progress.ts
4653
+ var SETUP_PROGRESS_CODES;
4654
+ var init_setup_progress = __esm({
4655
+ "../../packages/memory-engine/src/setup-progress.ts"() {
4656
+ "use strict";
4657
+ SETUP_PROGRESS_CODES = [
4658
+ /** Installing the guidance and settings. */
4659
+ "writing_config",
4660
+ /** Uploading the chosen folder's notes. */
4661
+ "uploading",
4662
+ /** An upload chosen on the screen, held until somebody at the terminal checks the account. */
4663
+ "upload_waits_for_check",
4664
+ /** Finished. */
4665
+ "done",
4666
+ /** This machine is signed in to a different account; nothing was changed. */
4667
+ "different_account",
4668
+ /** Failed for another reason; `detail` says which. */
4669
+ "failed"
4670
+ ];
4671
+ }
4672
+ });
4673
+
4674
+ // ../../packages/memory-engine/src/recall/query-prefix.ts
4675
+ var QUERY_EMBEDDING_PREFIX;
4676
+ var init_query_prefix = __esm({
4677
+ "../../packages/memory-engine/src/recall/query-prefix.ts"() {
4678
+ "use strict";
4679
+ QUERY_EMBEDDING_PREFIX = "task: search result | query: ";
4680
+ }
4681
+ });
4682
+
4683
+ // ../../packages/memory-engine/src/recall/scope-spread.ts
4684
+ function declaredScope(memory) {
4685
+ return memory.provenance?.scopeSource === "explicit" ? memory.scope ?? null : null;
4686
+ }
4687
+ function scopeSpread(returned, brain) {
4688
+ const scoped = returned.filter((s) => typeof s === "string" && s !== "");
4689
+ const distinct = new Set(scoped);
4690
+ if (distinct.size !== 1) return null;
4691
+ const only = [...distinct][0];
4692
+ const others = /* @__PURE__ */ new Set();
4693
+ for (const scope of brain) if (scope && scope !== only) others.add(scope);
4694
+ if (others.size === 0) return null;
4695
+ const sorted = [...others].sort((a, b) => a.localeCompare(b));
4696
+ return {
4697
+ scope: only,
4698
+ cards: scoped.length,
4699
+ of: returned.length,
4700
+ elsewhere: sorted.slice(0, ELSEWHERE_SHOWN),
4701
+ elsewhere_total: sorted.length
4702
+ };
4703
+ }
4704
+ var SCOPE_SPREAD_FIELD, ELSEWHERE_SHOWN;
4705
+ var init_scope_spread = __esm({
4706
+ "../../packages/memory-engine/src/recall/scope-spread.ts"() {
4707
+ "use strict";
4708
+ SCOPE_SPREAD_FIELD = "scope_spread";
4709
+ ELSEWHERE_SHOWN = 5;
4710
+ }
4711
+ });
4712
+
4203
4713
  // ../../packages/memory-engine/src/proposal/schema.ts
4204
4714
  import { z as z24 } from "zod";
4205
4715
  function isLifecycleAction(action) {
@@ -4208,8 +4718,17 @@ function isLifecycleAction(action) {
4208
4718
  function isClassifyAction(action) {
4209
4719
  return action === "reclassify";
4210
4720
  }
4721
+ function isBindingAction(action) {
4722
+ return action === "unbind";
4723
+ }
4724
+ function isScopeAction(action) {
4725
+ return action === "rescope";
4726
+ }
4727
+ function isValidityAction(action) {
4728
+ return action === "expire";
4729
+ }
4211
4730
  function carriesNoClaim(action) {
4212
- return isLifecycleAction(action) || isClassifyAction(action);
4731
+ return isLifecycleAction(action) || isClassifyAction(action) || isBindingAction(action) || isScopeAction(action) || isValidityAction(action);
4213
4732
  }
4214
4733
  function isAccepted(value) {
4215
4734
  return typeof value === "object" && value !== null && value[ACCEPTED] === true;
@@ -4238,6 +4757,47 @@ var init_schema2 = __esm({
4238
4757
  "retire",
4239
4758
  "unretire",
4240
4759
  "reclassify",
4760
+ /**
4761
+ * A memory stops binding. Nothing else about it changes.
4762
+ *
4763
+ * It exists because there was no way to do this without saying something
4764
+ * else. Taking a marking back meant `revise --claim`, which is a verb about
4765
+ * conclusions — so unbinding thirteen rules on a real hosted brain required
4766
+ * restating thirteen conclusions the author had not changed, from text the
4767
+ * CLI hands back TRUNCATED. The person doing it read the note bodies out of
4768
+ * the database to avoid corrupting them, which is not a thing a user can do.
4769
+ *
4770
+ * One direction on purpose. `unbind` that could also bind would be a name
4771
+ * that lies half the time; marking something as a rule is already reachable
4772
+ * (`remember --rule`, or a `revise` that is genuinely saying something new).
4773
+ */
4774
+ "unbind",
4775
+ /**
4776
+ * Where a memory binds changes. Nothing it SAYS changes.
4777
+ *
4778
+ * `scope` is read by `declaredScopeOf` and decides which of the two rule
4779
+ * budgets a decision is drawn from, so it is the field that answers "does
4780
+ * this bind me here". Nothing could write it. The one live consequence:
4781
+ * folding a project-scoped rule into an unscoped one required superseding
4782
+ * it, which retires the loser — so the rule would have vanished everywhere
4783
+ * except the project it was scoped to.
4784
+ *
4785
+ * The value is a scope NAME, or the reserved word `global`. Never null: see
4786
+ * the refinement below for why "absent" and "widen" must not be the same
4787
+ * value on the wire.
4788
+ */
4789
+ "rescope",
4790
+ /**
4791
+ * A memory gets an end date. It is not retired, and it still recalls.
4792
+ *
4793
+ * `freshnessOf` has always returned `expired` for a claim whose
4794
+ * `valid_until` has passed, and `CURRENCY_RANK` has always ranked that just
4795
+ * below `current` — read, ranked, and labelled, with nothing able to write
4796
+ * it. A date-stamped fact ("Phase 1 lands by the end of October") therefore
4797
+ * had two options, both wrong: retire it, which says it no longer holds, or
4798
+ * leave it, which says it holds forever.
4799
+ */
4800
+ "expire",
4241
4801
  "ignore"
4242
4802
  ]);
4243
4803
  StorableActionSchema = ProposalActionSchema.exclude(["ignore"]);
@@ -4397,6 +4957,37 @@ var init_schema2 = __esm({
4397
4957
  * reclassify is pending — approve, reject, or let it expire first.
4398
4958
  */
4399
4959
  proposed_kind: MemoryKindSchema.nullable().default(null),
4960
+ /**
4961
+ * Where `rescope` moves the memory to, and null for everything else.
4962
+ *
4963
+ * NEVER null to mean "everywhere". That was the first design and it is the
4964
+ * one thing this field must not do: "the caller sent no scope" and "the
4965
+ * caller asked to widen this rule" would be the same bytes, so the schema
4966
+ * could not require a scope on a rescope — and a request that LOST the
4967
+ * field, an older client, a hand-built body, a proposal rebuilt from disk,
4968
+ * would silently widen what a rule binds instead of failing.
4969
+ *
4970
+ * `global` is the word instead, and it is not invented here: `GLOBAL_SCOPE`
4971
+ * is already the reserved value a rule write uses to say it holds
4972
+ * everywhere, and `remember` already stores it as no scope at all. So the
4973
+ * widening is spelled, the refinement below can demand it, and the patcher
4974
+ * still writes what the thirty global rules already on a real brain look
4975
+ * like — no key.
4976
+ *
4977
+ * COMPATIBILITY BOUNDARY, as for `proposed_kind`: a proposal file carrying
4978
+ * `action: "rescope"` does not parse on a CLI built before this, and
4979
+ * `loadProposal` throws rather than silently dropping it.
4980
+ */
4981
+ proposed_scope: z24.string().min(1).max(SCOPE_LIMIT).nullable().default(null),
4982
+ /**
4983
+ * The end date `expire` writes onto the current claim, null elsewhere.
4984
+ *
4985
+ * A date rather than a status: `freshnessOf` reads `valid_until` off the
4986
+ * claim and returns `expired`, which recall already labels and ranks below
4987
+ * `current`. Nothing here decides what expiry MEANS — that was decided
4988
+ * years of code ago and only the writer was missing.
4989
+ */
4990
+ proposed_valid_until: z24.string().min(1).nullable().default(null),
4400
4991
  relations: ProposedRelationsSchema,
4401
4992
  /** Why the AI thinks this change is right. Shown at approval time. */
4402
4993
  reason: z24.string().min(1),
@@ -4433,7 +5024,7 @@ var init_schema2 = __esm({
4433
5024
  path: ["source_excerpt"]
4434
5025
  }
4435
5026
  ).refine((p) => carriesNoClaim(p.action) || p.claim !== null, {
4436
- message: "only retire, unretire and reclassify may omit the claim",
5027
+ message: "only retire, unretire, reclassify, unbind, rescope and expire may omit the claim",
4437
5028
  path: ["claim"]
4438
5029
  }).refine((p) => isClassifyAction(p.action) === (p.proposed_kind !== null), {
4439
5030
  message: "proposed_kind belongs to reclassify, and reclassify requires one",
@@ -4441,6 +5032,18 @@ var init_schema2 = __esm({
4441
5032
  }).refine((p) => !isClassifyAction(p.action) || p.claim === null, {
4442
5033
  message: "reclassify says what sort of memory this is; it does not take a claim",
4443
5034
  path: ["claim"]
5035
+ }).refine((p) => isScopeAction(p.action) === (p.proposed_scope !== null), {
5036
+ message: "proposed_scope belongs to rescope, and rescope requires one",
5037
+ path: ["proposed_scope"]
5038
+ }).refine((p) => isValidityAction(p.action) === (p.proposed_valid_until !== null), {
5039
+ message: "proposed_valid_until belongs to expire, and expire requires one",
5040
+ path: ["proposed_valid_until"]
5041
+ }).refine((p) => !isScopeAction(p.action) || p.claim === null, {
5042
+ message: "rescope says where a memory binds; it does not take a claim",
5043
+ path: ["claim"]
5044
+ }).refine((p) => !isValidityAction(p.action) || p.claim === null, {
5045
+ message: "expire says when a memory stops being current; it does not take a claim",
5046
+ path: ["claim"]
4444
5047
  });
4445
5048
  ACCEPTED = /* @__PURE__ */ Symbol("memgineering.approved");
4446
5049
  }
@@ -4490,12 +5093,42 @@ function detectConsolidation(entries, opts = {}) {
4490
5093
  ...titles
4491
5094
  ];
4492
5095
  const out = rankCandidates(scored);
5096
+ const limit = opts.limit ?? DEFAULT_LIMIT;
4493
5097
  return {
4494
- candidates: out.slice(0, opts.limit ?? DEFAULT_LIMIT),
5098
+ candidates: shareTheList(out, limit),
5099
+ by_rule: countByRule(out),
4495
5100
  scanned: budget.scanned,
4496
5101
  truncated: budget.truncated
4497
5102
  };
4498
5103
  }
5104
+ function shareTheList(ranked, limit) {
5105
+ const lanes = /* @__PURE__ */ new Map();
5106
+ for (const candidate of ranked) {
5107
+ const lane = lanes.get(candidate.rule);
5108
+ if (lane) lane.push(candidate);
5109
+ else lanes.set(candidate.rule, [candidate]);
5110
+ }
5111
+ const out = [];
5112
+ const queues = [...lanes.values()];
5113
+ let round = 0;
5114
+ for (; ; ) {
5115
+ let added = false;
5116
+ for (const queue of queues) {
5117
+ if (out.length >= limit) return out;
5118
+ const next = queue[round];
5119
+ if (next === void 0) continue;
5120
+ out.push(next);
5121
+ added = true;
5122
+ }
5123
+ if (!added) return out;
5124
+ round += 1;
5125
+ }
5126
+ }
5127
+ function countByRule(ranked) {
5128
+ const out = {};
5129
+ for (const candidate of ranked) out[candidate.rule] = (out[candidate.rule] ?? 0) + 1;
5130
+ return out;
5131
+ }
4499
5132
  function titleTerms(title) {
4500
5133
  const out = /* @__PURE__ */ new Set();
4501
5134
  for (const term of terms(title)) {
@@ -4843,19 +5476,38 @@ function kindPromotions(entries) {
4843
5476
  const out = [];
4844
5477
  for (const entry of entries) {
4845
5478
  if (entry.memory.binding !== true) continue;
4846
- if (entry.memory.kind === "decision") continue;
5479
+ const kind = entry.memory.kind;
5480
+ if (kind !== void 0 && RULE_SHAPED_KINDS.has(kind)) continue;
5481
+ if (kind === void 0) {
5482
+ out.push({
5483
+ candidate: {
5484
+ rule: "binding_unclassified",
5485
+ memory_id: entry.memory.id,
5486
+ path: entry.path,
5487
+ title: entry.memory.title,
5488
+ other_id: null,
5489
+ other_path: null,
5490
+ other_title: null,
5491
+ evidence: "marked as binding and left unclassified \u2014 either a decision nobody classified, or something that was never a rule",
5492
+ suggested_action: "decide",
5493
+ suggested_kind: null
5494
+ },
5495
+ confidence: 4
5496
+ });
5497
+ continue;
5498
+ }
4847
5499
  out.push({
4848
5500
  candidate: {
4849
- rule: "kind_promotion",
5501
+ rule: "rule_demotion",
4850
5502
  memory_id: entry.memory.id,
4851
5503
  path: entry.path,
4852
5504
  title: entry.memory.title,
4853
5505
  other_id: null,
4854
5506
  other_path: null,
4855
5507
  other_title: null,
4856
- evidence: entry.memory.kind === void 0 ? "marked as binding but left unclassified \u2014 a memory that binds is a decision" : `marked as binding but classified as ${entry.memory.kind} \u2014 a memory that binds is a decision`,
4857
- suggested_action: "reclassify",
4858
- suggested_kind: "decision"
5508
+ evidence: `marked as binding but classified as ${kind} \u2014 the author said it is not a decision, so it may not be a rule`,
5509
+ suggested_action: "unmark_rule",
5510
+ suggested_kind: null
4859
5511
  },
4860
5512
  confidence: 4
4861
5513
  });
@@ -4962,7 +5614,7 @@ function* pairsFrom(buckets, budget) {
4962
5614
  }
4963
5615
  }
4964
5616
  }
4965
- var CONSOLIDATION_RULES, DEFAULT_LIMIT, DEFAULT_PAIR_BUDGET, SEMANTIC_PAIR_BUDGET, TITLE_OVERLAP, SAME_SCOPE_SEMANTIC_SIMILARITY, SHARED_ENTITY_SEMANTIC_SIMILARITY, GLOBAL_SEMANTIC_SIMILARITY, RECIPROCAL_NEIGHBOURS, CandidateComponents;
5617
+ var CONSOLIDATION_RULES, DEFAULT_LIMIT, DEFAULT_PAIR_BUDGET, SEMANTIC_PAIR_BUDGET, TITLE_OVERLAP, SAME_SCOPE_SEMANTIC_SIMILARITY, SHARED_ENTITY_SEMANTIC_SIMILARITY, GLOBAL_SEMANTIC_SIMILARITY, RECIPROCAL_NEIGHBOURS, RULE_SHAPED_KINDS, CandidateComponents;
4966
5618
  var init_detect = __esm({
4967
5619
  "../../packages/memory-engine/src/proposal/detect.ts"() {
4968
5620
  "use strict";
@@ -4973,7 +5625,9 @@ var init_detect = __esm({
4973
5625
  "semantic_similarity",
4974
5626
  "shared_context",
4975
5627
  "similar_title",
4976
- "kind_promotion"
5628
+ "kind_promotion",
5629
+ "rule_demotion",
5630
+ "binding_unclassified"
4977
5631
  ];
4978
5632
  DEFAULT_LIMIT = 50;
4979
5633
  DEFAULT_PAIR_BUDGET = 2e5;
@@ -4983,6 +5637,7 @@ var init_detect = __esm({
4983
5637
  SHARED_ENTITY_SEMANTIC_SIMILARITY = 0.75;
4984
5638
  GLOBAL_SEMANTIC_SIMILARITY = 0.72;
4985
5639
  RECIPROCAL_NEIGHBOURS = 2;
5640
+ RULE_SHAPED_KINDS = /* @__PURE__ */ new Set(["decision", "preference"]);
4986
5641
  CandidateComponents = class {
4987
5642
  parent = /* @__PURE__ */ new Map();
4988
5643
  root(id) {
@@ -5319,12 +5974,12 @@ function commit(prepared, input, opts = {}) {
5319
5974
  if (carriesNoClaim(parsed.action)) {
5320
5975
  if (parsed.claim !== null) {
5321
5976
  throw Object.assign(new Error(`${parsed.action} does not take a claim`), {
5322
- hint: isClassifyAction(parsed.action) ? `${parsed.action} says what sort of memory this is, not what it says. To change the conclusion too, use supersede.` : `${parsed.action} changes whether a memory is current, not what it says. To change the conclusion too, use supersede.`
5977
+ hint: isClassifyAction(parsed.action) ? `${parsed.action} says what sort of memory this is, not what it says. To change the conclusion too, use supersede.` : isBindingAction(parsed.action) ? `${parsed.action} takes a marking back and leaves the conclusion exactly as it is \u2014 that is the whole reason it exists. To change what the note says as well, use supersede.` : isScopeAction(parsed.action) ? `${parsed.action} says where a memory binds, not what it says. To change the conclusion too, use supersede.` : isValidityAction(parsed.action) ? `${parsed.action} gives a memory an end date and leaves the conclusion exactly as it is. To change what the note says as well, use supersede.` : `${parsed.action} changes whether a memory is current, not what it says. To change the conclusion too, use supersede.`
5323
5978
  });
5324
5979
  }
5325
5980
  } else if (parsed.claim === null) {
5326
5981
  throw Object.assign(new Error(`${parsed.action} needs a claim`), {
5327
- hint: "Only retire, unretire and reclassify may omit it. They change a memory's standing or its classification rather than its content."
5982
+ hint: "Only retire, unretire, reclassify, unbind, rescope and expire may omit it. They change a memory's standing, its classification, whether it binds, where it binds or when it stops being current \u2014 rather than its content."
5328
5983
  });
5329
5984
  } else if ((parsed.action === "supersede" || parsed.action === "create") && parsed.claim.summary === null) {
5330
5985
  throw Object.assign(new Error(`${parsed.action} needs a summary as well as a claim`), {
@@ -5341,6 +5996,26 @@ function commit(prepared, input, opts = {}) {
5341
5996
  hint: "Only `reclassify` moves a memory's kind, and it does nothing else. Propose it separately, with its own reason."
5342
5997
  });
5343
5998
  }
5999
+ if (isScopeAction(parsed.action) && parsed.proposed_scope === null) {
6000
+ throw Object.assign(new Error(`${parsed.action} needs the scope it is proposing`), {
6001
+ hint: `Pass a project name, or \`${GLOBAL_SCOPE}\` to say the memory holds everywhere. Widening is spelled out because it cannot be taken back by accident.`
6002
+ });
6003
+ }
6004
+ if (!isScopeAction(parsed.action) && parsed.proposed_scope !== null) {
6005
+ throw Object.assign(new Error(`${parsed.action} does not set the scope`), {
6006
+ hint: "Only `rescope` moves where a memory binds, and it does nothing else. Propose it separately, with its own reason."
6007
+ });
6008
+ }
6009
+ if (isValidityAction(parsed.action) && parsed.proposed_valid_until === null) {
6010
+ throw Object.assign(new Error(`${parsed.action} needs the date it is proposing`), {
6011
+ hint: "Pass an ISO date \u2014 the day the memory stops being current. It is not retired by this: recall still returns it, ranked below what is current and labelled."
6012
+ });
6013
+ }
6014
+ if (!isValidityAction(parsed.action) && parsed.proposed_valid_until !== null) {
6015
+ throw Object.assign(new Error(`${parsed.action} does not set an end date`), {
6016
+ hint: "Only `expire` gives a memory an end date, and it does nothing else. Propose it separately, with its own reason."
6017
+ });
6018
+ }
5344
6019
  const now = opts.now ?? /* @__PURE__ */ new Date();
5345
6020
  const at = now.toISOString();
5346
6021
  const draft = {
@@ -5350,6 +6025,8 @@ function commit(prepared, input, opts = {}) {
5350
6025
  memory_id: parsed.memory_id,
5351
6026
  claim: parsed.claim,
5352
6027
  proposed_kind: parsed.proposed_kind,
6028
+ proposed_scope: parsed.proposed_scope,
6029
+ proposed_valid_until: parsed.proposed_valid_until,
5353
6030
  relations: parsed.relations,
5354
6031
  reason: parsed.reason,
5355
6032
  source_excerpt: parsed.source_excerpt,
@@ -5380,14 +6057,22 @@ var init_commit = __esm({
5380
6057
  */
5381
6058
  memory_id: z26.string().min(1).nullable().default(null),
5382
6059
  /**
5383
- * Omitted by `retire` / `unretire` / `reclassify`, which change a memory's
5384
- * standing or its classification rather than what it says. Checked below with
5385
- * the other action rules so the error can name the action instead of pointing
5386
- * at a field.
6060
+ * Omitted by `retire` / `unretire` / `reclassify` / `unbind`, which change a
6061
+ * memory's standing, its classification or whether it binds — rather than
6062
+ * what it says. Checked below with the other action rules so the error can
6063
+ * name the action instead of pointing at a field.
5387
6064
  */
5388
6065
  claim: ProposedClaimSchema.nullable().default(null),
5389
6066
  /** What `reclassify` is reclassifying this memory AS. Only that action takes one. */
5390
6067
  proposed_kind: MemoryKindSchema.nullable().default(null),
6068
+ /**
6069
+ * Where `rescope` moves this memory to — a scope name, or the reserved word
6070
+ * `global`. Only that action takes one, and it may not be null: see
6071
+ * `ProposalSchema.proposed_scope` for why "absent" must not spell "widen".
6072
+ */
6073
+ proposed_scope: z26.string().min(1).max(SCOPE_LIMIT).nullable().default(null),
6074
+ /** The end date `expire` writes onto the current claim. Only that action takes one. */
6075
+ proposed_valid_until: z26.string().min(1).nullable().default(null),
5391
6076
  relations: ProposedRelationsSchema.default({}),
5392
6077
  reason: z26.string().min(1),
5393
6078
  /** The passage from the note that justifies this. Required to submit. */
@@ -5488,6 +6173,68 @@ function reclassifyInput(entry, kind, opts = {}) {
5488
6173
  target_path: entry.path
5489
6174
  };
5490
6175
  }
6176
+ function unbindInput(entry, opts = {}) {
6177
+ return {
6178
+ action: "unbind",
6179
+ memory_id: entry.memory.id,
6180
+ claim: null,
6181
+ reason: opts.reason ?? "no longer binding",
6182
+ source_excerpt: opts.because ?? entry.memory.summary ?? entry.memory.title,
6183
+ target_path: entry.path
6184
+ };
6185
+ }
6186
+ function rescopeInput(entry, scope, opts = {}) {
6187
+ const wanted = scope.trim();
6188
+ if (wanted === "") {
6189
+ throw Object.assign(new Error("a scope cannot be empty"), {
6190
+ hint: `Name the project this memory binds in, or \`${GLOBAL_SCOPE}\` to say it holds everywhere.`
6191
+ });
6192
+ }
6193
+ if ([...wanted].length > SCOPE_LIMIT) {
6194
+ throw Object.assign(new Error(`a scope may be at most ${SCOPE_LIMIT} characters`), {
6195
+ hint: "A scope names a project. If it needs a sentence, it belongs in the note."
6196
+ });
6197
+ }
6198
+ const declared = declaredScopeOf(entry.memory);
6199
+ const same = isGlobalScope(wanted) ? declared === null : declared === wanted;
6200
+ if (same) {
6201
+ throw Object.assign(new Error(`${entry.memory.title} already binds there`), {
6202
+ hint: "Nothing was written, and nothing needed to be."
6203
+ });
6204
+ }
6205
+ return {
6206
+ action: "rescope",
6207
+ memory_id: entry.memory.id,
6208
+ claim: null,
6209
+ proposed_scope: wanted,
6210
+ reason: opts.reason ?? `binds in ${wanted}`,
6211
+ source_excerpt: opts.because ?? entry.memory.summary ?? entry.memory.title,
6212
+ target_path: entry.path
6213
+ };
6214
+ }
6215
+ function expireInput(entry, until, opts = {}) {
6216
+ const wanted = until.trim();
6217
+ const at = Date.parse(wanted);
6218
+ if (!Number.isFinite(at)) {
6219
+ throw Object.assign(new Error(`${until} is not a date`), {
6220
+ hint: "Pass an ISO date, such as 2026-10-31 or 2026-10-31T00:00:00Z."
6221
+ });
6222
+ }
6223
+ if ((entry.claims[0]?.valid_until ?? entry.memory.valid_until ?? null) === wanted) {
6224
+ throw Object.assign(new Error(`${entry.memory.title} already ends then`), {
6225
+ hint: "Nothing was written, and nothing needed to be."
6226
+ });
6227
+ }
6228
+ return {
6229
+ action: "expire",
6230
+ memory_id: entry.memory.id,
6231
+ claim: null,
6232
+ proposed_valid_until: wanted,
6233
+ reason: opts.reason ?? `holds until ${wanted}`,
6234
+ source_excerpt: opts.because ?? entry.memory.summary ?? entry.memory.title,
6235
+ target_path: entry.path
6236
+ };
6237
+ }
5491
6238
  function saysNothingNew(entry, input, normalize) {
5492
6239
  const norm = (v) => normalize(v ?? "").trim();
5493
6240
  const claim = input.claim;
@@ -5495,6 +6242,7 @@ function saysNothingNew(entry, input, normalize) {
5495
6242
  if (norm(claim.text) !== norm(previousClaim(entry))) return false;
5496
6243
  if (claim.summary != null && norm(claim.summary) !== norm(entry.memory.summary)) return false;
5497
6244
  if (claim.title != null && norm(claim.title) !== norm(entry.memory.title)) return false;
6245
+ if (claim.binding != null && claim.binding !== (entry.memory.binding === true)) return false;
5498
6246
  const declared = new Set(
5499
6247
  entry.relations.filter((r) => r.type === "contradicts").map((r) => r.to)
5500
6248
  );
@@ -5507,11 +6255,6 @@ function bodyStillAsserts(content, oldClaim, newClaim, normalize) {
5507
6255
  if (line === normalize(newClaim?.split("\n")[0] ?? "").trim()) return null;
5508
6256
  return bodyOf(content).includes(line) ? line : null;
5509
6257
  }
5510
- function bodyOf(content) {
5511
- const stripped = content.replace(/^/, "");
5512
- const fence2 = /^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(\r?\n|$)/.exec(stripped);
5513
- return fence2 ? stripped.slice(fence2[0].length) : stripped;
5514
- }
5515
6258
  function previousClaim(entry) {
5516
6259
  const current = entry.claims.find((claim) => claim.id === entry.memory.current_claim) ?? entry.claims[0];
5517
6260
  return current?.text ?? null;
@@ -5527,6 +6270,9 @@ var init_curate = __esm({
5527
6270
  "../../packages/memory-engine/src/capture/curate.ts"() {
5528
6271
  "use strict";
5529
6272
  init_planner();
6273
+ init_schema();
6274
+ init_rule_budget();
6275
+ init_note_body();
5530
6276
  NO_REASON_GIVEN = "revised directly";
5531
6277
  noLifecycleReason = (action) => `${action}d directly`;
5532
6278
  }
@@ -5577,6 +6323,25 @@ function desiredFields(proposal, existing) {
5577
6323
  out["kind"] = proposal.proposed_kind;
5578
6324
  return out;
5579
6325
  }
6326
+ if (isBindingAction(proposal.action)) {
6327
+ delete out["binding"];
6328
+ return out;
6329
+ }
6330
+ if (isScopeAction(proposal.action)) {
6331
+ if (proposal.proposed_scope === null) {
6332
+ throw new Error(`${proposal.action} reached the patcher with no scope`);
6333
+ }
6334
+ if (isGlobalScope(proposal.proposed_scope)) delete out["scope"];
6335
+ else out["scope"] = proposal.proposed_scope;
6336
+ return out;
6337
+ }
6338
+ if (isValidityAction(proposal.action)) {
6339
+ if (proposal.proposed_valid_until === null) {
6340
+ throw new Error(`${proposal.action} reached the patcher with no date`);
6341
+ }
6342
+ out["valid_until"] = proposal.proposed_valid_until;
6343
+ return out;
6344
+ }
5580
6345
  const claim = proposal.claim;
5581
6346
  if (claim === null) {
5582
6347
  throw new Error(`${proposal.action} reached the patcher with no claim`);
@@ -5874,6 +6639,108 @@ function verifyPatch(patched, proposal, before) {
5874
6639
  }
5875
6640
  return;
5876
6641
  }
6642
+ if (isBindingAction(proposal.action)) {
6643
+ if (mapped.memory.binding === true) {
6644
+ throw new PatchError(
6645
+ "the patched note still reads as binding",
6646
+ "This is a defect in the patcher, not in your note. Nothing was written."
6647
+ );
6648
+ }
6649
+ if (before === void 0) {
6650
+ throw new PatchError(
6651
+ `${proposal.action} cannot be verified without the note it started from`,
6652
+ "This is a defect in the caller, not in your note. Nothing was written."
6653
+ );
6654
+ }
6655
+ const was = mapNoteToMemory({ path: proposal.target.path, content: before, revision: null });
6656
+ if (was.claims[0]?.text !== mapped.claims[0]?.text) {
6657
+ throw new PatchError(
6658
+ `${proposal.action} changed the conclusion, which it must never do`,
6659
+ "Taking a marking back leaves what the note says exactly as it was. Nothing was written."
6660
+ );
6661
+ }
6662
+ if (was.memory.summary !== mapped.memory.summary) {
6663
+ throw new PatchError(
6664
+ `${proposal.action} changed the summary, which it must never do`,
6665
+ "Taking a marking back leaves what the note says exactly as it was \u2014 and the summary is what recall matches on. Nothing was written."
6666
+ );
6667
+ }
6668
+ if (was.memory.title !== mapped.memory.title) {
6669
+ throw new PatchError(
6670
+ `${proposal.action} changed the title, which it must never do`,
6671
+ "Taking a marking back leaves what the note says exactly as it was. Nothing was written."
6672
+ );
6673
+ }
6674
+ if (was.memory.status !== mapped.memory.status) {
6675
+ throw new PatchError(
6676
+ `${proposal.action} changed the status, which it must never do`,
6677
+ "A memory that no longer binds is still current. To take it out of the answers as well, use retire. Nothing was written."
6678
+ );
6679
+ }
6680
+ return;
6681
+ }
6682
+ if (isScopeAction(proposal.action) || isValidityAction(proposal.action)) {
6683
+ if (before === void 0) {
6684
+ throw new PatchError(
6685
+ `${proposal.action} cannot be verified without the note it started from`,
6686
+ "This is a defect in the caller, not in your note. Nothing was written."
6687
+ );
6688
+ }
6689
+ if (isScopeAction(proposal.action)) {
6690
+ const want = proposal.proposed_scope;
6691
+ const got = declaredScopeOf(mapped.memory);
6692
+ const landed = isGlobalScope(want) ? got === null : got === want;
6693
+ if (!landed) {
6694
+ throw new PatchError(
6695
+ "the patched note does not declare the scope that was asked for",
6696
+ "This is a defect in the patcher, not in your note. Nothing was written."
6697
+ );
6698
+ }
6699
+ } else if (
6700
+ // Read back the way `freshnessOf` reads it: the claim's date when the
6701
+ // note states a claim, the note's own otherwise. Asserting only on the
6702
+ // claim would reject every note `remember` wrote, since it has never
6703
+ // written a `claim:` key.
6704
+ (mapped.claims[0]?.valid_until ?? mapped.memory.valid_until ?? null) !== proposal.proposed_valid_until
6705
+ ) {
6706
+ throw new PatchError(
6707
+ "the patched note does not carry the end date that was asked for",
6708
+ "This is a defect in the patcher, not in your note. Nothing was written."
6709
+ );
6710
+ }
6711
+ const was = mapNoteToMemory({ path: proposal.target.path, content: before, revision: null });
6712
+ if (was.claims[0]?.text !== mapped.claims[0]?.text) {
6713
+ throw new PatchError(
6714
+ `${proposal.action} changed the conclusion, which it must never do`,
6715
+ "It changes one field and leaves what the note says exactly as it was. Nothing was written."
6716
+ );
6717
+ }
6718
+ if (was.memory.summary !== mapped.memory.summary) {
6719
+ throw new PatchError(
6720
+ `${proposal.action} changed the summary, which it must never do`,
6721
+ "The summary is what recall matches on, and this action does not touch what the note says. Nothing was written."
6722
+ );
6723
+ }
6724
+ if (was.memory.title !== mapped.memory.title) {
6725
+ throw new PatchError(
6726
+ `${proposal.action} changed the title, which it must never do`,
6727
+ "It changes one field and leaves what the note says exactly as it was. Nothing was written."
6728
+ );
6729
+ }
6730
+ if (was.memory.status !== mapped.memory.status) {
6731
+ throw new PatchError(
6732
+ `${proposal.action} changed the status, which it must never do`,
6733
+ "Moving where a memory binds, or saying when it stops being current, does not retire it. To take it out of the answers, use retire. Nothing was written."
6734
+ );
6735
+ }
6736
+ if (was.memory.binding !== mapped.memory.binding) {
6737
+ throw new PatchError(
6738
+ `${proposal.action} changed whether the memory binds, which it must never do`,
6739
+ "To take a marking back, use `revise --no-rule`. Nothing was written."
6740
+ );
6741
+ }
6742
+ return;
6743
+ }
5877
6744
  const supersedes = new Set(
5878
6745
  mapped.relations.filter((r) => r.type === "supersedes").map((r) => r.to)
5879
6746
  );
@@ -5944,11 +6811,13 @@ function reversePatch(content, patch) {
5944
6811
  }
5945
6812
  return null;
5946
6813
  }
5947
- var PATCHABLE_KEYS, NEVER_WRITTEN_KEYS, STATUS_WRITING_ACTIONS, KIND_WRITING_ACTIONS, NAMESPACE2, FENCE_RE, LOOKS_FENCED_RE, PatchError, BLOCK_KEY_RE, ANY_INDENTED_KEY_RE;
6814
+ var PATCHABLE_KEYS, NEVER_WRITTEN_KEYS, STATUS_WRITING_ACTIONS, KIND_WRITING_ACTIONS, SCOPE_WRITING_ACTIONS, NAMESPACE2, FENCE_RE, LOOKS_FENCED_RE, PatchError, BLOCK_KEY_RE, ANY_INDENTED_KEY_RE;
5948
6815
  var init_patch = __esm({
5949
6816
  "../../packages/memory-engine/src/proposal/patch.ts"() {
5950
6817
  "use strict";
5951
6818
  init_markdown_mapping();
6819
+ init_rule_budget();
6820
+ init_schema();
5952
6821
  init_schema2();
5953
6822
  PATCHABLE_KEYS = [
5954
6823
  // The conclusion itself.
@@ -5972,6 +6841,7 @@ var init_patch = __esm({
5972
6841
  NEVER_WRITTEN_KEYS = ["id", "type", "scope", "aliases", "status", "kind"];
5973
6842
  STATUS_WRITING_ACTIONS = ["retire", "unretire"];
5974
6843
  KIND_WRITING_ACTIONS = ["reclassify"];
6844
+ SCOPE_WRITING_ACTIONS = ["rescope"];
5975
6845
  NAMESPACE2 = "memgineering";
5976
6846
  FENCE_RE = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(\r?\n|$)/;
5977
6847
  LOOKS_FENCED_RE = /^---[ \t]*\r?\n/;
@@ -6013,15 +6883,15 @@ async function applyProposal(adapter, accepted) {
6013
6883
  const patch = computePatch(current.content, proposal);
6014
6884
  verifyPatch(patch.content, proposal, current.content);
6015
6885
  if (patch.content === current.content) {
6016
- const base = proposal.target.baseRevision;
6017
- if (base === null || base === void 0) {
6886
+ const base2 = proposal.target.baseRevision;
6887
+ if (base2 === null || base2 === void 0) {
6018
6888
  return {
6019
6889
  ok: false,
6020
6890
  reason: "revision_conflict",
6021
6891
  proposal: needsRebase(proposal, current.revision)
6022
6892
  };
6023
6893
  }
6024
- if (adapter.compareRevision(base, current.revision) === "diverged") {
6894
+ if (adapter.compareRevision(base2, current.revision) === "diverged") {
6025
6895
  return {
6026
6896
  ok: false,
6027
6897
  reason: "revision_conflict",
@@ -6088,23 +6958,36 @@ function rank(entries, events, opts) {
6088
6958
  const recalls = events.filter((e) => !AUTOMATIC.has(e.verb));
6089
6959
  const here = opts.here ? recalls.filter((e) => opts.here(e)) : [];
6090
6960
  const hereLabel = opts.hereLabel ?? "in this folder";
6091
- const countIn = (source) => {
6092
- const counts = /* @__PURE__ */ new Map();
6961
+ const usageIn = (source) => {
6962
+ const usage = /* @__PURE__ */ new Map();
6963
+ const at = (id) => {
6964
+ let u = usage.get(id);
6965
+ if (!u) usage.set(id, u = { opened: 0, surfaced: 0, credit: 0 });
6966
+ return u;
6967
+ };
6093
6968
  for (const event of source) {
6094
- for (const id of event.returned_ids) counts.set(id, (counts.get(id) ?? 0) + 1);
6095
- if (event.opened_id) counts.set(event.opened_id, (counts.get(event.opened_id) ?? 0) + 1);
6969
+ if (event.verb === "recall") {
6970
+ event.returned_ids.forEach((id, i) => {
6971
+ const u = at(id);
6972
+ u.surfaced += 1;
6973
+ if (i === 0) u.credit += SURFACED_SHARE;
6974
+ });
6975
+ } else if (DELIBERATE.has(event.verb)) {
6976
+ const ids = event.opened_id ? [event.opened_id] : event.returned_ids;
6977
+ for (const id of new Set(ids)) at(id).opened += 1;
6978
+ }
6096
6979
  }
6097
- return counts;
6980
+ return usage;
6098
6981
  };
6099
- const hereCounts = countIn(here);
6100
- const anyCounts = countIn(recalls);
6982
+ const evidence = (u) => u ? u.opened + u.credit : 0;
6983
+ const hereUsage = usageIn(here);
6984
+ const anyUsage = usageIn(recalls);
6101
6985
  const lastTouched = /* @__PURE__ */ new Map();
6102
6986
  for (const event of recalls) {
6103
6987
  const t = Date.parse(event.ts);
6104
6988
  if (!Number.isFinite(t)) continue;
6105
- for (const id of [...event.returned_ids, event.opened_id].filter(Boolean)) {
6106
- lastTouched.set(id, Math.max(lastTouched.get(id) ?? 0, t));
6107
- }
6989
+ const touched = event.verb === "recall" ? event.returned_ids.slice(0, 1) : DELIBERATE.has(event.verb) ? [event.opened_id ?? "", ...event.returned_ids].filter(Boolean) : [];
6990
+ for (const id of touched) lastTouched.set(id, Math.max(lastTouched.get(id) ?? 0, t));
6108
6991
  }
6109
6992
  const hasBase = entries.some((e) => e.path.startsWith("01_BASE/"));
6110
6993
  const scored = [];
@@ -6114,16 +6997,30 @@ function rank(entries, events, opts) {
6114
6997
  if (freshness === "retired") continue;
6115
6998
  const why = [];
6116
6999
  let score = 0;
6117
- const hereCount = hereCounts.get(id) ?? 0;
6118
- if (hereCount > 0) {
6119
- score += WEIGHT.contextRecall * Math.log2(1 + hereCount);
6120
- why.push(`recalled ${hereCount}\xD7 ${hereLabel}`);
6121
- }
6122
- const anyCount = (anyCounts.get(id) ?? 0) - hereCount;
6123
- if (anyCount > 0) {
6124
- score += WEIGHT.anyRecall * Math.log2(1 + anyCount);
6125
- if (hereCount === 0) why.push(`recalled ${anyCount}\xD7 elsewhere`);
7000
+ const hereU = hereUsage.get(id);
7001
+ const anyU = anyUsage.get(id);
7002
+ const hereEvidence = evidence(hereU);
7003
+ const elsewhereEvidence = evidence(anyU) - hereEvidence;
7004
+ if (hereEvidence > 0) {
7005
+ score += WEIGHT.contextRecall * Math.log2(1 + hereEvidence);
7006
+ why.push(usageLine(hereU, hereLabel));
7007
+ }
7008
+ if (elsewhereEvidence > 1e-9) {
7009
+ score += WEIGHT.anyRecall * Math.log2(1 + elsewhereEvidence);
7010
+ if (hereEvidence === 0) {
7011
+ why.push(
7012
+ usageLine(
7013
+ {
7014
+ opened: anyU.opened - (hereU?.opened ?? 0),
7015
+ surfaced: anyU.surfaced - (hereU?.surfaced ?? 0)
7016
+ },
7017
+ "elsewhere"
7018
+ )
7019
+ );
7020
+ }
6126
7021
  }
7022
+ const hereCount = (hereU?.opened ?? 0) + (hereU?.surfaced ?? 0);
7023
+ const anyCount = (anyU?.opened ?? 0) + (anyU?.surfaced ?? 0) - hereCount;
6127
7024
  const touched = lastTouched.get(id);
6128
7025
  if (touched !== void 0) {
6129
7026
  const days = (opts.now.getTime() - touched) / 864e5;
@@ -6144,16 +7041,28 @@ function rank(entries, events, opts) {
6144
7041
  }
6145
7042
  if (score === 0 && !entry.path.startsWith("01_BASE/")) {
6146
7043
  score += WEIGHT.neverReached;
6147
- why.push("never surfaced yet");
7044
+ why.push(
7045
+ anyU && anyU.surfaced > 0 ? usageLine(
7046
+ { opened: 0, surfaced: anyU.surfaced },
7047
+ // Labelled by where it happened: all here, none here, or both —
7048
+ // a total said "in this project" would claim the whole count.
7049
+ !hereU?.surfaced ? "elsewhere" : hereU.surfaced === anyU.surfaced ? hereLabel : `(${hereU.surfaced}\xD7 ${hereLabel})`
7050
+ ) : "never surfaced yet"
7051
+ );
6148
7052
  }
6149
7053
  if (freshness !== "current") why.push(freshness);
6150
- if (score > 0) scored.push({ entry, score, why });
7054
+ if (score > 0) scored.push({ entry, score, why, used: hereCount > 0 || anyCount > 0 });
6151
7055
  }
6152
7056
  return scored.sort(
6153
7057
  (a, b) => b.score - a.score || a.entry.memory.id.localeCompare(b.entry.memory.id)
6154
7058
  );
6155
7059
  }
6156
- var WEIGHT, STALE_BASE_DAYS;
7060
+ function usageLine(u, where) {
7061
+ if (u.opened === 0) return `surfaced ${u.surfaced}\xD7 ${where}, never opened`;
7062
+ if (u.surfaced === 0) return `opened ${u.opened}\xD7 ${where}`;
7063
+ return `opened ${u.opened}\xD7 \xB7 surfaced ${u.surfaced}\xD7 ${where}`;
7064
+ }
7065
+ var WEIGHT, STALE_BASE_DAYS, SURFACED_SHARE, DELIBERATE;
6157
7066
  var init_rank = __esm({
6158
7067
  "../../packages/memory-engine/src/resurface/rank.ts"() {
6159
7068
  "use strict";
@@ -6206,26 +7115,35 @@ var init_rank = __esm({
6206
7115
  binding: 5
6207
7116
  };
6208
7117
  STALE_BASE_DAYS = 30;
7118
+ SURFACED_SHARE = 0.5;
7119
+ DELIBERATE = /* @__PURE__ */ new Set(["open", "publish", "adapt", "read-public"]);
6209
7120
  }
6210
7121
  });
6211
7122
 
6212
7123
  // ../../packages/memory-engine/src/index.ts
6213
7124
  var src_exports2 = {};
6214
7125
  __export(src_exports2, {
7126
+ BASE_TEMPLATES: () => BASE_TEMPLATES,
6215
7127
  CONSOLIDATION_RULES: () => CONSOLIDATION_RULES,
6216
7128
  CaptureCandidateSchema: () => CaptureCandidateSchema,
6217
7129
  ClaimConfidenceSchema: () => ClaimConfidenceSchema,
6218
7130
  ClaimSchema: () => ClaimSchema,
6219
7131
  CommitInputSchema: () => CommitInputSchema,
7132
+ DEFAULT_RULE_BUDGET: () => DEFAULT_RULE_BUDGET,
6220
7133
  DEFAULT_TTL_DAYS: () => DEFAULT_TTL_DAYS,
7134
+ DERIVED_BLUEPRINT: () => DERIVED_BLUEPRINT,
7135
+ DERIVED_DIR: () => DERIVED_DIR,
7136
+ GLOBAL_SCOPE: () => GLOBAL_SCOPE,
6221
7137
  IllegalTransitionError: () => IllegalTransitionError,
6222
7138
  KIND_WRITING_ACTIONS: () => KIND_WRITING_ACTIONS,
7139
+ MEANING_ONLY_NOTE: () => MEANING_ONLY_NOTE,
6223
7140
  MappingModeSchema: () => MappingModeSchema,
6224
7141
  MemoryKindSchema: () => MemoryKindSchema,
6225
7142
  MemorySchema: () => MemorySchema,
6226
7143
  MemoryStatusSchema: () => MemoryStatusSchema,
6227
7144
  NEVER_WRITTEN_KEYS: () => NEVER_WRITTEN_KEYS,
6228
7145
  NO_REASON_GIVEN: () => NO_REASON_GIVEN,
7146
+ OBSERVATIONS_DIR: () => OBSERVATIONS_DIR,
6229
7147
  PATCHABLE_KEYS: () => PATCHABLE_KEYS,
6230
7148
  PatchError: () => PatchError,
6231
7149
  ProposalActionSchema: () => ProposalActionSchema,
@@ -6234,14 +7152,23 @@ __export(src_exports2, {
6234
7152
  ProposedClaimSchema: () => ProposedClaimSchema,
6235
7153
  ProposedRelationsSchema: () => ProposedRelationsSchema,
6236
7154
  ProvenanceSchema: () => ProvenanceSchema,
7155
+ QUERY_EMBEDDING_PREFIX: () => QUERY_EMBEDDING_PREFIX,
6237
7156
  RECALL_DEPTHS: () => RECALL_DEPTHS,
7157
+ RETIRED_BASE_TEMPLATES: () => RETIRED_BASE_TEMPLATES,
7158
+ RULE_BLOCK_BUDGET: () => RULE_BLOCK_BUDGET,
7159
+ RULE_WIDTH_MAX: () => RULE_WIDTH_MAX,
7160
+ RULE_WIDTH_MIN: () => RULE_WIDTH_MIN,
7161
+ RULE_WIRE_WIDTH: () => RULE_WIRE_WIDTH,
6238
7162
  RecallOptionError: () => RecallOptionError,
6239
7163
  RelationSchema: () => RelationSchema,
6240
7164
  RelationSourceSchema: () => RelationSourceSchema,
6241
7165
  RelationTypeSchema: () => RelationTypeSchema,
6242
7166
  SCOPE_LIMIT: () => SCOPE_LIMIT,
7167
+ SCOPE_SPREAD_FIELD: () => SCOPE_SPREAD_FIELD,
7168
+ SCOPE_WRITING_ACTIONS: () => SCOPE_WRITING_ACTIONS,
6243
7169
  SEMANTIC_CANDIDATES: () => SEMANTIC_CANDIDATES,
6244
7170
  SEMANTIC_FIELDS: () => SEMANTIC_FIELDS,
7171
+ SETUP_PROGRESS_CODES: () => SETUP_PROGRESS_CODES,
6245
7172
  STATUS_FOR_ACTION: () => STATUS_FOR_ACTION,
6246
7173
  STATUS_WRITING_ACTIONS: () => STATUS_WRITING_ACTIONS,
6247
7174
  StorableActionSchema: () => StorableActionSchema,
@@ -6249,6 +7176,7 @@ __export(src_exports2, {
6249
7176
  TELEMETRY_MESSAGE_LIMIT: () => TELEMETRY_MESSAGE_LIMIT,
6250
7177
  TelemetryErrorPayloadSchema: () => TelemetryErrorPayloadSchema2,
6251
7178
  TransitionSchema: () => TransitionSchema,
7179
+ UNFILLED_FIELDS: () => UNFILLED_FIELDS,
6252
7180
  accept: () => accept,
6253
7181
  addedLines: () => addedLines,
6254
7182
  applyProposal: () => applyProposal,
@@ -6270,28 +7198,39 @@ __export(src_exports2, {
6270
7198
  commit: () => commit,
6271
7199
  computePatch: () => computePatch,
6272
7200
  confidenceOf: () => confidenceOf,
7201
+ declaredScope: () => declaredScope,
7202
+ declaredScopeOf: () => declaredScopeOf,
6273
7203
  defaultExpiry: () => defaultExpiry,
6274
7204
  defaultStateRoot: () => defaultStateRoot,
6275
7205
  deleteProposal: () => deleteProposal,
6276
7206
  detectConsolidation: () => detectConsolidation,
6277
7207
  expireIfDue: () => expireIfDue,
7208
+ expireInput: () => expireInput,
6278
7209
  findConflicts: () => findConflicts,
6279
7210
  freshnessOf: () => freshnessOf,
7211
+ isBindingAction: () => isBindingAction,
6280
7212
  isClassifyAction: () => isClassifyAction,
7213
+ isExactUntouchedBase: () => isExactUntouchedBase,
6281
7214
  isExpired: () => isExpired2,
7215
+ isGlobalScope: () => isGlobalScope,
6282
7216
  isLifecycleAction: () => isLifecycleAction,
6283
7217
  isRecallDepth: () => isRecallDepth,
7218
+ isScopeAction: () => isScopeAction,
7219
+ isTemplateBody: () => isTemplateBody,
6284
7220
  isTerminal: () => isTerminal,
6285
- isUntouchedBase: () => isUntouchedBase,
7221
+ isUnwrittenBody: () => isUnwrittenBody,
7222
+ isValidityAction: () => isValidityAction,
6286
7223
  lifecycleInput: () => lifecycleInput,
6287
7224
  listProposals: () => listProposals,
6288
7225
  loadProposal: () => loadProposal,
6289
7226
  mapNoteToMemory: () => mapNoteToMemory,
7227
+ markUnfilled: () => markUnfilled,
6290
7228
  matchBoundaryTerms: () => matchBoundaryTerms,
6291
7229
  missingCounterparts: () => missingCounterparts,
6292
7230
  needsRebase: () => needsRebase,
6293
7231
  newProposalId: () => newProposalId,
6294
7232
  noLifecycleReason: () => noLifecycleReason,
7233
+ ownEntry: () => ownEntry,
6295
7234
  prepare: () => prepare,
6296
7235
  prepareForMemory: () => prepareForMemory,
6297
7236
  previewProposal: () => previewProposal,
@@ -6307,15 +7246,23 @@ __export(src_exports2, {
6307
7246
  reconcile: () => reconcile,
6308
7247
  redact: () => redact,
6309
7248
  renderNewNote: () => renderNewNote,
7249
+ rescopeInput: () => rescopeInput,
6310
7250
  resolveDetail: () => resolveDetail,
6311
7251
  reversePatch: () => reversePatch,
6312
7252
  reviseInput: () => reviseInput,
7253
+ ruleLane: () => ruleLane,
7254
+ rulePassage: () => rulePassage,
7255
+ ruleTextOf: () => ruleTextOf,
7256
+ ruleWidthFor: () => ruleWidthFor,
6313
7257
  saveProposal: () => saveProposal,
6314
7258
  saysNothingNew: () => saysNothingNew,
7259
+ scopeSpread: () => scopeSpread,
6315
7260
  searchMemories: () => searchMemories,
6316
7261
  semanticSourceFromVectors: () => semanticSourceFromVectors,
7262
+ splitRuleBudget: () => splitRuleBudget,
6317
7263
  terms: () => terms,
6318
7264
  transition: () => transition,
7265
+ unbindInput: () => unbindInput,
6319
7266
  validateBoundaryTerm: () => validateBoundaryTerm,
6320
7267
  variantsOf: () => variantsOf,
6321
7268
  verifyPatch: () => verifyPatch,
@@ -6326,6 +7273,9 @@ var init_src3 = __esm({
6326
7273
  "../../packages/memory-engine/src/index.ts"() {
6327
7274
  "use strict";
6328
7275
  init_schema();
7276
+ init_layout();
7277
+ init_rule_budget();
7278
+ init_rule_text();
6329
7279
  init_boundary();
6330
7280
  init_markdown_mapping();
6331
7281
  init_build();
@@ -6333,6 +7283,11 @@ var init_src3 = __esm({
6333
7283
  init_reconcile();
6334
7284
  init_planner();
6335
7285
  init_semantic();
7286
+ init_unfilled();
7287
+ init_setup_progress();
7288
+ init_query_prefix();
7289
+ init_base_templates();
7290
+ init_scope_spread();
6336
7291
  init_conflicts();
6337
7292
  init_search();
6338
7293
  init_schema2();
@@ -8609,15 +9564,15 @@ function shouldExclude(relPath, isDir) {
8609
9564
  if (EXCLUDE_EXTS.has(ext)) return true;
8610
9565
  return false;
8611
9566
  }
8612
- async function walkDir(base, relDir, collected) {
8613
- const entries = await readdir2(join8(base, relDir), { withFileTypes: true });
9567
+ async function walkDir(base2, relDir, collected) {
9568
+ const entries = await readdir2(join8(base2, relDir), { withFileTypes: true });
8614
9569
  for (const entry of entries) {
8615
9570
  const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
8616
9571
  if (shouldExclude(rel, entry.isDirectory())) continue;
8617
9572
  if (entry.isDirectory()) {
8618
- await walkDir(base, rel, collected);
9573
+ await walkDir(base2, rel, collected);
8619
9574
  } else if (entry.isFile()) {
8620
- collected.push({ rel, abs: join8(base, rel) });
9575
+ collected.push({ rel, abs: join8(base2, rel) });
8621
9576
  }
8622
9577
  }
8623
9578
  }
@@ -10023,6 +10978,7 @@ import { homedir as homedir4 } from "os";
10023
10978
  import { join as join11 } from "path";
10024
10979
  var PER_FILE_CAP = 16 * 1024;
10025
10980
  var TOTAL_CAP = 48 * 1024;
10981
+ var TRUNCATION_RESERVE = 64;
10026
10982
  async function pathExists3(p) {
10027
10983
  try {
10028
10984
  await access3(p);
@@ -10033,6 +10989,7 @@ async function pathExists3(p) {
10033
10989
  }
10034
10990
  function capContent(raw, cap) {
10035
10991
  if (Buffer.byteLength(raw, "utf8") <= cap) return { content: raw, truncated: false };
10992
+ const fill = cap - TRUNCATION_RESERVE;
10036
10993
  const lines = raw.split("\n");
10037
10994
  let acc = 0;
10038
10995
  let lastHeadingIdx = -1;
@@ -10041,7 +10998,7 @@ function capContent(raw, cap) {
10041
10998
  const line = lines[i] ?? "";
10042
10999
  const lineBytes = Buffer.byteLength(`${line}
10043
11000
  `, "utf8");
10044
- if (acc + lineBytes > cap) {
11001
+ if (acc + lineBytes > fill) {
10045
11002
  cutIdx = i;
10046
11003
  break;
10047
11004
  }
@@ -10049,6 +11006,7 @@ function capContent(raw, cap) {
10049
11006
  if (/^#{1,6}\s/.test(line)) lastHeadingIdx = i;
10050
11007
  }
10051
11008
  const cut = lastHeadingIdx > cutIdx / 2 ? lastHeadingIdx : cutIdx;
11009
+ if (cut === 0) return null;
10052
11010
  const kept = lines.slice(0, cut).join("\n");
10053
11011
  return {
10054
11012
  content: `${kept}
@@ -10077,7 +11035,6 @@ async function readHarnessContext(cwd = process.cwd(), home = homedir4()) {
10077
11035
  const files = [];
10078
11036
  let totalBytes = 0;
10079
11037
  for (const cand of candidates) {
10080
- if (totalBytes >= TOTAL_CAP) break;
10081
11038
  if (!await pathExists3(cand.path)) continue;
10082
11039
  let raw;
10083
11040
  try {
@@ -10086,7 +11043,9 @@ async function readHarnessContext(cwd = process.cwd(), home = homedir4()) {
10086
11043
  continue;
10087
11044
  }
10088
11045
  const cap = Math.min(PER_FILE_CAP, TOTAL_CAP - totalBytes);
10089
- const { content, truncated } = capContent(raw, cap);
11046
+ const capped2 = capContent(raw, cap);
11047
+ if (capped2 === null) break;
11048
+ const { content, truncated } = capped2;
10090
11049
  totalBytes += Buffer.byteLength(content, "utf8");
10091
11050
  files.push({
10092
11051
  path: cand.display,
@@ -12743,9 +13702,9 @@ function assertNothingLost(before, after, action) {
12743
13702
  });
12744
13703
  }
12745
13704
  }
12746
- async function commit2(root, base, next) {
13705
+ async function commit2(root, base2, next) {
12747
13706
  const current = await readIgnoreFileState(root);
12748
- if (current.hash !== base.hash) {
13707
+ if (current.hash !== base2.hash) {
12749
13708
  return { ok: false, reason: "revision_conflict", current };
12750
13709
  }
12751
13710
  if (next === current.content) return { ok: true, content: next, changed: false };
@@ -12791,15 +13750,15 @@ function planRemoveIgnoreEntry(content, relPath) {
12791
13750
  assertNothingLost(next, content, "un-excluding");
12792
13751
  return { content: next, changed: true };
12793
13752
  }
12794
- async function addIgnoreEntry(root, relPath, base) {
12795
- const edit = planAddIgnoreEntry(base.content, relPath);
12796
- if (!edit.changed) return { ok: true, content: base.content, changed: false };
12797
- return commit2(root, base, edit.content);
13753
+ async function addIgnoreEntry(root, relPath, base2) {
13754
+ const edit = planAddIgnoreEntry(base2.content, relPath);
13755
+ if (!edit.changed) return { ok: true, content: base2.content, changed: false };
13756
+ return commit2(root, base2, edit.content);
12798
13757
  }
12799
- async function removeIgnoreEntry(root, relPath, base) {
12800
- const edit = planRemoveIgnoreEntry(base.content, relPath);
12801
- if (!edit.changed) return { ok: true, content: base.content, changed: false };
12802
- return commit2(root, base, edit.content);
13758
+ async function removeIgnoreEntry(root, relPath, base2) {
13759
+ const edit = planRemoveIgnoreEntry(base2.content, relPath);
13760
+ if (!edit.changed) return { ok: true, content: base2.content, changed: false };
13761
+ return commit2(root, base2, edit.content);
12803
13762
  }
12804
13763
 
12805
13764
  // src/commands/memory/exclude.ts
@@ -12845,8 +13804,8 @@ async function openVaultOrExplain(path2) {
12845
13804
  // src/commands/memory/link.ts
12846
13805
  init_brand();
12847
13806
  function sameBasename(all, entry) {
12848
- const base = (entry.split("/").pop() ?? entry).normalize("NFC").toLowerCase();
12849
- return all.filter((p) => (p.split("/").pop() ?? "").normalize("NFC").toLowerCase() === base);
13807
+ const base2 = (entry.split("/").pop() ?? entry).normalize("NFC").toLowerCase();
13808
+ return all.filter((p) => (p.split("/").pop() ?? "").normalize("NFC").toLowerCase() === base2);
12850
13809
  }
12851
13810
  function spreadAcrossFolders(rows, max) {
12852
13811
  const seen = /* @__PURE__ */ new Set();
@@ -13110,7 +14069,7 @@ function excludeCommand() {
13110
14069
  return new Command28("exclude").description("stop reading a note entirely \u2014 the note itself is not changed").argument("<path>", "vault-relative path of the note, e.g. decisions/launch-date.md").option("--yes", "skip the confirmation prompt").option("--vault <path>", "which linked vault the note is in").action(async (path2, opts) => {
13111
14070
  const root = await resolveVaultRoot(opts.vault);
13112
14071
  const target = await mustMatchAFile(root, path2);
13113
- const base = await readIgnoreFileState(root);
14072
+ const base2 = await readIgnoreFileState(root);
13114
14073
  if (!opts.yes && isJsonMode()) {
13115
14074
  throw memoryError(
13116
14075
  "refusing to change what this vault reads without confirmation",
@@ -13134,7 +14093,7 @@ function excludeCommand() {
13134
14093
  return;
13135
14094
  }
13136
14095
  }
13137
- const result = await addIgnoreEntry(root, target, base);
14096
+ const result = await addIgnoreEntry(root, target, base2);
13138
14097
  if (!result.ok) {
13139
14098
  printDual({
13140
14099
  json: {
@@ -13172,7 +14131,7 @@ function unexcludeCommand() {
13172
14131
  return new Command28("unexclude").description("read a note again after excluding it").argument("<path>", "vault-relative path of the note").option("--yes", "skip the confirmation prompt").option("--vault <path>", "which linked vault the note is in").action(async (path2, opts) => {
13173
14132
  const root = await resolveVaultRoot(opts.vault);
13174
14133
  const target = path2.normalize("NFC");
13175
- const base = await readIgnoreFileState(root);
14134
+ const base2 = await readIgnoreFileState(root);
13176
14135
  if (!opts.yes && isJsonMode()) {
13177
14136
  throw memoryError(
13178
14137
  "refusing to change what this vault reads without confirmation",
@@ -13195,7 +14154,7 @@ function unexcludeCommand() {
13195
14154
  return;
13196
14155
  }
13197
14156
  }
13198
- const result = await removeIgnoreEntry(root, target, base);
14157
+ const result = await removeIgnoreEntry(root, target, base2);
13199
14158
  if (!result.ok) {
13200
14159
  printDual({
13201
14160
  json: {