@henols/vice-mcp 0.2.2 → 0.2.3
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/README.md +2 -2
- package/THIRD-PARTY-NOTICES.md +422 -1
- package/anno-bank.ts +171 -0
- package/anno-cli.ts +1674 -99
- package/anno-enum-gen.ts +416 -30
- package/anno-export-asm.ts +1175 -89
- package/anno-graphics.ts +338 -0
- package/anno-hazard-report.ts +1367 -0
- package/anno-import.ts +495 -0
- package/anno-join.ts +480 -0
- package/anno-provenance-ledger.ts +472 -0
- package/anno-register.ts +159 -0
- package/anno-store-export.ts +661 -0
- package/anno-store.ts +518 -2
- package/anno-tools.ts +1169 -16
- package/anno-types.ts +275 -2
- package/backend-detect.mts +124 -312
- package/build.ts +3 -1
- package/capture-predicate.ts +597 -0
- package/channel-lock.ts +349 -0
- package/evid-ingest.ts +217 -0
- package/evid-reconcile.ts +316 -0
- package/host-tool-client.ts +430 -0
- package/incident-record.ts +23 -12
- package/install-resources.ts +29 -13
- package/memmap-lookup.ts +285 -0
- package/package.json +27 -8
- package/prg-image.ts +1 -2
- package/repo-root.ts +87 -3
- package/resources/backend-detect.mjs +98 -236
- package/resources/broker-control.mjs +189 -16
- package/resources/broker-epoch.mjs +1 -1
- package/resources/broker-kill.mjs +8 -2
- package/resources/broker-launch.mjs +365 -210
- package/resources/broker-state.mjs +64 -18
- package/resources/container-guard.mjs +1 -1
- package/resources/ghidra-project.mjs +790 -0
- package/resources/host-tool.mjs +2561 -0
- package/resources/vice-broker.mjs +330 -184
- package/resources/vice-launcher.sh +127 -9
- package/stock-address.ts +1 -1
- package/stock-condition.ts +1 -1
- package/stock-connect.ts +9 -5
- package/stock-derived.ts +29 -37
- package/stock-diagnose.ts +200 -36
- package/stock-dispatch.ts +179 -77
- package/stock-handler.ts +1 -1
- package/stock-paths.ts +18 -14
- package/stock-petscii.ts +1 -1
- package/stock-protocol.ts +1 -1
- package/stock-recycle.ts +83 -2
- package/stock-reproducible-run.ts +811 -0
- package/stock-run-until.ts +100 -1
- package/stock-symbols.ts +4 -4
- package/stock-timing.ts +1 -1
- package/stop-oracle.ts +167 -0
- package/text-capability-probe.ts +660 -0
- package/text-connect.ts +157 -0
- package/text-protocol.ts +810 -0
- package/text-tools.ts +778 -0
- package/textmon-backtrace.ts +385 -0
- package/textmon-cpuhistory.ts +335 -0
- package/textmon-memmap.ts +494 -0
- package/textmon-profile.ts +458 -0
- package/textmon-registers.ts +748 -0
- package/tools-manifest.stock.json +864 -3
- package/vice-broker-client.ts +189 -42
- package/vice-errors.ts +268 -0
- package/vice-proxy.ts +339 -2144
- package/vsf-slice.ts +640 -0
- package/anno-d64.ts +0 -310
- package/capability-registry.ts +0 -390
- package/refresh-manifest.ts +0 -124
- package/tools-manifest.json +0 -1223
- package/vice-probe.ts +0 -278
- package/vice-sync.ts +0 -336
- package/vice.ts +0 -772
package/anno-store.ts
CHANGED
|
@@ -138,11 +138,17 @@ import {
|
|
|
138
138
|
assertCommentType,
|
|
139
139
|
assertDataType,
|
|
140
140
|
assertEnumName,
|
|
141
|
+
assertEvidSourceBank,
|
|
141
142
|
assertLabelKind,
|
|
142
143
|
assertLegalLabel,
|
|
143
144
|
assertRangeShape,
|
|
145
|
+
assertRunIdentityDigest,
|
|
146
|
+
assertRunIdentitySeed,
|
|
144
147
|
isSplitDataType,
|
|
145
148
|
splitEntryAddressPairs,
|
|
149
|
+
ADDRESS_MAX,
|
|
150
|
+
ADDRESS_MIN,
|
|
151
|
+
AnnoCommentError,
|
|
146
152
|
AnnoCommentGradeError,
|
|
147
153
|
AnnoLabelError,
|
|
148
154
|
AnnoRangeShapeError,
|
|
@@ -162,9 +168,13 @@ import {
|
|
|
162
168
|
type CommentType,
|
|
163
169
|
type ContradictedComment,
|
|
164
170
|
type DataType,
|
|
171
|
+
type EvidExecRow,
|
|
172
|
+
type EvidSourceBank,
|
|
173
|
+
type ExcludedRangeRow,
|
|
165
174
|
type LabelKind,
|
|
166
175
|
type LabelRow,
|
|
167
176
|
type EnumUsageRow,
|
|
177
|
+
type ObservedRunRow,
|
|
168
178
|
type ProjectEnumRow,
|
|
169
179
|
type RangeRow,
|
|
170
180
|
type ScopeRow,
|
|
@@ -179,7 +189,7 @@ import { CONFIDENCE_GRADES, parseConfidencePrefix, AnnoConfidenceGradeError } fr
|
|
|
179
189
|
// decide "rethrow unchanged" versus "wrap". Every `Anno*Error` in `anno-types.ts`
|
|
180
190
|
// already extends it, so nothing new enters the module graph -- `anno-types.ts`
|
|
181
191
|
// imports the same class from the same file.
|
|
182
|
-
import { ViceError } from "./vice.ts";
|
|
192
|
+
import { ViceError } from "./vice-errors.ts";
|
|
183
193
|
|
|
184
194
|
/**
|
|
185
195
|
* What every write entry point in this module returns.
|
|
@@ -241,6 +251,29 @@ export interface AnnoWriteResult {
|
|
|
241
251
|
* that NOTHING in this module interprets and no code path reads except the row
|
|
242
252
|
* mapper in `listRanges()`. It is reserved, and every row written today has it
|
|
243
253
|
* null.
|
|
254
|
+
*
|
|
255
|
+
* AT `SCHEMA_VERSION` 4 (EVID-01/EVID-02), ONE MORE TABLE IS ADDED:
|
|
256
|
+
* `anno_evid_exec`, the durable runtime-execution evidence table. See
|
|
257
|
+
* `anno-types.ts`'s `SCHEMA_VERSION` doc comment for what the bump buys and
|
|
258
|
+
* the decided, dated fate of an existing version-3 store (`reaffirm-refusal`
|
|
259
|
+
* -- no migration arm). `anno_evid_exec` carries NO `bank` column and NO
|
|
260
|
+
* nullable column at all: unlike the annotation tables above, every field on
|
|
261
|
+
* a row here is a fact the runtime evidence layer is licensed to assert, or
|
|
262
|
+
* the row does not exist. Its run-identity key is the bare triple
|
|
263
|
+
* `(image_sha256, argv_digest, seed)`, selected by plan 43-01's live A/B
|
|
264
|
+
* (`docs/phase43-instrumentation-perturbation-ab.md`, verdict
|
|
265
|
+
* `no-perturbation`) -- there is deliberately no `run_class` column.
|
|
266
|
+
*
|
|
267
|
+
* AT `SCHEMA_VERSION` 5 (`BUILD-07`), ONE MORE TABLE IS ADDED:
|
|
268
|
+
* `anno_excluded_range`, the durable record of a user-requested exclusion --
|
|
269
|
+
* its extent and the reason the user gave. See `anno-types.ts`'s
|
|
270
|
+
* `SCHEMA_VERSION` doc comment for what the bump buys, why a table was chosen
|
|
271
|
+
* over a column on `anno_range`, and the decided, dated fate of an existing
|
|
272
|
+
* version-4 store (`reaffirm-refusal` -- no migration arm). `reason` is `not
|
|
273
|
+
* null`: an exclusion with no reason is a hole with a row in front of it.
|
|
274
|
+
* `anno_excluded_range` carries NO `bank` column, for the same reason
|
|
275
|
+
* `ExcludedRangeRow`'s own doc comment gives -- an exclusion is a statement
|
|
276
|
+
* about the subject program, not a memory view.
|
|
244
277
|
*/
|
|
245
278
|
export const DDL = `
|
|
246
279
|
create table anno_meta (
|
|
@@ -307,11 +340,31 @@ create table anno_snapshot (
|
|
|
307
340
|
revision integer primary key
|
|
308
341
|
);
|
|
309
342
|
|
|
343
|
+
create table anno_evid_exec (
|
|
344
|
+
id integer primary key autoincrement,
|
|
345
|
+
image_sha256 text not null,
|
|
346
|
+
argv_digest text not null,
|
|
347
|
+
seed text not null,
|
|
348
|
+
address integer not null,
|
|
349
|
+
source_bank text not null,
|
|
350
|
+
unique(image_sha256, argv_digest, seed, address, source_bank)
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
create table anno_excluded_range (
|
|
354
|
+
id integer primary key autoincrement,
|
|
355
|
+
start integer not null,
|
|
356
|
+
end_inclusive integer not null,
|
|
357
|
+
reason text not null,
|
|
358
|
+
unique(start, end_inclusive)
|
|
359
|
+
);
|
|
360
|
+
|
|
310
361
|
create index anno_range_end_start on anno_range(end_inclusive, start);
|
|
311
362
|
create index anno_label_address on anno_label(address);
|
|
312
363
|
create index anno_comment_address on anno_comment(address);
|
|
313
364
|
create index anno_enum_usage_address on anno_enum_usage(address);
|
|
314
365
|
create index anno_xref_to on anno_xref(to_address);
|
|
366
|
+
create index anno_evid_exec_address on anno_evid_exec(address);
|
|
367
|
+
create index anno_excluded_range_start on anno_excluded_range(start);
|
|
315
368
|
`;
|
|
316
369
|
|
|
317
370
|
/** An open store: the connection, the resolved store path, and the directory
|
|
@@ -539,7 +592,19 @@ export function openStore(
|
|
|
539
592
|
|
|
540
593
|
if (meta.schema_version !== SCHEMA_VERSION) {
|
|
541
594
|
db.close();
|
|
542
|
-
|
|
595
|
+
// NAMES THE REMEDY AND DENIES NOTHING IS LOST (EVID-02's checkpoint,
|
|
596
|
+
// condition 2). This build refuses rather than upgrades -- see
|
|
597
|
+
// `anno-types.ts`'s `SCHEMA_VERSION` doc comment for the decided,
|
|
598
|
+
// dated reason -- and the refusal happens BEFORE any write, so the
|
|
599
|
+
// file on disk is exactly what it was a moment ago: its labels,
|
|
600
|
+
// comments and enums are not lost, only unreadable by this build.
|
|
601
|
+
throw new AnnoStoreCorruptError(
|
|
602
|
+
`${resolved}: schema_version ${meta.schema_version}, expected ${SCHEMA_VERSION} -- refusing to open rather than upgrade. This file is ` +
|
|
603
|
+
`left exactly as it was: nothing on it is read, rewritten or deleted by this refusal. Open it with a build whose SCHEMA_VERSION is ` +
|
|
604
|
+
`${meta.schema_version} to read it (see anno-types.ts's SCHEMA_VERSION doc comment for what changed at each version), or hand-copy ` +
|
|
605
|
+
`its rows into a fresh store at this build's version.`,
|
|
606
|
+
{ path: resolved },
|
|
607
|
+
);
|
|
543
608
|
}
|
|
544
609
|
|
|
545
610
|
// WR-04, THE LAST KNOWN FAMILY ESCAPE IN THIS FUNCTION. The two blocks either
|
|
@@ -3091,6 +3156,197 @@ export function removeScope(
|
|
|
3091
3156
|
return { revision, changed: result };
|
|
3092
3157
|
}
|
|
3093
3158
|
|
|
3159
|
+
/**
|
|
3160
|
+
* 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`).
|
|
3163
|
+
*
|
|
3164
|
+
* RECORDING AN EXCLUSION CHANGES NOTHING ABOUT WHICH BYTES THE EXPORT EMITS.
|
|
3165
|
+
* The exporter still walks this range's full byte span and emits a real
|
|
3166
|
+
* block, tagged with a visible marker comment, rather than a hole -- that is
|
|
3167
|
+
* `BUILD-07`'s whole invariant. An exporter implementation that skipped the
|
|
3168
|
+
* block on seeing an exclusion row would satisfy the word "exclude" and fail
|
|
3169
|
+
* the requirement outright: this table is a RECORD, never a filter, and the
|
|
3170
|
+
* store answers "what did the user record", never "should this range be
|
|
3171
|
+
* excluded".
|
|
3172
|
+
*
|
|
3173
|
+
* `reason` is validated through `assertCommentText()` -- the ONE comment-text
|
|
3174
|
+
* vocabulary this store has -- BEFORE the write opens, and an empty or
|
|
3175
|
+
* whitespace-only reason is refused with its own message: a `not null`
|
|
3176
|
+
* column satisfied by `""` records that something was excluded and loses WHY,
|
|
3177
|
+
* which is precisely the half of criterion 2 this record exists to carry.
|
|
3178
|
+
*
|
|
3179
|
+
* IDEMPOTENCE FIRST, inside the transaction and BEFORE the overlap check, the
|
|
3180
|
+
* same shape `addScope` uses: an identical repeat -- same extent, same reason
|
|
3181
|
+
* -- is an accepted NO-OP reporting `changed: false`. The SAME extent with a
|
|
3182
|
+
* DIFFERENT reason is REFUSED rather than silently overwritten -- the stored
|
|
3183
|
+
* reason is left exactly as it was, and the route to change it is to remove
|
|
3184
|
+
* the record with `removeExcludedRange` and add it again. Silently replacing
|
|
3185
|
+
* what somebody wrote and reporting success is the failure mode this store's
|
|
3186
|
+
* comment and label verbs already refuse.
|
|
3187
|
+
*
|
|
3188
|
+
* OVERLAP IS REFUSED using `addScope()`'s EXACT predicate --
|
|
3189
|
+
* `start <= ? and end_inclusive >= ?` with the two arguments TRANSPOSED, and
|
|
3190
|
+
* `order by id limit 1` so the message is reproducible -- so adjacency falls
|
|
3191
|
+
* out of the `>=` rather than a second rule: two exclusion records that
|
|
3192
|
+
* merely TOUCH at a boundary are disjoint and both accepted, and they stay
|
|
3193
|
+
* TWO records. The incoming record is NEVER trimmed or split; a caller
|
|
3194
|
+
* wanting a disjoint span reads `listExcludedRanges()` first, or removes the
|
|
3195
|
+
* conflicting record with `removeExcludedRange`.
|
|
3196
|
+
*/
|
|
3197
|
+
export function addExcludedRange(
|
|
3198
|
+
handle: AnnoStoreHandle,
|
|
3199
|
+
args: { start: number | string; endInclusive: number | string; reason: string; baseRevision?: number },
|
|
3200
|
+
): AnnoWriteResult {
|
|
3201
|
+
const start = parseStoreAddress(args.start, { what: "start" });
|
|
3202
|
+
const endInclusive = parseStoreAddress(args.endInclusive, { what: "endInclusive" });
|
|
3203
|
+
assertRangeShape(start, endInclusive, "byte");
|
|
3204
|
+
|
|
3205
|
+
const reason = assertCommentText(args.reason, { what: "exclusion reason" });
|
|
3206
|
+
if (reason.trim() === "") {
|
|
3207
|
+
throw new AnnoCommentError(
|
|
3208
|
+
`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 BUILD-07's criterion 2 this record exists to carry. Supply the reason the user gave.`,
|
|
3210
|
+
{ reason: "empty reason" },
|
|
3211
|
+
);
|
|
3212
|
+
}
|
|
3213
|
+
|
|
3214
|
+
const { revision, result } = applyWrite(
|
|
3215
|
+
handle,
|
|
3216
|
+
(db) => {
|
|
3217
|
+
// IDEMPOTENCE FIRST, `addScope`'s own shape: read the existing row
|
|
3218
|
+
// inside the transaction and BEFORE the overlap check, because a
|
|
3219
|
+
// byte-identical exclusion overlaps itself and would otherwise be
|
|
3220
|
+
// refused rather than accepted as the no-op an identical repeat requires.
|
|
3221
|
+
const identical = db
|
|
3222
|
+
.prepare("select id, reason from anno_excluded_range where start = ? and end_inclusive = ?")
|
|
3223
|
+
.get(start, endInclusive) as { id: number; reason: string } | undefined;
|
|
3224
|
+
if (identical) {
|
|
3225
|
+
if (identical.reason === reason) return false;
|
|
3226
|
+
throw new AnnoRangeShapeError(
|
|
3227
|
+
`exclusion ${start}..${endInclusive} ($${start.toString(16).padStart(4, "0")}..$${endInclusive.toString(16).padStart(4, "0")}) ` +
|
|
3228
|
+
`is already recorded (id=${identical.id}) with a DIFFERENT reason -- the stored reason is left EXACTLY as it was. Refusing rather ` +
|
|
3229
|
+
`than silently overwriting what somebody wrote: remove the record with removeExcludedRange and add it again to change the reason.`,
|
|
3230
|
+
{ start, endInclusive },
|
|
3231
|
+
);
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
// TWO EXCLUSIONS OVERLAP IFF each starts at or before the other ends.
|
|
3235
|
+
// ADJACENCY FALLS OUT OF THE `>=`, `addScope()`'s exact predicate: an
|
|
3236
|
+
// existing exclusion ending at exactly `start - 1` fails
|
|
3237
|
+
// `end_inclusive >= start`, so touching is not overlapping. `order by id
|
|
3238
|
+
// limit 1` reports the FIRST conflicting row so the message is
|
|
3239
|
+
// reproducible.
|
|
3240
|
+
const overlapper = db
|
|
3241
|
+
.prepare("select id, start, end_inclusive from anno_excluded_range where start <= ? and end_inclusive >= ? order by id limit 1")
|
|
3242
|
+
.get(endInclusive, start) as { id: number; start: number; end_inclusive: number } | undefined;
|
|
3243
|
+
if (overlapper) {
|
|
3244
|
+
throw new AnnoRangeShapeError(
|
|
3245
|
+
`exclusion ${start}..${endInclusive} ($${start.toString(16).padStart(4, "0")}..$${endInclusive.toString(16).padStart(4, "0")}) ` +
|
|
3246
|
+
`overlaps the existing exclusion id=${overlapper.id} ${overlapper.start}..${overlapper.end_inclusive} ` +
|
|
3247
|
+
`($${overlapper.start.toString(16).padStart(4, "0")}..$${overlapper.end_inclusive.toString(16).padStart(4, "0")}) -- the incoming ` +
|
|
3248
|
+
`record is NOT trimmed and NOT split: supply a range disjoint from every existing exclusion. Two exclusions that merely TOUCH at a ` +
|
|
3249
|
+
`boundary are disjoint and both accepted. Read listExcludedRanges() first, or removeExcludedRange the conflicting record.`,
|
|
3250
|
+
{ start, endInclusive },
|
|
3251
|
+
);
|
|
3252
|
+
}
|
|
3253
|
+
|
|
3254
|
+
db.prepare("insert into anno_excluded_range(start, end_inclusive, reason) values (?, ?, ?)").run(start, endInclusive, reason);
|
|
3255
|
+
return true;
|
|
3256
|
+
},
|
|
3257
|
+
{ baseRevision: args.baseRevision },
|
|
3258
|
+
);
|
|
3259
|
+
return { revision, changed: result };
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3262
|
+
/** Every recorded exclusion, in ascending `id` order -- insertion order,
|
|
3263
|
+
* matching `listScopes()` and `listRanges()`. A consumer needing address
|
|
3264
|
+
* order sorts it itself, because a second ordering in the store would be a
|
|
3265
|
+
* second answer to the same question. `anno_excluded_range` has no `bank`
|
|
3266
|
+
* column, in the same shape `listScopes()`'s own doc comment uses for the
|
|
3267
|
+
* same absence: an exclusion is a statement about the subject program, not a
|
|
3268
|
+
* memory view. */
|
|
3269
|
+
export function listExcludedRanges(handle: AnnoStoreHandle): ExcludedRangeRow[] {
|
|
3270
|
+
const rows = handle.db.prepare("select id, start, end_inclusive, reason from anno_excluded_range order by id").all() as {
|
|
3271
|
+
id: number;
|
|
3272
|
+
start: number;
|
|
3273
|
+
end_inclusive: number;
|
|
3274
|
+
reason: string;
|
|
3275
|
+
}[];
|
|
3276
|
+
return rows.map((row) => ({ id: row.id, start: row.start, endInclusive: row.end_inclusive, reason: row.reason }));
|
|
3277
|
+
}
|
|
3278
|
+
|
|
3279
|
+
/**
|
|
3280
|
+
* Removes the exclusion whose span is EXACTLY `start..endInclusive`, and
|
|
3281
|
+
* returns `changed: false` when NO exclusion overlaps that span at all --
|
|
3282
|
+
* the exact inverse of `addExcludedRange`, following `removeScope()`.
|
|
3283
|
+
*
|
|
3284
|
+
* THE SPAN MUST MATCH EXACTLY -- both ends, as stored. A record is never
|
|
3285
|
+
* trimmed, split, or partially removed: a span that PARTIALLY OVERLAPS an
|
|
3286
|
+
* existing record (but does not match it end-for-end) is REFUSED BY NAME
|
|
3287
|
+
* rather than silently ignored, because a partial removal would leave a
|
|
3288
|
+
* shape nothing downstream can express, while reporting success. This is
|
|
3289
|
+
* stricter than `removeScope()`, which reports a mismatched span as a plain
|
|
3290
|
+
* no-op -- an exclusion's reason makes a near-miss removal more dangerous to
|
|
3291
|
+
* treat as "nothing happened", since a caller who meant to clear the record
|
|
3292
|
+
* would otherwise walk away believing it gone. A caller that does not know
|
|
3293
|
+
* the stored span reads it from `listExcludedRanges()` first.
|
|
3294
|
+
*
|
|
3295
|
+
* REMOVING A SPAN THAT DOES NOT OVERLAP ANYTHING STORED IS AN ACCEPTED NO-OP
|
|
3296
|
+
* reporting `changed: false`, matching `removeScope`'s own direction: an
|
|
3297
|
+
* inverse that refuses when there is genuinely nothing to undo makes "undo
|
|
3298
|
+
* this" conditional on knowing whether it was ever done.
|
|
3299
|
+
*/
|
|
3300
|
+
export function removeExcludedRange(
|
|
3301
|
+
handle: AnnoStoreHandle,
|
|
3302
|
+
args: { start: number | string; endInclusive: number | string; baseRevision?: number },
|
|
3303
|
+
): AnnoWriteResult {
|
|
3304
|
+
const start = parseStoreAddress(args.start, { what: "start" });
|
|
3305
|
+
const endInclusive = parseStoreAddress(args.endInclusive, { what: "endInclusive" });
|
|
3306
|
+
// The SAME non-split shape check `addExcludedRange`/`removeScope` use, and
|
|
3307
|
+
// for the same reason: an exclusion is not a table, so the split-table
|
|
3308
|
+
// even-count rule must not apply to it.
|
|
3309
|
+
assertRangeShape(start, endInclusive, "byte");
|
|
3310
|
+
|
|
3311
|
+
const { revision, result } = applyWrite(
|
|
3312
|
+
handle,
|
|
3313
|
+
(db) => {
|
|
3314
|
+
const existing = db.prepare("select id from anno_excluded_range where start = ? and end_inclusive = ?").get(start, endInclusive) as
|
|
3315
|
+
| { id: number }
|
|
3316
|
+
| undefined;
|
|
3317
|
+
if (existing) {
|
|
3318
|
+
db.prepare("delete from anno_excluded_range where id = ?").run(existing.id);
|
|
3319
|
+
return true;
|
|
3320
|
+
}
|
|
3321
|
+
|
|
3322
|
+
// NO EXACT MATCH. Before reporting the ordinary "nothing to undo"
|
|
3323
|
+
// no-op, check whether the incoming span PARTIALLY overlaps a stored
|
|
3324
|
+
// record -- the same overlap predicate `addExcludedRange` uses. That
|
|
3325
|
+
// case is refused BY NAME rather than treated as a no-op, because the
|
|
3326
|
+
// caller plainly meant to remove something that exists and a silent
|
|
3327
|
+
// no-op would misreport the outcome.
|
|
3328
|
+
const overlapper = db
|
|
3329
|
+
.prepare("select id, start, end_inclusive from anno_excluded_range where start <= ? and end_inclusive >= ? order by id limit 1")
|
|
3330
|
+
.get(endInclusive, start) as { id: number; start: number; end_inclusive: number } | undefined;
|
|
3331
|
+
if (overlapper) {
|
|
3332
|
+
throw new AnnoRangeShapeError(
|
|
3333
|
+
`removeExcludedRange: ${start}..${endInclusive} ($${start.toString(16).padStart(4, "0")}..${endInclusive
|
|
3334
|
+
.toString(16)
|
|
3335
|
+
.padStart(4, "0")}) does not EXACTLY match the existing exclusion id=${overlapper.id} ${overlapper.start}..${overlapper.end_inclusive} ` +
|
|
3336
|
+
`($${overlapper.start.toString(16).padStart(4, "0")}..$${overlapper.end_inclusive.toString(16).padStart(4, "0")}), which it ` +
|
|
3337
|
+
`partially overlaps -- a record is never trimmed, split, or partially removed, because that would leave a shape nothing downstream ` +
|
|
3338
|
+
`can express while reporting success. Read listExcludedRanges() first to find the exact stored span.`,
|
|
3339
|
+
{ start, endInclusive },
|
|
3340
|
+
);
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3343
|
+
return false;
|
|
3344
|
+
},
|
|
3345
|
+
{ baseRevision: args.baseRevision },
|
|
3346
|
+
);
|
|
3347
|
+
return { revision, changed: result };
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3094
3350
|
/**
|
|
3095
3351
|
* Validates one project enum's variants mapping and returns it with its KEYS
|
|
3096
3352
|
* VERBATIM.
|
|
@@ -3406,6 +3662,266 @@ export function listEnumUsage(handle: AnnoStoreHandle): EnumUsageRow[] {
|
|
|
3406
3662
|
}));
|
|
3407
3663
|
}
|
|
3408
3664
|
|
|
3665
|
+
// ---------------------------------------------------------------------------
|
|
3666
|
+
// THE RUNTIME EVIDENCE TABLE (`anno_evid_exec`, `SCHEMA_VERSION` 4, EVID-01).
|
|
3667
|
+
// One raw shape used by all three functions below.
|
|
3668
|
+
// ---------------------------------------------------------------------------
|
|
3669
|
+
|
|
3670
|
+
/** One `anno_evid_exec` row exactly as the column names read on disk --
|
|
3671
|
+
* `snake_case`, matching every other raw-row shape in this module.
|
|
3672
|
+
*
|
|
3673
|
+
* A `type` ALIAS RATHER THAN AN `interface`, and that is load-bearing rather
|
|
3674
|
+
* than stylistic: `node:sqlite`'s `all()` returns `Record<string,
|
|
3675
|
+
* SQLOutputValue>[]`, and casting that to a NAMED `interface` fails TS's
|
|
3676
|
+
* type-assertion comparability check ("neither type sufficiently overlaps")
|
|
3677
|
+
* even though the shapes are identical -- a `type` alias to the same object
|
|
3678
|
+
* shape is accepted. Measured against this exact query shape during this
|
|
3679
|
+
* plan's own implementation. */
|
|
3680
|
+
type RawEvidExecRow = {
|
|
3681
|
+
id: number;
|
|
3682
|
+
image_sha256: string;
|
|
3683
|
+
argv_digest: string;
|
|
3684
|
+
seed: string;
|
|
3685
|
+
address: number;
|
|
3686
|
+
source_bank: string;
|
|
3687
|
+
};
|
|
3688
|
+
|
|
3689
|
+
/** `RawEvidExecRow` -> `EvidExecRow`, the one mapping site both read functions
|
|
3690
|
+
* below share, so the two never drift into disagreeing about the shape. */
|
|
3691
|
+
function toEvidExecRow(row: RawEvidExecRow): EvidExecRow {
|
|
3692
|
+
return {
|
|
3693
|
+
id: row.id,
|
|
3694
|
+
imageSha256: row.image_sha256,
|
|
3695
|
+
argvDigest: row.argv_digest,
|
|
3696
|
+
seed: row.seed,
|
|
3697
|
+
address: row.address,
|
|
3698
|
+
sourceBank: row.source_bank as EvidSourceBank,
|
|
3699
|
+
};
|
|
3700
|
+
}
|
|
3701
|
+
|
|
3702
|
+
/**
|
|
3703
|
+
* Inserts one or more runtime-execution observations for one run identity,
|
|
3704
|
+
* keyed `(imageSha256, argvDigest, seed, address, sourceBank)` -- the
|
|
3705
|
+
* `no-change` composite plan 43-01's live A/B selected, with no `run_class`
|
|
3706
|
+
* discriminator (`docs/phase43-instrumentation-perturbation-ab.md`).
|
|
3707
|
+
*
|
|
3708
|
+
* EVERY FIELD IS VALIDATED BEFORE THE FIRST STATEMENT RUNS, and every
|
|
3709
|
+
* refusal is a named `AnnoTypeError`/`AnnoAddressError` carrying the
|
|
3710
|
+
* offending value and the valid domain -- this module's existing refusal
|
|
3711
|
+
* register, never a fresh one. A caller-supplied `imageSha256`/`argvDigest`
|
|
3712
|
+
* that is not exactly 64 lowercase hex characters, a `seed` that is empty,
|
|
3713
|
+
* an `address` outside `ADDRESS_MIN..ADDRESS_MAX`, or a `sourceBank` outside
|
|
3714
|
+
* the frozen three is refused BEFORE the write transaction opens, so a bad
|
|
3715
|
+
* argument never reaches SQL and never partially inserts the rest of the
|
|
3716
|
+
* batch.
|
|
3717
|
+
*
|
|
3718
|
+
* THE WHOLE INSERT IS ONE `applyWrite` CALLBACK (T-43-09): every observation
|
|
3719
|
+
* in `args.observations` is written -- or skipped -- inside the SAME
|
|
3720
|
+
* transaction that `commitTransaction` commits, so a kill mid-ingest leaves
|
|
3721
|
+
* the set fully committed or fully absent, never a partial row set.
|
|
3722
|
+
*
|
|
3723
|
+
* AN OBSERVATION ALREADY PRESENT IS SKIPPED, NOT RE-INSERTED (EVID-01's
|
|
3724
|
+
* idempotent re-ingest): the existing row is selected first, by the full
|
|
3725
|
+
* unique key, and the insert only runs when it is absent. `changed` is
|
|
3726
|
+
* `false` exactly when every observation in this call was already present --
|
|
3727
|
+
* the same `changed`-is-the-only-no-op-signal contract `AnnoWriteResult`
|
|
3728
|
+
* states for every other write entry point in this module. `revision` still
|
|
3729
|
+
* advances by exactly one on every accepted call, no-op or not, for the same
|
|
3730
|
+
* reason.
|
|
3731
|
+
*
|
|
3732
|
+
* `insertedCount` (WR-02) IS COUNTED INSIDE THIS SAME TRANSACTION, never
|
|
3733
|
+
* derived from a separate read taken before `applyWrite` opens it: a caller
|
|
3734
|
+
* that wants "how many of these rows were actually new" must not be handed a
|
|
3735
|
+
* number computed from a `listExecObservations()` snapshot that a concurrent
|
|
3736
|
+
* writer to the SAME run identity could have moved past between that read
|
|
3737
|
+
* and this insert's own commit. Counting the per-row `existing`/insert
|
|
3738
|
+
* branch already taken above is the one place this number can be exact.
|
|
3739
|
+
*/
|
|
3740
|
+
export interface InsertExecObservationsResult extends AnnoWriteResult {
|
|
3741
|
+
insertedCount: number;
|
|
3742
|
+
}
|
|
3743
|
+
|
|
3744
|
+
export function insertExecObservations(
|
|
3745
|
+
handle: AnnoStoreHandle,
|
|
3746
|
+
args: {
|
|
3747
|
+
imageSha256: unknown;
|
|
3748
|
+
argvDigest: unknown;
|
|
3749
|
+
seed: unknown;
|
|
3750
|
+
observations: readonly { address: unknown; sourceBank: unknown }[];
|
|
3751
|
+
baseRevision?: number;
|
|
3752
|
+
},
|
|
3753
|
+
): InsertExecObservationsResult {
|
|
3754
|
+
const imageSha256 = assertRunIdentityDigest(args.imageSha256, "imageSha256");
|
|
3755
|
+
const argvDigest = assertRunIdentityDigest(args.argvDigest, "argvDigest");
|
|
3756
|
+
const seed = assertRunIdentitySeed(args.seed);
|
|
3757
|
+
|
|
3758
|
+
if (!Array.isArray(args.observations) || args.observations.length === 0) {
|
|
3759
|
+
throw new AnnoTypeError(`observations must be a non-empty array, got ${JSON.stringify(args.observations)}`, {
|
|
3760
|
+
dataType: args.observations,
|
|
3761
|
+
});
|
|
3762
|
+
}
|
|
3763
|
+
// VALIDATED IN FULL BEFORE THE FIRST STATEMENT, per this function's own
|
|
3764
|
+
// doc comment: a bad entry at index 9 must not leave entries 0..8 written.
|
|
3765
|
+
const parsedObservations = args.observations.map((obs) => ({
|
|
3766
|
+
address: parseStoreAddress((obs as { address: unknown }).address, { what: "address" }),
|
|
3767
|
+
sourceBank: assertEvidSourceBank((obs as { sourceBank: unknown }).sourceBank),
|
|
3768
|
+
}));
|
|
3769
|
+
|
|
3770
|
+
const { revision, result } = applyWrite(
|
|
3771
|
+
handle,
|
|
3772
|
+
(db) => {
|
|
3773
|
+
let insertedCount = 0;
|
|
3774
|
+
for (const obs of parsedObservations) {
|
|
3775
|
+
const existing = db
|
|
3776
|
+
.prepare("select id from anno_evid_exec where image_sha256 = ? and argv_digest = ? and seed = ? and address = ? and source_bank = ?")
|
|
3777
|
+
.get(imageSha256, argvDigest, seed, obs.address, obs.sourceBank) as { id: number } | undefined;
|
|
3778
|
+
if (existing) continue;
|
|
3779
|
+
db.prepare("insert into anno_evid_exec(image_sha256, argv_digest, seed, address, source_bank) values (?, ?, ?, ?, ?)").run(
|
|
3780
|
+
imageSha256,
|
|
3781
|
+
argvDigest,
|
|
3782
|
+
seed,
|
|
3783
|
+
obs.address,
|
|
3784
|
+
obs.sourceBank,
|
|
3785
|
+
);
|
|
3786
|
+
insertedCount++;
|
|
3787
|
+
}
|
|
3788
|
+
return insertedCount;
|
|
3789
|
+
},
|
|
3790
|
+
{ baseRevision: args.baseRevision },
|
|
3791
|
+
);
|
|
3792
|
+
return { revision, changed: result > 0, insertedCount: result };
|
|
3793
|
+
}
|
|
3794
|
+
|
|
3795
|
+
/** One shape used by `listExecObservations`'s four fixed queries below. */
|
|
3796
|
+
type EvidRunIdentityFilter = { imageSha256: string; argvDigest: string; seed: string };
|
|
3797
|
+
|
|
3798
|
+
/**
|
|
3799
|
+
* Every runtime-execution observation, in ascending ADDRESS then `id` order,
|
|
3800
|
+
* with optional filters on `address` and on the full run identity. Never a
|
|
3801
|
+
* `select *` -- every column is named.
|
|
3802
|
+
*
|
|
3803
|
+
* A RUN-IDENTITY FILTER IS ALL THREE FIELDS TOGETHER OR NONE. A partial
|
|
3804
|
+
* identity (the seed alone, say) would silently widen the match to every
|
|
3805
|
+
* image/argv pair that happens to share it, which is not what "filter by run
|
|
3806
|
+
* identity" means -- refused BY NAME rather than accepted as a wider query
|
|
3807
|
+
* nobody asked for.
|
|
3808
|
+
*/
|
|
3809
|
+
export function listExecObservations(
|
|
3810
|
+
handle: AnnoStoreHandle,
|
|
3811
|
+
opts: { address?: number | string; imageSha256?: unknown; argvDigest?: unknown; seed?: unknown } = {},
|
|
3812
|
+
): EvidExecRow[] {
|
|
3813
|
+
const hasAddress = opts.address !== undefined;
|
|
3814
|
+
const identityFieldsGiven = [opts.imageSha256, opts.argvDigest, opts.seed].filter((v) => v !== undefined).length;
|
|
3815
|
+
if (identityFieldsGiven > 0 && identityFieldsGiven < 3) {
|
|
3816
|
+
throw new AnnoTypeError(
|
|
3817
|
+
"listExecObservations: a run-identity filter requires imageSha256, argvDigest AND seed together -- a partial identity would silently widen the match",
|
|
3818
|
+
{ dataType: { imageSha256: opts.imageSha256, argvDigest: opts.argvDigest, seed: opts.seed } },
|
|
3819
|
+
);
|
|
3820
|
+
}
|
|
3821
|
+
const hasIdentity = identityFieldsGiven === 3;
|
|
3822
|
+
|
|
3823
|
+
const address = hasAddress ? parseStoreAddress(opts.address as number | string, { what: "address" }) : undefined;
|
|
3824
|
+
const identity: EvidRunIdentityFilter | undefined = hasIdentity
|
|
3825
|
+
? {
|
|
3826
|
+
imageSha256: assertRunIdentityDigest(opts.imageSha256, "imageSha256"),
|
|
3827
|
+
argvDigest: assertRunIdentityDigest(opts.argvDigest, "argvDigest"),
|
|
3828
|
+
seed: assertRunIdentitySeed(opts.seed),
|
|
3829
|
+
}
|
|
3830
|
+
: undefined;
|
|
3831
|
+
|
|
3832
|
+
let rows: RawEvidExecRow[];
|
|
3833
|
+
if (address !== undefined && identity !== undefined) {
|
|
3834
|
+
rows = handle.db
|
|
3835
|
+
.prepare(
|
|
3836
|
+
"select id, image_sha256, argv_digest, seed, address, source_bank from anno_evid_exec where address = ? and image_sha256 = ? and argv_digest = ? and seed = ? order by address, id",
|
|
3837
|
+
)
|
|
3838
|
+
.all(address, identity.imageSha256, identity.argvDigest, identity.seed) as RawEvidExecRow[];
|
|
3839
|
+
} else if (address !== undefined) {
|
|
3840
|
+
rows = handle.db
|
|
3841
|
+
.prepare("select id, image_sha256, argv_digest, seed, address, source_bank from anno_evid_exec where address = ? order by address, id")
|
|
3842
|
+
.all(address) as RawEvidExecRow[];
|
|
3843
|
+
} else if (identity !== undefined) {
|
|
3844
|
+
rows = handle.db
|
|
3845
|
+
.prepare(
|
|
3846
|
+
"select id, image_sha256, argv_digest, seed, address, source_bank from anno_evid_exec where image_sha256 = ? and argv_digest = ? and seed = ? order by address, id",
|
|
3847
|
+
)
|
|
3848
|
+
.all(identity.imageSha256, identity.argvDigest, identity.seed) as RawEvidExecRow[];
|
|
3849
|
+
} else {
|
|
3850
|
+
rows = handle.db
|
|
3851
|
+
.prepare("select id, image_sha256, argv_digest, seed, address, source_bank from anno_evid_exec order by address, id")
|
|
3852
|
+
.all() as RawEvidExecRow[];
|
|
3853
|
+
}
|
|
3854
|
+
|
|
3855
|
+
return rows.map(toEvidExecRow);
|
|
3856
|
+
}
|
|
3857
|
+
|
|
3858
|
+
/**
|
|
3859
|
+
* 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).
|
|
3862
|
+
*
|
|
3863
|
+
* WHY A DENOMINATOR IS RETURNED AT ALL, AND WHY IT NEVER FORMS A PERCENTAGE
|
|
3864
|
+
* ITSELF. A bare count invites the reading "the rest is data" -- exactly the
|
|
3865
|
+
* soundness violation EVID-04 forbids (see `RuntimeExecClass`'s own doc
|
|
3866
|
+
* comment in `anno-types.ts`). `denominator` is `ADDRESS_MAX - ADDRESS_MIN +
|
|
3867
|
+
* 1`, read from `anno-types.ts`'s own constants rather than the literal
|
|
3868
|
+
* `65536` -- a caller comparing a run's `observationCount` against it forms
|
|
3869
|
+
* its own fraction, and there is deliberately no rounding site in this
|
|
3870
|
+
* module to do that division for them (see this file's `toFixed` census in
|
|
3871
|
+
* this task's own acceptance criteria).
|
|
3872
|
+
*/
|
|
3873
|
+
export function listObservedRuns(handle: AnnoStoreHandle): { runs: ObservedRunRow[]; denominator: number } {
|
|
3874
|
+
const rows = handle.db
|
|
3875
|
+
.prepare(
|
|
3876
|
+
"select image_sha256, argv_digest, seed, count(*) as observation_count from anno_evid_exec group by image_sha256, argv_digest, seed order by image_sha256, argv_digest, seed",
|
|
3877
|
+
)
|
|
3878
|
+
.all() as { image_sha256: string; argv_digest: string; seed: string; observation_count: number }[];
|
|
3879
|
+
return {
|
|
3880
|
+
runs: rows.map((row) => ({
|
|
3881
|
+
imageSha256: row.image_sha256,
|
|
3882
|
+
argvDigest: row.argv_digest,
|
|
3883
|
+
seed: row.seed,
|
|
3884
|
+
observationCount: row.observation_count,
|
|
3885
|
+
})),
|
|
3886
|
+
denominator: ADDRESS_MAX - ADDRESS_MIN + 1,
|
|
3887
|
+
};
|
|
3888
|
+
}
|
|
3889
|
+
|
|
3890
|
+
/**
|
|
3891
|
+
* Deletes every `anno_evid_exec` row for one run identity -- a bracket
|
|
3892
|
+
* reset (EVID-05). A run identity holding no rows returns `changed: false`
|
|
3893
|
+
* and is NOT an error: resetting an empty bracket is the ordinary thing,
|
|
3894
|
+
* matching `clearEnumUsage`'s own direction for the identical case.
|
|
3895
|
+
*
|
|
3896
|
+
* THIS IS THE MODULE'S FIFTH ROW-DELETING STATEMENT. `removeScope`'s doc
|
|
3897
|
+
* block (`anno-store.ts`) states the count as four; this one supersedes it.
|
|
3898
|
+
* The count is written in prose, deliberately without spelling the SQL
|
|
3899
|
+
* prefix a census greps for, so a `grep` over this module counts STATEMENTS
|
|
3900
|
+
* and not the sentences describing them. This statement runs inside the
|
|
3901
|
+
* write sequence's transaction, so a refusal raised anywhere in the
|
|
3902
|
+
* sequence rolls it back with everything else -- and a refusal here can
|
|
3903
|
+
* only come from validating the run-identity arguments themselves, since
|
|
3904
|
+
* deleting zero rows is success, not an error.
|
|
3905
|
+
*/
|
|
3906
|
+
export function deleteExecObservationsForRun(
|
|
3907
|
+
handle: AnnoStoreHandle,
|
|
3908
|
+
args: { imageSha256: unknown; argvDigest: unknown; seed: unknown; baseRevision?: number },
|
|
3909
|
+
): AnnoWriteResult {
|
|
3910
|
+
const imageSha256 = assertRunIdentityDigest(args.imageSha256, "imageSha256");
|
|
3911
|
+
const argvDigest = assertRunIdentityDigest(args.argvDigest, "argvDigest");
|
|
3912
|
+
const seed = assertRunIdentitySeed(args.seed);
|
|
3913
|
+
|
|
3914
|
+
const { revision, result } = applyWrite(
|
|
3915
|
+
handle,
|
|
3916
|
+
(db) => {
|
|
3917
|
+
const info = db.prepare("delete from anno_evid_exec where image_sha256 = ? and argv_digest = ? and seed = ?").run(imageSha256, argvDigest, seed);
|
|
3918
|
+
return Number(info.changes) > 0;
|
|
3919
|
+
},
|
|
3920
|
+
{ baseRevision: args.baseRevision },
|
|
3921
|
+
);
|
|
3922
|
+
return { revision, changed: result };
|
|
3923
|
+
}
|
|
3924
|
+
|
|
3409
3925
|
/**
|
|
3410
3926
|
* Records ONE NON-DERIVABLE cross-reference.
|
|
3411
3927
|
*
|