bantamkit-mcp 0.32.0 → 0.32.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +77 -1
- package/dist/memory/component.d.ts +23 -6
- package/dist/memory/component.js +59 -8
- package/dist/memory/store.d.ts +29 -0
- package/dist/memory/store.js +47 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -80,7 +80,7 @@ import { packFileCount } from './mcp/identity.js';
|
|
|
80
80
|
import { buildServer } from './mcp/server.js';
|
|
81
81
|
import { RawStdioTransport } from './mcp/transport.js';
|
|
82
82
|
import { ArgumentTypeError, ArgvError, formatHelp, formatUsageBlock, HelpRequested, helpWidth, parseArgs as parseWithSpec, pyIntStrict, } from './pyargparse.js';
|
|
83
|
-
import { pyRepr } from './memory/pyfs.js';
|
|
83
|
+
import { pyJoin, PyOSError, pyRepr, pyStatIsDir } from './memory/pyfs.js';
|
|
84
84
|
import { HOSTS, install as installHost, InstallError, thisCommand } from './hostinstall.js';
|
|
85
85
|
import { runUpdate } from './selfupdate.js';
|
|
86
86
|
import { statusLine } from './statusline.js';
|
|
@@ -330,10 +330,86 @@ function buildMemory(options) {
|
|
|
330
330
|
if (options.store !== null) {
|
|
331
331
|
if (options.store === '')
|
|
332
332
|
throw new Refusal('--store requires a non-empty path');
|
|
333
|
+
checkStoreFlag(options.store);
|
|
333
334
|
return new Memory(options.store, shared);
|
|
334
335
|
}
|
|
335
336
|
return Memory.layered(options.start, shared);
|
|
336
337
|
}
|
|
338
|
+
/**
|
|
339
|
+
* `--store` may name a store that is missing, never one that is not a store.
|
|
340
|
+
*
|
|
341
|
+
* THIS CHECK IS RESTORED, NOT INVENTED, and the distinction is the point. Until the project
|
|
342
|
+
* layer was built lazily there WAS a `--store` validation, and it was entirely accidental: the
|
|
343
|
+
* constructor ran `pyMkdirParents`, so a `--store` pointing at a regular file or at a path
|
|
344
|
+
* under a directory that does not exist took the process down with a `PyOSError` stack out of
|
|
345
|
+
* `pyfs`. The reference's `test_statusline.py::
|
|
346
|
+
* test_the_flag_returns_before_anything_a_server_would_touch` depends on it — it arms
|
|
347
|
+
* `--store <a regular file>` as a trap and shows `--statusline` walking past it — and a lazy
|
|
348
|
+
* layer disarms the trap by making that same argv exit 0. The honest way to keep that test
|
|
349
|
+
* true is to mean the refusal on purpose.
|
|
350
|
+
*
|
|
351
|
+
* IT IS `layers.pinnedStore`'s CHECK MINUS ONE ARM, and the missing arm is deliberate and
|
|
352
|
+
* measured. The pin refuses a path that is not there ("nothing was created"); this flag does
|
|
353
|
+
* NOT, and must not, for two reasons that are both already pinned elsewhere in this
|
|
354
|
+
* repository:
|
|
355
|
+
*
|
|
356
|
+
* - The sibling CLI's identical flag is contracted to CREATE one. `tools/conformance/suites/
|
|
357
|
+
* memorycli.mjs` carries `status-creates-a-missing-store`, `--store {BED}/nowhere` over an
|
|
358
|
+
* empty bed, whose whole job is to say the two runtimes create the same two directories
|
|
359
|
+
* there. Refusing a missing `--store` here would put two flags of the same name, in two
|
|
360
|
+
* programs of the same product, in direct contradiction.
|
|
361
|
+
* - "Missing" is no longer a broken state anywhere in this program. The walk designates a
|
|
362
|
+
* project store without creating it, and the first save brings it into existence or refuses
|
|
363
|
+
* by name (`store.ensureDirs`). A `--store` at a path that is not there is that same
|
|
364
|
+
* designated state, reached by being told instead of by searching. MEASURED on the reference
|
|
365
|
+
* side: requiring existence reddened 14 tests that have nothing to do with this defect —
|
|
366
|
+
* `test_tool_manifest.py` ×7, `test_mcpserver.py` ×6, `test_mcp_endpoint.py` — every one of
|
|
367
|
+
* them a fixture naming a store under `tmp_path` it never made, because that is what this
|
|
368
|
+
* flag has always meant.
|
|
369
|
+
*
|
|
370
|
+
* What is left is the arm the accident actually covered and the only one it covered: a
|
|
371
|
+
* `--store` that names something which EXISTS and is NOT A DIRECTORY. That is not a store and
|
|
372
|
+
* never becomes one — `mkdir` under it is ENOTDIR on both runtimes and on every platform, the
|
|
373
|
+
* one row of the errno table where they already agree — so it is refused here, by name, before
|
|
374
|
+
* a transport exists.
|
|
375
|
+
*
|
|
376
|
+
* `FileNotFoundError` and not an errno comparison: the absent case is selected by the exception
|
|
377
|
+
* CPython raises for it, and `pyfs`' `PyOSError` carries that class as its `name`. Every other
|
|
378
|
+
* `stat` failure — a permission wall on the parent, a symlink loop — is a real fault about a
|
|
379
|
+
* path the operator named, and it keeps the pin's sentence.
|
|
380
|
+
*
|
|
381
|
+
* `pyStatIsDir` rather than an `existsSync`/`isDirectory` pair, and that too is `pinnedStore`'s
|
|
382
|
+
* reasoning: a predicate that swallows `PermissionError` and answers false would report an
|
|
383
|
+
* operator's real store as a typo. A single named path gets the accurate reason.
|
|
384
|
+
*
|
|
385
|
+
* `Refusal` and not `MemoryValidationError`, so that both halves of this flag's refusal family
|
|
386
|
+
* read the same way on the terminal: `--store requires a non-empty path` is already bare on
|
|
387
|
+
* stderr at exit 1, and the reference spells both as `SystemExit`.
|
|
388
|
+
*/
|
|
389
|
+
function checkStoreFlag(raw) {
|
|
390
|
+
// NOT `pyExpanduser`, unlike the pin. `new Memory(store, ...)` builds the root from this
|
|
391
|
+
// string verbatim, so expanding here would check one path and serve another: `--store ~/x`
|
|
392
|
+
// left unexpanded by the shell would pass a check against the home directory and then build
|
|
393
|
+
// a store in a directory literally named `~`. The check must stat the path the store is
|
|
394
|
+
// going to be. `pyJoin` of the one argument is `Path(raw)`: the same normalisation
|
|
395
|
+
// `MemoryStore` applies to its root, so the path this stats and the path it names in a
|
|
396
|
+
// refusal are the path the store would have used.
|
|
397
|
+
const store = pyJoin(raw);
|
|
398
|
+
let isDir;
|
|
399
|
+
try {
|
|
400
|
+
isDir = pyStatIsDir(store);
|
|
401
|
+
}
|
|
402
|
+
catch (e) {
|
|
403
|
+
if (!(e instanceof PyOSError))
|
|
404
|
+
throw e;
|
|
405
|
+
// designated, not broken: the first save makes it or refuses by name
|
|
406
|
+
if (e.name === 'FileNotFoundError')
|
|
407
|
+
return;
|
|
408
|
+
throw new Refusal(`--store is unreachable: ${store}: ${e.strerror}; nothing was created`);
|
|
409
|
+
}
|
|
410
|
+
if (!isDir)
|
|
411
|
+
throw new Refusal(`--store is not a directory: ${store}`);
|
|
412
|
+
}
|
|
337
413
|
/**
|
|
338
414
|
* The version this build declares — the same bytes npm packs the tarball from.
|
|
339
415
|
*
|
|
@@ -128,10 +128,14 @@ export declare class Memory {
|
|
|
128
128
|
private readonly layers;
|
|
129
129
|
private showLayers;
|
|
130
130
|
/**
|
|
131
|
-
* How this store came to be the store, captured once. It
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
131
|
+
* How this store came to be the store, captured once. It is still captured here and not
|
|
132
|
+
* re-derived later, but the reason has changed with the constructor below and the old one is
|
|
133
|
+
* recorded rather than deleted: it WAS that the constructor had already made the directory,
|
|
134
|
+
* so a store that was only DESIGNATED a moment ago was indistinguishable on disk from one
|
|
135
|
+
* that was found empty. That complaint is now discharged — nothing is created — and the
|
|
136
|
+
* binding is kept because it also carries WHY this path (`origin`, `searchedFrom`), which no
|
|
137
|
+
* amount of looking at the disk recovers. `null` means the caller named the path outright
|
|
138
|
+
* and no resolution happened to report.
|
|
135
139
|
*/
|
|
136
140
|
private readonly binding;
|
|
137
141
|
constructor(store: string, options?: MemoryOptions, binding?: StoreBinding | null);
|
|
@@ -140,8 +144,15 @@ export declare class Memory {
|
|
|
140
144
|
*
|
|
141
145
|
* `resolveProjectStore` rather than `discoverProjectStore`: the path both return is the
|
|
142
146
|
* same path, but only the binding carries WHY it is that path and whether it holds
|
|
143
|
-
* anything — and `recall` cannot recover either fact afterwards
|
|
144
|
-
*
|
|
147
|
+
* anything — and `recall` cannot recover either fact afterwards.
|
|
148
|
+
*
|
|
149
|
+
* AMENDMENT 2026-09-12 (job48, J48-2), correcting the reason the clause above used to give
|
|
150
|
+
* and not the clause itself. WAS: "because constructing the store creates the directory."
|
|
151
|
+
* NOW that is false — the project layer is built `create: false` (see the constructor) and
|
|
152
|
+
* constructing it creates nothing. The binding is still the only carrier of WHY, because
|
|
153
|
+
* `origin` and `searchedFrom` were never on the disk to begin with; what the disk has
|
|
154
|
+
* stopped being able to answer is merely "was this empty or brand new", which it now answers
|
|
155
|
+
* correctly for the first time.
|
|
145
156
|
*/
|
|
146
157
|
static layered(start?: string | null, options?: MemoryOptions): Memory;
|
|
147
158
|
/** The labels, in order. `_layers` is private in the reference too; this is for the tests. */
|
|
@@ -280,6 +291,12 @@ export declare class Memory {
|
|
|
280
291
|
* at construction and has been saved to since really can answer. The DIAGNOSIS is keyed on
|
|
281
292
|
* the binding, because "designated" stops being visible on disk the moment `MemoryStore`
|
|
282
293
|
* creates the directory.
|
|
294
|
+
*
|
|
295
|
+
* AMENDMENT 2026-09-12 (job48, J48-2). The reason in the clause above no longer holds and
|
|
296
|
+
* the keying does: the project layer is built `create: false`, so a designated store IS now
|
|
297
|
+
* distinguishable on disk — it is absent. The DIAGNOSIS still keys on the binding because
|
|
298
|
+
* only the binding carries `origin` and `searchedFrom`, which the disk never held, and
|
|
299
|
+
* because an absent directory and an emptied one must not be told apart by a race.
|
|
283
300
|
*/
|
|
284
301
|
private nothingToReport;
|
|
285
302
|
/**
|
package/dist/memory/component.js
CHANGED
|
@@ -123,17 +123,45 @@ export class Memory {
|
|
|
123
123
|
layers;
|
|
124
124
|
showLayers = false;
|
|
125
125
|
/**
|
|
126
|
-
* How this store came to be the store, captured once. It
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
126
|
+
* How this store came to be the store, captured once. It is still captured here and not
|
|
127
|
+
* re-derived later, but the reason has changed with the constructor below and the old one is
|
|
128
|
+
* recorded rather than deleted: it WAS that the constructor had already made the directory,
|
|
129
|
+
* so a store that was only DESIGNATED a moment ago was indistinguishable on disk from one
|
|
130
|
+
* that was found empty. That complaint is now discharged — nothing is created — and the
|
|
131
|
+
* binding is kept because it also carries WHY this path (`origin`, `searchedFrom`), which no
|
|
132
|
+
* amount of looking at the disk recovers. `null` means the caller named the path outright
|
|
133
|
+
* and no resolution happened to report.
|
|
130
134
|
*/
|
|
131
135
|
binding;
|
|
132
136
|
constructor(store, options = {}, binding = null) {
|
|
133
137
|
const k = options.k ?? 3;
|
|
138
|
+
// `create: false`, AND IT IS THE WHOLE OF THE cwd-`/` FIX. This was the last eager layer —
|
|
139
|
+
// grants and the profile store have always been lazy — so a cwd that no directory can be
|
|
140
|
+
// created under was a STARTUP CRASH: every GUI MCP host launches its child with cwd `/`,
|
|
141
|
+
// measured live on this machine (`lsof -p <pid> -a -d cwd` on Claude Desktop and all four
|
|
142
|
+
// of its `bantamkit-mcp` children returns `/`), and the client saw only CONNECTION_CLOSED.
|
|
143
|
+
// Refusing at startup with a sentence was the other candidate and it is REFUTED by
|
|
144
|
+
// measurement: it leaves those same four processes dead, which is strictly worse than
|
|
145
|
+
// today. Built lazily, the server starts from `/`, binds `['project', 'profile']`, and
|
|
146
|
+
// answers out of the profile layer. The sentence still exists — it is what
|
|
147
|
+
// `MemoryStore.save`/`compact` say when a write actually needs the directory
|
|
148
|
+
// (`store.ensureDirs`).
|
|
149
|
+
//
|
|
150
|
+
// It is legal because every READ is already defined over an absent directory:
|
|
151
|
+
// `recall() -> []`, `archived() -> []`, `indexText() -> ''`. `facts()`' docstring says so
|
|
152
|
+
// outright, and `create: false` is one of the three callers it names.
|
|
153
|
+
//
|
|
154
|
+
// THE PRICE, stated rather than discovered: a bare server start in a perfectly good cwd no
|
|
155
|
+
// longer scatters `.bantamkit/memory/{facts,archive}` there before anything is saved. That
|
|
156
|
+
// moves TOWARD a stance this file already holds — see the binding comment above, and
|
|
157
|
+
// `cli.ts`'s bare-TTY guard.
|
|
158
|
+
//
|
|
159
|
+
// The reference is `component.py`'s `Memory.__init__` (job48, J48-1); this is the same
|
|
160
|
+
// change in the same place, and the two sentences below are spelled byte for byte.
|
|
134
161
|
this.store = new MemoryStore(store, {
|
|
135
162
|
indexBudget: options.indexBudget ?? DEFAULT_INDEX_BUDGET,
|
|
136
163
|
k,
|
|
164
|
+
create: false,
|
|
137
165
|
...(options.today ? { today: options.today } : {}),
|
|
138
166
|
});
|
|
139
167
|
this.k = k;
|
|
@@ -145,8 +173,15 @@ export class Memory {
|
|
|
145
173
|
*
|
|
146
174
|
* `resolveProjectStore` rather than `discoverProjectStore`: the path both return is the
|
|
147
175
|
* same path, but only the binding carries WHY it is that path and whether it holds
|
|
148
|
-
* anything — and `recall` cannot recover either fact afterwards
|
|
149
|
-
*
|
|
176
|
+
* anything — and `recall` cannot recover either fact afterwards.
|
|
177
|
+
*
|
|
178
|
+
* AMENDMENT 2026-09-12 (job48, J48-2), correcting the reason the clause above used to give
|
|
179
|
+
* and not the clause itself. WAS: "because constructing the store creates the directory."
|
|
180
|
+
* NOW that is false — the project layer is built `create: false` (see the constructor) and
|
|
181
|
+
* constructing it creates nothing. The binding is still the only carrier of WHY, because
|
|
182
|
+
* `origin` and `searchedFrom` were never on the disk to begin with; what the disk has
|
|
183
|
+
* stopped being able to answer is merely "was this empty or brand new", which it now answers
|
|
184
|
+
* correctly for the first time.
|
|
150
185
|
*/
|
|
151
186
|
static layered(start, options = {}) {
|
|
152
187
|
const binding = resolveProjectStore(start);
|
|
@@ -566,6 +601,12 @@ export class Memory {
|
|
|
566
601
|
* at construction and has been saved to since really can answer. The DIAGNOSIS is keyed on
|
|
567
602
|
* the binding, because "designated" stops being visible on disk the moment `MemoryStore`
|
|
568
603
|
* creates the directory.
|
|
604
|
+
*
|
|
605
|
+
* AMENDMENT 2026-09-12 (job48, J48-2). The reason in the clause above no longer holds and
|
|
606
|
+
* the keying does: the project layer is built `create: false`, so a designated store IS now
|
|
607
|
+
* distinguishable on disk — it is absent. The DIAGNOSIS still keys on the binding because
|
|
608
|
+
* only the binding carries `origin` and `searchedFrom`, which the disk never held, and
|
|
609
|
+
* because an absent directory and an emptied one must not be told apart by a race.
|
|
569
610
|
*/
|
|
570
611
|
nothingToReport() {
|
|
571
612
|
let status;
|
|
@@ -670,9 +711,19 @@ export class Memory {
|
|
|
670
711
|
where = `The project store ${root} is empty.`;
|
|
671
712
|
}
|
|
672
713
|
else if (binding.state === 'designated') {
|
|
714
|
+
// AMENDED 2026-09-12 (job48, J48-2) because the sentence became FALSE, not because it
|
|
715
|
+
// read badly. WAS: "No memory store existed at or above <from>, so the empty <root> was
|
|
716
|
+
// created for this session." Under a lazily-built project layer nothing is created — the
|
|
717
|
+
// path is designated and stays absent until a save needs it — so the old sentence told
|
|
718
|
+
// an operator to go look for a directory that is not there. The new one borrows the
|
|
719
|
+
// pin's own closing clause, "nothing was created", because it is the same claim about
|
|
720
|
+
// the same thing, and it reuses the binding's existing `designated` vocabulary rather
|
|
721
|
+
// than minting a word for it. The remedy that follows — "otherwise save a memory to
|
|
722
|
+
// start this one" — is now literally true: the save is what brings the directory into
|
|
723
|
+
// existence. Byte for byte the reference's, `component.py`'s `_binding_diagnosis`.
|
|
673
724
|
where =
|
|
674
|
-
`No memory store existed at or above ${binding.searchedFrom}, so
|
|
675
|
-
`
|
|
725
|
+
`No memory store existed at or above ${binding.searchedFrom}, so ${root} ` +
|
|
726
|
+
`was designated for this session; nothing was created there.`;
|
|
676
727
|
}
|
|
677
728
|
else if (binding.origin === 'pin') {
|
|
678
729
|
where = `The project store ${root} is empty; ${MEMORY_DIR_ENV} pinned it.`;
|
package/dist/memory/store.d.ts
CHANGED
|
@@ -213,6 +213,35 @@ export declare class MemoryStore {
|
|
|
213
213
|
readonly k: number;
|
|
214
214
|
private readonly today;
|
|
215
215
|
constructor(root: string, options?: MemoryStoreOptions);
|
|
216
|
+
/**
|
|
217
|
+
* Bring the store's directories into existence, or refuse the write that needs them.
|
|
218
|
+
*
|
|
219
|
+
* THE PROPERTY: a store that cannot be brought into existence cannot be written to, and the
|
|
220
|
+
* refusal names which directory and why. It is a refusal and not a crash because the only
|
|
221
|
+
* callers are the two WRITES (`save`, `compact`); every read is already defined over an
|
|
222
|
+
* absent directory — `facts()`' own docstring rules it, "AN ABSENT DIRECTORY IS `[]`, NOT AN
|
|
223
|
+
* ERROR" — which is what makes a lazily-built project layer legal at all.
|
|
224
|
+
*
|
|
225
|
+
* THE PREDICATE IS THE OUTCOME AND NEVER AN ERRNO, and that is measured rather than tidy.
|
|
226
|
+
* One cwd, `/`, gives CPython EROFS(30) at `/.bantamkit` and Node ENOENT(-2) at
|
|
227
|
+
* `/.bantamkit/memory/facts`, because Node's recursive mkdir does not pass EROFS through and
|
|
228
|
+
* stats the missing path instead — re-measured here at cwd `/`,
|
|
229
|
+
* `mkdirSync('/.bantamkit', {recursive: true})` is ENOENT(-2) where `mkdirSync('/.bantamkit')`
|
|
230
|
+
* is EROFS(-30); a `chmod 555` directory gives EACCES(13) on both; and on Linux `/` is a
|
|
231
|
+
* writable root owned by root, so the same cwd gives EACCES there. Three platforms, three
|
|
232
|
+
* numbers, one fact — the directory could not be made — so the `catch` is the whole of
|
|
233
|
+
* `PyOSError`, which is this runtime's `OSError`, and the sentence carries no errno. A fix
|
|
234
|
+
* keyed on the number would have been green on Linux CI and wrong on the machine the bug was
|
|
235
|
+
* reported from, and on THIS side it would additionally have been keyed on a number libuv
|
|
236
|
+
* invented rather than on the one the filesystem returned.
|
|
237
|
+
*
|
|
238
|
+
* THE SENTENCE NAMES `this.root` AND NOTHING DEEPER, for the same reason. The two runtimes
|
|
239
|
+
* fail at different components of the same path (`/.bantamkit` against
|
|
240
|
+
* `/.bantamkit/memory/facts`) and report different strerrors for it, so a sentence carrying
|
|
241
|
+
* the failing leaf, or that strerror, would be a divergence manufactured by mkdir's
|
|
242
|
+
* internals. `this.root` is the path the caller named and both sides agree on it, which is
|
|
243
|
+
* what lets one conformance case pin this line byte for byte.
|
|
244
|
+
*/
|
|
216
245
|
private ensureDirs;
|
|
217
246
|
/**
|
|
218
247
|
* Write a fact, or refuse it as a near-duplicate of one already here.
|
package/dist/memory/store.js
CHANGED
|
@@ -505,9 +505,46 @@ export class MemoryStore {
|
|
|
505
505
|
if (options.create ?? true)
|
|
506
506
|
this.ensureDirs();
|
|
507
507
|
}
|
|
508
|
+
/**
|
|
509
|
+
* Bring the store's directories into existence, or refuse the write that needs them.
|
|
510
|
+
*
|
|
511
|
+
* THE PROPERTY: a store that cannot be brought into existence cannot be written to, and the
|
|
512
|
+
* refusal names which directory and why. It is a refusal and not a crash because the only
|
|
513
|
+
* callers are the two WRITES (`save`, `compact`); every read is already defined over an
|
|
514
|
+
* absent directory — `facts()`' own docstring rules it, "AN ABSENT DIRECTORY IS `[]`, NOT AN
|
|
515
|
+
* ERROR" — which is what makes a lazily-built project layer legal at all.
|
|
516
|
+
*
|
|
517
|
+
* THE PREDICATE IS THE OUTCOME AND NEVER AN ERRNO, and that is measured rather than tidy.
|
|
518
|
+
* One cwd, `/`, gives CPython EROFS(30) at `/.bantamkit` and Node ENOENT(-2) at
|
|
519
|
+
* `/.bantamkit/memory/facts`, because Node's recursive mkdir does not pass EROFS through and
|
|
520
|
+
* stats the missing path instead — re-measured here at cwd `/`,
|
|
521
|
+
* `mkdirSync('/.bantamkit', {recursive: true})` is ENOENT(-2) where `mkdirSync('/.bantamkit')`
|
|
522
|
+
* is EROFS(-30); a `chmod 555` directory gives EACCES(13) on both; and on Linux `/` is a
|
|
523
|
+
* writable root owned by root, so the same cwd gives EACCES there. Three platforms, three
|
|
524
|
+
* numbers, one fact — the directory could not be made — so the `catch` is the whole of
|
|
525
|
+
* `PyOSError`, which is this runtime's `OSError`, and the sentence carries no errno. A fix
|
|
526
|
+
* keyed on the number would have been green on Linux CI and wrong on the machine the bug was
|
|
527
|
+
* reported from, and on THIS side it would additionally have been keyed on a number libuv
|
|
528
|
+
* invented rather than on the one the filesystem returned.
|
|
529
|
+
*
|
|
530
|
+
* THE SENTENCE NAMES `this.root` AND NOTHING DEEPER, for the same reason. The two runtimes
|
|
531
|
+
* fail at different components of the same path (`/.bantamkit` against
|
|
532
|
+
* `/.bantamkit/memory/facts`) and report different strerrors for it, so a sentence carrying
|
|
533
|
+
* the failing leaf, or that strerror, would be a divergence manufactured by mkdir's
|
|
534
|
+
* internals. `this.root` is the path the caller named and both sides agree on it, which is
|
|
535
|
+
* what lets one conformance case pin this line byte for byte.
|
|
536
|
+
*/
|
|
508
537
|
ensureDirs() {
|
|
509
|
-
|
|
510
|
-
|
|
538
|
+
try {
|
|
539
|
+
pyMkdirParents(pyJoin(this.root, 'facts'));
|
|
540
|
+
pyMkdirParents(pyJoin(this.root, 'archive'));
|
|
541
|
+
}
|
|
542
|
+
catch (e) {
|
|
543
|
+
if (!(e instanceof PyOSError))
|
|
544
|
+
throw e;
|
|
545
|
+
throw new MemoryValidationError(`memory store could not be created: ${this.root}; the directory is not` +
|
|
546
|
+
' there and this filesystem would not make it, so nothing was written');
|
|
547
|
+
}
|
|
511
548
|
}
|
|
512
549
|
// ---- ops ----
|
|
513
550
|
/**
|
|
@@ -742,6 +779,14 @@ export class MemoryStore {
|
|
|
742
779
|
*/
|
|
743
780
|
compact(reserve = null) {
|
|
744
781
|
const facts = this.facts();
|
|
782
|
+
// SECOND, NOT FIRST, and the order is the reference's own ruling kept intact: the listing
|
|
783
|
+
// still refuses an unreadable store before anything here moves a file. What this line adds
|
|
784
|
+
// is the other half of the same stance — this method WRITES (every `pyReplace` below lands
|
|
785
|
+
// in `archive/`, and `rebuildIndex` writes `index.md` into the root whether or not a single
|
|
786
|
+
// fact was archived), so a root that does not exist and cannot be made is refused here, by
|
|
787
|
+
// name, instead of surfacing as a `FileNotFoundError` out of the final write. `exist_ok`
|
|
788
|
+
// semantics make it free for every store that is already there.
|
|
789
|
+
this.ensureDirs();
|
|
745
790
|
const sizes = new Map();
|
|
746
791
|
for (const fact of facts) {
|
|
747
792
|
sizes.set(pyHashKey(fact.name), Buffer.byteLength(this.indexLine(fact), 'utf8'));
|