@henols/vice-mcp 0.2.3 → 0.2.4
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 +139 -144
- package/anno-symbols.ts +7 -7
- package/anno-types.ts +55 -55
- 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/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
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
// module is not yet reachable from the published entry point's import closure,
|
|
55
55
|
// and `scripts/check-npm-packages.mjs` asserts only one direction -- every
|
|
56
56
|
// REACHABLE module must be listed -- never the converse. The real reason to
|
|
57
|
-
// list it:
|
|
57
|
+
// list it: the shipped-module assertion scans `shippedTsModules()`, which is derived
|
|
58
58
|
// from `files[]`, so an unlisted module makes that assertion VACUOUS. It would
|
|
59
59
|
// pass by scanning a set this file is not in. Copying the reachability sentence
|
|
60
60
|
// here would plant a false claim in a brand-new seam header, which is the
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
// `LIKE 'prefix%'` is 2.02 ms against FTS5 `MATCH`'s 2.99 ms, with a
|
|
86
86
|
// 121.8 ms index rebuild, over 20,000 rows. Adding FTS5 later is
|
|
87
87
|
// ADDITIVE; removing it is a schema migration. The search surface belongs
|
|
88
|
-
//
|
|
88
|
+
// elsewhere and this module must simply not foreclose it.
|
|
89
89
|
// 6. NEVER add an explicit save or flush verb. Durability is this module's
|
|
90
90
|
// responsibility, not the caller's: every accepted write commits before it
|
|
91
91
|
// returns. A save verb is a way for a caller to lose data by forgetting.
|
|
@@ -103,9 +103,9 @@
|
|
|
103
103
|
// look like tightening and are the opposite. A REFUSAL would push a caller
|
|
104
104
|
// toward deleting the comment to get the retype through, converting a
|
|
105
105
|
// reported loss into a silent one -- the exact outcome the report exists to
|
|
106
|
-
// prevent
|
|
106
|
+
// prevent. A WIDENED rule would fire on every retype of a
|
|
107
107
|
// commented range, and a report that fires every time is a report nobody
|
|
108
|
-
// reads, so the one case that matters stops being noticed
|
|
108
|
+
// reads, so the one case that matters stops being noticed.
|
|
109
109
|
// 10. NEVER prune the snapshot ring INSIDE the write transaction, and never
|
|
110
110
|
// let a revert fall back to the nearest retained revision. A filesystem
|
|
111
111
|
// unlink is not part of the transaction, so pruning inside it means a
|
|
@@ -125,7 +125,7 @@
|
|
|
125
125
|
// conclusion. And a revert that SUBSTITUTES the nearest
|
|
126
126
|
// retained revision for the one asked for changes the caller's intent with
|
|
127
127
|
// nothing recording that it happened, so a revert past the bound is
|
|
128
|
-
// refused BY NAME instead
|
|
128
|
+
// refused BY NAME instead.
|
|
129
129
|
import { randomUUID } from "node:crypto";
|
|
130
130
|
import { closeSync, copyFileSync, existsSync, fsyncSync, mkdirSync, openSync, readdirSync, renameSync, rmSync } from "node:fs";
|
|
131
131
|
import { basename, dirname, join, resolve } from "node:path";
|
|
@@ -216,8 +216,8 @@ export interface AnnoWriteResult {
|
|
|
216
216
|
* the very first write. The SECOND half became false: `anno_snapshot` carried a
|
|
217
217
|
* `path text not null` column holding the snapshot's ABSOLUTE location, and two
|
|
218
218
|
* destructive consequences were reproduced against committed code -- two stores
|
|
219
|
-
* in one directory sharing one ring
|
|
220
|
-
* write destroying the whole revert history
|
|
219
|
+
* in one directory sharing one ring and a directory rename plus one
|
|
220
|
+
* write destroying the whole revert history. The column is DROPPED at
|
|
221
221
|
* `SCHEMA_VERSION` 2 and the location is computed from the handle by
|
|
222
222
|
* `snapshotDirFor()` at every read and every delete, so there is no persisted
|
|
223
223
|
* absolute string left for a second namespace -- a bind mount seen from the
|
|
@@ -230,7 +230,7 @@ export interface AnnoWriteResult {
|
|
|
230
230
|
* was byte-identical to version 1's.
|
|
231
231
|
*
|
|
232
232
|
* THAT SENTENCE IS KEPT AND SCOPED RATHER THAN DELETED, because at
|
|
233
|
-
* `SCHEMA_VERSION` 3 it stopped being the whole truth:
|
|
233
|
+
* `SCHEMA_VERSION` 3 it stopped being the whole truth: version 3 (2026-08-29) ADDS
|
|
234
234
|
* one table, `anno_enum_usage`, and its index. It changes no existing table's
|
|
235
235
|
* column list, so the scoped claim above still holds of every table version 2
|
|
236
236
|
* had. The version 3 table associates ONE address with ONE `anno_enum` row by
|
|
@@ -239,7 +239,7 @@ export interface AnnoWriteResult {
|
|
|
239
239
|
* the day that cost was accepted.
|
|
240
240
|
*
|
|
241
241
|
* `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:
|
|
242
|
+
* Two requirement texts look like they conflict here and do not: one
|
|
243
243
|
* requires the column, while the cross-reference criterion forbids CACHING a
|
|
244
244
|
* DERIVED cross-reference on disk. Both hold at once -- the table exists, and
|
|
245
245
|
* only non-derivable references (hand-asserted, or resolved from something
|
|
@@ -252,7 +252,7 @@ export interface AnnoWriteResult {
|
|
|
252
252
|
* mapper in `listRanges()`. It is reserved, and every row written today has it
|
|
253
253
|
* null.
|
|
254
254
|
*
|
|
255
|
-
* AT `SCHEMA_VERSION` 4
|
|
255
|
+
* AT `SCHEMA_VERSION` 4, ONE MORE TABLE IS ADDED:
|
|
256
256
|
* `anno_evid_exec`, the durable runtime-execution evidence table. See
|
|
257
257
|
* `anno-types.ts`'s `SCHEMA_VERSION` doc comment for what the bump buys and
|
|
258
258
|
* the decided, dated fate of an existing version-3 store (`reaffirm-refusal`
|
|
@@ -260,11 +260,11 @@ export interface AnnoWriteResult {
|
|
|
260
260
|
* nullable column at all: unlike the annotation tables above, every field on
|
|
261
261
|
* a row here is a fact the runtime evidence layer is licensed to assert, or
|
|
262
262
|
* the row does not exist. Its run-identity key is the bare triple
|
|
263
|
-
* `(image_sha256, argv_digest, seed)`, selected by
|
|
264
|
-
*
|
|
265
|
-
* `
|
|
263
|
+
* `(image_sha256, argv_digest, seed)`, selected by a live A/B that measured
|
|
264
|
+
* `no-perturbation` from instrumentation -- there is deliberately no
|
|
265
|
+
* `run_class` column.
|
|
266
266
|
*
|
|
267
|
-
* AT `SCHEMA_VERSION` 5
|
|
267
|
+
* AT `SCHEMA_VERSION` 5, ONE MORE TABLE IS ADDED:
|
|
268
268
|
* `anno_excluded_range`, the durable record of a user-requested exclusion --
|
|
269
269
|
* its extent and the reason the user gave. See `anno-types.ts`'s
|
|
270
270
|
* `SCHEMA_VERSION` doc comment for what the bump buys, why a table was chosen
|
|
@@ -378,7 +378,7 @@ export interface AnnoStoreHandle {
|
|
|
378
378
|
/**
|
|
379
379
|
* THIS CONNECTION'S TRANSACTION STATE IS UNKNOWN: a housekeeping sweep run on
|
|
380
380
|
* it reported that its own `rollback` threw, so it may still hold an open
|
|
381
|
-
* transaction and the store's write lock
|
|
381
|
+
* transaction and the store's write lock.
|
|
382
382
|
*
|
|
383
383
|
* THE REMEDY IS THE ONE THE COMMIT HANDLER ALREADY PRINTS, in the same words:
|
|
384
384
|
* CLOSE IT AND REOPEN rather than reusing it. Node 22's `DatabaseSync` exposes
|
|
@@ -393,7 +393,7 @@ export interface AnnoStoreHandle {
|
|
|
393
393
|
* what prohibition 28-11 P5 forbids, and would send a caller to retry an
|
|
394
394
|
* additive verb. So the accepted write returns its revision unchanged and it
|
|
395
395
|
* is the NEXT call on this connection that refuses BY NAME -- which is what
|
|
396
|
-
* turns
|
|
396
|
+
* turns the bare `cannot start a transaction within a transaction` error into a
|
|
397
397
|
* diagnosis.
|
|
398
398
|
*
|
|
399
399
|
* `false` on every freshly opened handle, set in `openStore` at the one place
|
|
@@ -440,7 +440,7 @@ function fsyncPath(path: string): void {
|
|
|
440
440
|
*
|
|
441
441
|
* A `workspaceRoot` IS REQUIRED unless the caller explicitly asks for the
|
|
442
442
|
* unconfined path with `unconfinedModuleDerivedPath: true`, and the inversion is
|
|
443
|
-
* deliberate
|
|
443
|
+
* deliberate. Confinement used to be opt-IN, which made the mitigation
|
|
444
444
|
* for the one unvalidated input this module's own header calls out the one a
|
|
445
445
|
* caller could forget -- and two of this store's recorded blockers were confinement
|
|
446
446
|
* escapes. The escape exists for exactly one shape: a path THIS MODULE derived
|
|
@@ -486,12 +486,12 @@ export function openStore(
|
|
|
486
486
|
path: string,
|
|
487
487
|
opts: { workspaceRoot?: string; mustExist?: boolean; unconfinedModuleDerivedPath?: boolean } = {},
|
|
488
488
|
): AnnoStoreHandle {
|
|
489
|
-
// CONFINEMENT IS THE DEFAULT, AND THE ESCAPE IS A WORD A GREP CAN FIND
|
|
490
|
-
//
|
|
489
|
+
// CONFINEMENT IS THE DEFAULT, AND THE ESCAPE IS A WORD A GREP CAN FIND.
|
|
490
|
+
// `anno-types.ts`'s header names the three things nothing upstream
|
|
491
491
|
// validates -- "an address of 65536, a misspelled data type, and a store path
|
|
492
492
|
// pointing outside the workspace all look identical to the transport" -- and
|
|
493
493
|
// this was the only one of the three whose mitigation a caller could simply
|
|
494
|
-
// forget. Two of this
|
|
494
|
+
// forget. Two of this project's own review findings were confinement
|
|
495
495
|
// escapes.
|
|
496
496
|
//
|
|
497
497
|
// REFUSED BEFORE THE PATH IS RESOLVED AND LONG BEFORE `new DatabaseSync`, for
|
|
@@ -592,8 +592,7 @@ export function openStore(
|
|
|
592
592
|
|
|
593
593
|
if (meta.schema_version !== SCHEMA_VERSION) {
|
|
594
594
|
db.close();
|
|
595
|
-
// NAMES THE REMEDY AND DENIES NOTHING IS LOST
|
|
596
|
-
// condition 2). This build refuses rather than upgrades -- see
|
|
595
|
+
// NAMES THE REMEDY AND DENIES NOTHING IS LOST. This build refuses rather than upgrades -- see
|
|
597
596
|
// `anno-types.ts`'s `SCHEMA_VERSION` doc comment for the decided,
|
|
598
597
|
// dated reason -- and the refusal happens BEFORE any write, so the
|
|
599
598
|
// file on disk is exactly what it was a moment ago: its labels,
|
|
@@ -607,7 +606,7 @@ export function openStore(
|
|
|
607
606
|
);
|
|
608
607
|
}
|
|
609
608
|
|
|
610
|
-
//
|
|
609
|
+
// THE LAST KNOWN FAMILY ESCAPE IN THIS FUNCTION. The two blocks either
|
|
611
610
|
// side of this one are already wrapped, and for the same two reasons: an
|
|
612
611
|
// unwrapped failure here leaks the CONNECTION as well as escaping the
|
|
613
612
|
// `ViceError` family, so the caller loses the file handle with no way to
|
|
@@ -648,14 +647,14 @@ export function currentRevision(handle: AnnoStoreHandle): number {
|
|
|
648
647
|
* The suffix appended to the store FILENAME to name its snapshot ring
|
|
649
648
|
* directory. Appended to the FILENAME rather than being a fixed directory name
|
|
650
649
|
* (`<dir>/snapshots`, which is what this was), and the distinction is the whole
|
|
651
|
-
* of
|
|
650
|
+
* of the fix: two distinct store files in one directory have distinct
|
|
652
651
|
* basenames by definition of a filesystem, so distinct basenames give distinct
|
|
653
652
|
* rings BY CONSTRUCTION rather than by an ownership predicate layered over a
|
|
654
653
|
* shared location.
|
|
655
654
|
*
|
|
656
655
|
* THE PREDICATE ROUTE WAS ALREADY TRIED AND COULD NOT SEE THE DEFECT. Plan
|
|
657
656
|
* 28-07 added a per-revision ownership check over the shared `<dir>/snapshots`
|
|
658
|
-
* ring; it was structurally blind to
|
|
657
|
+
* ring; it was structurally blind to the collision because revision numbers are not
|
|
659
658
|
* unique ACROSS stores -- two stores in one directory both write `r1.db`, and
|
|
660
659
|
* every per-revision predicate says "yes, revision 1 is mine" to both of them.
|
|
661
660
|
* A location that cannot collide has no such blind spot to test for.
|
|
@@ -669,12 +668,12 @@ const SNAPSHOT_DIR_SUFFIX = ".snapshots";
|
|
|
669
668
|
* `<dir>/proj.annostore.snapshots`.
|
|
670
669
|
*
|
|
671
670
|
* THE RESIDUAL, STATED RATHER THAN CLAIMED CLOSED -- AND RESTATED AFTER THIS
|
|
672
|
-
* PARAGRAPH'S EARLIER VERSION WAS FALSIFIED BY DRIVING THE CODE
|
|
671
|
+
* PARAGRAPH'S EARLIER VERSION WAS FALSIFIED BY DRIVING THE CODE. What
|
|
673
672
|
* it got RIGHT and keeps: the location is a pure function of the handle, the
|
|
674
673
|
* sweep only ever reads `snapshotDirFor(handle)` so it cannot see a ring it
|
|
675
674
|
* 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
|
|
675
|
+
* -- the ring moves with the directory, so nothing is lost, confirmed by a
|
|
676
|
+
* directory-rename test. What became FALSE: it claimed the old ring was never deleted at
|
|
678
677
|
* all and that `retainedRevisions()` reporting an empty list was therefore a
|
|
679
678
|
* truthful under-claim. That was true of the FILES and false of the ROWS -- so
|
|
680
679
|
* the claim is not repeated here even to disown it, because the next reader
|
|
@@ -687,7 +686,7 @@ const SNAPSHOT_DIR_SUFFIX = ".snapshots";
|
|
|
687
686
|
* a SYMLINK ALIAS, or a store-file rename (`mv proj.annostore
|
|
688
687
|
* other.annostore`) -- names a DIFFERENT ring, so a handle opened under it
|
|
689
688
|
* publishes into a SECOND ring. The first ring's files are never deleted, and
|
|
690
|
-
* since
|
|
689
|
+
* since the fix above its pointer rows are never deleted BY THE SWEEP -- but
|
|
691
690
|
* `pruneSnapshots`' doomed loop still deletes every row below
|
|
692
691
|
* `currentRevision() - MAX_SNAPSHOT_REVISIONS`, so restoring the original name
|
|
693
692
|
* restores the floor ONLY while the wrong-spelling handle has not advanced past
|
|
@@ -737,8 +736,8 @@ export const NO_RETAINED_REVISION = -1;
|
|
|
737
736
|
* reconciliation below derives from a filename alone.
|
|
738
737
|
*
|
|
739
738
|
* 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
|
|
739
|
+
* this module's own staging mechanism writes per-attempt STAGING files in this
|
|
740
|
+
* same directory under a different suffix, and a sweep that matched them would delete another
|
|
742
741
|
* writer's in-flight snapshot -- the exact loss this reconciliation exists to
|
|
743
742
|
* prevent, committed by the repair itself.
|
|
744
743
|
*
|
|
@@ -757,7 +756,7 @@ const SNAPSHOT_FILE_PATTERN = /^r(\d+)\.db$/;
|
|
|
757
756
|
*
|
|
758
757
|
* IT REPLACED A PRESENCE TEST AT BOTH OF THE TWO SITES THAT CARRIED ONE -- the
|
|
759
758
|
* filter inside `retainedRevisions` and `revertTo`'s step-2 gate -- and the
|
|
760
|
-
* promotion is the whole of
|
|
759
|
+
* promotion is the whole of the supporting half of this fix. Presence was never a
|
|
761
760
|
* witness that a file is a store: this module's FIRST MEASURED FACT (header,
|
|
762
761
|
* `:22-31`) is that a ZERO-LENGTH FILE OPENS as a SQLite database and reports
|
|
763
762
|
* `integrity_check ok`. So the store advertised a revision whose image was not a
|
|
@@ -854,7 +853,7 @@ function claimedRevisions(handle: AnnoStoreHandle): number[] {
|
|
|
854
853
|
* one file are two things that can disagree. They did, twice, both reproduced:
|
|
855
854
|
* a directory rename invalidated every persisted path at once, after which this
|
|
856
855
|
* function reported NO retained revisions while the files sat there on disk, and
|
|
857
|
-
* the next write's prune destroyed them
|
|
856
|
+
* the next write's prune destroyed them. The same shape covers every
|
|
858
857
|
* adjacent case rather than just that one repro -- a bind mount seen from two
|
|
859
858
|
* namespaces (this repo's entire architecture is built around that boundary), a
|
|
860
859
|
* symlinked ancestor, a container/host path pair, a case-insensitive filesystem,
|
|
@@ -880,8 +879,8 @@ function claimedRevisions(handle: AnnoStoreHandle): number[] {
|
|
|
880
879
|
* `begin immediate` would have opened up to 32 databases with the store's write
|
|
881
880
|
* lock held. Every remaining consumer reads this function rather than deciding
|
|
882
881
|
* 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
|
|
882
|
+
* the three answers came to disagree -- that was the gap between two of them --
|
|
883
|
+
* and a fourth would also hide the row-only regression from the proofs that exist to
|
|
885
884
|
* catch it.
|
|
886
885
|
*/
|
|
887
886
|
export function retainedRevisions(handle: AnnoStoreHandle): number[] {
|
|
@@ -907,7 +906,7 @@ export function retainedRevisions(handle: AnnoStoreHandle): number[] {
|
|
|
907
906
|
* the first element of `retainedRevisions()` -- and that reading was then one
|
|
908
907
|
* step short a SECOND time, in the same direction: an existence check published
|
|
909
908
|
* `0` on a store whose `r0.db` was present but was not a database, and
|
|
910
|
-
* following THAT floor destroyed the live store
|
|
909
|
+
* following THAT floor destroyed the live store. The floor now requires
|
|
911
910
|
* the image to OPEN, not merely to exist, so the store still cannot publish a
|
|
912
911
|
* number it will then refuse -- in either direction.
|
|
913
912
|
*/
|
|
@@ -925,8 +924,8 @@ export function oldestRetainedRevision(handle: AnnoStoreHandle): number {
|
|
|
925
924
|
*
|
|
926
925
|
* * AN ORPHAN ROW (a pointer row whose file is gone) IS NO LONGER SWEPT AT
|
|
927
926
|
* 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
|
-
*
|
|
927
|
+
* from an absence. This function used to delete every such row. A live
|
|
928
|
+
* reproduction showed, twice, what that costs: the ring is named from
|
|
930
929
|
* `basename(handle.path)` -- a PATH SPELLING -- so a SYMLINK ALIAS of the
|
|
931
930
|
* store file, or a store-file rename (`mv proj.annostore
|
|
932
931
|
* other.annostore`), makes `retainedRevisions()` report every EXISTING
|
|
@@ -954,11 +953,11 @@ export function oldestRetainedRevision(handle: AnnoStoreHandle): number {
|
|
|
954
953
|
* ring. That is no longer true: the keep-set below reads
|
|
955
954
|
* `claimedRevisions`, so this sweep now looks at rows the advertisement
|
|
956
955
|
* ignores. THE NEW BASIS IS BETTER RATHER THAN WEAKER, and it is the
|
|
957
|
-
* conclusion
|
|
956
|
+
* conclusion that promotion forced: a row the sweep KEEPS is precisely what makes a
|
|
958
957
|
* corrupt image survive on disk as EVIDENCE instead of being unlinked. A
|
|
959
958
|
* sweep that deleted the image of a failure would be destroying the only
|
|
960
959
|
* record of the failure that has to be diagnosed -- a second destruction
|
|
961
|
-
* dressed as a repair.
|
|
960
|
+
* dressed as a repair. That conclusion is unchanged: the row direction
|
|
962
961
|
* stays abandoned, for the ownership reason above.
|
|
963
962
|
*
|
|
964
963
|
* THE KEEP-SET QUERY RUNS ON THE CONNECTION ALREADY IN HAND, and that is
|
|
@@ -1002,7 +1001,7 @@ export function oldestRetainedRevision(handle: AnnoStoreHandle): number {
|
|
|
1002
1001
|
* over a ring with no half-states). It must NOT run from `openStore`:
|
|
1003
1002
|
* `anno-durability.test.ts:291-347` asserts that an orphan snapshot file left
|
|
1004
1003
|
* in the kill window SURVIVES a reopen and is identified by its revision, and
|
|
1005
|
-
* that is a verified truth
|
|
1004
|
+
* that is a verified truth -- merely LOOKING at a store must not
|
|
1006
1005
|
* change it, and the orphan a kill window leaves is deliberately the harmless
|
|
1007
1006
|
* direction. Reconciling on open would redden that test, and rightly. BOTH
|
|
1008
1007
|
* SITES ARE OUTSIDE ANY OPEN TRANSACTION, which is now a REQUIREMENT rather
|
|
@@ -1010,7 +1009,7 @@ export function oldestRetainedRevision(handle: AnnoStoreHandle): number {
|
|
|
1010
1009
|
* own, so calling it from inside one is not supported.
|
|
1011
1010
|
*
|
|
1012
1011
|
* IT NOW TAKES THE STORE'S WRITE LOCK BEFORE IT DECIDES ANYTHING, and the
|
|
1013
|
-
* reason is a reproduced defect
|
|
1012
|
+
* reason is a reproduced defect rather than caution. A snapshot becomes
|
|
1014
1013
|
* a FILESYSTEM fact (the `renameSync` inside `publishSnapshot`) before it
|
|
1015
1014
|
* becomes a TRANSACTIONAL one (the pointer-row insert), so a sweep reading only
|
|
1016
1015
|
* its own committed view sees a live writer's published file as unowned and
|
|
@@ -1065,7 +1064,7 @@ export function oldestRetainedRevision(handle: AnnoStoreHandle): number {
|
|
|
1065
1064
|
* NO SECOND DISCRIMINATOR WAS ADDED FOR `deferred`'s TWO CAUSES, and the reason
|
|
1066
1065
|
* is not economy. Its only consumer, `pruneSnapshots`, returns early
|
|
1067
1066
|
* identically in both cases, so a discriminator would have no reader -- and
|
|
1068
|
-
*
|
|
1067
|
+
* the actual complaint here, that a LEAKED transaction makes every later sweep
|
|
1069
1068
|
* report `deferred` indistinguishably from contention, is removed AT ITS SOURCE
|
|
1070
1069
|
* by the handler rather than papered over with a label. A field describing a
|
|
1071
1070
|
* state this code can no longer reach would be exactly the kind of comment
|
|
@@ -1077,7 +1076,7 @@ export function oldestRetainedRevision(handle: AnnoStoreHandle): number {
|
|
|
1077
1076
|
* for up to the connection's five-second `busy_timeout` before it proceeds --
|
|
1078
1077
|
* at BOTH of the two call sites: every accepted write (through `pruneSnapshots`
|
|
1079
1078
|
* at `runWriteSequence` step 9) and every `revertTo` (through its own step-6
|
|
1080
|
-
* sweep on the restored handle).
|
|
1079
|
+
* sweep on the restored handle). Both sit on an MCP tool path. Each is
|
|
1081
1080
|
* bounded at ONE timeout and not two, because `pruneSnapshots` returns early
|
|
1082
1081
|
* when this function reports `deferred` rather than running its own autocommit
|
|
1083
1082
|
* deletes into the same contention. An honest cost stated at the seam is worth
|
|
@@ -1089,8 +1088,8 @@ export function oldestRetainedRevision(handle: AnnoStoreHandle): number {
|
|
|
1089
1088
|
* left (see the ORPHAN ROW bullet above), so what the ordering now guarantees is
|
|
1090
1089
|
* narrower and is stated narrowly: an interruption between the commit and the
|
|
1091
1090
|
* 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
|
|
1091
|
+
* Pinned by a source-order control in `anno-store.test.ts`, which since the fix
|
|
1092
|
+
* above asserts the ABSENCE of any pointer-row delete in this body as well as the
|
|
1094
1093
|
* surviving commit-before-unlink order -- a presence assertion cannot see
|
|
1095
1094
|
* either.
|
|
1096
1095
|
*/
|
|
@@ -1119,12 +1118,12 @@ export function reconcileSnapshotRing(handle: AnnoStoreHandle): { droppedFiles:
|
|
|
1119
1118
|
const orphanFiles: string[] = [];
|
|
1120
1119
|
|
|
1121
1120
|
// EVERYTHING FROM HERE TO THE COMMIT IS BRACKETED, AND THE BRACKET IS THE
|
|
1122
|
-
// FIX
|
|
1121
|
+
// FIX. `begin immediate` above has already opened a transaction on
|
|
1123
1122
|
// the CALLER's connection. Before this handler existed, any throw between
|
|
1124
1123
|
// that statement and the commit -- `readdirSync` on a ring directory that
|
|
1125
1124
|
// became unreadable, a failure of the keep-set `select`, anything --
|
|
1126
1125
|
// propagated out with the transaction still
|
|
1127
|
-
// OPEN. Step 9's
|
|
1126
|
+
// OPEN. Step 9's own error-swallowing wrap then swallowed it, so an ordinary `setDataType`
|
|
1128
1127
|
// reported SUCCESS while leaving the handle permanently inside a transaction:
|
|
1129
1128
|
// every later write failed with "cannot start a transaction within a
|
|
1130
1129
|
// transaction", and every later sweep reported `deferred` indistinguishably
|
|
@@ -1132,8 +1131,8 @@ export function reconcileSnapshotRing(handle: AnnoStoreHandle): { droppedFiles:
|
|
|
1132
1131
|
// path-dependent: there is no route out of this block that does not either
|
|
1133
1132
|
// commit or roll back.
|
|
1134
1133
|
try {
|
|
1135
|
-
// STEP 2. With the lock held, compute the drop set -- which since
|
|
1136
|
-
// exactly ONE direction, the FILE direction -- before changing anything.
|
|
1134
|
+
// STEP 2. With the lock held, compute the drop set -- which since the fix
|
|
1135
|
+
// above has exactly ONE direction, the FILE direction -- before changing anything.
|
|
1137
1136
|
//
|
|
1138
1137
|
// THE KEEP-SET IS THE POINTER-ROW SET, AND THAT IS A DIFFERENT QUESTION
|
|
1139
1138
|
// rather than a fourth answer to "what is retained". This resolver still
|
|
@@ -1145,12 +1144,12 @@ export function reconcileSnapshotRing(handle: AnnoStoreHandle): { droppedFiles:
|
|
|
1145
1144
|
// WHY THE QUESTION IS GENUINELY DIFFERENT, AND WHY THE ANSWERS ONLY DIVERGE
|
|
1146
1145
|
// NOW. A sweep over FILES asks "is this file claimed by a pointer row";
|
|
1147
1146
|
// `retainedRevisions` asks "can a caller revert to this revision". Before
|
|
1148
|
-
//
|
|
1147
|
+
// that promotion those two were identical for every reachable input,
|
|
1149
1148
|
// because the presence half of the old definition is trivially true of a
|
|
1150
1149
|
// file `readdirSync` just returned. The promotion is what separates them,
|
|
1151
1150
|
// and the separation runs in the SAFE direction: an image that fails to open
|
|
1152
1151
|
// but that a row still claims is kept on disk as EVIDENCE. Leaving this
|
|
1153
|
-
// keep-set on `retainedRevisions` would have made
|
|
1152
|
+
// keep-set on `retainedRevisions` would have made that fix its own
|
|
1154
1153
|
// second destroyer -- the sweep would unlink exactly the corrupt image whose
|
|
1155
1154
|
// refusal has to be diagnosed, one ordinary write after the refusal.
|
|
1156
1155
|
//
|
|
@@ -1183,7 +1182,7 @@ export function reconcileSnapshotRing(handle: AnnoStoreHandle): { droppedFiles:
|
|
|
1183
1182
|
}
|
|
1184
1183
|
}
|
|
1185
1184
|
|
|
1186
|
-
// STEP 3 IS GONE ON PURPOSE, and its absence is the fix
|
|
1185
|
+
// STEP 3 IS GONE ON PURPOSE, and its absence is the fix described above. It
|
|
1187
1186
|
// deleted every pointer row this handle's spelling of the ring could not
|
|
1188
1187
|
// vouch for; under a second spelling of the same store file that was every
|
|
1189
1188
|
// row it had. The whole argument is in the ORPHAN ROW bullet above.
|
|
@@ -1198,7 +1197,7 @@ export function reconcileSnapshotRing(handle: AnnoStoreHandle): { droppedFiles:
|
|
|
1198
1197
|
} catch {
|
|
1199
1198
|
// ROLLED BACK INSIDE ITS OWN SWALLOWING `try`: there is nothing useful to
|
|
1200
1199
|
// 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
|
|
1200
|
+
// WHAT IS NEW IS THAT THE OUTCOME IS RECORDED RATHER THAN ASSUMED.
|
|
1202
1201
|
// Node 22's `DatabaseSync` exposes no transaction-state accessor, so this
|
|
1203
1202
|
// boolean is the only thing that can tell a caller which of the two
|
|
1204
1203
|
// happened -- and this function cannot tell it by throwing, because
|
|
@@ -1212,7 +1211,7 @@ export function reconcileSnapshotRing(handle: AnnoStoreHandle): { droppedFiles:
|
|
|
1212
1211
|
rolledBack = false;
|
|
1213
1212
|
}
|
|
1214
1213
|
// AND DELIBERATELY NOT RETHROWN. A throw from here is swallowed by step 9's
|
|
1215
|
-
//
|
|
1214
|
+
// own error-swallowing wrap anyway, so rethrowing would buy nothing on the write path --
|
|
1216
1215
|
// and on `revertTo`'s own step-6 call site it would convert a COMMITTED
|
|
1217
1216
|
// write into a caller-visible failure, which prohibition 28-11 P5 forbids.
|
|
1218
1217
|
// The sweep changed nothing, which is precisely what `deferred` reports --
|
|
@@ -1306,7 +1305,7 @@ export function pruneSnapshots(handle: AnnoStoreHandle): boolean {
|
|
|
1306
1305
|
// revision number, and the same direction a deferred sweep already accepts.
|
|
1307
1306
|
//
|
|
1308
1307
|
// AND ITS `rollbackFailed` IS CONSUMED FOR THE SAME REASON, RETURNED RATHER
|
|
1309
|
-
// THAN DISCARDED
|
|
1308
|
+
// THAN DISCARDED. The argument recorded above for consuming
|
|
1310
1309
|
// `.deferred` is the argument for consuming this one, so it is extended here
|
|
1311
1310
|
// rather than restated: a fact this function throws away is a fact its caller
|
|
1312
1311
|
// cannot act on, and `rollbackFailed` reports the ONE state the sweep's own
|
|
@@ -1345,7 +1344,7 @@ export function pruneSnapshots(handle: AnnoStoreHandle): boolean {
|
|
|
1345
1344
|
// an orphan FILE -- which the reconciliation at the top of the NEXT prune
|
|
1346
1345
|
// can still see and retry. Under the earlier arrangement the row was
|
|
1347
1346
|
// deleted unconditionally after a swallowed failure, so the file became
|
|
1348
|
-
// invisible to the bound forever
|
|
1347
|
+
// invisible to the bound forever.
|
|
1349
1348
|
try {
|
|
1350
1349
|
// THE PATH IS COMPUTED HERE, at the delete, from the handle -- never read
|
|
1351
1350
|
// from the row. A persisted absolute path is environment-controlled input
|
|
@@ -1391,7 +1390,7 @@ export function pruneSnapshots(handle: AnnoStoreHandle): boolean {
|
|
|
1391
1390
|
* because `vacuum into` refuses an existing target and because two writers
|
|
1392
1391
|
* filling one file is the very collision this staging exists to remove.
|
|
1393
1392
|
*
|
|
1394
|
-
* AND THE IMAGE IS FSYNCED BEFORE THIS FUNCTION RETURNS
|
|
1393
|
+
* AND THE IMAGE IS FSYNCED BEFORE THIS FUNCTION RETURNS. The pointer row
|
|
1395
1394
|
* that names this file is inserted inside the write transaction and committed by
|
|
1396
1395
|
* SQLite, WHICH DOES FSYNC -- so without the `fsyncPath` below the ROW is
|
|
1397
1396
|
* durable and the FILE it names is not. Trap 10's durability premise covers a
|
|
@@ -1399,7 +1398,7 @@ export function pruneSnapshots(handle: AnnoStoreHandle): boolean {
|
|
|
1399
1398
|
* anyway; it does NOT cover a host crash, which loses the cache. The consequence
|
|
1400
1399
|
* is not an extra file: it is a PRESENT, PARTIAL snapshot that
|
|
1401
1400
|
* `retainedRevisions()` would advertise as revertible, which is exactly the input
|
|
1402
|
-
*
|
|
1401
|
+
* this defect was reproduced with. 28-16's step-2 and step-3b gates make that input a
|
|
1403
1402
|
* REFUSAL rather than a destruction; this call removes the input at its source
|
|
1404
1403
|
* rather than relying on the refusal, because a refusal on the only route back
|
|
1405
1404
|
* is still a lost history.
|
|
@@ -1443,7 +1442,7 @@ export function stageSnapshot(handle: AnnoStoreHandle, revision: number): string
|
|
|
1443
1442
|
* store that has been reverted and written forward again, rather than leaving
|
|
1444
1443
|
* it as an argument.
|
|
1445
1444
|
*
|
|
1446
|
-
* AND THE RING DIRECTORY IS FSYNCED AFTER THE RENAME
|
|
1445
|
+
* AND THE RING DIRECTORY IS FSYNCED AFTER THE RENAME. A rename is
|
|
1447
1446
|
* VISIBLE immediately and DURABLE only after the directory is fsynced -- the
|
|
1448
1447
|
* distinction `fsyncPath`'s own doc sentence records. The pointer row that names
|
|
1449
1448
|
* this file is inserted inside the write transaction a few statements below and
|
|
@@ -1454,7 +1453,7 @@ export function stageSnapshot(handle: AnnoStoreHandle, revision: number): string
|
|
|
1454
1453
|
* `SIGKILL` and NOT of a host crash, which loses the page cache; the surviving
|
|
1455
1454
|
* half-state there is a durable row naming a file whose bytes never reached
|
|
1456
1455
|
* disk, i.e. the PRESENT, PARTIAL snapshot `retainedRevisions()` would advertise
|
|
1457
|
-
* and the exact input
|
|
1456
|
+
* and the exact input this defect was reproduced with. 28-16 made that input a refusal
|
|
1458
1457
|
* rather than a destruction; this call removes the input at its source instead of
|
|
1459
1458
|
* relying on that refusal. The order is the same as `revertTo`'s steps 3 and 5
|
|
1460
1459
|
* and uses the same helper, deliberately -- a second durability idiom in one
|
|
@@ -1474,7 +1473,7 @@ export function stageSnapshot(handle: AnnoStoreHandle, revision: number): string
|
|
|
1474
1473
|
* half-state for a store that cannot be written at all: `openSync(dir, "r")`
|
|
1475
1474
|
* needs the ring directory READABLE, so a writable-but-unreadable ring (mode
|
|
1476
1475
|
* 0300 -- measured) would make every `setDataType` throw, and that is also the
|
|
1477
|
-
* precondition
|
|
1476
|
+
* precondition this fix's only behavioural control is built from. So the two
|
|
1478
1477
|
* reachable outcomes after an interruption stay exactly the two this module
|
|
1479
1478
|
* bounds them to -- a missing entry, or an entry whose contents ARE durable --
|
|
1480
1479
|
* and the failure of this call moves the outcome from the second to the first
|
|
@@ -1500,7 +1499,7 @@ function publishSnapshot(stagingPath: string, snapPath: string): void {
|
|
|
1500
1499
|
* are on.
|
|
1501
1500
|
*
|
|
1502
1501
|
* AND IT IS THE ONE PLACE A STAGING FILE IS REMOVED, which is why `revertTo`'s
|
|
1503
|
-
* three cleanup exits route through it too
|
|
1502
|
+
* three cleanup exits route through it too. Those three used to
|
|
1504
1503
|
* be bare `rmSync(staging, { force: true })` calls, so the module had two
|
|
1505
1504
|
* answers to "where does a staging file get removed" and a later reader looking
|
|
1506
1505
|
* for the one place found only half of them. The swallowing semantics below are
|
|
@@ -1575,12 +1574,12 @@ function runWriteSequence<T>(
|
|
|
1575
1574
|
doCommit: boolean,
|
|
1576
1575
|
baseRevision?: number,
|
|
1577
1576
|
): { revision: number; result: T } {
|
|
1578
|
-
// BEFORE `begin immediate`, AND BEFORE ANYTHING IS READ
|
|
1577
|
+
// BEFORE `begin immediate`, AND BEFORE ANYTHING IS READ. A previous
|
|
1579
1578
|
// write's housekeeping sweep ran on THIS connection and reported that its own
|
|
1580
1579
|
// `rollback` threw, so the connection may still hold an open transaction and
|
|
1581
1580
|
// the store's write lock. Without this refusal the next `begin immediate`
|
|
1582
1581
|
// surfaces SQLite's bare `cannot start a transaction within a transaction` --
|
|
1583
|
-
//
|
|
1582
|
+
// the exact reported symptom, outside the `ViceError` family, with nothing
|
|
1584
1583
|
// naming the cause or the remedy.
|
|
1585
1584
|
//
|
|
1586
1585
|
// AN EXISTING IN-FAMILY CLASS, NOT A NEW ONE: this is the same fact the commit
|
|
@@ -1607,7 +1606,7 @@ function runWriteSequence<T>(
|
|
|
1607
1606
|
);
|
|
1608
1607
|
}
|
|
1609
1608
|
|
|
1610
|
-
//
|
|
1609
|
+
// THE PRE-LOCK ARM. `stageSnapshot` runs BEFORE `begin immediate`, so
|
|
1611
1610
|
// it gets its own handler rather than sharing the outer one below: there is
|
|
1612
1611
|
// no transaction to roll back yet and no staged file to discard, so the two
|
|
1613
1612
|
// arms genuinely differ in what they have to undo. The reachable input is a
|
|
@@ -1626,7 +1625,7 @@ function runWriteSequence<T>(
|
|
|
1626
1625
|
);
|
|
1627
1626
|
}
|
|
1628
1627
|
|
|
1629
|
-
//
|
|
1628
|
+
// THE MAIN WINDOW: `begin immediate`, the compare-and-swap, the
|
|
1630
1629
|
// publication and the pointer-row insert, wrapped as ONE region. Its catch
|
|
1631
1630
|
// undoes both kinds of state this region can leave behind -- an open
|
|
1632
1631
|
// transaction with the compare-and-swap applied, and a staged `.tmp` -- and
|
|
@@ -1635,7 +1634,7 @@ function runWriteSequence<T>(
|
|
|
1635
1634
|
// keeps the family closed.
|
|
1636
1635
|
//
|
|
1637
1636
|
// 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
|
|
1637
|
+
// REDUNDANT AND MUST NOT BE "SIMPLIFIED" AWAY. `anno-store.test.ts`'s own
|
|
1639
1638
|
// control extracts the slice between `cas.changes` and `publishSnapshot` and
|
|
1640
1639
|
// asserts a `rollback` is present inside it, positioned after the
|
|
1641
1640
|
// `select revision from anno_meta` read -- that positioning is a VERIFIED
|
|
@@ -1679,7 +1678,7 @@ function runWriteSequence<T>(
|
|
|
1679
1678
|
// claims (see `retainedRevisions`).
|
|
1680
1679
|
handle.db.prepare("insert into anno_snapshot(revision) values (?)").run(rev);
|
|
1681
1680
|
} catch (e) {
|
|
1682
|
-
// THE ROLLBACK'S OUTCOME IS RECORDED, NOT ASSUMED
|
|
1681
|
+
// THE ROLLBACK'S OUTCOME IS RECORDED, NOT ASSUMED. The message below
|
|
1683
1682
|
// used to state the rollback as a fact after this `catch` had swallowed that
|
|
1684
1683
|
// rollback's own failure, so the one case in which the claim is false is
|
|
1685
1684
|
// exactly the case in which it was printed. Node 22's `DatabaseSync` exposes
|
|
@@ -1753,7 +1752,7 @@ function runWriteSequence<T>(
|
|
|
1753
1752
|
}
|
|
1754
1753
|
|
|
1755
1754
|
if (doCommit) {
|
|
1756
|
-
//
|
|
1755
|
+
// THE COMMIT ARM. This is the ONE statement in the sequence whose
|
|
1757
1756
|
// failure leaves the transaction OPEN with everything already applied -- the
|
|
1758
1757
|
// compare-and-swap, the caller's mutation and the pointer-row insert -- and
|
|
1759
1758
|
// it was outside every handler until this arm was added. A concurrent READER
|
|
@@ -1763,7 +1762,7 @@ function runWriteSequence<T>(
|
|
|
1763
1762
|
// transaction: a bare `Error: database is locked` after the connection's
|
|
1764
1763
|
// 5000 ms `busy_timeout`, outside the `ViceError` family, with the write
|
|
1765
1764
|
// lock still held and `currentRevision()` on this connection reporting the
|
|
1766
|
-
// ADVANCED revision for a write that never landed. On the
|
|
1765
|
+
// ADVANCED revision for a write that never landed. On the MCP tool
|
|
1767
1766
|
// path a handle lives as long as the session, so the leaked write lock
|
|
1768
1767
|
// locks every other connection out for that long.
|
|
1769
1768
|
//
|
|
@@ -1785,18 +1784,18 @@ function runWriteSequence<T>(
|
|
|
1785
1784
|
// accepted write's sweep reclaims.
|
|
1786
1785
|
//
|
|
1787
1786
|
// The refusal carries `code` from the underlying error when it has one
|
|
1788
|
-
// (
|
|
1787
|
+
// (the cheap half of that, on this wrap only), so a caller can ask whether the
|
|
1789
1788
|
// failure was lock contention without substring-matching the message.
|
|
1790
1789
|
// `cause` is deliberately NOT added: that needs a new field on
|
|
1791
1790
|
// `ViceErrorOptions` in `vice.ts`, a shared module outside this phase.
|
|
1792
1791
|
try {
|
|
1793
1792
|
commitTransaction(handle.db);
|
|
1794
1793
|
} catch (e) {
|
|
1795
|
-
// RECORDED, NOT ASSERTED
|
|
1794
|
+
// RECORDED, NOT ASSERTED. "the transaction has been rolled back"
|
|
1796
1795
|
// was stated as a fact directly under a `catch` that swallowed the
|
|
1797
1796
|
// 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
|
|
1797
|
+
// was still printed, and a refusal that reports that half-committed state
|
|
1798
|
+
// as its own repair sends the caller straight back into reusing a connection that may
|
|
1800
1799
|
// still hold the store's write lock. There is no cheap check available:
|
|
1801
1800
|
// Node 22's `DatabaseSync` exposes no transaction-state accessor (surface
|
|
1802
1801
|
// measured on this host: `open, close, prepare, exec, function, location,
|
|
@@ -1826,8 +1825,8 @@ function runWriteSequence<T>(
|
|
|
1826
1825
|
// The wording here is FREE. It used to be constrained: the
|
|
1827
1826
|
// single-commit-site control in `anno-seam.test.ts` counted the WORD
|
|
1828
1827
|
// `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
|
|
1828
|
+
// reading "commit ..." reddened a control in a different file. A later
|
|
1829
|
+
// revision replaced that count with a match on `exec()` calls carrying a bare
|
|
1831
1830
|
// statement literal, which no error message can satisfy, and the
|
|
1832
1831
|
// constraint went with it -- this value is unchanged only because
|
|
1833
1832
|
// changing it would be a gratuitous behaviour change.
|
|
@@ -1840,12 +1839,12 @@ function runWriteSequence<T>(
|
|
|
1840
1839
|
// branch because a sequence that never commits has no accepted write to
|
|
1841
1840
|
// bound.
|
|
1842
1841
|
//
|
|
1843
|
-
//
|
|
1842
|
+
// WRAPPED, AND DELIBERATELY NOT RETHROWN. By this line the
|
|
1844
1843
|
// transaction has already returned, so THE WRITE HAPPENED -- the mutation
|
|
1845
1844
|
// and the pointer row are durable. A housekeeping failure that threw from
|
|
1846
1845
|
// 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
|
-
//
|
|
1846
|
+
// would retry an ADDITIVE verb and produce a second row. That is exactly
|
|
1847
|
+
// the failure this wrap exists to prevent, and it became more likely rather than less once the
|
|
1849
1848
|
// sweep started taking the write lock.
|
|
1850
1849
|
//
|
|
1851
1850
|
// The consequence of swallowing is an UN-PRUNED RING -- extra files, the
|
|
@@ -1855,7 +1854,7 @@ function runWriteSequence<T>(
|
|
|
1855
1854
|
// one here would be new surface with its own stdio hazards on an MCP
|
|
1856
1855
|
// transport.
|
|
1857
1856
|
//
|
|
1858
|
-
// AND ITS REPORT IS CONSUMED
|
|
1857
|
+
// AND ITS REPORT IS CONSUMED. `pruneSnapshots` returns the sweep's
|
|
1859
1858
|
// `rollbackFailed` -- the one state the sweep's own handler cannot fix --
|
|
1860
1859
|
// and it is RECORDED ON THE HANDLE rather than thrown or logged. Not thrown,
|
|
1861
1860
|
// because by this line the write is committed and 28-11 P5 forbids reporting
|
|
@@ -1907,7 +1906,7 @@ export function applyWriteWithoutCommit<T>(
|
|
|
1907
1906
|
|
|
1908
1907
|
/**
|
|
1909
1908
|
* The module's ONE range insert. `bank` is a parameter rather than a hardcoded
|
|
1910
|
-
* `null
|
|
1909
|
+
* `null`: a remainder re-inserted by split-and-preserve carries the
|
|
1911
1910
|
* overlapped row's own `bank` forward, and a newly typed range carries `null`.
|
|
1912
1911
|
* `bank` is reserved and interpreted by nothing today, which is exactly why a
|
|
1913
1912
|
* write path that silently dropped it would be an unobservable loss a future
|
|
@@ -1996,7 +1995,7 @@ function entryPairKey(pair: readonly [number, number]): string {
|
|
|
1996
1995
|
|
|
1997
1996
|
/**
|
|
1998
1997
|
* What fragmenting `row` at the caller's range COSTS, or `null` when it costs
|
|
1999
|
-
* nothing this record could describe
|
|
1998
|
+
* nothing this record could describe.
|
|
2000
1999
|
*
|
|
2001
2000
|
* `null` in exactly two cases, both of them honest:
|
|
2002
2001
|
* * the row is not a split-table layout -- asked through `isSplitDataType`,
|
|
@@ -2097,15 +2096,14 @@ function splitReinterpretation(
|
|
|
2097
2096
|
* signal that distinguishes a no-op, and the revision is never that signal.
|
|
2098
2097
|
*
|
|
2099
2098
|
* ---------------------------------------------------------------------------
|
|
2100
|
-
* DECISION 1: THE SPLIT-ROW REMAINDER RULE -- TWO OUTCOMES, NEITHER SILENT
|
|
2101
|
-
* (CR-09 and CR-10).
|
|
2099
|
+
* DECISION 1: THE SPLIT-ROW REMAINDER RULE -- TWO OUTCOMES, NEITHER SILENT.
|
|
2102
2100
|
* ---------------------------------------------------------------------------
|
|
2103
2101
|
* A split-table row has TWO things that can go wrong when a caller's range
|
|
2104
2102
|
* fragments it, and this function answers them differently on purpose. The
|
|
2105
2103
|
* comment and the code below state ONE rule, and both halves of it are here.
|
|
2106
2104
|
*
|
|
2107
|
-
* (1) THE ODD REMAINDER IS REFUSED, and the whole retype is refused with it
|
|
2108
|
-
*
|
|
2105
|
+
* (1) THE ODD REMAINDER IS REFUSED, and the whole retype is refused with it.
|
|
2106
|
+
* A remainder that is not a legal shape for its OWN type -- the
|
|
2109
2107
|
* odd-byte-count tail of a fragmented split table is the reachable case -- is a
|
|
2110
2108
|
* row `setDataType` would decline to create and `resolveSplitTargets()` cannot
|
|
2111
2109
|
* decode. The store never persists a range row it would refuse at its own entry
|
|
@@ -2117,7 +2115,7 @@ function splitReinterpretation(
|
|
|
2117
2115
|
* outward to an entry boundary is forbidden outright by `anno-types.ts` trap 7.
|
|
2118
2116
|
*
|
|
2119
2117
|
* (2) THE EVEN REMAINDER IS ACCEPTED **WITH A REPORT** -- never accepted
|
|
2120
|
-
* silently
|
|
2118
|
+
* silently. Parity is not the only thing a fragment can break. A split
|
|
2121
2119
|
* table pairs byte `i` with byte `n + i`, so an entry's partner is a function of
|
|
2122
2120
|
* the row's START and its LENGTH, and changing either end re-pairs EVERY entry.
|
|
2123
2121
|
*
|
|
@@ -2156,17 +2154,17 @@ function splitReinterpretation(
|
|
|
2156
2154
|
* `delete`, and the ordering is the guarantee, not a tidiness preference: a
|
|
2157
2155
|
* refusal must cost nothing observable, and leaning on the transaction's
|
|
2158
2156
|
* rollback to undo a half-applied mutation would make that depend on a rollback
|
|
2159
|
-
* that the
|
|
2157
|
+
* that the commit handler's own `rollbackFailed` handling shows can itself fail.
|
|
2160
2158
|
* Compute, refuse, then mutate. The parity check runs FIRST and is untouched by
|
|
2161
2159
|
* the disclosure: a refusing retype returns no report because it returns nothing
|
|
2162
2160
|
* at all.
|
|
2163
2161
|
*
|
|
2164
2162
|
* ---------------------------------------------------------------------------
|
|
2165
|
-
* DECISION 2: THE UNION COLLAPSE IS INTENDED
|
|
2163
|
+
* DECISION 2: THE UNION COLLAPSE IS INTENDED.
|
|
2166
2164
|
* ---------------------------------------------------------------------------
|
|
2167
2165
|
* 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
|
-
*
|
|
2166
|
+
* inserts one row. That is intended, and it does not contradict the store's
|
|
2167
|
+
* own adjacency rule: that rule forbids the store joining adjacent ranges OF ITS OWN ACCORD, and
|
|
2170
2168
|
* here the caller asked for exactly one range and got exactly one range. The
|
|
2171
2169
|
* store still never joins two rows nobody asked about -- see the behavioural
|
|
2172
2170
|
* and structural adjacency controls.
|
|
@@ -2213,7 +2211,7 @@ function retype(
|
|
|
2213
2211
|
// for a split row that survives that question, what the fragmentation COSTS.
|
|
2214
2212
|
//
|
|
2215
2213
|
// WHAT THIS GATE DOES NOT ASK, said here because "THE GATE" reads absolute
|
|
2216
|
-
// and a reader will otherwise take it for one (
|
|
2214
|
+
// and a reader will otherwise take it for one (28-21 P1 / 28-07 P3):
|
|
2217
2215
|
// the shape question is asked of REMAINDERS, never of the overlapped row
|
|
2218
2216
|
// itself. A row the caller's range covers in full has no head and no tail,
|
|
2219
2217
|
// so neither branch below runs and its shape is never examined -- correctly,
|
|
@@ -2462,7 +2460,7 @@ export function listRanges(handle: AnnoStoreHandle): RangeRow[] {
|
|
|
2462
2460
|
* retained for it", with the oldest retained revision, the bound and the
|
|
2463
2461
|
* available list.
|
|
2464
2462
|
* * A ROW CLAIMS IT BUT ITS IMAGE WILL NOT OPEN as an annotation store (step
|
|
2465
|
-
* 2, second arm) -- the arm
|
|
2463
|
+
* 2, second arm) -- the arm added to cover an absent image and a
|
|
2466
2464
|
* present-but-unusable one alike, with the underlying reason quoted so the
|
|
2467
2465
|
* caller can tell which. This is the arm the old presence-only gate did not
|
|
2468
2466
|
* have, and its absence is what let the store be destroyed installing an
|
|
@@ -2485,7 +2483,7 @@ export function listRanges(handle: AnnoStoreHandle): RangeRow[] {
|
|
|
2485
2483
|
*/
|
|
2486
2484
|
/**
|
|
2487
2485
|
* The one gate on a revision-shaped argument, and it exists because ONE
|
|
2488
|
-
* unvalidated value lands in TWO places that can then disagree
|
|
2486
|
+
* unvalidated value lands in TWO places that can then disagree: a bound
|
|
2489
2487
|
* SQL parameter, and a snapshot FILENAME.
|
|
2490
2488
|
*
|
|
2491
2489
|
* Accepts a non-negative safe integer and nothing else. A numeric STRING is
|
|
@@ -2496,9 +2494,9 @@ export function listRanges(handle: AnnoStoreHandle): RangeRow[] {
|
|
|
2496
2494
|
* ring, which is the confusion `AnnoRevisionArgumentError`'s doc comment records
|
|
2497
2495
|
* in full.
|
|
2498
2496
|
*
|
|
2499
|
-
* DELIBERATELY NOT REUSED FOR `baseRevision`.
|
|
2500
|
-
*
|
|
2501
|
-
* this round, so the declination is recorded here rather than left looking like
|
|
2497
|
+
* DELIBERATELY NOT REUSED FOR `baseRevision`. An earlier round's review sketch
|
|
2498
|
+
* suggested it, but that suggestion is a separate finding the same round's
|
|
2499
|
+
* verification did not route to this round, so the declination is recorded here rather than left looking like
|
|
2502
2500
|
* an omission -- `runWriteSequence`'s existing `baseRevision` staleness refusal
|
|
2503
2501
|
* is this validator's SIBLING, not its client.
|
|
2504
2502
|
*/
|
|
@@ -2521,10 +2519,10 @@ function assertRevisionArgument(value: unknown, parameter: string): number {
|
|
|
2521
2519
|
}
|
|
2522
2520
|
|
|
2523
2521
|
export function revertTo(handle: AnnoStoreHandle, revision: number): AnnoStoreHandle {
|
|
2524
|
-
// STEP 0, AND IT IS FIRST FOR THE REASON
|
|
2522
|
+
// STEP 0, AND IT IS FIRST FOR THE REASON RECORDED ABOVE: this argument reaches
|
|
2525
2523
|
// a bound SQL parameter AND a filename, so it is judged before either exists.
|
|
2526
2524
|
// Before this line, `revertTo(handle, "1")` silently reverted the store and
|
|
2527
|
-
// `revertTo(handle, "0001")` refused with
|
|
2525
|
+
// `revertTo(handle, "0001")` refused with a CORRUPTION message.
|
|
2528
2526
|
assertRevisionArgument(revision, "revision");
|
|
2529
2527
|
|
|
2530
2528
|
// STEP 1. The pointer row -- the INDEX half of "retained". An EXISTENCE check
|
|
@@ -2542,8 +2540,8 @@ export function revertTo(handle: AnnoStoreHandle, revision: number): AnnoStoreHa
|
|
|
2542
2540
|
//
|
|
2543
2541
|
// THE FILE HALF NOW READS `snapshotOpenFailure` -- the same witness
|
|
2544
2542
|
// `retainedRevisions` reads, in its other position. It used to read
|
|
2545
|
-
// `existsSync` and nothing more, which
|
|
2546
|
-
// a database
|
|
2543
|
+
// `existsSync` and nothing more, which let a present image that was not
|
|
2544
|
+
// a database pass this gate, and the store was destroyed installing it. The
|
|
2547
2545
|
// "oldest retained" and "available revisions" figures are built from
|
|
2548
2546
|
// `retainedRevisions()` and never from the raw rows, so a refusal cannot
|
|
2549
2547
|
// steer the caller at a revision the very next call would also refuse.
|
|
@@ -2551,8 +2549,8 @@ export function revertTo(handle: AnnoStoreHandle, revision: number): AnnoStoreHa
|
|
|
2551
2549
|
// THE ARMS SPLIT ON THE ROW, NOT ON THE FILE'S PRESENCE, and that is
|
|
2552
2550
|
// deliberate rather than an omission. Asking "is the image absent" separately
|
|
2553
2551
|
// 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
|
|
2552
|
+
// path -- a second truth about one file, which is exactly how two of this
|
|
2553
|
+
// store's past defects happened. The two file-half sub-cases are distinguished by the QUOTED
|
|
2556
2554
|
// REASON instead: an absent image quotes "the file does not exist", a corrupt
|
|
2557
2555
|
// one quotes what SQLite or `openStore` said. One witness, one message, and
|
|
2558
2556
|
// the caller can still tell them apart.
|
|
@@ -2585,7 +2583,7 @@ export function revertTo(handle: AnnoStoreHandle, revision: number): AnnoStoreHa
|
|
|
2585
2583
|
|
|
2586
2584
|
const storePath = handle.path;
|
|
2587
2585
|
const dir = handle.dir;
|
|
2588
|
-
// THE STAGING NAME IS UNIQUE PER ATTEMPT, NOT PER (PID, REVISION)
|
|
2586
|
+
// THE STAGING NAME IS UNIQUE PER ATTEMPT, NOT PER (PID, REVISION),
|
|
2589
2587
|
// and it is built from `randomUUID` -- the SAME primitive `stageSnapshot`
|
|
2590
2588
|
// uses, so there is ONE answer in this module to "how is a staging name made
|
|
2591
2589
|
// unique" rather than two that can drift. `stageSnapshot`'s own doc comment
|
|
@@ -2602,12 +2600,12 @@ export function revertTo(handle: AnnoStoreHandle, revision: number): AnnoStoreHa
|
|
|
2602
2600
|
// addressed the same path.
|
|
2603
2601
|
//
|
|
2604
2602
|
// 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
|
-
//
|
|
2603
|
+
// would be prohibition 28-07 P3's exact shape: THE LEAK HALF STAYS OPEN.
|
|
2604
|
+
// A process killed between the copy and any of the three cleanups
|
|
2607
2605
|
// still leaves this file behind, and it sits beside the store rather than
|
|
2608
2606
|
// inside the ring directory, so `reconcileSnapshotRing`'s sweep -- anchored on
|
|
2609
2607
|
// `r<digits>.db` inside `snapshotDirFor()` -- does not and must not match it.
|
|
2610
|
-
// Nothing reclaims it. That is
|
|
2608
|
+
// Nothing reclaims it. That is the other half of this gap and it is not closed here.
|
|
2611
2609
|
//
|
|
2612
2610
|
// THE PID IS KEPT deliberately: it is the diagnostic that lets a human finding
|
|
2613
2611
|
// a leaked file say which process produced it, and the `.revert-` marker is
|
|
@@ -2618,8 +2616,8 @@ export function revertTo(handle: AnnoStoreHandle, revision: number): AnnoStoreHa
|
|
|
2618
2616
|
// failure reachable here -- `ENOENT`, `ENOSPC`, `EACCES` -- therefore leaves
|
|
2619
2617
|
// the caller a USABLE handle: nothing has been replaced yet, so
|
|
2620
2618
|
// `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.
|
|
2619
|
+
// caller can decide what to do. That is exactly the failure this ordering
|
|
2620
|
+
// exists to prevent, and it is fixed by ordering rather than by a rescue path.
|
|
2623
2621
|
try {
|
|
2624
2622
|
copyFileSync(snapPath, staging);
|
|
2625
2623
|
fsyncPath(staging);
|
|
@@ -2633,7 +2631,7 @@ export function revertTo(handle: AnnoStoreHandle, revision: number): AnnoStoreHa
|
|
|
2633
2631
|
);
|
|
2634
2632
|
}
|
|
2635
2633
|
|
|
2636
|
-
// STEP 3b, AND ITS POSITION IS THE GUARANTEE
|
|
2634
|
+
// STEP 3b, AND ITS POSITION IS THE GUARANTEE. OPEN THE STAGED IMAGE
|
|
2637
2635
|
// BEFORE ANYTHING IS CLOSED AND BEFORE ANYTHING IS RENAMED. Until this step
|
|
2638
2636
|
// existed the only witness that the image about to be installed was a store at
|
|
2639
2637
|
// all was `existsSync` -- and this module's own FIRST MEASURED FACT (see the
|
|
@@ -2698,8 +2696,8 @@ export function revertTo(handle: AnnoStoreHandle, revision: number): AnnoStoreHa
|
|
|
2698
2696
|
// function. The image just restored carries `anno_snapshot` rows for
|
|
2699
2697
|
// revisions whose files an earlier prune removed, and it leaves every
|
|
2700
2698
|
// 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
|
|
2699
|
+
// RESOLVED HERE, and the ROW half is deliberately left: since the fix above
|
|
2700
|
+
// the sweep abstains from the pointer-row direction entirely, because it cannot
|
|
2703
2701
|
// establish ownership of a row under a second spelling of the store file, so
|
|
2704
2702
|
// the restored image's stale rows are TOLERATED rather than deleted. This
|
|
2705
2703
|
// sentence previously claimed BOTH halves were resolved here -- the reversal
|
|
@@ -2733,7 +2731,7 @@ export function revertTo(handle: AnnoStoreHandle, revision: number): AnnoStoreHa
|
|
|
2733
2731
|
// sweep below can itself stall for up to the connection's five-second
|
|
2734
2732
|
// `busy_timeout` before `revertTo` returns.
|
|
2735
2733
|
//
|
|
2736
|
-
//
|
|
2734
|
+
// THE THIRD PROPERTY: THE SWEEP IS HOUSEKEEPING AND MUST NEVER COST THE
|
|
2737
2735
|
// CALLER A HANDLE. By this line the revert has ALREADY SUCCEEDED ON DISK --
|
|
2738
2736
|
// step 5's rename and directory fsync have returned, so the store file at
|
|
2739
2737
|
// `storePath` IS the reverted image whatever happens next. Round 3 observed
|
|
@@ -2762,7 +2760,7 @@ export function revertTo(handle: AnnoStoreHandle, revision: number): AnnoStoreHa
|
|
|
2762
2760
|
// thing in both of its controls rather than letting a green test imply a
|
|
2763
2761
|
// behavioural proof it does not carry.
|
|
2764
2762
|
//
|
|
2765
|
-
// AND THE REOPEN IS NOW INSIDE THE SAME GUARANTEE
|
|
2763
|
+
// AND THE REOPEN IS NOW INSIDE THE SAME GUARANTEE, WHICH IS WHERE IT
|
|
2766
2764
|
// BELONGED. The sentence above -- "a housekeeping failure never costs the
|
|
2767
2765
|
// caller a handle" -- used to hold only for the branch that CANNOT fire. The
|
|
2768
2766
|
// sweep call was guarded and is unreachable; the two `openStore` calls were
|
|
@@ -2804,14 +2802,14 @@ export function revertTo(handle: AnnoStoreHandle, revision: number): AnnoStoreHa
|
|
|
2804
2802
|
);
|
|
2805
2803
|
}
|
|
2806
2804
|
//
|
|
2807
|
-
// AND THE SWEEP'S RESULT IS BOUND RATHER THAN DISCARDED
|
|
2805
|
+
// AND THE SWEEP'S RESULT IS BOUND RATHER THAN DISCARDED, WHICH IS THE
|
|
2808
2806
|
// ARM THE `catch` ABOVE CANNOT SEE. `reconcileSnapshotRing` does not throw
|
|
2809
2807
|
// when its own `rollback` fails -- rethrowing there is forbidden by 28-11 P5,
|
|
2810
2808
|
// because on this very call site it would convert a LANDED revert into a
|
|
2811
2809
|
// caller-visible failure -- so it REPORTS the fact in `rollbackFailed`
|
|
2812
2810
|
// instead. Reaching that state WITHOUT a throw is exactly why the existing
|
|
2813
2811
|
// catch arm alone was not enough: `revertTo` would hand back a connection that
|
|
2814
|
-
// may still hold the store's write lock, which is
|
|
2812
|
+
// may still hold the store's write lock, which is the same reported symptom
|
|
2815
2813
|
// re-created on the revert path.
|
|
2816
2814
|
//
|
|
2817
2815
|
// THE REMEDY IS THE SAME BLOCK, REUSED RATHER THAN COPIED: close the
|
|
@@ -3016,7 +3014,7 @@ export function listComments(handle: AnnoStoreHandle): CommentRow[] {
|
|
|
3016
3014
|
* the existing one's id and two ends. Before that refusal existed this comment
|
|
3017
3015
|
* and `ScopeRow`'s made a claim the code did not honour, which is exactly the
|
|
3018
3016
|
* shape prohibition 28-07 P3 forbids. Adjacency is NOT overlap: two scopes that
|
|
3019
|
-
* merely touch at a boundary are two scopes, consistent with
|
|
3017
|
+
* merely touch at a boundary are two scopes, consistent with this store's own
|
|
3020
3018
|
* treatment of ranges.
|
|
3021
3019
|
*
|
|
3022
3020
|
* A BYTE-IDENTICAL REPEAT IS AN ACCEPTED NO-OP reporting `changed: false`, and
|
|
@@ -3028,7 +3026,7 @@ export function listComments(handle: AnnoStoreHandle): CommentRow[] {
|
|
|
3028
3026
|
* `AnnoWriteResult`'s own doc comment states that `changed` is the ONLY signal
|
|
3029
3027
|
* distinguishing a no-op from a real edit -- and for scopes it could never say
|
|
3030
3028
|
* no-op, so the module already had the policy and simply could not express it
|
|
3031
|
-
* here. Second,
|
|
3029
|
+
* here. Second, the MCP surface's own success criterion requires a repeated edit to
|
|
3032
3030
|
* SUCCEED reporting no change, so the surface this store mirrors does have the
|
|
3033
3031
|
* policy after all. The repeat is therefore accepted rather than refused, and
|
|
3034
3032
|
* the revision still advances by one, exactly like every other write entry
|
|
@@ -3049,7 +3047,7 @@ export function addScope(
|
|
|
3049
3047
|
// read the existing row inside the transaction and return `false`. It has
|
|
3050
3048
|
// to run before the overlap check, because a byte-identical scope
|
|
3051
3049
|
// overlaps itself and would otherwise be refused rather than accepted as
|
|
3052
|
-
// the no-op
|
|
3050
|
+
// the no-op that surface's own criterion requires.
|
|
3053
3051
|
const identical = db.prepare("select id from anno_scope where start = ? and end_inclusive = ?").get(start, endInclusive) as
|
|
3054
3052
|
| { id: number }
|
|
3055
3053
|
| undefined;
|
|
@@ -3099,10 +3097,10 @@ export function listScopes(handle: AnnoStoreHandle): ScopeRow[] {
|
|
|
3099
3097
|
* `changed: false` when no scope has that span.
|
|
3100
3098
|
*
|
|
3101
3099
|
* WHY THIS EXISTS, and why it is not an omission being corrected quietly.
|
|
3102
|
-
*
|
|
3103
|
-
* no inverse and carried the finding
|
|
3100
|
+
* A prior verification round recorded that `addScope`'s overlap refusal had
|
|
3101
|
+
* no inverse and carried the finding forward in as many words, "which puts
|
|
3104
3102
|
* `addScope` on an agent-driven surface where a mistyped span is likelier".
|
|
3105
|
-
*
|
|
3103
|
+
* The review that raised it spells out the consequence: one transposed end --
|
|
3106
3104
|
* `addScope($1000, $ffff)` -- makes every future scope from `$1000` upward
|
|
3107
3105
|
* permanently unaddable, recoverable only through `revertTo` inside the
|
|
3108
3106
|
* 32-revision ring, after which the mistake is permanent for the life of the
|
|
@@ -3158,13 +3156,12 @@ export function removeScope(
|
|
|
3158
3156
|
|
|
3159
3157
|
/**
|
|
3160
3158
|
* 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`).
|
|
3159
|
+
* stating WHY the user asked for it -- added at `SCHEMA_VERSION` 5.
|
|
3163
3160
|
*
|
|
3164
3161
|
* RECORDING AN EXCLUSION CHANGES NOTHING ABOUT WHICH BYTES THE EXPORT EMITS.
|
|
3165
3162
|
* The exporter still walks this range's full byte span and emits a real
|
|
3166
3163
|
* block, tagged with a visible marker comment, rather than a hole -- that is
|
|
3167
|
-
*
|
|
3164
|
+
* this feature's whole invariant. An exporter implementation that skipped the
|
|
3168
3165
|
* block on seeing an exclusion row would satisfy the word "exclude" and fail
|
|
3169
3166
|
* the requirement outright: this table is a RECORD, never a filter, and the
|
|
3170
3167
|
* store answers "what did the user record", never "should this range be
|
|
@@ -3206,7 +3203,7 @@ export function addExcludedRange(
|
|
|
3206
3203
|
if (reason.trim() === "") {
|
|
3207
3204
|
throw new AnnoCommentError(
|
|
3208
3205
|
`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
|
|
3206
|
+
`and loses WHY, which is precisely the half of criterion 2 this record exists to carry. Supply the reason the user gave.`,
|
|
3210
3207
|
{ reason: "empty reason" },
|
|
3211
3208
|
);
|
|
3212
3209
|
}
|
|
@@ -3528,7 +3525,7 @@ export function listProjectEnums(handle: AnnoStoreHandle): ProjectEnumRow[] {
|
|
|
3528
3525
|
|
|
3529
3526
|
/**
|
|
3530
3527
|
* Associates ONE address with ONE project enum, so the address's operand is
|
|
3531
|
-
* formatted through that enum's variants (`SCHEMA_VERSION` 3
|
|
3528
|
+
* formatted through that enum's variants (`SCHEMA_VERSION` 3).
|
|
3532
3529
|
*
|
|
3533
3530
|
* THE ASSOCIATION IS BY `anno_enum.id`, NEVER BY NAME, and that is the whole
|
|
3534
3531
|
* design of the table. `updateProjectEnum` renames an enum in place, keeping
|
|
@@ -3552,8 +3549,8 @@ export function listProjectEnums(handle: AnnoStoreHandle): ProjectEnumRow[] {
|
|
|
3552
3549
|
*
|
|
3553
3550
|
* Every argument is validated through `anno-types.ts`'s own assertions before
|
|
3554
3551
|
* any SQL runs. `parseStoreAddress` in particular refuses an UNPREFIXED numeric
|
|
3555
|
-
* string such as `"53280"` outright rather than guessing a base --
|
|
3556
|
-
*
|
|
3552
|
+
* string such as `"53280"` outright rather than guessing a base -- a recorded
|
|
3553
|
+
* failure in which a JSON `"1"` arrived verbatim and SQLite's column
|
|
3557
3554
|
* affinity turned an argument error into a corruption refusal.
|
|
3558
3555
|
*/
|
|
3559
3556
|
export function applyEnumUsage(
|
|
@@ -3663,7 +3660,7 @@ export function listEnumUsage(handle: AnnoStoreHandle): EnumUsageRow[] {
|
|
|
3663
3660
|
}
|
|
3664
3661
|
|
|
3665
3662
|
// ---------------------------------------------------------------------------
|
|
3666
|
-
// THE RUNTIME EVIDENCE TABLE (`anno_evid_exec`, `SCHEMA_VERSION` 4
|
|
3663
|
+
// THE RUNTIME EVIDENCE TABLE (`anno_evid_exec`, `SCHEMA_VERSION` 4).
|
|
3667
3664
|
// One raw shape used by all three functions below.
|
|
3668
3665
|
// ---------------------------------------------------------------------------
|
|
3669
3666
|
|
|
@@ -3701,9 +3698,9 @@ function toEvidExecRow(row: RawEvidExecRow): EvidExecRow {
|
|
|
3701
3698
|
|
|
3702
3699
|
/**
|
|
3703
3700
|
* Inserts one or more runtime-execution observations for one run identity,
|
|
3704
|
-
* keyed `(imageSha256, argvDigest, seed, address, sourceBank)` --
|
|
3705
|
-
* `no-
|
|
3706
|
-
*
|
|
3701
|
+
* keyed `(imageSha256, argvDigest, seed, address, sourceBank)` -- a live A/B
|
|
3702
|
+
* measured `no-perturbation` from instrumentation, so there is deliberately
|
|
3703
|
+
* no `run_class` discriminator.
|
|
3707
3704
|
*
|
|
3708
3705
|
* EVERY FIELD IS VALIDATED BEFORE THE FIRST STATEMENT RUNS, and every
|
|
3709
3706
|
* refusal is a named `AnnoTypeError`/`AnnoAddressError` carrying the
|
|
@@ -3720,8 +3717,7 @@ function toEvidExecRow(row: RawEvidExecRow): EvidExecRow {
|
|
|
3720
3717
|
* transaction that `commitTransaction` commits, so a kill mid-ingest leaves
|
|
3721
3718
|
* the set fully committed or fully absent, never a partial row set.
|
|
3722
3719
|
*
|
|
3723
|
-
* AN OBSERVATION ALREADY PRESENT IS SKIPPED, NOT RE-INSERTED
|
|
3724
|
-
* idempotent re-ingest): the existing row is selected first, by the full
|
|
3720
|
+
* AN OBSERVATION ALREADY PRESENT IS SKIPPED, NOT RE-INSERTED: the existing row is selected first, by the full
|
|
3725
3721
|
* unique key, and the insert only runs when it is absent. `changed` is
|
|
3726
3722
|
* `false` exactly when every observation in this call was already present --
|
|
3727
3723
|
* the same `changed`-is-the-only-no-op-signal contract `AnnoWriteResult`
|
|
@@ -3729,7 +3725,7 @@ function toEvidExecRow(row: RawEvidExecRow): EvidExecRow {
|
|
|
3729
3725
|
* advances by exactly one on every accepted call, no-op or not, for the same
|
|
3730
3726
|
* reason.
|
|
3731
3727
|
*
|
|
3732
|
-
* `insertedCount`
|
|
3728
|
+
* `insertedCount` IS COUNTED INSIDE THIS SAME TRANSACTION, never
|
|
3733
3729
|
* derived from a separate read taken before `applyWrite` opens it: a caller
|
|
3734
3730
|
* that wants "how many of these rows were actually new" must not be handed a
|
|
3735
3731
|
* number computed from a `listExecObservations()` snapshot that a concurrent
|
|
@@ -3857,12 +3853,11 @@ export function listExecObservations(
|
|
|
3857
3853
|
|
|
3858
3854
|
/**
|
|
3859
3855
|
* 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).
|
|
3856
|
+
* observation count, and the `denominator` those counts are a fraction of.
|
|
3862
3857
|
*
|
|
3863
3858
|
* WHY A DENOMINATOR IS RETURNED AT ALL, AND WHY IT NEVER FORMS A PERCENTAGE
|
|
3864
3859
|
* ITSELF. A bare count invites the reading "the rest is data" -- exactly the
|
|
3865
|
-
* soundness violation
|
|
3860
|
+
* soundness violation this table's own discipline forbids (see `RuntimeExecClass`'s own doc
|
|
3866
3861
|
* comment in `anno-types.ts`). `denominator` is `ADDRESS_MAX - ADDRESS_MIN +
|
|
3867
3862
|
* 1`, read from `anno-types.ts`'s own constants rather than the literal
|
|
3868
3863
|
* `65536` -- a caller comparing a run's `observationCount` against it forms
|
|
@@ -3889,7 +3884,7 @@ export function listObservedRuns(handle: AnnoStoreHandle): { runs: ObservedRunRo
|
|
|
3889
3884
|
|
|
3890
3885
|
/**
|
|
3891
3886
|
* Deletes every `anno_evid_exec` row for one run identity -- a bracket
|
|
3892
|
-
* reset
|
|
3887
|
+
* reset. A run identity holding no rows returns `changed: false`
|
|
3893
3888
|
* and is NOT an error: resetting an empty bracket is the ordinary thing,
|
|
3894
3889
|
* matching `clearEnumUsage`'s own direction for the identical case.
|
|
3895
3890
|
*
|
|
@@ -3927,13 +3922,13 @@ export function deleteExecObservationsForRun(
|
|
|
3927
3922
|
*
|
|
3928
3923
|
* THIS IS THE C-5 RECONCILIATION, written down here for a reader of the code
|
|
3929
3924
|
* rather than left in a plan. Two requirement texts look like they conflict:
|
|
3930
|
-
*
|
|
3925
|
+
* one requires cross-reference rows to carry their access kind from the
|
|
3931
3926
|
* first write, while the cross-reference criterion requires references to be
|
|
3932
3927
|
* DERIVED on every query and never cached on disk. Both hold at once, and this
|
|
3933
3928
|
* entry point is where:
|
|
3934
3929
|
*
|
|
3935
3930
|
* * the table and its `access_kind` column exist from the first write (the
|
|
3936
|
-
* `DDL` above), so
|
|
3931
|
+
* `DDL` above), so that requirement is satisfied structurally;
|
|
3937
3932
|
* * the only rows ever written here are references that CANNOT be recovered
|
|
3938
3933
|
* from the bytes -- hand-asserted, or resolved from something outside the
|
|
3939
3934
|
* program image. The `COMPUTED_JUMP` case is exactly that: a computed
|