@henols/vice-mcp 0.2.3 → 0.2.5
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/anno-cli.ts +156 -158
- package/anno-confidence.ts +2 -2
- package/anno-derive.ts +6 -6
- package/anno-details.ts +4 -4
- package/anno-export-asm.ts +100 -101
- package/anno-graphics.ts +16 -16
- package/anno-hazard-report.ts +2 -2
- package/anno-import.ts +15 -15
- package/anno-index.ts +8 -8
- package/anno-join.ts +35 -35
- package/anno-memmap-render.ts +22 -21
- package/anno-provenance-ledger.ts +4 -4
- package/anno-regbits-gen.ts +13 -13
- package/anno-store-export.ts +11 -11
- package/anno-store.ts +145 -165
- package/anno-symbols.ts +7 -7
- package/anno-types.ts +55 -55
- package/capture-predicate.ts +2 -6
- package/evid-ingest.ts +1 -3
- package/memmap-lookup.ts +1 -1
- package/package.json +1 -1
- package/resources/broker-control.mjs +85 -92
- package/resources/broker-epoch.mjs +6 -7
- package/resources/broker-kill.mjs +29 -30
- package/resources/broker-launch.mjs +352 -370
- package/resources/broker-state.mjs +9 -10
- package/resources/host-tool.mjs +636 -664
- package/resources/vice-broker.mjs +189 -191
- package/stock-derived.ts +1 -0
- package/stock-dispatch.ts +5 -1
- package/text-protocol.ts +295 -25
- package/text-tools.ts +106 -1
- package/tools-manifest.stock.json +63 -0
- package/vice-broker-client.ts +98 -100
package/anno-store.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
// The ONE module in this repo that names `node:sqlite`. Nothing else may open,
|
|
5
5
|
// query or write an annotation store file; every other module reaches the
|
|
6
|
-
// store through the functions below
|
|
6
|
+
// store through the functions below.
|
|
7
7
|
//
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
9
9
|
// WHY THIS FILE EXISTS
|
|
@@ -12,9 +12,7 @@
|
|
|
12
12
|
// surface can change under a patch release, and it emits an
|
|
13
13
|
// `ExperimentalWarning` on first load. A dependency with that profile earns a
|
|
14
14
|
// blast radius of exactly one file -- and, more to the point, a CONFINEMENT
|
|
15
|
-
// THAT IS ASSERTED rather than promised.
|
|
16
|
-
// module set and fails if any second module names the specifier, through any of
|
|
17
|
-
// its four working access routes.
|
|
15
|
+
// THAT IS ASSERTED rather than promised.
|
|
18
16
|
//
|
|
19
17
|
// Three measured facts shaped the code below, and each one is here because the
|
|
20
18
|
// obvious reading of SQLite's behaviour is wrong:
|
|
@@ -54,7 +52,7 @@
|
|
|
54
52
|
// module is not yet reachable from the published entry point's import closure,
|
|
55
53
|
// and `scripts/check-npm-packages.mjs` asserts only one direction -- every
|
|
56
54
|
// REACHABLE module must be listed -- never the converse. The real reason to
|
|
57
|
-
// list it:
|
|
55
|
+
// list it: the shipped-module assertion scans `shippedTsModules()`, which is derived
|
|
58
56
|
// from `files[]`, so an unlisted module makes that assertion VACUOUS. It would
|
|
59
57
|
// pass by scanning a set this file is not in. Copying the reachability sentence
|
|
60
58
|
// here would plant a false claim in a brand-new seam header, which is the
|
|
@@ -67,8 +65,6 @@
|
|
|
67
65
|
// methods on `DatabaseSync.prototype`, and not through the constructor
|
|
68
66
|
// option that permits them. Both exist, and either one turns this
|
|
69
67
|
// module's caller-supplied FILE ARGUMENT into arbitrary code loading.
|
|
70
|
-
// `anno-seam.test.ts` asserts all three names are absent from this
|
|
71
|
-
// module's code.
|
|
72
68
|
// 2. NEVER write a double-quoted SQL string literal. `node:sqlite` disables
|
|
73
69
|
// the double-quoted-string misfeature by default, so
|
|
74
70
|
// `insert into t values ("a")` throws `no such column: "a"` rather than
|
|
@@ -85,7 +81,7 @@
|
|
|
85
81
|
// `LIKE 'prefix%'` is 2.02 ms against FTS5 `MATCH`'s 2.99 ms, with a
|
|
86
82
|
// 121.8 ms index rebuild, over 20,000 rows. Adding FTS5 later is
|
|
87
83
|
// ADDITIVE; removing it is a schema migration. The search surface belongs
|
|
88
|
-
//
|
|
84
|
+
// elsewhere and this module must simply not foreclose it.
|
|
89
85
|
// 6. NEVER add an explicit save or flush verb. Durability is this module's
|
|
90
86
|
// responsibility, not the caller's: every accepted write commits before it
|
|
91
87
|
// returns. A save verb is a way for a caller to lose data by forgetting.
|
|
@@ -103,9 +99,9 @@
|
|
|
103
99
|
// look like tightening and are the opposite. A REFUSAL would push a caller
|
|
104
100
|
// toward deleting the comment to get the retype through, converting a
|
|
105
101
|
// reported loss into a silent one -- the exact outcome the report exists to
|
|
106
|
-
// prevent
|
|
102
|
+
// prevent. A WIDENED rule would fire on every retype of a
|
|
107
103
|
// commented range, and a report that fires every time is a report nobody
|
|
108
|
-
// reads, so the one case that matters stops being noticed
|
|
104
|
+
// reads, so the one case that matters stops being noticed.
|
|
109
105
|
// 10. NEVER prune the snapshot ring INSIDE the write transaction, and never
|
|
110
106
|
// let a revert fall back to the nearest retained revision. A filesystem
|
|
111
107
|
// unlink is not part of the transaction, so pruning inside it means a
|
|
@@ -125,7 +121,7 @@
|
|
|
125
121
|
// conclusion. And a revert that SUBSTITUTES the nearest
|
|
126
122
|
// retained revision for the one asked for changes the caller's intent with
|
|
127
123
|
// nothing recording that it happened, so a revert past the bound is
|
|
128
|
-
// refused BY NAME instead
|
|
124
|
+
// refused BY NAME instead.
|
|
129
125
|
import { randomUUID } from "node:crypto";
|
|
130
126
|
import { closeSync, copyFileSync, existsSync, fsyncSync, mkdirSync, openSync, readdirSync, renameSync, rmSync } from "node:fs";
|
|
131
127
|
import { basename, dirname, join, resolve } from "node:path";
|
|
@@ -216,8 +212,8 @@ export interface AnnoWriteResult {
|
|
|
216
212
|
* the very first write. The SECOND half became false: `anno_snapshot` carried a
|
|
217
213
|
* `path text not null` column holding the snapshot's ABSOLUTE location, and two
|
|
218
214
|
* destructive consequences were reproduced against committed code -- two stores
|
|
219
|
-
* in one directory sharing one ring
|
|
220
|
-
* write destroying the whole revert history
|
|
215
|
+
* in one directory sharing one ring and a directory rename plus one
|
|
216
|
+
* write destroying the whole revert history. The column is DROPPED at
|
|
221
217
|
* `SCHEMA_VERSION` 2 and the location is computed from the handle by
|
|
222
218
|
* `snapshotDirFor()` at every read and every delete, so there is no persisted
|
|
223
219
|
* absolute string left for a second namespace -- a bind mount seen from the
|
|
@@ -230,7 +226,7 @@ export interface AnnoWriteResult {
|
|
|
230
226
|
* was byte-identical to version 1's.
|
|
231
227
|
*
|
|
232
228
|
* THAT SENTENCE IS KEPT AND SCOPED RATHER THAN DELETED, because at
|
|
233
|
-
* `SCHEMA_VERSION` 3 it stopped being the whole truth:
|
|
229
|
+
* `SCHEMA_VERSION` 3 it stopped being the whole truth: version 3 (2026-08-29) ADDS
|
|
234
230
|
* one table, `anno_enum_usage`, and its index. It changes no existing table's
|
|
235
231
|
* column list, so the scoped claim above still holds of every table version 2
|
|
236
232
|
* had. The version 3 table associates ONE address with ONE `anno_enum` row by
|
|
@@ -239,7 +235,7 @@ export interface AnnoWriteResult {
|
|
|
239
235
|
* the day that cost was accepted.
|
|
240
236
|
*
|
|
241
237
|
* `anno_xref` and its `access_kind` column exist from the very first write.
|
|
242
|
-
* Two requirement texts look like they conflict here and do not:
|
|
238
|
+
* Two requirement texts look like they conflict here and do not: one
|
|
243
239
|
* requires the column, while the cross-reference criterion forbids CACHING a
|
|
244
240
|
* DERIVED cross-reference on disk. Both hold at once -- the table exists, and
|
|
245
241
|
* only non-derivable references (hand-asserted, or resolved from something
|
|
@@ -252,7 +248,7 @@ export interface AnnoWriteResult {
|
|
|
252
248
|
* mapper in `listRanges()`. It is reserved, and every row written today has it
|
|
253
249
|
* null.
|
|
254
250
|
*
|
|
255
|
-
* AT `SCHEMA_VERSION` 4
|
|
251
|
+
* AT `SCHEMA_VERSION` 4, ONE MORE TABLE IS ADDED:
|
|
256
252
|
* `anno_evid_exec`, the durable runtime-execution evidence table. See
|
|
257
253
|
* `anno-types.ts`'s `SCHEMA_VERSION` doc comment for what the bump buys and
|
|
258
254
|
* the decided, dated fate of an existing version-3 store (`reaffirm-refusal`
|
|
@@ -260,11 +256,11 @@ export interface AnnoWriteResult {
|
|
|
260
256
|
* nullable column at all: unlike the annotation tables above, every field on
|
|
261
257
|
* a row here is a fact the runtime evidence layer is licensed to assert, or
|
|
262
258
|
* the row does not exist. Its run-identity key is the bare triple
|
|
263
|
-
* `(image_sha256, argv_digest, seed)`, selected by
|
|
264
|
-
*
|
|
265
|
-
* `
|
|
259
|
+
* `(image_sha256, argv_digest, seed)`, selected by a live A/B that measured
|
|
260
|
+
* `no-perturbation` from instrumentation -- there is deliberately no
|
|
261
|
+
* `run_class` column.
|
|
266
262
|
*
|
|
267
|
-
* AT `SCHEMA_VERSION` 5
|
|
263
|
+
* AT `SCHEMA_VERSION` 5, ONE MORE TABLE IS ADDED:
|
|
268
264
|
* `anno_excluded_range`, the durable record of a user-requested exclusion --
|
|
269
265
|
* its extent and the reason the user gave. See `anno-types.ts`'s
|
|
270
266
|
* `SCHEMA_VERSION` doc comment for what the bump buys, why a table was chosen
|
|
@@ -378,7 +374,7 @@ export interface AnnoStoreHandle {
|
|
|
378
374
|
/**
|
|
379
375
|
* THIS CONNECTION'S TRANSACTION STATE IS UNKNOWN: a housekeeping sweep run on
|
|
380
376
|
* it reported that its own `rollback` threw, so it may still hold an open
|
|
381
|
-
* transaction and the store's write lock
|
|
377
|
+
* transaction and the store's write lock.
|
|
382
378
|
*
|
|
383
379
|
* THE REMEDY IS THE ONE THE COMMIT HANDLER ALREADY PRINTS, in the same words:
|
|
384
380
|
* CLOSE IT AND REOPEN rather than reusing it. Node 22's `DatabaseSync` exposes
|
|
@@ -393,7 +389,7 @@ export interface AnnoStoreHandle {
|
|
|
393
389
|
* what prohibition 28-11 P5 forbids, and would send a caller to retry an
|
|
394
390
|
* additive verb. So the accepted write returns its revision unchanged and it
|
|
395
391
|
* is the NEXT call on this connection that refuses BY NAME -- which is what
|
|
396
|
-
* turns
|
|
392
|
+
* turns the bare `cannot start a transaction within a transaction` error into a
|
|
397
393
|
* diagnosis.
|
|
398
394
|
*
|
|
399
395
|
* `false` on every freshly opened handle, set in `openStore` at the one place
|
|
@@ -440,15 +436,14 @@ function fsyncPath(path: string): void {
|
|
|
440
436
|
*
|
|
441
437
|
* A `workspaceRoot` IS REQUIRED unless the caller explicitly asks for the
|
|
442
438
|
* unconfined path with `unconfinedModuleDerivedPath: true`, and the inversion is
|
|
443
|
-
* deliberate
|
|
439
|
+
* deliberate. Confinement used to be opt-IN, which made the mitigation
|
|
444
440
|
* for the one unvalidated input this module's own header calls out the one a
|
|
445
441
|
* caller could forget -- and two of this store's recorded blockers were confinement
|
|
446
442
|
* escapes. The escape exists for exactly one shape: a path THIS MODULE derived
|
|
447
443
|
* itself (a snapshot image path, a staging path, or the live store path
|
|
448
444
|
* `revertTo` already resolved), where there is no caller argument left to
|
|
449
445
|
* confine. Every such site below carries a one-line comment naming the
|
|
450
|
-
* module-derived value that produced its path
|
|
451
|
-
* that no other shipped module names the option at all.
|
|
446
|
+
* module-derived value that produced its path.
|
|
452
447
|
*
|
|
453
448
|
* When `workspaceRoot` is supplied the path is confined to it first. The
|
|
454
449
|
* fresh-versus-existing decision is made with `existsSync` BEFORE the
|
|
@@ -486,12 +481,12 @@ export function openStore(
|
|
|
486
481
|
path: string,
|
|
487
482
|
opts: { workspaceRoot?: string; mustExist?: boolean; unconfinedModuleDerivedPath?: boolean } = {},
|
|
488
483
|
): AnnoStoreHandle {
|
|
489
|
-
// CONFINEMENT IS THE DEFAULT, AND THE ESCAPE IS A WORD A GREP CAN FIND
|
|
490
|
-
//
|
|
484
|
+
// CONFINEMENT IS THE DEFAULT, AND THE ESCAPE IS A WORD A GREP CAN FIND.
|
|
485
|
+
// `anno-types.ts`'s header names the three things nothing upstream
|
|
491
486
|
// validates -- "an address of 65536, a misspelled data type, and a store path
|
|
492
487
|
// pointing outside the workspace all look identical to the transport" -- and
|
|
493
488
|
// this was the only one of the three whose mitigation a caller could simply
|
|
494
|
-
// forget. Two of this
|
|
489
|
+
// forget. Two of this project's own review findings were confinement
|
|
495
490
|
// escapes.
|
|
496
491
|
//
|
|
497
492
|
// REFUSED BEFORE THE PATH IS RESOLVED AND LONG BEFORE `new DatabaseSync`, for
|
|
@@ -592,8 +587,7 @@ export function openStore(
|
|
|
592
587
|
|
|
593
588
|
if (meta.schema_version !== SCHEMA_VERSION) {
|
|
594
589
|
db.close();
|
|
595
|
-
// NAMES THE REMEDY AND DENIES NOTHING IS LOST
|
|
596
|
-
// condition 2). This build refuses rather than upgrades -- see
|
|
590
|
+
// NAMES THE REMEDY AND DENIES NOTHING IS LOST. This build refuses rather than upgrades -- see
|
|
597
591
|
// `anno-types.ts`'s `SCHEMA_VERSION` doc comment for the decided,
|
|
598
592
|
// dated reason -- and the refusal happens BEFORE any write, so the
|
|
599
593
|
// file on disk is exactly what it was a moment ago: its labels,
|
|
@@ -607,7 +601,7 @@ export function openStore(
|
|
|
607
601
|
);
|
|
608
602
|
}
|
|
609
603
|
|
|
610
|
-
//
|
|
604
|
+
// THE LAST KNOWN FAMILY ESCAPE IN THIS FUNCTION. The two blocks either
|
|
611
605
|
// side of this one are already wrapped, and for the same two reasons: an
|
|
612
606
|
// unwrapped failure here leaks the CONNECTION as well as escaping the
|
|
613
607
|
// `ViceError` family, so the caller loses the file handle with no way to
|
|
@@ -648,14 +642,14 @@ export function currentRevision(handle: AnnoStoreHandle): number {
|
|
|
648
642
|
* The suffix appended to the store FILENAME to name its snapshot ring
|
|
649
643
|
* directory. Appended to the FILENAME rather than being a fixed directory name
|
|
650
644
|
* (`<dir>/snapshots`, which is what this was), and the distinction is the whole
|
|
651
|
-
* of
|
|
645
|
+
* of the fix: two distinct store files in one directory have distinct
|
|
652
646
|
* basenames by definition of a filesystem, so distinct basenames give distinct
|
|
653
647
|
* rings BY CONSTRUCTION rather than by an ownership predicate layered over a
|
|
654
648
|
* shared location.
|
|
655
649
|
*
|
|
656
650
|
* THE PREDICATE ROUTE WAS ALREADY TRIED AND COULD NOT SEE THE DEFECT. Plan
|
|
657
651
|
* 28-07 added a per-revision ownership check over the shared `<dir>/snapshots`
|
|
658
|
-
* ring; it was structurally blind to
|
|
652
|
+
* ring; it was structurally blind to the collision because revision numbers are not
|
|
659
653
|
* unique ACROSS stores -- two stores in one directory both write `r1.db`, and
|
|
660
654
|
* every per-revision predicate says "yes, revision 1 is mine" to both of them.
|
|
661
655
|
* A location that cannot collide has no such blind spot to test for.
|
|
@@ -669,12 +663,12 @@ const SNAPSHOT_DIR_SUFFIX = ".snapshots";
|
|
|
669
663
|
* `<dir>/proj.annostore.snapshots`.
|
|
670
664
|
*
|
|
671
665
|
* THE RESIDUAL, STATED RATHER THAN CLAIMED CLOSED -- AND RESTATED AFTER THIS
|
|
672
|
-
* PARAGRAPH'S EARLIER VERSION WAS FALSIFIED BY DRIVING THE CODE
|
|
666
|
+
* PARAGRAPH'S EARLIER VERSION WAS FALSIFIED BY DRIVING THE CODE. What
|
|
673
667
|
* it got RIGHT and keeps: the location is a pure function of the handle, the
|
|
674
668
|
* sweep only ever reads `snapshotDirFor(handle)` so it cannot see a ring it
|
|
675
669
|
* does not name, and renaming the containing DIRECTORY is not a residual at all
|
|
676
|
-
* -- the ring moves with the directory, so nothing is lost
|
|
677
|
-
* rename test
|
|
670
|
+
* -- the ring moves with the directory, so nothing is lost, confirmed by a
|
|
671
|
+
* directory-rename test. What became FALSE: it claimed the old ring was never deleted at
|
|
678
672
|
* all and that `retainedRevisions()` reporting an empty list was therefore a
|
|
679
673
|
* truthful under-claim. That was true of the FILES and false of the ROWS -- so
|
|
680
674
|
* the claim is not repeated here even to disown it, because the next reader
|
|
@@ -687,7 +681,7 @@ const SNAPSHOT_DIR_SUFFIX = ".snapshots";
|
|
|
687
681
|
* a SYMLINK ALIAS, or a store-file rename (`mv proj.annostore
|
|
688
682
|
* other.annostore`) -- names a DIFFERENT ring, so a handle opened under it
|
|
689
683
|
* publishes into a SECOND ring. The first ring's files are never deleted, and
|
|
690
|
-
* since
|
|
684
|
+
* since the fix above its pointer rows are never deleted BY THE SWEEP -- but
|
|
691
685
|
* `pruneSnapshots`' doomed loop still deletes every row below
|
|
692
686
|
* `currentRevision() - MAX_SNAPSHOT_REVISIONS`, so restoring the original name
|
|
693
687
|
* restores the floor ONLY while the wrong-spelling handle has not advanced past
|
|
@@ -737,15 +731,12 @@ export const NO_RETAINED_REVISION = -1;
|
|
|
737
731
|
* reconciliation below derives from a filename alone.
|
|
738
732
|
*
|
|
739
733
|
* ANCHORED ON PURPOSE, and the anchoring is load-bearing rather than tidy:
|
|
740
|
-
*
|
|
741
|
-
* a different suffix, and a sweep that matched them would delete another
|
|
734
|
+
* this module's own staging mechanism writes per-attempt STAGING files in this
|
|
735
|
+
* same directory under a different suffix, and a sweep that matched them would delete another
|
|
742
736
|
* writer's in-flight snapshot -- the exact loss this reconciliation exists to
|
|
743
737
|
* prevent, committed by the repair itself.
|
|
744
738
|
*
|
|
745
|
-
* A frozen `RegExp` literal is NOT module-level mutable state
|
|
746
|
-
* `anno-seam.test.ts` matches `new Map|Set|WeakMap|WeakSet` and array/object
|
|
747
|
-
* initialisers, so this constant sits outside it by construction rather than
|
|
748
|
-
* by exemption.
|
|
739
|
+
* A frozen `RegExp` literal is NOT module-level mutable state.
|
|
749
740
|
*/
|
|
750
741
|
const SNAPSHOT_FILE_PATTERN = /^r(\d+)\.db$/;
|
|
751
742
|
|
|
@@ -757,7 +748,7 @@ const SNAPSHOT_FILE_PATTERN = /^r(\d+)\.db$/;
|
|
|
757
748
|
*
|
|
758
749
|
* IT REPLACED A PRESENCE TEST AT BOTH OF THE TWO SITES THAT CARRIED ONE -- the
|
|
759
750
|
* filter inside `retainedRevisions` and `revertTo`'s step-2 gate -- and the
|
|
760
|
-
* promotion is the whole of
|
|
751
|
+
* promotion is the whole of the supporting half of this fix. Presence was never a
|
|
761
752
|
* witness that a file is a store: this module's FIRST MEASURED FACT (header,
|
|
762
753
|
* `:22-31`) is that a ZERO-LENGTH FILE OPENS as a SQLite database and reports
|
|
763
754
|
* `integrity_check ok`. So the store advertised a revision whose image was not a
|
|
@@ -854,7 +845,7 @@ function claimedRevisions(handle: AnnoStoreHandle): number[] {
|
|
|
854
845
|
* one file are two things that can disagree. They did, twice, both reproduced:
|
|
855
846
|
* a directory rename invalidated every persisted path at once, after which this
|
|
856
847
|
* function reported NO retained revisions while the files sat there on disk, and
|
|
857
|
-
* the next write's prune destroyed them
|
|
848
|
+
* the next write's prune destroyed them. The same shape covers every
|
|
858
849
|
* adjacent case rather than just that one repro -- a bind mount seen from two
|
|
859
850
|
* namespaces (this repo's entire architecture is built around that boundary), a
|
|
860
851
|
* symlinked ancestor, a container/host path pair, a case-insensitive filesystem,
|
|
@@ -880,8 +871,8 @@ function claimedRevisions(handle: AnnoStoreHandle): number[] {
|
|
|
880
871
|
* `begin immediate` would have opened up to 32 databases with the store's write
|
|
881
872
|
* lock held. Every remaining consumer reads this function rather than deciding
|
|
882
873
|
* for itself what "retained" means: three independent decisions is precisely how
|
|
883
|
-
* the three answers came to disagree
|
|
884
|
-
* a fourth would also hide the row-only regression from the proofs that exist to
|
|
874
|
+
* the three answers came to disagree -- that was the gap between two of them --
|
|
875
|
+
* and a fourth would also hide the row-only regression from the proofs that exist to
|
|
885
876
|
* catch it.
|
|
886
877
|
*/
|
|
887
878
|
export function retainedRevisions(handle: AnnoStoreHandle): number[] {
|
|
@@ -907,7 +898,7 @@ export function retainedRevisions(handle: AnnoStoreHandle): number[] {
|
|
|
907
898
|
* the first element of `retainedRevisions()` -- and that reading was then one
|
|
908
899
|
* step short a SECOND time, in the same direction: an existence check published
|
|
909
900
|
* `0` on a store whose `r0.db` was present but was not a database, and
|
|
910
|
-
* following THAT floor destroyed the live store
|
|
901
|
+
* following THAT floor destroyed the live store. The floor now requires
|
|
911
902
|
* the image to OPEN, not merely to exist, so the store still cannot publish a
|
|
912
903
|
* number it will then refuse -- in either direction.
|
|
913
904
|
*/
|
|
@@ -925,8 +916,8 @@ export function oldestRetainedRevision(handle: AnnoStoreHandle): number {
|
|
|
925
916
|
*
|
|
926
917
|
* * AN ORPHAN ROW (a pointer row whose file is gone) IS NO LONGER SWEPT AT
|
|
927
918
|
* ALL, and the reversal is recorded here rather than left to be inferred
|
|
928
|
-
* from an absence. This function used to delete every such row.
|
|
929
|
-
*
|
|
919
|
+
* from an absence. This function used to delete every such row. A live
|
|
920
|
+
* reproduction showed, twice, what that costs: the ring is named from
|
|
930
921
|
* `basename(handle.path)` -- a PATH SPELLING -- so a SYMLINK ALIAS of the
|
|
931
922
|
* store file, or a store-file rename (`mv proj.annostore
|
|
932
923
|
* other.annostore`), makes `retainedRevisions()` report every EXISTING
|
|
@@ -954,11 +945,11 @@ export function oldestRetainedRevision(handle: AnnoStoreHandle): number {
|
|
|
954
945
|
* ring. That is no longer true: the keep-set below reads
|
|
955
946
|
* `claimedRevisions`, so this sweep now looks at rows the advertisement
|
|
956
947
|
* ignores. THE NEW BASIS IS BETTER RATHER THAN WEAKER, and it is the
|
|
957
|
-
* conclusion
|
|
948
|
+
* conclusion that promotion forced: a row the sweep KEEPS is precisely what makes a
|
|
958
949
|
* corrupt image survive on disk as EVIDENCE instead of being unlinked. A
|
|
959
950
|
* sweep that deleted the image of a failure would be destroying the only
|
|
960
951
|
* record of the failure that has to be diagnosed -- a second destruction
|
|
961
|
-
* dressed as a repair.
|
|
952
|
+
* dressed as a repair. That conclusion is unchanged: the row direction
|
|
962
953
|
* stays abandoned, for the ownership reason above.
|
|
963
954
|
*
|
|
964
955
|
* THE KEEP-SET QUERY RUNS ON THE CONNECTION ALREADY IN HAND, and that is
|
|
@@ -1002,7 +993,7 @@ export function oldestRetainedRevision(handle: AnnoStoreHandle): number {
|
|
|
1002
993
|
* over a ring with no half-states). It must NOT run from `openStore`:
|
|
1003
994
|
* `anno-durability.test.ts:291-347` asserts that an orphan snapshot file left
|
|
1004
995
|
* in the kill window SURVIVES a reopen and is identified by its revision, and
|
|
1005
|
-
* that is a verified truth
|
|
996
|
+
* that is a verified truth -- merely LOOKING at a store must not
|
|
1006
997
|
* change it, and the orphan a kill window leaves is deliberately the harmless
|
|
1007
998
|
* direction. Reconciling on open would redden that test, and rightly. BOTH
|
|
1008
999
|
* SITES ARE OUTSIDE ANY OPEN TRANSACTION, which is now a REQUIREMENT rather
|
|
@@ -1010,7 +1001,7 @@ export function oldestRetainedRevision(handle: AnnoStoreHandle): number {
|
|
|
1010
1001
|
* own, so calling it from inside one is not supported.
|
|
1011
1002
|
*
|
|
1012
1003
|
* IT NOW TAKES THE STORE'S WRITE LOCK BEFORE IT DECIDES ANYTHING, and the
|
|
1013
|
-
* reason is a reproduced defect
|
|
1004
|
+
* reason is a reproduced defect rather than caution. A snapshot becomes
|
|
1014
1005
|
* a FILESYSTEM fact (the `renameSync` inside `publishSnapshot`) before it
|
|
1015
1006
|
* becomes a TRANSACTIONAL one (the pointer-row insert), so a sweep reading only
|
|
1016
1007
|
* its own committed view sees a live writer's published file as unowned and
|
|
@@ -1065,7 +1056,7 @@ export function oldestRetainedRevision(handle: AnnoStoreHandle): number {
|
|
|
1065
1056
|
* NO SECOND DISCRIMINATOR WAS ADDED FOR `deferred`'s TWO CAUSES, and the reason
|
|
1066
1057
|
* is not economy. Its only consumer, `pruneSnapshots`, returns early
|
|
1067
1058
|
* identically in both cases, so a discriminator would have no reader -- and
|
|
1068
|
-
*
|
|
1059
|
+
* the actual complaint here, that a LEAKED transaction makes every later sweep
|
|
1069
1060
|
* report `deferred` indistinguishably from contention, is removed AT ITS SOURCE
|
|
1070
1061
|
* by the handler rather than papered over with a label. A field describing a
|
|
1071
1062
|
* state this code can no longer reach would be exactly the kind of comment
|
|
@@ -1077,7 +1068,7 @@ export function oldestRetainedRevision(handle: AnnoStoreHandle): number {
|
|
|
1077
1068
|
* for up to the connection's five-second `busy_timeout` before it proceeds --
|
|
1078
1069
|
* at BOTH of the two call sites: every accepted write (through `pruneSnapshots`
|
|
1079
1070
|
* at `runWriteSequence` step 9) and every `revertTo` (through its own step-6
|
|
1080
|
-
* sweep on the restored handle).
|
|
1071
|
+
* sweep on the restored handle). Both sit on an MCP tool path. Each is
|
|
1081
1072
|
* bounded at ONE timeout and not two, because `pruneSnapshots` returns early
|
|
1082
1073
|
* when this function reports `deferred` rather than running its own autocommit
|
|
1083
1074
|
* deletes into the same contention. An honest cost stated at the seam is worth
|
|
@@ -1089,8 +1080,8 @@ export function oldestRetainedRevision(handle: AnnoStoreHandle): number {
|
|
|
1089
1080
|
* left (see the ORPHAN ROW bullet above), so what the ordering now guarantees is
|
|
1090
1081
|
* narrower and is stated narrowly: an interruption between the commit and the
|
|
1091
1082
|
* unlinks leaves extra FILES, never a pointer row aimed at a deleted file.
|
|
1092
|
-
* Pinned by a source-order control in `anno-store.test.ts`, which since
|
|
1093
|
-
* asserts the ABSENCE of any pointer-row delete in this body as well as the
|
|
1083
|
+
* Pinned by a source-order control in `anno-store.test.ts`, which since the fix
|
|
1084
|
+
* above asserts the ABSENCE of any pointer-row delete in this body as well as the
|
|
1094
1085
|
* surviving commit-before-unlink order -- a presence assertion cannot see
|
|
1095
1086
|
* either.
|
|
1096
1087
|
*/
|
|
@@ -1119,12 +1110,12 @@ export function reconcileSnapshotRing(handle: AnnoStoreHandle): { droppedFiles:
|
|
|
1119
1110
|
const orphanFiles: string[] = [];
|
|
1120
1111
|
|
|
1121
1112
|
// EVERYTHING FROM HERE TO THE COMMIT IS BRACKETED, AND THE BRACKET IS THE
|
|
1122
|
-
// FIX
|
|
1113
|
+
// FIX. `begin immediate` above has already opened a transaction on
|
|
1123
1114
|
// the CALLER's connection. Before this handler existed, any throw between
|
|
1124
1115
|
// that statement and the commit -- `readdirSync` on a ring directory that
|
|
1125
1116
|
// became unreadable, a failure of the keep-set `select`, anything --
|
|
1126
1117
|
// propagated out with the transaction still
|
|
1127
|
-
// OPEN. Step 9's
|
|
1118
|
+
// OPEN. Step 9's own error-swallowing wrap then swallowed it, so an ordinary `setDataType`
|
|
1128
1119
|
// reported SUCCESS while leaving the handle permanently inside a transaction:
|
|
1129
1120
|
// every later write failed with "cannot start a transaction within a
|
|
1130
1121
|
// transaction", and every later sweep reported `deferred` indistinguishably
|
|
@@ -1132,8 +1123,8 @@ export function reconcileSnapshotRing(handle: AnnoStoreHandle): { droppedFiles:
|
|
|
1132
1123
|
// path-dependent: there is no route out of this block that does not either
|
|
1133
1124
|
// commit or roll back.
|
|
1134
1125
|
try {
|
|
1135
|
-
// STEP 2. With the lock held, compute the drop set -- which since
|
|
1136
|
-
// exactly ONE direction, the FILE direction -- before changing anything.
|
|
1126
|
+
// STEP 2. With the lock held, compute the drop set -- which since the fix
|
|
1127
|
+
// above has exactly ONE direction, the FILE direction -- before changing anything.
|
|
1137
1128
|
//
|
|
1138
1129
|
// THE KEEP-SET IS THE POINTER-ROW SET, AND THAT IS A DIFFERENT QUESTION
|
|
1139
1130
|
// rather than a fourth answer to "what is retained". This resolver still
|
|
@@ -1145,12 +1136,12 @@ export function reconcileSnapshotRing(handle: AnnoStoreHandle): { droppedFiles:
|
|
|
1145
1136
|
// WHY THE QUESTION IS GENUINELY DIFFERENT, AND WHY THE ANSWERS ONLY DIVERGE
|
|
1146
1137
|
// NOW. A sweep over FILES asks "is this file claimed by a pointer row";
|
|
1147
1138
|
// `retainedRevisions` asks "can a caller revert to this revision". Before
|
|
1148
|
-
//
|
|
1139
|
+
// that promotion those two were identical for every reachable input,
|
|
1149
1140
|
// because the presence half of the old definition is trivially true of a
|
|
1150
1141
|
// file `readdirSync` just returned. The promotion is what separates them,
|
|
1151
1142
|
// and the separation runs in the SAFE direction: an image that fails to open
|
|
1152
1143
|
// but that a row still claims is kept on disk as EVIDENCE. Leaving this
|
|
1153
|
-
// keep-set on `retainedRevisions` would have made
|
|
1144
|
+
// keep-set on `retainedRevisions` would have made that fix its own
|
|
1154
1145
|
// second destroyer -- the sweep would unlink exactly the corrupt image whose
|
|
1155
1146
|
// refusal has to be diagnosed, one ordinary write after the refusal.
|
|
1156
1147
|
//
|
|
@@ -1183,22 +1174,19 @@ export function reconcileSnapshotRing(handle: AnnoStoreHandle): { droppedFiles:
|
|
|
1183
1174
|
}
|
|
1184
1175
|
}
|
|
1185
1176
|
|
|
1186
|
-
// STEP 3 IS GONE ON PURPOSE, and its absence is the fix
|
|
1177
|
+
// STEP 3 IS GONE ON PURPOSE, and its absence is the fix described above. It
|
|
1187
1178
|
// deleted every pointer row this handle's spelling of the ring could not
|
|
1188
1179
|
// vouch for; under a second spelling of the same store file that was every
|
|
1189
1180
|
// row it had. The whole argument is in the ORPHAN ROW bullet above.
|
|
1190
1181
|
|
|
1191
1182
|
// STEP 4. Close the sweep's own transaction through THE module's single
|
|
1192
1183
|
// commit site. It must be `commitTransaction` and never a second
|
|
1193
|
-
// `handle.db.exec` of the bare word
|
|
1194
|
-
// contains exactly ONE such statement, because the durability proof's planted
|
|
1195
|
-
// violation must have a single site -- a second literal would split that
|
|
1196
|
-
// planting and let half of it survive.
|
|
1184
|
+
// `handle.db.exec` of the bare word.
|
|
1197
1185
|
commitTransaction(handle.db);
|
|
1198
1186
|
} catch {
|
|
1199
1187
|
// ROLLED BACK INSIDE ITS OWN SWALLOWING `try`: there is nothing useful to
|
|
1200
1188
|
// do with a second error here, and reporting it would replace the first.
|
|
1201
|
-
// WHAT IS NEW IS THAT THE OUTCOME IS RECORDED RATHER THAN ASSUMED
|
|
1189
|
+
// WHAT IS NEW IS THAT THE OUTCOME IS RECORDED RATHER THAN ASSUMED.
|
|
1202
1190
|
// Node 22's `DatabaseSync` exposes no transaction-state accessor, so this
|
|
1203
1191
|
// boolean is the only thing that can tell a caller which of the two
|
|
1204
1192
|
// happened -- and this function cannot tell it by throwing, because
|
|
@@ -1212,7 +1200,7 @@ export function reconcileSnapshotRing(handle: AnnoStoreHandle): { droppedFiles:
|
|
|
1212
1200
|
rolledBack = false;
|
|
1213
1201
|
}
|
|
1214
1202
|
// AND DELIBERATELY NOT RETHROWN. A throw from here is swallowed by step 9's
|
|
1215
|
-
//
|
|
1203
|
+
// own error-swallowing wrap anyway, so rethrowing would buy nothing on the write path --
|
|
1216
1204
|
// and on `revertTo`'s own step-6 call site it would convert a COMMITTED
|
|
1217
1205
|
// write into a caller-visible failure, which prohibition 28-11 P5 forbids.
|
|
1218
1206
|
// The sweep changed nothing, which is precisely what `deferred` reports --
|
|
@@ -1306,7 +1294,7 @@ export function pruneSnapshots(handle: AnnoStoreHandle): boolean {
|
|
|
1306
1294
|
// revision number, and the same direction a deferred sweep already accepts.
|
|
1307
1295
|
//
|
|
1308
1296
|
// AND ITS `rollbackFailed` IS CONSUMED FOR THE SAME REASON, RETURNED RATHER
|
|
1309
|
-
// THAN DISCARDED
|
|
1297
|
+
// THAN DISCARDED. The argument recorded above for consuming
|
|
1310
1298
|
// `.deferred` is the argument for consuming this one, so it is extended here
|
|
1311
1299
|
// rather than restated: a fact this function throws away is a fact its caller
|
|
1312
1300
|
// cannot act on, and `rollbackFailed` reports the ONE state the sweep's own
|
|
@@ -1345,7 +1333,7 @@ export function pruneSnapshots(handle: AnnoStoreHandle): boolean {
|
|
|
1345
1333
|
// an orphan FILE -- which the reconciliation at the top of the NEXT prune
|
|
1346
1334
|
// can still see and retry. Under the earlier arrangement the row was
|
|
1347
1335
|
// deleted unconditionally after a swallowed failure, so the file became
|
|
1348
|
-
// invisible to the bound forever
|
|
1336
|
+
// invisible to the bound forever.
|
|
1349
1337
|
try {
|
|
1350
1338
|
// THE PATH IS COMPUTED HERE, at the delete, from the handle -- never read
|
|
1351
1339
|
// from the row. A persisted absolute path is environment-controlled input
|
|
@@ -1374,9 +1362,6 @@ export function pruneSnapshots(handle: AnnoStoreHandle): boolean {
|
|
|
1374
1362
|
* has to drive the IDENTICAL staging code the production writer uses. A
|
|
1375
1363
|
* hand-copied variant inside a test can drift out of agreement with the real
|
|
1376
1364
|
* one, and a proof that agrees with a copy proves nothing about the original.
|
|
1377
|
-
* `anno-seam.test.ts` asserts that no shipped module other than this one so
|
|
1378
|
-
* much as names it -- the same bound `applyWriteWithoutCommit` carries, by the
|
|
1379
|
-
* same mechanism rather than a second one.
|
|
1380
1365
|
*
|
|
1381
1366
|
* THE STAGING SUFFIX IS DELIBERATELY OUTSIDE `SNAPSHOT_FILE_PATTERN`. That
|
|
1382
1367
|
* pattern is anchored on `r<digits>.db`, and `reconcileSnapshotRing`'s
|
|
@@ -1391,7 +1376,7 @@ export function pruneSnapshots(handle: AnnoStoreHandle): boolean {
|
|
|
1391
1376
|
* because `vacuum into` refuses an existing target and because two writers
|
|
1392
1377
|
* filling one file is the very collision this staging exists to remove.
|
|
1393
1378
|
*
|
|
1394
|
-
* AND THE IMAGE IS FSYNCED BEFORE THIS FUNCTION RETURNS
|
|
1379
|
+
* AND THE IMAGE IS FSYNCED BEFORE THIS FUNCTION RETURNS. The pointer row
|
|
1395
1380
|
* that names this file is inserted inside the write transaction and committed by
|
|
1396
1381
|
* SQLite, WHICH DOES FSYNC -- so without the `fsyncPath` below the ROW is
|
|
1397
1382
|
* durable and the FILE it names is not. Trap 10's durability premise covers a
|
|
@@ -1399,7 +1384,7 @@ export function pruneSnapshots(handle: AnnoStoreHandle): boolean {
|
|
|
1399
1384
|
* anyway; it does NOT cover a host crash, which loses the cache. The consequence
|
|
1400
1385
|
* is not an extra file: it is a PRESENT, PARTIAL snapshot that
|
|
1401
1386
|
* `retainedRevisions()` would advertise as revertible, which is exactly the input
|
|
1402
|
-
*
|
|
1387
|
+
* this defect was reproduced with. 28-16's step-2 and step-3b gates make that input a
|
|
1403
1388
|
* REFUSAL rather than a destruction; this call removes the input at its source
|
|
1404
1389
|
* rather than relying on the refusal, because a refusal on the only route back
|
|
1405
1390
|
* is still a lost history.
|
|
@@ -1443,7 +1428,7 @@ export function stageSnapshot(handle: AnnoStoreHandle, revision: number): string
|
|
|
1443
1428
|
* store that has been reverted and written forward again, rather than leaving
|
|
1444
1429
|
* it as an argument.
|
|
1445
1430
|
*
|
|
1446
|
-
* AND THE RING DIRECTORY IS FSYNCED AFTER THE RENAME
|
|
1431
|
+
* AND THE RING DIRECTORY IS FSYNCED AFTER THE RENAME. A rename is
|
|
1447
1432
|
* VISIBLE immediately and DURABLE only after the directory is fsynced -- the
|
|
1448
1433
|
* distinction `fsyncPath`'s own doc sentence records. The pointer row that names
|
|
1449
1434
|
* this file is inserted inside the write transaction a few statements below and
|
|
@@ -1454,7 +1439,7 @@ export function stageSnapshot(handle: AnnoStoreHandle, revision: number): string
|
|
|
1454
1439
|
* `SIGKILL` and NOT of a host crash, which loses the page cache; the surviving
|
|
1455
1440
|
* half-state there is a durable row naming a file whose bytes never reached
|
|
1456
1441
|
* disk, i.e. the PRESENT, PARTIAL snapshot `retainedRevisions()` would advertise
|
|
1457
|
-
* and the exact input
|
|
1442
|
+
* and the exact input this defect was reproduced with. 28-16 made that input a refusal
|
|
1458
1443
|
* rather than a destruction; this call removes the input at its source instead of
|
|
1459
1444
|
* relying on that refusal. The order is the same as `revertTo`'s steps 3 and 5
|
|
1460
1445
|
* and uses the same helper, deliberately -- a second durability idiom in one
|
|
@@ -1474,7 +1459,7 @@ export function stageSnapshot(handle: AnnoStoreHandle, revision: number): string
|
|
|
1474
1459
|
* half-state for a store that cannot be written at all: `openSync(dir, "r")`
|
|
1475
1460
|
* needs the ring directory READABLE, so a writable-but-unreadable ring (mode
|
|
1476
1461
|
* 0300 -- measured) would make every `setDataType` throw, and that is also the
|
|
1477
|
-
* precondition
|
|
1462
|
+
* precondition this fix's only behavioural control is built from. So the two
|
|
1478
1463
|
* reachable outcomes after an interruption stay exactly the two this module
|
|
1479
1464
|
* bounds them to -- a missing entry, or an entry whose contents ARE durable --
|
|
1480
1465
|
* and the failure of this call moves the outcome from the second to the first
|
|
@@ -1500,7 +1485,7 @@ function publishSnapshot(stagingPath: string, snapPath: string): void {
|
|
|
1500
1485
|
* are on.
|
|
1501
1486
|
*
|
|
1502
1487
|
* AND IT IS THE ONE PLACE A STAGING FILE IS REMOVED, which is why `revertTo`'s
|
|
1503
|
-
* three cleanup exits route through it too
|
|
1488
|
+
* three cleanup exits route through it too. Those three used to
|
|
1504
1489
|
* be bare `rmSync(staging, { force: true })` calls, so the module had two
|
|
1505
1490
|
* answers to "where does a staging file get removed" and a later reader looking
|
|
1506
1491
|
* for the one place found only half of them. The swallowing semantics below are
|
|
@@ -1575,12 +1560,12 @@ function runWriteSequence<T>(
|
|
|
1575
1560
|
doCommit: boolean,
|
|
1576
1561
|
baseRevision?: number,
|
|
1577
1562
|
): { revision: number; result: T } {
|
|
1578
|
-
// BEFORE `begin immediate`, AND BEFORE ANYTHING IS READ
|
|
1563
|
+
// BEFORE `begin immediate`, AND BEFORE ANYTHING IS READ. A previous
|
|
1579
1564
|
// write's housekeeping sweep ran on THIS connection and reported that its own
|
|
1580
1565
|
// `rollback` threw, so the connection may still hold an open transaction and
|
|
1581
1566
|
// the store's write lock. Without this refusal the next `begin immediate`
|
|
1582
1567
|
// surfaces SQLite's bare `cannot start a transaction within a transaction` --
|
|
1583
|
-
//
|
|
1568
|
+
// the exact reported symptom, outside the `ViceError` family, with nothing
|
|
1584
1569
|
// naming the cause or the remedy.
|
|
1585
1570
|
//
|
|
1586
1571
|
// AN EXISTING IN-FAMILY CLASS, NOT A NEW ONE: this is the same fact the commit
|
|
@@ -1607,7 +1592,7 @@ function runWriteSequence<T>(
|
|
|
1607
1592
|
);
|
|
1608
1593
|
}
|
|
1609
1594
|
|
|
1610
|
-
//
|
|
1595
|
+
// THE PRE-LOCK ARM. `stageSnapshot` runs BEFORE `begin immediate`, so
|
|
1611
1596
|
// it gets its own handler rather than sharing the outer one below: there is
|
|
1612
1597
|
// no transaction to roll back yet and no staged file to discard, so the two
|
|
1613
1598
|
// arms genuinely differ in what they have to undo. The reachable input is a
|
|
@@ -1626,7 +1611,7 @@ function runWriteSequence<T>(
|
|
|
1626
1611
|
);
|
|
1627
1612
|
}
|
|
1628
1613
|
|
|
1629
|
-
//
|
|
1614
|
+
// THE MAIN WINDOW: `begin immediate`, the compare-and-swap, the
|
|
1630
1615
|
// publication and the pointer-row insert, wrapped as ONE region. Its catch
|
|
1631
1616
|
// undoes both kinds of state this region can leave behind -- an open
|
|
1632
1617
|
// transaction with the compare-and-swap applied, and a staged `.tmp` -- and
|
|
@@ -1635,7 +1620,7 @@ function runWriteSequence<T>(
|
|
|
1635
1620
|
// keeps the family closed.
|
|
1636
1621
|
//
|
|
1637
1622
|
// THE INNER ROLLBACK AND DISCARD IN THE CAS-FAILURE BRANCH BELOW ARE NOT
|
|
1638
|
-
// REDUNDANT AND MUST NOT BE "SIMPLIFIED" AWAY. `anno-store.test.ts`'s
|
|
1623
|
+
// REDUNDANT AND MUST NOT BE "SIMPLIFIED" AWAY. `anno-store.test.ts`'s own
|
|
1639
1624
|
// control extracts the slice between `cas.changes` and `publishSnapshot` and
|
|
1640
1625
|
// asserts a `rollback` is present inside it, positioned after the
|
|
1641
1626
|
// `select revision from anno_meta` read -- that positioning is a VERIFIED
|
|
@@ -1679,7 +1664,7 @@ function runWriteSequence<T>(
|
|
|
1679
1664
|
// claims (see `retainedRevisions`).
|
|
1680
1665
|
handle.db.prepare("insert into anno_snapshot(revision) values (?)").run(rev);
|
|
1681
1666
|
} catch (e) {
|
|
1682
|
-
// THE ROLLBACK'S OUTCOME IS RECORDED, NOT ASSUMED
|
|
1667
|
+
// THE ROLLBACK'S OUTCOME IS RECORDED, NOT ASSUMED. The message below
|
|
1683
1668
|
// used to state the rollback as a fact after this `catch` had swallowed that
|
|
1684
1669
|
// rollback's own failure, so the one case in which the claim is false is
|
|
1685
1670
|
// exactly the case in which it was printed. Node 22's `DatabaseSync` exposes
|
|
@@ -1753,7 +1738,7 @@ function runWriteSequence<T>(
|
|
|
1753
1738
|
}
|
|
1754
1739
|
|
|
1755
1740
|
if (doCommit) {
|
|
1756
|
-
//
|
|
1741
|
+
// THE COMMIT ARM. This is the ONE statement in the sequence whose
|
|
1757
1742
|
// failure leaves the transaction OPEN with everything already applied -- the
|
|
1758
1743
|
// compare-and-swap, the caller's mutation and the pointer-row insert -- and
|
|
1759
1744
|
// it was outside every handler until this arm was added. A concurrent READER
|
|
@@ -1763,7 +1748,7 @@ function runWriteSequence<T>(
|
|
|
1763
1748
|
// transaction: a bare `Error: database is locked` after the connection's
|
|
1764
1749
|
// 5000 ms `busy_timeout`, outside the `ViceError` family, with the write
|
|
1765
1750
|
// lock still held and `currentRevision()` on this connection reporting the
|
|
1766
|
-
// ADVANCED revision for a write that never landed. On the
|
|
1751
|
+
// ADVANCED revision for a write that never landed. On the MCP tool
|
|
1767
1752
|
// path a handle lives as long as the session, so the leaked write lock
|
|
1768
1753
|
// locks every other connection out for that long.
|
|
1769
1754
|
//
|
|
@@ -1785,18 +1770,18 @@ function runWriteSequence<T>(
|
|
|
1785
1770
|
// accepted write's sweep reclaims.
|
|
1786
1771
|
//
|
|
1787
1772
|
// The refusal carries `code` from the underlying error when it has one
|
|
1788
|
-
// (
|
|
1773
|
+
// (the cheap half of that, on this wrap only), so a caller can ask whether the
|
|
1789
1774
|
// failure was lock contention without substring-matching the message.
|
|
1790
1775
|
// `cause` is deliberately NOT added: that needs a new field on
|
|
1791
1776
|
// `ViceErrorOptions` in `vice.ts`, a shared module outside this phase.
|
|
1792
1777
|
try {
|
|
1793
1778
|
commitTransaction(handle.db);
|
|
1794
1779
|
} catch (e) {
|
|
1795
|
-
// RECORDED, NOT ASSERTED
|
|
1780
|
+
// RECORDED, NOT ASSERTED. "the transaction has been rolled back"
|
|
1796
1781
|
// was stated as a fact directly under a `catch` that swallowed the
|
|
1797
1782
|
// rollback's own failure -- so on the one path where the claim is false it
|
|
1798
|
-
// was still printed, and a refusal that reports
|
|
1799
|
-
// repair sends the caller straight back into reusing a connection that may
|
|
1783
|
+
// was still printed, and a refusal that reports that half-committed state
|
|
1784
|
+
// as its own repair sends the caller straight back into reusing a connection that may
|
|
1800
1785
|
// still hold the store's write lock. There is no cheap check available:
|
|
1801
1786
|
// Node 22's `DatabaseSync` exposes no transaction-state accessor (surface
|
|
1802
1787
|
// measured on this host: `open, close, prepare, exec, function, location,
|
|
@@ -1824,10 +1809,10 @@ function runWriteSequence<T>(
|
|
|
1824
1809
|
// `rolledBack` is carried in `data` as well as in the prose so a caller
|
|
1825
1810
|
// can branch on the fact instead of substring-matching a message.
|
|
1826
1811
|
// The wording here is FREE. It used to be constrained: the
|
|
1827
|
-
// single-commit-site control
|
|
1812
|
+
// single-commit-site control counted the WORD
|
|
1828
1813
|
// `commit` over this module's stripped source, so a `step` value
|
|
1829
|
-
// reading "commit ..." reddened a control in a different file.
|
|
1830
|
-
// replaced that count with a match on `exec()` calls carrying a bare
|
|
1814
|
+
// reading "commit ..." reddened a control in a different file. A later
|
|
1815
|
+
// revision replaced that count with a match on `exec()` calls carrying a bare
|
|
1831
1816
|
// statement literal, which no error message can satisfy, and the
|
|
1832
1817
|
// constraint went with it -- this value is unchanged only because
|
|
1833
1818
|
// changing it would be a gratuitous behaviour change.
|
|
@@ -1840,12 +1825,12 @@ function runWriteSequence<T>(
|
|
|
1840
1825
|
// branch because a sequence that never commits has no accepted write to
|
|
1841
1826
|
// bound.
|
|
1842
1827
|
//
|
|
1843
|
-
//
|
|
1828
|
+
// WRAPPED, AND DELIBERATELY NOT RETHROWN. By this line the
|
|
1844
1829
|
// transaction has already returned, so THE WRITE HAPPENED -- the mutation
|
|
1845
1830
|
// and the pointer row are durable. A housekeeping failure that threw from
|
|
1846
1831
|
// here would report a write that succeeded as a failure, and the caller
|
|
1847
|
-
// would retry an ADDITIVE verb and produce a second row. That is
|
|
1848
|
-
//
|
|
1832
|
+
// would retry an ADDITIVE verb and produce a second row. That is exactly
|
|
1833
|
+
// the failure this wrap exists to prevent, and it became more likely rather than less once the
|
|
1849
1834
|
// sweep started taking the write lock.
|
|
1850
1835
|
//
|
|
1851
1836
|
// The consequence of swallowing is an UN-PRUNED RING -- extra files, the
|
|
@@ -1855,7 +1840,7 @@ function runWriteSequence<T>(
|
|
|
1855
1840
|
// one here would be new surface with its own stdio hazards on an MCP
|
|
1856
1841
|
// transport.
|
|
1857
1842
|
//
|
|
1858
|
-
// AND ITS REPORT IS CONSUMED
|
|
1843
|
+
// AND ITS REPORT IS CONSUMED. `pruneSnapshots` returns the sweep's
|
|
1859
1844
|
// `rollbackFailed` -- the one state the sweep's own handler cannot fix --
|
|
1860
1845
|
// and it is RECORDED ON THE HANDLE rather than thrown or logged. Not thrown,
|
|
1861
1846
|
// because by this line the write is committed and 28-11 P5 forbids reporting
|
|
@@ -1894,8 +1879,7 @@ export function applyWrite<T>(
|
|
|
1894
1879
|
* real one.
|
|
1895
1880
|
*
|
|
1896
1881
|
* Its only caller is a spawned, test-only helper that is deliberately absent
|
|
1897
|
-
* from `package.json`'s `files[]
|
|
1898
|
-
* shipped module other than this one so much as names it.
|
|
1882
|
+
* from `package.json`'s `files[]`.
|
|
1899
1883
|
*/
|
|
1900
1884
|
export function applyWriteWithoutCommit<T>(
|
|
1901
1885
|
handle: AnnoStoreHandle,
|
|
@@ -1907,7 +1891,7 @@ export function applyWriteWithoutCommit<T>(
|
|
|
1907
1891
|
|
|
1908
1892
|
/**
|
|
1909
1893
|
* The module's ONE range insert. `bank` is a parameter rather than a hardcoded
|
|
1910
|
-
* `null
|
|
1894
|
+
* `null`: a remainder re-inserted by split-and-preserve carries the
|
|
1911
1895
|
* overlapped row's own `bank` forward, and a newly typed range carries `null`.
|
|
1912
1896
|
* `bank` is reserved and interpreted by nothing today, which is exactly why a
|
|
1913
1897
|
* write path that silently dropped it would be an unobservable loss a future
|
|
@@ -1996,7 +1980,7 @@ function entryPairKey(pair: readonly [number, number]): string {
|
|
|
1996
1980
|
|
|
1997
1981
|
/**
|
|
1998
1982
|
* What fragmenting `row` at the caller's range COSTS, or `null` when it costs
|
|
1999
|
-
* nothing this record could describe
|
|
1983
|
+
* nothing this record could describe.
|
|
2000
1984
|
*
|
|
2001
1985
|
* `null` in exactly two cases, both of them honest:
|
|
2002
1986
|
* * the row is not a split-table layout -- asked through `isSplitDataType`,
|
|
@@ -2097,15 +2081,14 @@ function splitReinterpretation(
|
|
|
2097
2081
|
* signal that distinguishes a no-op, and the revision is never that signal.
|
|
2098
2082
|
*
|
|
2099
2083
|
* ---------------------------------------------------------------------------
|
|
2100
|
-
* DECISION 1: THE SPLIT-ROW REMAINDER RULE -- TWO OUTCOMES, NEITHER SILENT
|
|
2101
|
-
* (CR-09 and CR-10).
|
|
2084
|
+
* DECISION 1: THE SPLIT-ROW REMAINDER RULE -- TWO OUTCOMES, NEITHER SILENT.
|
|
2102
2085
|
* ---------------------------------------------------------------------------
|
|
2103
2086
|
* A split-table row has TWO things that can go wrong when a caller's range
|
|
2104
2087
|
* fragments it, and this function answers them differently on purpose. The
|
|
2105
2088
|
* comment and the code below state ONE rule, and both halves of it are here.
|
|
2106
2089
|
*
|
|
2107
|
-
* (1) THE ODD REMAINDER IS REFUSED, and the whole retype is refused with it
|
|
2108
|
-
*
|
|
2090
|
+
* (1) THE ODD REMAINDER IS REFUSED, and the whole retype is refused with it.
|
|
2091
|
+
* A remainder that is not a legal shape for its OWN type -- the
|
|
2109
2092
|
* odd-byte-count tail of a fragmented split table is the reachable case -- is a
|
|
2110
2093
|
* row `setDataType` would decline to create and `resolveSplitTargets()` cannot
|
|
2111
2094
|
* decode. The store never persists a range row it would refuse at its own entry
|
|
@@ -2117,7 +2100,7 @@ function splitReinterpretation(
|
|
|
2117
2100
|
* outward to an entry boundary is forbidden outright by `anno-types.ts` trap 7.
|
|
2118
2101
|
*
|
|
2119
2102
|
* (2) THE EVEN REMAINDER IS ACCEPTED **WITH A REPORT** -- never accepted
|
|
2120
|
-
* silently
|
|
2103
|
+
* silently. Parity is not the only thing a fragment can break. A split
|
|
2121
2104
|
* table pairs byte `i` with byte `n + i`, so an entry's partner is a function of
|
|
2122
2105
|
* the row's START and its LENGTH, and changing either end re-pairs EVERY entry.
|
|
2123
2106
|
*
|
|
@@ -2156,17 +2139,17 @@ function splitReinterpretation(
|
|
|
2156
2139
|
* `delete`, and the ordering is the guarantee, not a tidiness preference: a
|
|
2157
2140
|
* refusal must cost nothing observable, and leaning on the transaction's
|
|
2158
2141
|
* rollback to undo a half-applied mutation would make that depend on a rollback
|
|
2159
|
-
* that the
|
|
2142
|
+
* that the commit handler's own `rollbackFailed` handling shows can itself fail.
|
|
2160
2143
|
* Compute, refuse, then mutate. The parity check runs FIRST and is untouched by
|
|
2161
2144
|
* the disclosure: a refusing retype returns no report because it returns nothing
|
|
2162
2145
|
* at all.
|
|
2163
2146
|
*
|
|
2164
2147
|
* ---------------------------------------------------------------------------
|
|
2165
|
-
* DECISION 2: THE UNION COLLAPSE IS INTENDED
|
|
2148
|
+
* DECISION 2: THE UNION COLLAPSE IS INTENDED.
|
|
2166
2149
|
* ---------------------------------------------------------------------------
|
|
2167
2150
|
* A caller range that SPANS several existing rows deletes all of them and
|
|
2168
|
-
* inserts one row. That is intended, and it does not contradict
|
|
2169
|
-
*
|
|
2151
|
+
* inserts one row. That is intended, and it does not contradict the store's
|
|
2152
|
+
* own adjacency rule: that rule forbids the store joining adjacent ranges OF ITS OWN ACCORD, and
|
|
2170
2153
|
* here the caller asked for exactly one range and got exactly one range. The
|
|
2171
2154
|
* store still never joins two rows nobody asked about -- see the behavioural
|
|
2172
2155
|
* and structural adjacency controls.
|
|
@@ -2213,7 +2196,7 @@ function retype(
|
|
|
2213
2196
|
// for a split row that survives that question, what the fragmentation COSTS.
|
|
2214
2197
|
//
|
|
2215
2198
|
// WHAT THIS GATE DOES NOT ASK, said here because "THE GATE" reads absolute
|
|
2216
|
-
// and a reader will otherwise take it for one (
|
|
2199
|
+
// and a reader will otherwise take it for one (28-21 P1 / 28-07 P3):
|
|
2217
2200
|
// the shape question is asked of REMAINDERS, never of the overlapped row
|
|
2218
2201
|
// itself. A row the caller's range covers in full has no head and no tail,
|
|
2219
2202
|
// so neither branch below runs and its shape is never examined -- correctly,
|
|
@@ -2462,7 +2445,7 @@ export function listRanges(handle: AnnoStoreHandle): RangeRow[] {
|
|
|
2462
2445
|
* retained for it", with the oldest retained revision, the bound and the
|
|
2463
2446
|
* available list.
|
|
2464
2447
|
* * A ROW CLAIMS IT BUT ITS IMAGE WILL NOT OPEN as an annotation store (step
|
|
2465
|
-
* 2, second arm) -- the arm
|
|
2448
|
+
* 2, second arm) -- the arm added to cover an absent image and a
|
|
2466
2449
|
* present-but-unusable one alike, with the underlying reason quoted so the
|
|
2467
2450
|
* caller can tell which. This is the arm the old presence-only gate did not
|
|
2468
2451
|
* have, and its absence is what let the store be destroyed installing an
|
|
@@ -2485,7 +2468,7 @@ export function listRanges(handle: AnnoStoreHandle): RangeRow[] {
|
|
|
2485
2468
|
*/
|
|
2486
2469
|
/**
|
|
2487
2470
|
* The one gate on a revision-shaped argument, and it exists because ONE
|
|
2488
|
-
* unvalidated value lands in TWO places that can then disagree
|
|
2471
|
+
* unvalidated value lands in TWO places that can then disagree: a bound
|
|
2489
2472
|
* SQL parameter, and a snapshot FILENAME.
|
|
2490
2473
|
*
|
|
2491
2474
|
* Accepts a non-negative safe integer and nothing else. A numeric STRING is
|
|
@@ -2496,9 +2479,9 @@ export function listRanges(handle: AnnoStoreHandle): RangeRow[] {
|
|
|
2496
2479
|
* ring, which is the confusion `AnnoRevisionArgumentError`'s doc comment records
|
|
2497
2480
|
* in full.
|
|
2498
2481
|
*
|
|
2499
|
-
* DELIBERATELY NOT REUSED FOR `baseRevision`.
|
|
2500
|
-
*
|
|
2501
|
-
* this round, so the declination is recorded here rather than left looking like
|
|
2482
|
+
* DELIBERATELY NOT REUSED FOR `baseRevision`. An earlier round's review sketch
|
|
2483
|
+
* suggested it, but that suggestion is a separate finding the same round's
|
|
2484
|
+
* verification did not route to this round, so the declination is recorded here rather than left looking like
|
|
2502
2485
|
* an omission -- `runWriteSequence`'s existing `baseRevision` staleness refusal
|
|
2503
2486
|
* is this validator's SIBLING, not its client.
|
|
2504
2487
|
*/
|
|
@@ -2521,10 +2504,10 @@ function assertRevisionArgument(value: unknown, parameter: string): number {
|
|
|
2521
2504
|
}
|
|
2522
2505
|
|
|
2523
2506
|
export function revertTo(handle: AnnoStoreHandle, revision: number): AnnoStoreHandle {
|
|
2524
|
-
// STEP 0, AND IT IS FIRST FOR THE REASON
|
|
2507
|
+
// STEP 0, AND IT IS FIRST FOR THE REASON RECORDED ABOVE: this argument reaches
|
|
2525
2508
|
// a bound SQL parameter AND a filename, so it is judged before either exists.
|
|
2526
2509
|
// Before this line, `revertTo(handle, "1")` silently reverted the store and
|
|
2527
|
-
// `revertTo(handle, "0001")` refused with
|
|
2510
|
+
// `revertTo(handle, "0001")` refused with a CORRUPTION message.
|
|
2528
2511
|
assertRevisionArgument(revision, "revision");
|
|
2529
2512
|
|
|
2530
2513
|
// STEP 1. The pointer row -- the INDEX half of "retained". An EXISTENCE check
|
|
@@ -2542,8 +2525,8 @@ export function revertTo(handle: AnnoStoreHandle, revision: number): AnnoStoreHa
|
|
|
2542
2525
|
//
|
|
2543
2526
|
// THE FILE HALF NOW READS `snapshotOpenFailure` -- the same witness
|
|
2544
2527
|
// `retainedRevisions` reads, in its other position. It used to read
|
|
2545
|
-
// `existsSync` and nothing more, which
|
|
2546
|
-
// a database
|
|
2528
|
+
// `existsSync` and nothing more, which let a present image that was not
|
|
2529
|
+
// a database pass this gate, and the store was destroyed installing it. The
|
|
2547
2530
|
// "oldest retained" and "available revisions" figures are built from
|
|
2548
2531
|
// `retainedRevisions()` and never from the raw rows, so a refusal cannot
|
|
2549
2532
|
// steer the caller at a revision the very next call would also refuse.
|
|
@@ -2551,8 +2534,8 @@ export function revertTo(handle: AnnoStoreHandle, revision: number): AnnoStoreHa
|
|
|
2551
2534
|
// THE ARMS SPLIT ON THE ROW, NOT ON THE FILE'S PRESENCE, and that is
|
|
2552
2535
|
// deliberate rather than an omission. Asking "is the image absent" separately
|
|
2553
2536
|
// from "does the image open" would put a SECOND predicate back on a snapshot
|
|
2554
|
-
// path -- a second truth about one file, which is how
|
|
2555
|
-
// happened. The two file-half sub-cases are distinguished by the QUOTED
|
|
2537
|
+
// path -- a second truth about one file, which is exactly how two of this
|
|
2538
|
+
// store's past defects happened. The two file-half sub-cases are distinguished by the QUOTED
|
|
2556
2539
|
// REASON instead: an absent image quotes "the file does not exist", a corrupt
|
|
2557
2540
|
// one quotes what SQLite or `openStore` said. One witness, one message, and
|
|
2558
2541
|
// the caller can still tell them apart.
|
|
@@ -2585,7 +2568,7 @@ export function revertTo(handle: AnnoStoreHandle, revision: number): AnnoStoreHa
|
|
|
2585
2568
|
|
|
2586
2569
|
const storePath = handle.path;
|
|
2587
2570
|
const dir = handle.dir;
|
|
2588
|
-
// THE STAGING NAME IS UNIQUE PER ATTEMPT, NOT PER (PID, REVISION)
|
|
2571
|
+
// THE STAGING NAME IS UNIQUE PER ATTEMPT, NOT PER (PID, REVISION),
|
|
2589
2572
|
// and it is built from `randomUUID` -- the SAME primitive `stageSnapshot`
|
|
2590
2573
|
// uses, so there is ONE answer in this module to "how is a staging name made
|
|
2591
2574
|
// unique" rather than two that can drift. `stageSnapshot`'s own doc comment
|
|
@@ -2602,12 +2585,12 @@ export function revertTo(handle: AnnoStoreHandle, revision: number): AnnoStoreHa
|
|
|
2602
2585
|
// addressed the same path.
|
|
2603
2586
|
//
|
|
2604
2587
|
// AND WHAT IT DOES NOT CLOSE, stated because a comment that implied otherwise
|
|
2605
|
-
// would be prohibition 28-07 P3's exact shape: THE LEAK HALF STAYS OPEN
|
|
2606
|
-
//
|
|
2588
|
+
// would be prohibition 28-07 P3's exact shape: THE LEAK HALF STAYS OPEN.
|
|
2589
|
+
// A process killed between the copy and any of the three cleanups
|
|
2607
2590
|
// still leaves this file behind, and it sits beside the store rather than
|
|
2608
2591
|
// inside the ring directory, so `reconcileSnapshotRing`'s sweep -- anchored on
|
|
2609
2592
|
// `r<digits>.db` inside `snapshotDirFor()` -- does not and must not match it.
|
|
2610
|
-
// Nothing reclaims it. That is
|
|
2593
|
+
// Nothing reclaims it. That is the other half of this gap and it is not closed here.
|
|
2611
2594
|
//
|
|
2612
2595
|
// THE PID IS KEPT deliberately: it is the diagnostic that lets a human finding
|
|
2613
2596
|
// a leaked file say which process produced it, and the `.revert-` marker is
|
|
@@ -2618,8 +2601,8 @@ export function revertTo(handle: AnnoStoreHandle, revision: number): AnnoStoreHa
|
|
|
2618
2601
|
// failure reachable here -- `ENOENT`, `ENOSPC`, `EACCES` -- therefore leaves
|
|
2619
2602
|
// the caller a USABLE handle: nothing has been replaced yet, so
|
|
2620
2603
|
// `currentRevision(handle)` and `listRanges(handle)` still answer and the
|
|
2621
|
-
// caller can decide what to do. That is
|
|
2622
|
-
// fixed by ordering rather than by a rescue path.
|
|
2604
|
+
// caller can decide what to do. That is exactly the failure this ordering
|
|
2605
|
+
// exists to prevent, and it is fixed by ordering rather than by a rescue path.
|
|
2623
2606
|
try {
|
|
2624
2607
|
copyFileSync(snapPath, staging);
|
|
2625
2608
|
fsyncPath(staging);
|
|
@@ -2633,7 +2616,7 @@ export function revertTo(handle: AnnoStoreHandle, revision: number): AnnoStoreHa
|
|
|
2633
2616
|
);
|
|
2634
2617
|
}
|
|
2635
2618
|
|
|
2636
|
-
// STEP 3b, AND ITS POSITION IS THE GUARANTEE
|
|
2619
|
+
// STEP 3b, AND ITS POSITION IS THE GUARANTEE. OPEN THE STAGED IMAGE
|
|
2637
2620
|
// BEFORE ANYTHING IS CLOSED AND BEFORE ANYTHING IS RENAMED. Until this step
|
|
2638
2621
|
// existed the only witness that the image about to be installed was a store at
|
|
2639
2622
|
// all was `existsSync` -- and this module's own FIRST MEASURED FACT (see the
|
|
@@ -2698,8 +2681,8 @@ export function revertTo(handle: AnnoStoreHandle, revision: number): AnnoStoreHa
|
|
|
2698
2681
|
// function. The image just restored carries `anno_snapshot` rows for
|
|
2699
2682
|
// revisions whose files an earlier prune removed, and it leaves every
|
|
2700
2683
|
// snapshot taken AFTER `revision` unclaimed by any row. ONLY THE FILE HALF IS
|
|
2701
|
-
// RESOLVED HERE, and the ROW half is deliberately left: since
|
|
2702
|
-
// abstains from the pointer-row direction entirely, because it cannot
|
|
2684
|
+
// RESOLVED HERE, and the ROW half is deliberately left: since the fix above
|
|
2685
|
+
// the sweep abstains from the pointer-row direction entirely, because it cannot
|
|
2703
2686
|
// establish ownership of a row under a second spelling of the store file, so
|
|
2704
2687
|
// the restored image's stale rows are TOLERATED rather than deleted. This
|
|
2705
2688
|
// sentence previously claimed BOTH halves were resolved here -- the reversal
|
|
@@ -2733,7 +2716,7 @@ export function revertTo(handle: AnnoStoreHandle, revision: number): AnnoStoreHa
|
|
|
2733
2716
|
// sweep below can itself stall for up to the connection's five-second
|
|
2734
2717
|
// `busy_timeout` before `revertTo` returns.
|
|
2735
2718
|
//
|
|
2736
|
-
//
|
|
2719
|
+
// THE THIRD PROPERTY: THE SWEEP IS HOUSEKEEPING AND MUST NEVER COST THE
|
|
2737
2720
|
// CALLER A HANDLE. By this line the revert has ALREADY SUCCEEDED ON DISK --
|
|
2738
2721
|
// step 5's rename and directory fsync have returned, so the store file at
|
|
2739
2722
|
// `storePath` IS the reverted image whatever happens next. Round 3 observed
|
|
@@ -2762,7 +2745,7 @@ export function revertTo(handle: AnnoStoreHandle, revision: number): AnnoStoreHa
|
|
|
2762
2745
|
// thing in both of its controls rather than letting a green test imply a
|
|
2763
2746
|
// behavioural proof it does not carry.
|
|
2764
2747
|
//
|
|
2765
|
-
// AND THE REOPEN IS NOW INSIDE THE SAME GUARANTEE
|
|
2748
|
+
// AND THE REOPEN IS NOW INSIDE THE SAME GUARANTEE, WHICH IS WHERE IT
|
|
2766
2749
|
// BELONGED. The sentence above -- "a housekeeping failure never costs the
|
|
2767
2750
|
// caller a handle" -- used to hold only for the branch that CANNOT fire. The
|
|
2768
2751
|
// sweep call was guarded and is unreachable; the two `openStore` calls were
|
|
@@ -2804,14 +2787,14 @@ export function revertTo(handle: AnnoStoreHandle, revision: number): AnnoStoreHa
|
|
|
2804
2787
|
);
|
|
2805
2788
|
}
|
|
2806
2789
|
//
|
|
2807
|
-
// AND THE SWEEP'S RESULT IS BOUND RATHER THAN DISCARDED
|
|
2790
|
+
// AND THE SWEEP'S RESULT IS BOUND RATHER THAN DISCARDED, WHICH IS THE
|
|
2808
2791
|
// ARM THE `catch` ABOVE CANNOT SEE. `reconcileSnapshotRing` does not throw
|
|
2809
2792
|
// when its own `rollback` fails -- rethrowing there is forbidden by 28-11 P5,
|
|
2810
2793
|
// because on this very call site it would convert a LANDED revert into a
|
|
2811
2794
|
// caller-visible failure -- so it REPORTS the fact in `rollbackFailed`
|
|
2812
2795
|
// instead. Reaching that state WITHOUT a throw is exactly why the existing
|
|
2813
2796
|
// catch arm alone was not enough: `revertTo` would hand back a connection that
|
|
2814
|
-
// may still hold the store's write lock, which is
|
|
2797
|
+
// may still hold the store's write lock, which is the same reported symptom
|
|
2815
2798
|
// re-created on the revert path.
|
|
2816
2799
|
//
|
|
2817
2800
|
// THE REMEDY IS THE SAME BLOCK, REUSED RATHER THAN COPIED: close the
|
|
@@ -3016,7 +2999,7 @@ export function listComments(handle: AnnoStoreHandle): CommentRow[] {
|
|
|
3016
2999
|
* the existing one's id and two ends. Before that refusal existed this comment
|
|
3017
3000
|
* and `ScopeRow`'s made a claim the code did not honour, which is exactly the
|
|
3018
3001
|
* shape prohibition 28-07 P3 forbids. Adjacency is NOT overlap: two scopes that
|
|
3019
|
-
* merely touch at a boundary are two scopes, consistent with
|
|
3002
|
+
* merely touch at a boundary are two scopes, consistent with this store's own
|
|
3020
3003
|
* treatment of ranges.
|
|
3021
3004
|
*
|
|
3022
3005
|
* A BYTE-IDENTICAL REPEAT IS AN ACCEPTED NO-OP reporting `changed: false`, and
|
|
@@ -3028,7 +3011,7 @@ export function listComments(handle: AnnoStoreHandle): CommentRow[] {
|
|
|
3028
3011
|
* `AnnoWriteResult`'s own doc comment states that `changed` is the ONLY signal
|
|
3029
3012
|
* distinguishing a no-op from a real edit -- and for scopes it could never say
|
|
3030
3013
|
* no-op, so the module already had the policy and simply could not express it
|
|
3031
|
-
* here. Second,
|
|
3014
|
+
* here. Second, the MCP surface's own success criterion requires a repeated edit to
|
|
3032
3015
|
* SUCCEED reporting no change, so the surface this store mirrors does have the
|
|
3033
3016
|
* policy after all. The repeat is therefore accepted rather than refused, and
|
|
3034
3017
|
* the revision still advances by one, exactly like every other write entry
|
|
@@ -3049,7 +3032,7 @@ export function addScope(
|
|
|
3049
3032
|
// read the existing row inside the transaction and return `false`. It has
|
|
3050
3033
|
// to run before the overlap check, because a byte-identical scope
|
|
3051
3034
|
// overlaps itself and would otherwise be refused rather than accepted as
|
|
3052
|
-
// the no-op
|
|
3035
|
+
// the no-op that surface's own criterion requires.
|
|
3053
3036
|
const identical = db.prepare("select id from anno_scope where start = ? and end_inclusive = ?").get(start, endInclusive) as
|
|
3054
3037
|
| { id: number }
|
|
3055
3038
|
| undefined;
|
|
@@ -3099,10 +3082,10 @@ export function listScopes(handle: AnnoStoreHandle): ScopeRow[] {
|
|
|
3099
3082
|
* `changed: false` when no scope has that span.
|
|
3100
3083
|
*
|
|
3101
3084
|
* WHY THIS EXISTS, and why it is not an omission being corrected quietly.
|
|
3102
|
-
*
|
|
3103
|
-
* no inverse and carried the finding
|
|
3085
|
+
* A prior verification round recorded that `addScope`'s overlap refusal had
|
|
3086
|
+
* no inverse and carried the finding forward in as many words, "which puts
|
|
3104
3087
|
* `addScope` on an agent-driven surface where a mistyped span is likelier".
|
|
3105
|
-
*
|
|
3088
|
+
* The review that raised it spells out the consequence: one transposed end --
|
|
3106
3089
|
* `addScope($1000, $ffff)` -- makes every future scope from `$1000` upward
|
|
3107
3090
|
* permanently unaddable, recoverable only through `revertTo` inside the
|
|
3108
3091
|
* 32-revision ring, after which the mistake is permanent for the life of the
|
|
@@ -3158,13 +3141,12 @@ export function removeScope(
|
|
|
3158
3141
|
|
|
3159
3142
|
/**
|
|
3160
3143
|
* Records a user-requested exclusion of `start..endInclusive`, with `reason`
|
|
3161
|
-
* stating WHY the user asked for it -- added at `SCHEMA_VERSION` 5
|
|
3162
|
-
* (`BUILD-07`).
|
|
3144
|
+
* stating WHY the user asked for it -- added at `SCHEMA_VERSION` 5.
|
|
3163
3145
|
*
|
|
3164
3146
|
* RECORDING AN EXCLUSION CHANGES NOTHING ABOUT WHICH BYTES THE EXPORT EMITS.
|
|
3165
3147
|
* The exporter still walks this range's full byte span and emits a real
|
|
3166
3148
|
* block, tagged with a visible marker comment, rather than a hole -- that is
|
|
3167
|
-
*
|
|
3149
|
+
* this feature's whole invariant. An exporter implementation that skipped the
|
|
3168
3150
|
* block on seeing an exclusion row would satisfy the word "exclude" and fail
|
|
3169
3151
|
* the requirement outright: this table is a RECORD, never a filter, and the
|
|
3170
3152
|
* store answers "what did the user record", never "should this range be
|
|
@@ -3206,7 +3188,7 @@ export function addExcludedRange(
|
|
|
3206
3188
|
if (reason.trim() === "") {
|
|
3207
3189
|
throw new AnnoCommentError(
|
|
3208
3190
|
`exclusion reason is empty or whitespace-only -- a "reason" column satisfied by an empty string records that something was excluded ` +
|
|
3209
|
-
`and loses WHY, which is precisely the half of
|
|
3191
|
+
`and loses WHY, which is precisely the half of criterion 2 this record exists to carry. Supply the reason the user gave.`,
|
|
3210
3192
|
{ reason: "empty reason" },
|
|
3211
3193
|
);
|
|
3212
3194
|
}
|
|
@@ -3528,7 +3510,7 @@ export function listProjectEnums(handle: AnnoStoreHandle): ProjectEnumRow[] {
|
|
|
3528
3510
|
|
|
3529
3511
|
/**
|
|
3530
3512
|
* Associates ONE address with ONE project enum, so the address's operand is
|
|
3531
|
-
* formatted through that enum's variants (`SCHEMA_VERSION` 3
|
|
3513
|
+
* formatted through that enum's variants (`SCHEMA_VERSION` 3).
|
|
3532
3514
|
*
|
|
3533
3515
|
* THE ASSOCIATION IS BY `anno_enum.id`, NEVER BY NAME, and that is the whole
|
|
3534
3516
|
* design of the table. `updateProjectEnum` renames an enum in place, keeping
|
|
@@ -3552,8 +3534,8 @@ export function listProjectEnums(handle: AnnoStoreHandle): ProjectEnumRow[] {
|
|
|
3552
3534
|
*
|
|
3553
3535
|
* Every argument is validated through `anno-types.ts`'s own assertions before
|
|
3554
3536
|
* any SQL runs. `parseStoreAddress` in particular refuses an UNPREFIXED numeric
|
|
3555
|
-
* string such as `"53280"` outright rather than guessing a base --
|
|
3556
|
-
*
|
|
3537
|
+
* string such as `"53280"` outright rather than guessing a base -- a recorded
|
|
3538
|
+
* failure in which a JSON `"1"` arrived verbatim and SQLite's column
|
|
3557
3539
|
* affinity turned an argument error into a corruption refusal.
|
|
3558
3540
|
*/
|
|
3559
3541
|
export function applyEnumUsage(
|
|
@@ -3663,7 +3645,7 @@ export function listEnumUsage(handle: AnnoStoreHandle): EnumUsageRow[] {
|
|
|
3663
3645
|
}
|
|
3664
3646
|
|
|
3665
3647
|
// ---------------------------------------------------------------------------
|
|
3666
|
-
// THE RUNTIME EVIDENCE TABLE (`anno_evid_exec`, `SCHEMA_VERSION` 4
|
|
3648
|
+
// THE RUNTIME EVIDENCE TABLE (`anno_evid_exec`, `SCHEMA_VERSION` 4).
|
|
3667
3649
|
// One raw shape used by all three functions below.
|
|
3668
3650
|
// ---------------------------------------------------------------------------
|
|
3669
3651
|
|
|
@@ -3701,9 +3683,9 @@ function toEvidExecRow(row: RawEvidExecRow): EvidExecRow {
|
|
|
3701
3683
|
|
|
3702
3684
|
/**
|
|
3703
3685
|
* Inserts one or more runtime-execution observations for one run identity,
|
|
3704
|
-
* keyed `(imageSha256, argvDigest, seed, address, sourceBank)` --
|
|
3705
|
-
* `no-
|
|
3706
|
-
*
|
|
3686
|
+
* keyed `(imageSha256, argvDigest, seed, address, sourceBank)` -- a live A/B
|
|
3687
|
+
* measured `no-perturbation` from instrumentation, so there is deliberately
|
|
3688
|
+
* no `run_class` discriminator.
|
|
3707
3689
|
*
|
|
3708
3690
|
* EVERY FIELD IS VALIDATED BEFORE THE FIRST STATEMENT RUNS, and every
|
|
3709
3691
|
* refusal is a named `AnnoTypeError`/`AnnoAddressError` carrying the
|
|
@@ -3720,8 +3702,7 @@ function toEvidExecRow(row: RawEvidExecRow): EvidExecRow {
|
|
|
3720
3702
|
* transaction that `commitTransaction` commits, so a kill mid-ingest leaves
|
|
3721
3703
|
* the set fully committed or fully absent, never a partial row set.
|
|
3722
3704
|
*
|
|
3723
|
-
* AN OBSERVATION ALREADY PRESENT IS SKIPPED, NOT RE-INSERTED
|
|
3724
|
-
* idempotent re-ingest): the existing row is selected first, by the full
|
|
3705
|
+
* AN OBSERVATION ALREADY PRESENT IS SKIPPED, NOT RE-INSERTED: the existing row is selected first, by the full
|
|
3725
3706
|
* unique key, and the insert only runs when it is absent. `changed` is
|
|
3726
3707
|
* `false` exactly when every observation in this call was already present --
|
|
3727
3708
|
* the same `changed`-is-the-only-no-op-signal contract `AnnoWriteResult`
|
|
@@ -3729,7 +3710,7 @@ function toEvidExecRow(row: RawEvidExecRow): EvidExecRow {
|
|
|
3729
3710
|
* advances by exactly one on every accepted call, no-op or not, for the same
|
|
3730
3711
|
* reason.
|
|
3731
3712
|
*
|
|
3732
|
-
* `insertedCount`
|
|
3713
|
+
* `insertedCount` IS COUNTED INSIDE THIS SAME TRANSACTION, never
|
|
3733
3714
|
* derived from a separate read taken before `applyWrite` opens it: a caller
|
|
3734
3715
|
* that wants "how many of these rows were actually new" must not be handed a
|
|
3735
3716
|
* number computed from a `listExecObservations()` snapshot that a concurrent
|
|
@@ -3857,12 +3838,11 @@ export function listExecObservations(
|
|
|
3857
3838
|
|
|
3858
3839
|
/**
|
|
3859
3840
|
* Every distinct run identity with an `anno_evid_exec` row, its accumulated
|
|
3860
|
-
* observation count, and the `denominator` those counts are a fraction of
|
|
3861
|
-
* (EVID-04).
|
|
3841
|
+
* observation count, and the `denominator` those counts are a fraction of.
|
|
3862
3842
|
*
|
|
3863
3843
|
* WHY A DENOMINATOR IS RETURNED AT ALL, AND WHY IT NEVER FORMS A PERCENTAGE
|
|
3864
3844
|
* ITSELF. A bare count invites the reading "the rest is data" -- exactly the
|
|
3865
|
-
* soundness violation
|
|
3845
|
+
* soundness violation this table's own discipline forbids (see `RuntimeExecClass`'s own doc
|
|
3866
3846
|
* comment in `anno-types.ts`). `denominator` is `ADDRESS_MAX - ADDRESS_MIN +
|
|
3867
3847
|
* 1`, read from `anno-types.ts`'s own constants rather than the literal
|
|
3868
3848
|
* `65536` -- a caller comparing a run's `observationCount` against it forms
|
|
@@ -3889,7 +3869,7 @@ export function listObservedRuns(handle: AnnoStoreHandle): { runs: ObservedRunRo
|
|
|
3889
3869
|
|
|
3890
3870
|
/**
|
|
3891
3871
|
* Deletes every `anno_evid_exec` row for one run identity -- a bracket
|
|
3892
|
-
* reset
|
|
3872
|
+
* reset. A run identity holding no rows returns `changed: false`
|
|
3893
3873
|
* and is NOT an error: resetting an empty bracket is the ordinary thing,
|
|
3894
3874
|
* matching `clearEnumUsage`'s own direction for the identical case.
|
|
3895
3875
|
*
|
|
@@ -3927,13 +3907,13 @@ export function deleteExecObservationsForRun(
|
|
|
3927
3907
|
*
|
|
3928
3908
|
* THIS IS THE C-5 RECONCILIATION, written down here for a reader of the code
|
|
3929
3909
|
* rather than left in a plan. Two requirement texts look like they conflict:
|
|
3930
|
-
*
|
|
3910
|
+
* one requires cross-reference rows to carry their access kind from the
|
|
3931
3911
|
* first write, while the cross-reference criterion requires references to be
|
|
3932
3912
|
* DERIVED on every query and never cached on disk. Both hold at once, and this
|
|
3933
3913
|
* entry point is where:
|
|
3934
3914
|
*
|
|
3935
3915
|
* * the table and its `access_kind` column exist from the first write (the
|
|
3936
|
-
* `DDL` above), so
|
|
3916
|
+
* `DDL` above), so that requirement is satisfied structurally;
|
|
3937
3917
|
* * the only rows ever written here are references that CANNOT be recovered
|
|
3938
3918
|
* from the bytes -- hand-asserted, or resolved from something outside the
|
|
3939
3919
|
* program image. The `COMPUTED_JUMP` case is exactly that: a computed
|