bantamkit-mcp 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +398 -0
  3. package/assets/contracts/default.yaml +90 -0
  4. package/assets/evals/devteam/manifest.yaml +351 -0
  5. package/assets/evals/devteam/repo/HISTORY.md +18 -0
  6. package/assets/evals/devteam/repo/README.md +12 -0
  7. package/assets/evals/devteam/repo/docs/architecture.md +17 -0
  8. package/assets/evals/devteam/repo/docs/runbook.md +10 -0
  9. package/assets/evals/devteam/repo/issues/142-settlement-timeout.md +23 -0
  10. package/assets/evals/devteam/repo/patches/0009-retry-budget.patch +38 -0
  11. package/assets/evals/devteam/repo/src/ledger/__init__.py +3 -0
  12. package/assets/evals/devteam/repo/src/ledger/config.py +35 -0
  13. package/assets/evals/devteam/repo/src/ledger/errors.py +13 -0
  14. package/assets/evals/devteam/repo/src/ledger/posting.py +12 -0
  15. package/assets/evals/devteam/repo/src/ledger/registry.py +7 -0
  16. package/assets/evals/devteam/repo/src/ledger/report.py +9 -0
  17. package/assets/evals/devteam/repo/src/ledger/retry.py +17 -0
  18. package/assets/evals/devteam/repo/src/ledger/settle.py +16 -0
  19. package/assets/evals/devteam/repo/src/ledger/validate.py +14 -0
  20. package/assets/evals/devteam/repo/tests/test_posting.py +13 -0
  21. package/assets/evals/devteam/repo/tests/test_settle.py +9 -0
  22. package/assets/evals/devteam/tasks/dt-error-contract.yaml +186 -0
  23. package/assets/evals/devteam/tasks/dt-handler-map.yaml +183 -0
  24. package/assets/evals/devteam/tasks/dt-patch-before-after.yaml +182 -0
  25. package/assets/evals/devteam/tasks/dt-retry-attempts.yaml +181 -0
  26. package/assets/evals/devteam/tasks/dt-settlement-config.yaml +185 -0
  27. package/assets/evals/devteam/tasks/dt-symbol-home.yaml +181 -0
  28. package/assets/evals/devteam/tasks/dt-trace-blame.yaml +182 -0
  29. package/assets/evals/devteam/tasks/dt-unread-key.yaml +180 -0
  30. package/assets/evals/document/tasks/doc-large-in-137.yaml +38 -0
  31. package/assets/evals/document/tasks/doc-large-in-359.yaml +44 -0
  32. package/assets/evals/document/tasks/doc-large-in-372.yaml +38 -0
  33. package/assets/evals/document/tasks/doc-large-out-11764.yaml +37 -0
  34. package/assets/evals/document/tasks/doc-large-out-4137.yaml +37 -0
  35. package/assets/evals/document/tasks/doc-large-out-8022.yaml +37 -0
  36. package/assets/evals/document/tasks/doc-small-137.yaml +37 -0
  37. package/assets/evals/document/tasks/doc-small-261.yaml +37 -0
  38. package/assets/evals/document/tasks/doc-small-388.yaml +37 -0
  39. package/assets/evals/fixtures/.gitkeep +0 -0
  40. package/assets/evals/fixtures/catalog.json +6 -0
  41. package/assets/evals/perturbations/task-completion.yaml +576 -0
  42. package/assets/evals/tasks/.gitkeep +0 -0
  43. package/assets/evals/tasks/extract-contact.yaml +14 -0
  44. package/assets/evals/tasks/extract-invoice.yaml +14 -0
  45. package/assets/evals/tasks/extract-order.yaml +15 -0
  46. package/assets/evals/tasks/extract-schedule.yaml +14 -0
  47. package/assets/evals/tasks/extract-versions.yaml +17 -0
  48. package/assets/evals/tasks/nav-prod-port.yaml +84 -0
  49. package/assets/evals/tasks/nav-release-bundle.yaml +87 -0
  50. package/assets/evals/tasks/recall-audit-retention.yaml +17 -0
  51. package/assets/evals/tasks/recall-cache-ttl.yaml +13 -0
  52. package/assets/evals/tasks/recall-db-port.yaml +17 -0
  53. package/assets/evals/tasks/recall-deploy.yaml +13 -0
  54. package/assets/evals/tasks/recall-env-endpoint.yaml +18 -0
  55. package/assets/evals/tasks/recall-oncall-rotation.yaml +21 -0
  56. package/assets/evals/tasks/recall-oncall.yaml +13 -0
  57. package/assets/evals/tasks/recall-org-quota.yaml +18 -0
  58. package/assets/evals/tasks/recall-owner.yaml +13 -0
  59. package/assets/evals/tasks/shop-basket-total.yaml +10 -0
  60. package/assets/evals/tasks/shop-cheapest.yaml +9 -0
  61. package/assets/evals/tasks/shop-compare.yaml +9 -0
  62. package/assets/evals/tasks/shop-gadget-value.yaml +9 -0
  63. package/assets/evals/tasks/shop-stock-total.yaml +9 -0
  64. package/assets/evals/tasks/shop-total.yaml +9 -0
  65. package/assets/profiles/default.yaml +31 -0
  66. package/assets/profiles/patient.yaml +31 -0
  67. package/assets/rubrics/.gitkeep +0 -0
  68. package/assets/rubrics/code-quality.yaml +20 -0
  69. package/assets/rubrics/grounded-completion.yaml +37 -0
  70. package/assets/rubrics/task-completion.yaml +28 -0
  71. package/assets/schemas/shiftwork-checkpoint.json +188 -0
  72. package/assets/skills/.gitkeep +0 -0
  73. package/assets/skills/file-graph.md +7 -0
  74. package/assets/skills/memory.md +35 -0
  75. package/assets/tools/.gitkeep +0 -0
  76. package/assets/tools/bantamkit_read.json +48 -0
  77. package/assets/tools/bantamkit_status.json +25 -0
  78. package/assets/tools/build_identity.json +17 -0
  79. package/assets/tools/document_list.json +12 -0
  80. package/assets/tools/document_read.json +31 -0
  81. package/assets/tools/file_graph.json +12 -0
  82. package/assets/tools/memory_compact.json +31 -0
  83. package/assets/tools/memory_recall.json +38 -0
  84. package/assets/tools/memory_save.json +61 -0
  85. package/assets/tools/shiftwork_clock_in.json +25 -0
  86. package/assets/tools/shiftwork_clock_out.json +60 -0
  87. package/assets/tools/shiftwork_status.json +25 -0
  88. package/assets/tools/skill_audit.json +70 -0
  89. package/assets/tools/validate_json.json +31 -0
  90. package/dist/assets.d.ts +48 -0
  91. package/dist/assets.js +109 -0
  92. package/dist/charsets.d.ts +17 -0
  93. package/dist/charsets.js +580 -0
  94. package/dist/cli.d.ts +12 -0
  95. package/dist/cli.js +280 -0
  96. package/dist/contract.d.ts +145 -0
  97. package/dist/contract.js +608 -0
  98. package/dist/docread.d.ts +357 -0
  99. package/dist/docread.js +3315 -0
  100. package/dist/errors.d.ts +11 -0
  101. package/dist/errors.js +14 -0
  102. package/dist/eventlog.d.ts +137 -0
  103. package/dist/eventlog.js +294 -0
  104. package/dist/htmlentities.d.ts +2 -0
  105. package/dist/htmlentities.js +16 -0
  106. package/dist/index.d.ts +23 -0
  107. package/dist/index.js +13 -0
  108. package/dist/mcp/identity.d.ts +25 -0
  109. package/dist/mcp/identity.js +275 -0
  110. package/dist/mcp/pyargs.d.ts +80 -0
  111. package/dist/mcp/pyargs.js +362 -0
  112. package/dist/mcp/sdkjson.d.ts +56 -0
  113. package/dist/mcp/sdkjson.js +91 -0
  114. package/dist/mcp/server.d.ts +45 -0
  115. package/dist/mcp/server.js +740 -0
  116. package/dist/mcp/status.d.ts +210 -0
  117. package/dist/mcp/status.js +357 -0
  118. package/dist/mcp/transport.d.ts +68 -0
  119. package/dist/mcp/transport.js +111 -0
  120. package/dist/mcpreport.d.ts +214 -0
  121. package/dist/mcpreport.js +758 -0
  122. package/dist/memory/cli.d.ts +2 -0
  123. package/dist/memory/cli.js +396 -0
  124. package/dist/memory/component.d.ts +256 -0
  125. package/dist/memory/component.js +497 -0
  126. package/dist/memory/factfile.d.ts +120 -0
  127. package/dist/memory/factfile.js +406 -0
  128. package/dist/memory/layers.d.ts +92 -0
  129. package/dist/memory/layers.js +410 -0
  130. package/dist/memory/pyfs.d.ts +521 -0
  131. package/dist/memory/pyfs.js +1608 -0
  132. package/dist/memory/pyyaml.d.ts +155 -0
  133. package/dist/memory/pyyaml.js +868 -0
  134. package/dist/memory/store.d.ts +478 -0
  135. package/dist/memory/store.js +1034 -0
  136. package/dist/pyargparse.d.ts +141 -0
  137. package/dist/pyargparse.js +813 -0
  138. package/dist/pyjson.d.ts +110 -0
  139. package/dist/pyjson.js +524 -0
  140. package/dist/pyjsonschema.d.ts +50 -0
  141. package/dist/pyjsonschema.js +951 -0
  142. package/dist/shiftwork.d.ts +60 -0
  143. package/dist/shiftwork.js +324 -0
  144. package/dist/skillaudit.d.ts +172 -0
  145. package/dist/skillaudit.js +914 -0
  146. package/dist/statusline.d.ts +106 -0
  147. package/dist/statusline.js +281 -0
  148. package/package.json +45 -0
@@ -0,0 +1,478 @@
1
+ import { BantamError } from '../errors.js';
2
+ import type { Fact, FactValue } from './factfile.js';
3
+ export type { Fact } from './factfile.js';
4
+ /** `sorted(VALID_TYPES)` is what the error message interpolates, so the order is load-bearing. */
5
+ export declare const VALID_TYPES: readonly ["feedback", "project", "reference", "user"];
6
+ /**
7
+ * `DURABLE_TYPES` — the types whose worth does NOT decay with time-since-last-recall, and
8
+ * which `compact` therefore archives only after every other candidate is exhausted
9
+ * (`byEviction`).
10
+ *
11
+ * An ARRAY walked with one `pyEqualValue` per entry, never a `Set`: this is compared
12
+ * against a value that came out of YAML uncast, and Python's `in` on a tuple is `==` per
13
+ * element where `in` on a set hashes and raises on a `list`. `runtime-py`'s
14
+ * `store.DURABLE_TYPES`.
15
+ */
16
+ export declare const DURABLE_TYPES: readonly ["feedback", "user"];
17
+ export declare const DUPLICATE_JACCARD = 0.5;
18
+ /** See the reference's comment: measured against a real store, not chosen. */
19
+ export declare const DEFAULT_INDEX_BUDGET = 24000;
20
+ export declare class MemoryValidationError extends BantamError {
21
+ }
22
+ export declare class MemoryBudgetExceeded extends BantamError {
23
+ }
24
+ export interface SaveResult {
25
+ status: 'saved' | 'duplicate';
26
+ name: string;
27
+ /** `SaveResult.similar` is the OTHER fact's `name` field, verbatim — see `pyText`. */
28
+ similar: FactValue;
29
+ }
30
+ /** What one archived fact WAS, kept after its file has left `facts/`. */
31
+ export interface ArchivedFact {
32
+ name: FactValue;
33
+ type: FactValue;
34
+ description: FactValue;
35
+ indexBytes: number;
36
+ lastRecalled: FactValue | null;
37
+ created: FactValue | null;
38
+ }
39
+ /**
40
+ * Everything the caller needs to understand what compaction cost.
41
+ *
42
+ * Archiving is a one-way MOVE, not a delete: the file is still readable under `archive/` and
43
+ * `restore()` brings it back. This carries the description of each fact that left, so a
44
+ * caller that never looks in `archive/` can still say what it lost, and the byte arithmetic
45
+ * so it can see the headroom it bought. `headroom` is a `@property` in the reference and a
46
+ * plain field here — the value is the same and nothing mutates the result.
47
+ */
48
+ export interface CompactResult {
49
+ archived: ArchivedFact[];
50
+ indexBefore: number;
51
+ indexAfter: number;
52
+ budget: number;
53
+ target: number;
54
+ reserve: number;
55
+ headroom: number;
56
+ archiveDir: string;
57
+ }
58
+ export interface MemoryStoreOptions {
59
+ indexBudget?: number;
60
+ k?: number;
61
+ today?: () => string;
62
+ create?: boolean;
63
+ }
64
+ /**
65
+ * `set(re.findall(r"[a-z0-9]+", text.lower()))`.
66
+ *
67
+ * ASCII-only, no casefold and no Unicode normalization — so a wholly non-ASCII name and
68
+ * description produce the EMPTY set in both runtimes. That is not a defect to fix in a
69
+ * port; it is the behaviour the duplicate gate is built on, and `_jaccard` below is where
70
+ * it stops being a division by zero.
71
+ */
72
+ export declare function tokens(text: string): Set<string>;
73
+ /**
74
+ * `len(a & b) / len(a | b)`, except that an empty side is 0.0 and never a division.
75
+ *
76
+ * Python decided this and Python's answer is the one that ships: two facts whose tokens are
77
+ * both empty score 0.0, so they are NOT duplicates of each other and both save. A port that
78
+ * "fixed" it to 1.0 for two empty sets would refuse the second of two Thai descriptions.
79
+ */
80
+ export declare function jaccard(a: Set<string>, b: Set<string>): number;
81
+ /**
82
+ * Python's `f"{value}"` for the values a hand-edited frontmatter can put in a `str` field.
83
+ *
84
+ * `_facts` does no type check: `meta["name"]` is whatever the YAML resolved to, and the
85
+ * store then interpolates it into the index line, the token text and the FILE PATH. For
86
+ * `None` Python writes `None` and JS would write `null` — a different index line and a
87
+ * different file on disk from the same store. Only `null` is reproduced; see the ruling in
88
+ * `tools/conformance/suites/store.mjs` for the sequence case, which is left differing.
89
+ *
90
+ * Exported for `component._format`, which interpolates the same three fields into the line a
91
+ * model reads. Two spellings of one coercion is how the index line and the recall line would
92
+ * come to print a different name for the same fact.
93
+ */
94
+ export declare function pyText(value: unknown): string;
95
+ /**
96
+ * `hash`/`==` equivalence for the values a `Fact.name` can hold — the key `Memory.recall`'s
97
+ * `seen` set uses across layers.
98
+ *
99
+ * `pyText` is the WRONG key here and the difference is observable: a fact named `7` in the
100
+ * project layer and one named `'7'` in the profile layer are two different keys to Python
101
+ * and would collapse into one under `str`. The other direction is just as real — Python's
102
+ * numeric tower makes `7`, `7.0` and `True` all equal and all one key — so a key built from
103
+ * the JS type would drop a dedupe Python performs. This spells out both.
104
+ *
105
+ * Dates are keyed by their type as well as their text: `date(2026, 8, 23)` and the
106
+ * `datetime` at midnight of the same day are NOT equal in Python.
107
+ */
108
+ export declare function pyHashKey(value: FactValue): string;
109
+ /**
110
+ * `a == b` over the same values — the OTHER half of the tuple comparison, and not a
111
+ * rephrasing of `<`.
112
+ *
113
+ * `tuplerichcompare` walks the key tuple with `==` and calls `<` only on the first element
114
+ * where `==` answers False. That is what keeps `sorted` from raising on two facts both named
115
+ * `None`: `None == None` is True, the tuples are equal, and `None < None` — which DOES raise
116
+ * — is never reached. Measured: without this the port raised `'NoneType' and 'NoneType'`
117
+ * where CPython answered an order.
118
+ *
119
+ * `==` never raises. Across types it is simply False — `7 == 'a'`, a `date` against a
120
+ * `datetime`, a naive `datetime` against an aware one — while `<` on the same pairs raises.
121
+ * Inside the numeric family it is Python's: `True == 1`, `7 == 7.0`, and `nan == nan` is
122
+ * False, which is why NaN is not special-cased anywhere here.
123
+ */
124
+ export declare function pyEqualValue(a: FactValue, b: FactValue): boolean;
125
+ export declare function pyCompareLt(a: FactValue, b: FactValue): boolean;
126
+ /**
127
+ * `sorted(scored, key=lambda pair: (-pair[0], pair[1].name))`, comparison for comparison.
128
+ *
129
+ * RULING — WHY THE SORT IS SPELLED OUT AND NOT HANDED TO `Array.prototype.sort`. When every
130
+ * name is a `str` the two agree and this takes the fast path. When they are not, the sort
131
+ * can RAISE, and then WHICH pair CPython happens to compare first decides the sentence a
132
+ * model reads: `'int' and 'str'` or `'str' and 'int'`, and with three numeric types in play,
133
+ * which of `int`/`float`/`bool` gets named. `Array.prototype.sort`'s comparison order is
134
+ * unspecified and V8's is not Timsort's.
135
+ *
136
+ * So the slow path is CPython's `listsort` for a single run: `count_run` (adjacent
137
+ * `a[i] < a[i-1]`, reversing a strictly descending prefix) then `binarysort` (pivot on the
138
+ * LEFT of every probe, midpoint `l + ((r - l) >> 1)`), over key tuples compared by
139
+ * `tuplerichcompare` — `==` down the tuple and `<` only where `==` says they differ.
140
+ *
141
+ * MEASURED, and re-measured on every conformance run: the `recall tie-break` case in
142
+ * `tools/conformance/suites/store.mjs` generates 50,000 `(score, name)` lists from a seeded
143
+ * LCG — ten name spellings across all four families, n from 2 to 300 — and runs each through
144
+ * CPython's `sorted` and through this function. 45,629 of them raise. All 50,000 agree, on
145
+ * the order where there is one and on the TypeError text where there is not.
146
+ *
147
+ * TWO MODELS THIS REFUTED, kept because each looks right until it is run. (1) "raise if any
148
+ * equal-score pair is incomparable" gets the DECISION right — 20,000 of 20,000 in the
149
+ * derivation probe — and the SENTENCE wrong in 1,087 of 30,000, because the types it names
150
+ * are whichever pair the binary search actually probed. (2) Calling `<` wherever the scores
151
+ * tie raises `'NoneType' and 'NoneType'` for two facts both named `None`, where CPython
152
+ * answers an order: `==` short-circuits the tuple before `<` is ever reached.
153
+ *
154
+ * NOT MEASURED, and the honest edge: past 64 elements CPython splits the list into runs and
155
+ * merges them, while this binary-inserts the whole list. It can only matter if the FIRST
156
+ * incomparable pair lies past the first run, and the 8,000 generated lists with n between 65
157
+ * and 300 produced no such case. A store would need 65+ facts tied on one recall score with
158
+ * the type change late in the file order.
159
+ */
160
+ export declare function sortScored<T extends {
161
+ score: number;
162
+ name: FactValue;
163
+ }>(scored: readonly T[]): T[];
164
+ export declare class MemoryStore {
165
+ readonly root: string;
166
+ readonly indexBudget: number;
167
+ readonly k: number;
168
+ private readonly today;
169
+ constructor(root: string, options?: MemoryStoreOptions);
170
+ private ensureDirs;
171
+ /**
172
+ * Write a fact, or refuse it as a near-duplicate of one already here.
173
+ *
174
+ * THE FIRST OF TWO READS IS WHAT MAKES THIS SAFE, and the ordering is the reference's, not
175
+ * an implementation detail: `_facts()` runs for the duplicate check BEFORE `_writeFact`,
176
+ * so an unreadable store fails the whole op with nothing written and nothing to roll back.
177
+ * The measured alternative, one directory over in job37: a blind listing let the duplicate
178
+ * check pass vacuously, the fact landed, and `_rebuildIndex` then wrote a 13,472-byte
179
+ * index down to zero from the same empty listing.
180
+ */
181
+ save(type: string, name: string, description: string, body: string, links?: readonly string[]): SaveResult;
182
+ /**
183
+ * Top-`k` facts whose name+description share tokens with `query`.
184
+ *
185
+ * An unreadable store raises out of `facts()` rather than scoring zero facts and returning
186
+ * `[]` — the same answer as a real miss, which `component.Memory` used to turn into "no
187
+ * memories matched. Try different words", telling a person to rephrase a question at a
188
+ * filing cabinet nobody could open.
189
+ */
190
+ recall(query: string, k?: number | null, stamp?: boolean): Fact[];
191
+ /**
192
+ * `len(store._facts())` at the operator CLI, which is the only caller.
193
+ *
194
+ * `facts()` stays private: this hands out a COUNT, not the parsed list, so no caller
195
+ * outside this file can hold a fact array and drift from what is on disk. It is a full
196
+ * parse — an unreadable or malformed store raises here rather than counting 0, which is
197
+ * the same rule `indexText` holds and for the same reason.
198
+ */
199
+ factCount(): number;
200
+ /**
201
+ * Every fact parses and carries a valid type, and the index fits its budget.
202
+ *
203
+ * The raise on an unreadable store is the reference's and it is load-bearing: a checker
204
+ * that passes hardest on the store it could not open is worse than no checker. `_facts()`
205
+ * raises before the type sweep starts, and `_cmd_lint` in `memory/cli.ts` routes both that
206
+ * and the budget error to `lint: FAIL — …` on stderr with exit 1.
207
+ */
208
+ lint(): void;
209
+ /**
210
+ * Archive the stalest facts until the index sits at `budget - reserve` or below.
211
+ *
212
+ * THE TARGET IS BELOW THE BUDGET ON PURPOSE, and the reference's docstring records why:
213
+ * `save` rolls its fact back before raising, so by the time anyone is told to compact, the
214
+ * index already fits — a loop that stopped at "fits" would archive nothing at the only
215
+ * moment the remedy is ever named. The default `reserve` is the largest index line the
216
+ * store currently holds (capped at half the budget), so the headroom bought is "a fact as
217
+ * big as your biggest one will fit", measured from this store's own data.
218
+ *
219
+ * `facts()` runs FIRST, before any rename, so an unreadable store moves nothing.
220
+ *
221
+ * THE MOVE IS `os.replace`, AND THE REFERENCE'S IS NOW TOO — the difference this comment
222
+ * used to report is CLOSED. `pyReplace` is `os.replace`, which replaces an existing
223
+ * destination on every platform. `Path.rename` is `os.rename`, which replaces silently on
224
+ * POSIX and raises `FileExistsError` on Windows; the reference called it here until
225
+ * `d239480` moved it onto `Path.replace`, so Node-on-Windows used to match Python-on-POSIX
226
+ * and Python-on-Windows matched neither. Nothing about the port changed: this line has
227
+ * always been `pyReplace`.
228
+ *
229
+ * WHAT IS STILL TRUE is the reachability argument, which is why the fix needed a fixture
230
+ * built for it. The only state that tells `rename` and `replace` apart is an
231
+ * `archive/<name>.md` that ALREADY exists when this loop moves the live fact over it — an
232
+ * earlier compaction's copy of a fact that was restored and then went stale again.
233
+ * `restore` cannot produce it, because it moves the archived copy OUT. On POSIX the two
234
+ * calls are indistinguishable even in that state, so `memorycli`'s
235
+ * `compact re-archives over an existing archive entry` is a WINDOWS-ONLY regression guard:
236
+ * a revert to `rename` on either side stays green on this machine.
237
+ *
238
+ * THIS PARAGRAPH USED TO CLAIM THE OTHER TWO MOVES ARE UNOBSERVABLE BY CONSTRUCTION —
239
+ * "guarded by a `reachable` check that refuses when `facts/<name>.md` is live" and "moves
240
+ * back onto a path it has just emptied", so "there is no state in which the two calls could
241
+ * answer differently". THE FIRST HALF IS FALSE, and `archive` (2026-09-05) is what made it
242
+ * reachable enough to notice. `reachable` is `Path.exists()`, which FOLLOWS symlinks, so a
243
+ * DANGLING symlink at the destination is an occupied directory ENTRY that the guard reports
244
+ * as absent — measured on macOS at `archive/alpha.md`: `lexists` True, `exists` False, the
245
+ * guard passed, and the move landed on top of the link. `os.rename` would raise
246
+ * `FileExistsError` there on Windows and `pyReplace` would not, which is exactly the
247
+ * divergence `d239480` closed one method up. The reference's `archive` is therefore on
248
+ * `Path.replace` too, as of the same day, and its FORWARD move and this one are the same
249
+ * call on every platform. `restore`'s forward move has the identical hole one directory
250
+ * over (a dangling symlink at `facts/<name>.md`) and is NOT changed here: narrowing a
251
+ * shipped command is not this fix's to make, and it is registered instead.
252
+ *
253
+ * WHAT IS STILL UNOBSERVABLE BY CONSTRUCTION is the ROLLBACK on both methods, and only that:
254
+ * it moves back onto a path the forward move has just emptied, so it cannot meet an occupied
255
+ * destination at all — there is no red to demonstrate for it, which is the reason `d239480`
256
+ * gave for leaving restore's alone and the reason the reference's two rollbacks are still
257
+ * `rename` against this file's `pyReplace`.
258
+ *
259
+ * THE ORDER IS `byEviction`, NOT `byStaleness` — `sorted(facts, key=self._eviction_key)`.
260
+ * A `DURABLE_TYPES` fact — `feedback`, the user's standing instruction, or `user`, a
261
+ * durable fact about them — holds until revoked and its worth does not decay with
262
+ * time-since-last-recall, so a purely temporal key ranks those classes exactly
263
+ * backwards. Measured on the real project store (index 21698 of a 24000-byte budget): one
264
+ * auto-compaction archived 15 facts and 6 of them were `feedback`. See `byEviction`.
265
+ */
266
+ compact(reserve?: number | null): CompactResult;
267
+ /**
268
+ * Names of the facts sitting in `archive/` — everything `compact` moved out.
269
+ *
270
+ * This is the worst place in the module to answer "empty" wrongly, because `compact()` has
271
+ * already MOVED the operator's facts here; an archive that cannot be listed is not an empty
272
+ * archive, and saying so makes compaction look like deletion. Hence `listing`, not a glob.
273
+ *
274
+ * The sort is over the STEMS and it is a plain string sort — `sorted(Path(name).stem …)` —
275
+ * not `sortedPathNames`, whose Windows case fold belongs to comparing `Path`s.
276
+ */
277
+ archived(): string[];
278
+ /**
279
+ * Move an archived fact back into `facts/`; refuse if it would blow the budget.
280
+ *
281
+ * THE PROMISE IS THAT A FAILED RESTORE LEAVES THE STORE EXACTLY AS IT FOUND IT, and it
282
+ * takes both halves the reference has: a `facts()` PARSE before the move (a listing is not
283
+ * enough — a malformed fact passes a listing and fails the budget check afterwards), and a
284
+ * rollback keyed on "the op after the move failed" rather than on a list of error types,
285
+ * because when the ARCHIVED file is the bad one it is not a fact until after the rename.
286
+ *
287
+ * `restore` is deliberately NOT routed through `archived()`: it stats one named path
288
+ * instead of listing, so an unlistable-but-traversable `archive/` still restores. Refusing
289
+ * a recovery the filesystem was still willing to perform is the wrong direction for the
290
+ * door back.
291
+ */
292
+ /**
293
+ * Move one named fact out of `facts/` and into `archive/`.
294
+ *
295
+ * The door out, taken deliberately. `compact` already moves facts out, but it chooses
296
+ * them by eviction rank and stops as soon as the index fits the budget, so it can
297
+ * neither be asked for a PARTICULAR fact nor be used at all when the store is already
298
+ * under budget. `restore` has taken a name since it was written; until this method the
299
+ * store could bring a named fact back but not send one away.
300
+ *
301
+ * THE NAME IS CHECKED AGAINST `NAME_RE` BEFORE ANY SYSCALL, and the reference does the
302
+ * same as of 2026-09-05. `save` was the only op enforcing it, and `save` is not the only
303
+ * op that CREATES a filename: this one builds `archive/<name>.md` out of what it is
304
+ * handed. Measured on macOS before the check: `archive ALPHA` against a live
305
+ * `facts/alpha.md` exited 0 on BOTH runtimes and left `archive/ALPHA.md` whose
306
+ * frontmatter says `name: alpha`, because the filesystem is case-insensitive and nothing
307
+ * asked the store's naming rule about the destination; on a case-sensitive filesystem the
308
+ * same command refuses. `restore` is deliberately left unvalidated on both sides.
309
+ *
310
+ * Same promise as `restore` — a failed archive leaves the store exactly as it found it —
311
+ * and one of its three guards carries over while two drop out:
312
+ *
313
+ * - Both stats go through `reachable`, not an existence check, for the reason spelled at
314
+ * `ARCHIVE_UNREACHABLE`. The two sentences are their own constants because each names
315
+ * the side the fact is still on, and that side is the mirror of restore's.
316
+ * - NO budget check. Archiving removes an index line, so the index can only shrink;
317
+ * `checkIndexBudget` is restore's guard in restore's direction and here it could not
318
+ * fail.
319
+ * - NO `facts()` PARSE BEFORE THE MOVE, and the reference dropped it the same day. The
320
+ * parse reads EVERY fact, so ONE malformed file in `facts/` refused every archive in
321
+ * the store INCLUDING ITS OWN, and no other command removes a fact by name — the one
322
+ * file the store calls broken was the one file no CLI route could remove, which is the
323
+ * opposite of what this method exists for. Without it the same command succeeds for a
324
+ * reason: the move takes the bad file out of `facts/` first, so `rebuildIndex` parses a
325
+ * directory that no longer holds it. A DIFFERENT fact being malformed still fails at
326
+ * that rebuild, and the rollback below puts the moved fact back.
327
+ *
328
+ * The rollback is keyed on "the rebuild after the move failed", and the route that
329
+ * reaches it is `index.md` BEING A DIRECTORY — not, as the reference's docstring used to
330
+ * say, a directory at `archive/<name>.md`, which the guard above stats and refuses first.
331
+ */
332
+ archive(name: string): void;
333
+ restore(name: string): void;
334
+ /**
335
+ * The index as it should be on disk, derived from `facts/` and nothing else.
336
+ *
337
+ * An unreadable store raises here rather than answering `""` — that empty string was both
338
+ * the input `_rebuildIndex` wrote over `index.md` and the number `_checkIndexBudget`
339
+ * measured, which is the whole shape of the defect and is why the raise lives in the READ.
340
+ *
341
+ * THE BUDGET AND THE BYTES ON DISK AGREE IN THIS PORT, on every platform, because
342
+ * `pyWriteText` writes exactly this string. They do NOT agree in `runtime-py` on Windows:
343
+ * `write_text` opens with `newline=None`, so the file gets `\r\n` per line while
344
+ * `_checkIndexBudget` counts this LF text — 65 bytes apart on the live 65-fact store.
345
+ * Registered as a runtime-py defect, not fixed here (invariant 8).
346
+ */
347
+ indexText(): string;
348
+ private factPath;
349
+ private indexLine;
350
+ /**
351
+ * `sorted(facts, key=self._eviction_key)` — the eviction order `compact` archives in.
352
+ *
353
+ * CLASS FIRST, THEN STALENESS. A `feedback` fact is a standing instruction from the user:
354
+ * it holds until revoked, and its worth does not decay with time-since-last-recall, so the
355
+ * temporal key below is INVERTED for that kind of fact — the better an instruction has
356
+ * been internalised the less anything recalls it, the staler it looks, and the sooner it
357
+ * leaves the index that is loaded at session start. That class therefore ranks LAST and
358
+ * every other class is exhausted before any of it is archived. A priority and never a
359
+ * veto: the budget still wins, and with nothing else left it goes by staleness. Both sorts
360
+ * are STABLE, so a tied rank leaves the staleness answer below exactly as it was.
361
+ *
362
+ * `DURABLE_TYPES` AND NOT `"feedback"` ALONE, because that reason is a property of the
363
+ * class and not of the word — review round 4 (M12), mirroring `runtime-py` `1d9e5eb`.
364
+ * `assets/skills/memory.md` tells the model that `user` is "a durable fact about the
365
+ * user", and a durable fact does not become less true because nothing looked it up.
366
+ * Measured on the reference before the change, on four facts one per type where nothing
367
+ * has ever been recalled: one slot to free and `compact` archived `ausr`, a never-recalled
368
+ * `user` fact, ahead of a `project` note created seven months later. The change is
369
+ * MONOTONE — the protected set only grows — so no existing store loses a fact this rank
370
+ * kept for it before. The two protected types share ONE rank rather than being ordered
371
+ * against each other: the reason for protecting them is identical, so any order between
372
+ * them would be an invention, and a tied rank leaves the staleness key to answer.
373
+ *
374
+ * The protected class is deliberately UNCAPPED. Nothing bounds how much of the index it
375
+ * may hold, and that cannot make `compact` fail: once every decaying fact is archived the
376
+ * loop keeps going through the protected ones by staleness, so the budget still wins.
377
+ *
378
+ * `last_recalled` ALONE conflated two opposite facts: one written seconds ago and one
379
+ * nobody has asked for in a year both read as absent, and the empty string sorts before
380
+ * every real ISO date, so the NEWEST fact was the first evicted. Falling back to `created`
381
+ * makes absence of evidence mean "as stale as it is old". Ties break on name only after
382
+ * the dates are equal, so the alphabet can no longer decide a live question.
383
+ *
384
+ * `or`, not `??`: every FALSY value falls through, which is what Python's `or` does — a
385
+ * `last_recalled: ''` or a `created: 0` is not evidence of a recall. The comparison is
386
+ * `pyCompareLt`, Python's `<`, because these fields come out of YAML and a `date` beside a
387
+ * `str` raises there instead of sorting; both runtimes must fail the same way. Both sorts
388
+ * are STABLE, so equal keys keep listing order on either side.
389
+ */
390
+ private byEviction;
391
+ /**
392
+ * `path.exists()`, except that "I was not allowed to look" is never "it is not there".
393
+ *
394
+ * The same invariant as `listing`, one syscall down: `pyExists` swallows exactly
395
+ * `pathlib._IGNORED_ERRNOS` and re-raises the rest, so EACCES arrives as an OS error and is
396
+ * converted HERE — in the store, which owns the distinction — rather than in whichever
397
+ * command happened to call `restore`.
398
+ */
399
+ private reachable;
400
+ /**
401
+ * The `*.md` names in one of this store's directories, or a raise. Never a lie.
402
+ *
403
+ * THE THREE-WAY, which is the whole of job37's fix and is keyed on `lexists` PRECISELY so
404
+ * that it does not turn on an errno:
405
+ *
406
+ * 1. the scan failed with ENOENT and SOMETHING is at the path -> unreadable
407
+ * 2. the scan failed with ENOENT and nothing is at the path -> [] (a first run)
408
+ * 3. the scan failed any other way -> unreadable
409
+ *
410
+ * Arm 1 exists because the same shape reports different errnos on different platforms:
411
+ * POSIX raises ENOTDIR for a scan of a regular file and lands in arm 3, while a Windows
412
+ * directory scan of a non-directory reports the path as NOT FOUND and would land in arm 2
413
+ * — answering "empty" for a store that is a file. A dangling symlink reports ENOENT
414
+ * everywhere. Keying on "is anything there" gives one answer on all four CI jobs.
415
+ *
416
+ * Node's `fs` has none of this shape and libuv's Windows error mapping is not CPython's,
417
+ * so what is reproduced here is the DECISION. The syscalls and the `strerror` live in
418
+ * `pyfs.ts`; `layers.count_facts` deliberately decides differently on the same ones.
419
+ *
420
+ * `consequence` is the caller's half of the sentence, because the two directories fail
421
+ * differently and one wording cannot be true of both.
422
+ */
423
+ private listing;
424
+ /** Every `facts/*.md`, listed so that "I could not read it" is never "it is empty". */
425
+ private factPaths;
426
+ /** One sentence for a directory that could not be listed, and it never says "empty". */
427
+ private unreadable;
428
+ /**
429
+ * Every fact on disk, parsed.
430
+ *
431
+ * The `read_text` is OUTSIDE the try in the reference, so a fact file that is not UTF-8
432
+ * raises a bare `UnicodeDecodeError` rather than a `malformed fact file …` sentence. That
433
+ * ordering is reproduced: `pyReadText` raises `PyUnicodeDecodeError` with CPython's own
434
+ * message and it is not caught here.
435
+ *
436
+ * RULING — THE MESSAGE TEXT FOR A MALFORMED FILE. `_facts` interpolates the exception into
437
+ * `malformed fact file {name}: {e}`, and that reaches the model. Three of the four shapes
438
+ * are matched EXACTLY, because their text is Python's own and short: too few `---\n` parts
439
+ * (`ValueError` from the tuple unpack), frontmatter that is not a mapping (the store's own
440
+ * sentence), and a missing key (`KeyError.__str__`, which is the key in single quotes).
441
+ * The fourth — input outside the grammar `parseFrontmatter` accepts — is NOT matched:
442
+ * PyYAML answers with a `ScannerError`/`ParserError` carrying its marks and a rendered
443
+ * snippet, and reproducing that means porting PyYAML's scanner diagnostics, a surface
444
+ * larger than this whole module, for strings no test in `runtime-py/tests` pins. Node
445
+ * emits `parseFrontmatter`'s own reason after the identical prefix. The divergence is
446
+ * carried as a must-differ `ruling` case in the conformance suite so it is re-measured on
447
+ * every run instead of asserted once and forgotten.
448
+ */
449
+ private facts;
450
+ /**
451
+ * Date the recall.
452
+ *
453
+ * The reference's `snapshot` branch is absent here for the reason given at the top of the
454
+ * file: nothing on the MCP surface enters a snapshot scope, so `_snapshot` is always
455
+ * `None` and only this arm ever runs. No fact is left half-dated either way, because the
456
+ * listing inside `_facts` has already run before any write.
457
+ */
458
+ private stamp;
459
+ /** Write via `<name>.md.tmp` and `os.replace`, so a reader never sees a half-written fact. */
460
+ private writeFact;
461
+ /**
462
+ * Write `index.md` from the facts on disk. THE OP THE ORIGINAL DEFECT DESTROYED.
463
+ *
464
+ * Note what is deliberately NOT here: a guard refusing to shrink the index. It could not
465
+ * tell a wipe from a legitimate `compact()`, and it would leave every reader still being
466
+ * lied to. The read was wrong and the read is where it is fixed.
467
+ */
468
+ private rebuildIndex;
469
+ /**
470
+ * Raise `MemoryBudgetExceeded` if the index would not fit.
471
+ *
472
+ * This is a PARSE, not a listing: `indexText` reads every fact file, so a caller that
473
+ * pre-read with `factPaths` has not pre-read what this raises on. An unreadable store
474
+ * raises `MemoryValidationError` from in here — a DIFFERENT error from the budget one, and
475
+ * `save`'s rollback below is written to re-throw it untouched.
476
+ */
477
+ private checkIndexBudget;
478
+ }