@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
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
// anno-provenance-ledger.ts -- the ONE place `recovery/PROVENANCE.md`'s
|
|
2
|
+
// generated tier is turned back into typed rows (BUILD-05, phase 46 plan 01).
|
|
3
|
+
//
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// WHY THIS FILE EXISTS
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Nothing in this repository has ever READ the provenance ledger. It has one
|
|
8
|
+
// writer -- `renderLedger()` in `src/skills/c64-provenance-diff/scripts/
|
|
9
|
+
// diff-images.mjs` -- and zero readers, because BUILD-05's own text requires
|
|
10
|
+
// the verdict be READ from the existing ledger, "never re-derived"
|
|
11
|
+
// (assumption A2, `46-01-PLAN.md`). Recomputing it in-process would force the
|
|
12
|
+
// exporter to import a registry-resolution path it has no other reason to
|
|
13
|
+
// know about, and would make "ledger absent" ambiguous between three
|
|
14
|
+
// different missing things. This module exists so "read" has an implementation
|
|
15
|
+
// distinct from "recompute".
|
|
16
|
+
//
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// WHAT THIS IS THE ONE AUTHORITATIVE PLACE FOR
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Turning `renderLedger()`'s emitted Markdown table -- the generated tier of
|
|
21
|
+
// `recovery/PROVENANCE.md` -- back into typed `ProvenanceLedgerRow` values,
|
|
22
|
+
// plus the one pure address-range join (`provenanceForRange()`) a caller uses
|
|
23
|
+
// to find which rows overlap a block it is about to emit.
|
|
24
|
+
//
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// WHAT NOT TO DO
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// - Never recompute a verdict. This module parses TEXT `renderLedger()`
|
|
29
|
+
// already wrote; it never calls `diffRanges()`, never touches a release
|
|
30
|
+
// registry, and never derives a Verdict or Confidence value from bytes.
|
|
31
|
+
// - Never repair a malformed row. A row that does not split into exactly
|
|
32
|
+
// seven pipe-delimited cells, or whose Start/End cell is not the `$XXXX`
|
|
33
|
+
// shape `renderLedger()`'s own `hex4()` writes, is REFUSED -- never
|
|
34
|
+
// best-effort-parsed, never defaulted, never silently skipped. No
|
|
35
|
+
// trimming a cell into shape, no defaulting a missing column, no
|
|
36
|
+
// coercing a bad address to zero, no dropping a bad row and continuing
|
|
37
|
+
// with the rest: a partially-parsed ledger that reports success is worse
|
|
38
|
+
// than a refusal, because every downstream verdict then looks
|
|
39
|
+
// authoritative.
|
|
40
|
+
// - Never interpolate the ledger's own row text into an error message. A
|
|
41
|
+
// path, a line number and an expected shape are facts ABOUT a file; its
|
|
42
|
+
// contents are not, and quoting them turns a refusal into a
|
|
43
|
+
// content-disclosure oracle -- the same rule `anno-export-asm.ts`'s own
|
|
44
|
+
// header states and this module's refusals below all follow. THE
|
|
45
|
+
// PERMITTED VOCABULARY, enumerated rather than left as a general
|
|
46
|
+
// instruction so a later contributor adding a refusal has it in front of
|
|
47
|
+
// them, is exactly these five fact kinds and nothing else:
|
|
48
|
+
// 1. the ledger's PATH.
|
|
49
|
+
// 2. a 1-based LINE NUMBER, when the refusal is about one row.
|
|
50
|
+
// 3. a column NAME (`"Start"`, `"End"`, or one of
|
|
51
|
+
// `PROVENANCE_LEDGER_HEADER_CELLS`).
|
|
52
|
+
// 4. a cell COUNT (how many cells a row split into, vs. how many were
|
|
53
|
+
// expected).
|
|
54
|
+
// 5. a parsed ADDRESS (a `number` this module itself derived by calling
|
|
55
|
+
// `parseHexAddress()`, formatted back through this module's own
|
|
56
|
+
// `hex4()` -- never a cell's raw text passed through unexamined).
|
|
57
|
+
// Cell TEXT -- Kind, Verdict, Confidence, Agreeing releases, Evidence /
|
|
58
|
+
// Reason, or any cell that failed to parse -- is not on this list and
|
|
59
|
+
// must never appear in a thrown message.
|
|
60
|
+
// - Never re-derive `renderLedger()`'s CONTENT-shaped preconditions here.
|
|
61
|
+
// `renderLedger()` refuses to EMIT under three conditions: an UNKNOWN row
|
|
62
|
+
// with an empty reason, an ORIGINAL row with `agreeing_releases` below 2,
|
|
63
|
+
// and a generated tier that does not tile `$0000-$FFFF` exactly. The
|
|
64
|
+
// first two are claims about what a Verdict cell's CONTENT means --
|
|
65
|
+
// asserting them here would make this reader adjudicate a verdict's
|
|
66
|
+
// substance, exactly the "tool is the decider" shape this phase exists
|
|
67
|
+
// to make structurally unreachable. Coverage, disjointness and ascending
|
|
68
|
+
// order (the third condition, and the ones this module DOES assert
|
|
69
|
+
// below) are claims about ADDRESSES only, which is the one thing a join
|
|
70
|
+
// over ranges is allowed to reason about. A file violating an
|
|
71
|
+
// ADDRESS-shaped invariant was never producible by the one writer this
|
|
72
|
+
// format has, so the violation is evidence of a hand edit or corruption,
|
|
73
|
+
// not of a reader bug -- accepting it would mean joining a store range
|
|
74
|
+
// against a table that does not describe the whole address space.
|
|
75
|
+
//
|
|
76
|
+
// This module imports NOTHING from `src/skills/` (the shipped `@henols/
|
|
77
|
+
// vice-mcp` tarball does not contain that tree, and `anno-join.ts:26` already
|
|
78
|
+
// states the rule for this module family) and nothing from
|
|
79
|
+
// `hostpath.ts`/`containerpath.ts` -- every path this module reads is handed
|
|
80
|
+
// to it already resolved, exactly as `anno-export-asm.ts`'s own `imagePath`
|
|
81
|
+
// is documented: the CALLER owns confinement, this module only reads.
|
|
82
|
+
import { readFileSync } from "node:fs";
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The seven column names `renderLedger()` writes, in this exact order and
|
|
86
|
+
* spelling (`diff-images.mjs`'s own header row). ONE declaration, one
|
|
87
|
+
* spelling -- `readProvenanceLedger()` below locates the header row by
|
|
88
|
+
* matching every cell against this array, so a header row that drifted from
|
|
89
|
+
* this exact wording is treated as "no header found" rather than guessed at.
|
|
90
|
+
*/
|
|
91
|
+
export const PROVENANCE_LEDGER_HEADER_CELLS = Object.freeze([
|
|
92
|
+
"Start",
|
|
93
|
+
"End",
|
|
94
|
+
"Kind",
|
|
95
|
+
"Verdict",
|
|
96
|
+
"Confidence",
|
|
97
|
+
"Agreeing releases",
|
|
98
|
+
"Evidence / Reason",
|
|
99
|
+
] as const);
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* One row of the ledger's generated tier, copied verbatim off the table --
|
|
103
|
+
* every string field is the cell's own text, UN-ESCAPED (`\|` back to `|`)
|
|
104
|
+
* but never trimmed of meaning, normalised, upper-cased or mapped. An
|
|
105
|
+
* unrecognised Verdict or Confidence string is carried through exactly as
|
|
106
|
+
* written; this module does not know the vocabulary and does not check it.
|
|
107
|
+
*/
|
|
108
|
+
export interface ProvenanceLedgerRow {
|
|
109
|
+
/** First address the row covers, parsed from the `$XXXX` form `hex4()`
|
|
110
|
+
* writes. */
|
|
111
|
+
start: number;
|
|
112
|
+
/** Last address the row covers, INCLUSIVE -- `renderLedger()` writes
|
|
113
|
+
* `hex4(r.end)` and advances its own coverage cursor with `r.end + 1`, so
|
|
114
|
+
* the column this module reads is already an inclusive end. */
|
|
115
|
+
endInclusive: number;
|
|
116
|
+
/** The Kind cell, copied verbatim -- `game`/`loader`/`cracktro`/`io`/
|
|
117
|
+
* `unused`/`unresolved` in a well-formed ledger, but this module does not
|
|
118
|
+
* check membership; it copies whatever text is there. */
|
|
119
|
+
kind: string;
|
|
120
|
+
/** The Verdict cell, copied verbatim. Never normalised: an unrecognised
|
|
121
|
+
* string is carried through as text, exactly as the plan's own prohibition
|
|
122
|
+
* requires. */
|
|
123
|
+
verdict: string;
|
|
124
|
+
/** The Confidence cell, copied verbatim, on the same terms as `verdict`. */
|
|
125
|
+
confidence: string;
|
|
126
|
+
/**
|
|
127
|
+
* The Agreeing releases cell, carried as a STRING, never parsed to a
|
|
128
|
+
* number. `renderLedger()` interpolates `${r.agreeing_releases}` with no
|
|
129
|
+
* guard, so a `CRACKER-PATCH` or `UNKNOWN` row can legitimately carry the
|
|
130
|
+
* literal text `undefined` in this column -- inventing a number there
|
|
131
|
+
* would be this module deciding something the ledger itself never
|
|
132
|
+
* committed to.
|
|
133
|
+
*/
|
|
134
|
+
agreeingReleases: string;
|
|
135
|
+
/** The Evidence / Reason cell, un-escaped (`\|` back to `|`) but
|
|
136
|
+
* otherwise verbatim. */
|
|
137
|
+
evidence: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** One parsed ledger: the path it was read from, and every row the generated
|
|
141
|
+
* tier carried, in file order. */
|
|
142
|
+
export interface ProvenanceLedger {
|
|
143
|
+
path: string;
|
|
144
|
+
rows: readonly ProvenanceLedgerRow[];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface ProvenanceLedgerErrorOptions {
|
|
148
|
+
path: string;
|
|
149
|
+
/** 1-based line number of the offending row, when the refusal is about one
|
|
150
|
+
* specific row rather than the file as a whole. */
|
|
151
|
+
lineNumber?: number;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Thrown by `readProvenanceLedger()`. Follows `ViceError`'s shape convention
|
|
156
|
+
* (`vice.ts`) -- plain public fields, no sanitising -- rather than a
|
|
157
|
+
* message-only `Error`, so a caller that wants the path or the line number
|
|
158
|
+
* programmatically (a CLI wrapper printing its own summary, say) does not
|
|
159
|
+
* have to parse them back out of prose.
|
|
160
|
+
*/
|
|
161
|
+
export class ProvenanceLedgerError extends Error {
|
|
162
|
+
path: string;
|
|
163
|
+
lineNumber?: number;
|
|
164
|
+
|
|
165
|
+
constructor(message: string, { path, lineNumber }: ProvenanceLedgerErrorOptions) {
|
|
166
|
+
super(message);
|
|
167
|
+
this.name = "ProvenanceLedgerError";
|
|
168
|
+
this.path = path;
|
|
169
|
+
this.lineNumber = lineNumber;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The remedy every refusal below names -- one sentence, one place, so the
|
|
174
|
+
* two named routes stay in sync with each other rather than being retyped at
|
|
175
|
+
* every throw site. */
|
|
176
|
+
const LEDGER_REMEDY =
|
|
177
|
+
'regenerate it with c64-provenance-diff\'s "ledger" verb, or omit --ledger to export without provenance annotation';
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Splits one line into its pipe-delimited cells, or returns `undefined` when
|
|
181
|
+
* the line is not shaped like a table row at all (does not start with `|`
|
|
182
|
+
* once trimmed -- true of the blank line and the `## Prose tier` heading that
|
|
183
|
+
* follow the generated tier, and of every prose line before the table).
|
|
184
|
+
*
|
|
185
|
+
* `\|` is protected before splitting and restored afterward in every cell,
|
|
186
|
+
* matching the one escape `renderLedger()`'s writer applies (only ever to the
|
|
187
|
+
* Evidence/Reason cell in practice, but restored uniformly here since a
|
|
188
|
+
* split-time placeholder is an artefact of splitting, not a second escaping
|
|
189
|
+
* rule).
|
|
190
|
+
*
|
|
191
|
+
* `PLACEHOLDER` is a one-character NUL string, built at RUNTIME with
|
|
192
|
+
* String.fromCharCode(0) rather than written as a literal byte -- or any
|
|
193
|
+
* backslash escape sequence naming that byte -- anywhere in this file's
|
|
194
|
+
* own source text. A raw NUL byte on disk makes `git diff` render this
|
|
195
|
+
* module as `Bin 0 -> N bytes` with no line-level diff ever again, and
|
|
196
|
+
* hides the file from a plain (non-`-a`) `grep` census -- the exact
|
|
197
|
+
* hazard `anno-memmap-render.ts` already carries and that this module
|
|
198
|
+
* must not add a second instance of. `String.fromCharCode(0)` produces
|
|
199
|
+
* the IDENTICAL runtime character while keeping every byte of the
|
|
200
|
+
* source file printable ASCII, with no escape-sequence spelling for a
|
|
201
|
+
* reviewing tool (or a lossy copy-paste) to silently collapse into the
|
|
202
|
+
* raw byte it names. A plain space is NOT a safe substitute for the
|
|
203
|
+
* sentinel value: ordinary ledger prose ("Agreeing releases", "Evidence
|
|
204
|
+
* / Reason") already contains spaces, so restoring a space-shaped
|
|
205
|
+
* placeholder back to `|` would corrupt every cell that happens to
|
|
206
|
+
* contain one -- reproduced against this file's own header-row match
|
|
207
|
+
* failing outright. NUL is chosen because ordinary ledger text -- a
|
|
208
|
+
* Markdown table written by renderLedger(), plausibly hand-edited --
|
|
209
|
+
* cannot contain it.
|
|
210
|
+
*
|
|
211
|
+
* The leading and trailing empty cells a `| a | b |`-shaped line produces are
|
|
212
|
+
* dropped ONLY when they are genuinely empty -- a row missing its closing `|`
|
|
213
|
+
* is left with a non-empty last cell, so the seven-cell count check at the
|
|
214
|
+
* call site catches the malformed shape rather than this function silently
|
|
215
|
+
* repairing it.
|
|
216
|
+
*
|
|
217
|
+
* A raw NUL byte already present in the line -- e.g. in a hand-edited or
|
|
218
|
+
* corrupted ledger's Evidence/Reason cell -- is REFUSED rather than restored:
|
|
219
|
+
* without this check, an existing NUL would collide with the placeholder
|
|
220
|
+
* this function inserts for its own `\|` escaping, and the unconditional
|
|
221
|
+
* `.join("|")` restoration below would silently turn that pre-existing NUL
|
|
222
|
+
* into an extra, unescaped `|` that never delimited anything in the source
|
|
223
|
+
* file. That is exactly the "repair a malformed row" outcome this module's
|
|
224
|
+
* header forbids, so it is named and refused like every other malformed
|
|
225
|
+
* shape this function's caller checks for -- never coerced, never dropped.
|
|
226
|
+
* `ledgerPath` and `lineNumber` are threaded through only to build that
|
|
227
|
+
* refusal's message; they change no other behaviour of this function.
|
|
228
|
+
*/
|
|
229
|
+
function splitTableRow(line: string, ledgerPath: string, lineNumber: number): string[] | undefined {
|
|
230
|
+
const trimmed = line.trim();
|
|
231
|
+
if (!trimmed.startsWith("|")) return undefined;
|
|
232
|
+
|
|
233
|
+
const PLACEHOLDER = String.fromCharCode(0);
|
|
234
|
+
if (trimmed.includes(PLACEHOLDER)) {
|
|
235
|
+
throw new ProvenanceLedgerError(
|
|
236
|
+
`anno-provenance-ledger: row ${lineNumber} of "${ledgerPath}" contains a raw NUL byte -- refusing to parse a row that ` +
|
|
237
|
+
`could collide with this module's own internal escape-placeholder sentinel rather than silently turning it into an ` +
|
|
238
|
+
`unescaped "|". ${LEDGER_REMEDY}.`,
|
|
239
|
+
{ path: ledgerPath, lineNumber },
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
const protectedLine = trimmed.replace(/\\\|/g, PLACEHOLDER);
|
|
243
|
+
let cells = protectedLine.split("|");
|
|
244
|
+
if (cells[0] === "") cells = cells.slice(1);
|
|
245
|
+
if (cells.length > 0 && cells[cells.length - 1] === "") cells = cells.slice(0, -1);
|
|
246
|
+
|
|
247
|
+
return cells.map((cell) => cell.trim().split(PLACEHOLDER).join("|"));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Parses a `$XXXX` cell (exactly `hex4()`'s own output shape) into a
|
|
251
|
+
* number, or `undefined` when the cell does not match -- the caller turns
|
|
252
|
+
* `undefined` into a named refusal rather than this function guessing. */
|
|
253
|
+
function parseHexAddress(cell: string): number | undefined {
|
|
254
|
+
const match = /^\$([0-9a-fA-F]{4})$/.exec(cell);
|
|
255
|
+
if (!match) return undefined;
|
|
256
|
+
return Number.parseInt(match[1]!, 16);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Formats a PARSED address (never a cell's raw text) as `$XXXX`, for use in
|
|
260
|
+
* refusal messages -- the one fact kind on the permitted-vocabulary list that
|
|
261
|
+
* is a number this module derived itself, not text copied off the file. */
|
|
262
|
+
function hex4(value: number): string {
|
|
263
|
+
return `$${(value & 0xffff).toString(16).padStart(4, "0")}`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Throws the shared bad-address refusal for row `lineNumber`'s `column`
|
|
268
|
+
* cell (`"Start"` or `"End"`) -- ONE template for both columns, since the
|
|
269
|
+
* only fact that differs between them is the column NAME, which is on the
|
|
270
|
+
* permitted vocabulary list. The cell's own text is never read here or by
|
|
271
|
+
* either call site: the caller passes only the column name, never the
|
|
272
|
+
* string that failed to parse.
|
|
273
|
+
*/
|
|
274
|
+
function refuseBadAddress(ledgerPath: string, lineNumber: number, column: "Start" | "End"): never {
|
|
275
|
+
throw new ProvenanceLedgerError(
|
|
276
|
+
`anno-provenance-ledger: row ${lineNumber} of "${ledgerPath}" has a ${column} cell that is not the "$XXXX" shape ` +
|
|
277
|
+
`renderLedger()'s own hex4() writes -- refusing to parse an address this module cannot be sure of, and never guessing ` +
|
|
278
|
+
`one from the cell's own text. ${LEDGER_REMEDY}.`,
|
|
279
|
+
{ path: ledgerPath, lineNumber },
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Reads and parses `ledgerPath`'s generated tier.
|
|
285
|
+
*
|
|
286
|
+
* Locates the header row by matching every cell against
|
|
287
|
+
* `PROVENANCE_LEDGER_HEADER_CELLS`, skips the `|---|` separator line
|
|
288
|
+
* immediately below it without re-validating its own shape, then parses
|
|
289
|
+
* every following pipe-delimited line into a `ProvenanceLedgerRow` until the
|
|
290
|
+
* first line that is not a table row at all, then asserts the accepted rows
|
|
291
|
+
* as a WHOLE are strictly ascending, disjoint, and tile exactly
|
|
292
|
+
* `$0000-$FFFF`.
|
|
293
|
+
*
|
|
294
|
+
* Refuses (`ProvenanceLedgerError`) for eight distinct reasons, and REJECTS
|
|
295
|
+
* rather than repairs at every one of them -- no trimming, no defaulting, no
|
|
296
|
+
* coercing a bad address to zero, no dropping a bad row and continuing with
|
|
297
|
+
* the rest. Listed here in RULE order rather than strict execution order
|
|
298
|
+
* (the zero-data-rows check can only run once the per-row loop below has
|
|
299
|
+
* finished, so it fires textually after checks 4-6 even though it is
|
|
300
|
+
* conceptually "does the table hold any rows at all"):
|
|
301
|
+
* 1. the file cannot be read.
|
|
302
|
+
* 2. no header row matching all seven cells is found.
|
|
303
|
+
* 3. the header and separator are present but zero data rows follow.
|
|
304
|
+
* 4. a candidate row does not split into exactly seven cells.
|
|
305
|
+
* 5. a candidate row's Start or End cell is not the `$XXXX` shape
|
|
306
|
+
* `hex4()` writes.
|
|
307
|
+
* 6. a candidate row's End is below its Start.
|
|
308
|
+
* 7. two rows overlap, or are not strictly ascending by Start.
|
|
309
|
+
* 8. the accepted rows do not begin at `$0000`, leave a gap, or stop
|
|
310
|
+
* below `$FFFF`.
|
|
311
|
+
*
|
|
312
|
+
* Every refusal names the path and, for a single-row problem, the 1-based
|
|
313
|
+
* line number -- never the row's own cell text. See the module header's
|
|
314
|
+
* "WHAT NOT TO DO" section for the full permitted-fact vocabulary, and for
|
|
315
|
+
* why checks 7 and 8 are the ONLY two that mirror `renderLedger()`'s own
|
|
316
|
+
* emit-time preconditions: they are the ADDRESS-shaped ones, and the two
|
|
317
|
+
* CONTENT-shaped ones (UNKNOWN-with-empty-reason, ORIGINAL-agreeing-below-2)
|
|
318
|
+
* are deliberately never re-asserted here.
|
|
319
|
+
*/
|
|
320
|
+
export function readProvenanceLedger(ledgerPath: string): ProvenanceLedger {
|
|
321
|
+
let raw: string;
|
|
322
|
+
try {
|
|
323
|
+
raw = readFileSync(ledgerPath, "utf8");
|
|
324
|
+
} catch (err) {
|
|
325
|
+
throw new ProvenanceLedgerError(
|
|
326
|
+
`anno-provenance-ledger: could not read the ledger at "${ledgerPath}" (${err instanceof Error ? err.message : String(err)}) -- ` +
|
|
327
|
+
`refusing to annotate without it. ${LEDGER_REMEDY}.`,
|
|
328
|
+
{ path: ledgerPath },
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const lines = raw.split(/\r?\n/);
|
|
333
|
+
|
|
334
|
+
let headerLineIndex = -1;
|
|
335
|
+
for (let i = 0; i < lines.length; i++) {
|
|
336
|
+
const cells = splitTableRow(lines[i]!, ledgerPath, i + 1);
|
|
337
|
+
if (
|
|
338
|
+
cells !== undefined &&
|
|
339
|
+
cells.length === PROVENANCE_LEDGER_HEADER_CELLS.length &&
|
|
340
|
+
cells.every((cell, idx) => cell === PROVENANCE_LEDGER_HEADER_CELLS[idx])
|
|
341
|
+
) {
|
|
342
|
+
headerLineIndex = i;
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (headerLineIndex === -1) {
|
|
347
|
+
throw new ProvenanceLedgerError(
|
|
348
|
+
`anno-provenance-ledger: no header row matching the seven expected columns (${PROVENANCE_LEDGER_HEADER_CELLS.join(", ")}) ` +
|
|
349
|
+
`was found in "${ledgerPath}" -- refusing to guess which row starts the table. ${LEDGER_REMEDY}.`,
|
|
350
|
+
{ path: ledgerPath },
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const rows: ProvenanceLedgerRow[] = [];
|
|
355
|
+
/** 1-based source line number for `rows[k]`, kept parallel to `rows` so the
|
|
356
|
+
* ordering/overlap check below can name both offending rows without
|
|
357
|
+
* re-scanning `lines` -- an internal bookkeeping array, never exposed on
|
|
358
|
+
* `ProvenanceLedgerRow` itself. */
|
|
359
|
+
const rowLineNumbers: number[] = [];
|
|
360
|
+
// headerLineIndex + 1 is the `|---|` separator -- skipped, not
|
|
361
|
+
// re-validated. Parsing starts one line further on.
|
|
362
|
+
for (let i = headerLineIndex + 2; i < lines.length; i++) {
|
|
363
|
+
const line = lines[i]!;
|
|
364
|
+
const cells = splitTableRow(line, ledgerPath, i + 1);
|
|
365
|
+
if (cells === undefined) break; // first line that is not a table row at all ends the generated tier
|
|
366
|
+
|
|
367
|
+
if (cells.length !== PROVENANCE_LEDGER_HEADER_CELLS.length) {
|
|
368
|
+
throw new ProvenanceLedgerError(
|
|
369
|
+
`anno-provenance-ledger: row ${i + 1} of "${ledgerPath}" splits into ${cells.length} pipe-delimited cells, not the ` +
|
|
370
|
+
`${PROVENANCE_LEDGER_HEADER_CELLS.length} renderLedger() always writes -- refusing to parse a row whose own shape ` +
|
|
371
|
+
`is wrong. ${LEDGER_REMEDY}.`,
|
|
372
|
+
{ path: ledgerPath, lineNumber: i + 1 },
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const [startCell, endCell, kind, verdict, confidence, agreeingReleases, evidence] = cells as [
|
|
377
|
+
string,
|
|
378
|
+
string,
|
|
379
|
+
string,
|
|
380
|
+
string,
|
|
381
|
+
string,
|
|
382
|
+
string,
|
|
383
|
+
string,
|
|
384
|
+
];
|
|
385
|
+
const start = parseHexAddress(startCell);
|
|
386
|
+
if (start === undefined) refuseBadAddress(ledgerPath, i + 1, "Start");
|
|
387
|
+
const end = parseHexAddress(endCell);
|
|
388
|
+
if (end === undefined) refuseBadAddress(ledgerPath, i + 1, "End");
|
|
389
|
+
|
|
390
|
+
if (end < start) {
|
|
391
|
+
throw new ProvenanceLedgerError(
|
|
392
|
+
`anno-provenance-ledger: row ${i + 1} of "${ledgerPath}" has End ${hex4(end)} below its Start ${hex4(start)} -- ` +
|
|
393
|
+
`refusing to accept a row whose own span is inverted. ${LEDGER_REMEDY}.`,
|
|
394
|
+
{ path: ledgerPath, lineNumber: i + 1 },
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
rows.push({ start, endInclusive: end, kind, verdict, confidence, agreeingReleases, evidence });
|
|
399
|
+
rowLineNumbers.push(i + 1);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (rows.length === 0) {
|
|
403
|
+
throw new ProvenanceLedgerError(
|
|
404
|
+
`anno-provenance-ledger: the header and separator in "${ledgerPath}" are present but zero data rows follow -- ` +
|
|
405
|
+
`refusing to treat a table that parsed to no rows the same as no ledger having been asked for at all. ${LEDGER_REMEDY}.`,
|
|
406
|
+
{ path: ledgerPath },
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// CHECK 7 -- ADDRESS-SHAPED, mirrors `renderLedger()`'s own "gap or
|
|
411
|
+
// overlap" precondition. Two rows overlap, or the rows are not strictly
|
|
412
|
+
// ascending by Start, iff a later row's Start does not come strictly after
|
|
413
|
+
// the earlier row's own Start AND End. `rows` is in FILE order (not
|
|
414
|
+
// re-sorted), so this also catches a ledger whose rows were reordered by a
|
|
415
|
+
// hand edit, not only a genuine address collision.
|
|
416
|
+
for (let idx = 1; idx < rows.length; idx++) {
|
|
417
|
+
const prev = rows[idx - 1]!;
|
|
418
|
+
const cur = rows[idx]!;
|
|
419
|
+
if (cur.start <= prev.start || cur.start <= prev.endInclusive) {
|
|
420
|
+
throw new ProvenanceLedgerError(
|
|
421
|
+
`anno-provenance-ledger: rows ${rowLineNumbers[idx - 1]} and ${rowLineNumbers[idx]} of "${ledgerPath}" are not ` +
|
|
422
|
+
`strictly ascending and disjoint by Start -- row ${rowLineNumbers[idx - 1]} spans ${hex4(prev.start)}..${hex4(prev.endInclusive)} ` +
|
|
423
|
+
`and row ${rowLineNumbers[idx]} spans ${hex4(cur.start)}..${hex4(cur.endInclusive)} -- refusing to join a store range against rows ` +
|
|
424
|
+
`this module cannot order unambiguously. ${LEDGER_REMEDY}.`,
|
|
425
|
+
{ path: ledgerPath, lineNumber: rowLineNumbers[idx] },
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// CHECK 8 -- ADDRESS-SHAPED, mirrors `renderLedger()`'s own two coverage
|
|
431
|
+
// preconditions (starts at $0000 with no gap; reaches $FFFF). Run only
|
|
432
|
+
// after check 7 has already established the rows are disjoint and
|
|
433
|
+
// ascending, so `expected` is a running "next address a row must start at"
|
|
434
|
+
// cursor rather than a re-derivation of the ordering check above.
|
|
435
|
+
let expected = 0;
|
|
436
|
+
for (const row of rows) {
|
|
437
|
+
if (row.start !== expected) {
|
|
438
|
+
throw new ProvenanceLedgerError(
|
|
439
|
+
`anno-provenance-ledger: "${ledgerPath}"'s accepted rows do not cover $0000..$FFFF -- address ${hex4(expected)} is not ` +
|
|
440
|
+
`covered by any row -- refusing to join a store range against a table that does not describe the whole address space. ` +
|
|
441
|
+
`${LEDGER_REMEDY}.`,
|
|
442
|
+
{ path: ledgerPath },
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
expected = row.endInclusive + 1;
|
|
446
|
+
}
|
|
447
|
+
if (expected !== 0x10000) {
|
|
448
|
+
throw new ProvenanceLedgerError(
|
|
449
|
+
`anno-provenance-ledger: "${ledgerPath}"'s accepted rows stop at ${hex4(expected - 1)}, not $FFFF -- refusing to join a ` +
|
|
450
|
+
`store range against a table that does not describe the whole address space. ${LEDGER_REMEDY}.`,
|
|
451
|
+
{ path: ledgerPath },
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return { path: ledgerPath, rows };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Every row of `ledger` overlapping `[start, endInclusive]`, both ends
|
|
460
|
+
* inclusive, in ascending `start` order.
|
|
461
|
+
*
|
|
462
|
+
* Two ranges overlap iff each starts at or before the other ends -- the same
|
|
463
|
+
* `>=` reasoning `addScope()` in `anno-store.ts` already documents for
|
|
464
|
+
* scopes, so a row ending exactly at `start - 1` does NOT overlap. Returns
|
|
465
|
+
* EVERY match and never picks one: multiplicity is the caller's to record,
|
|
466
|
+
* never this function's to resolve. Takes no threshold, no filter predicate
|
|
467
|
+
* and no default -- the join is address-only, and no Verdict, Confidence or
|
|
468
|
+
* Kind value is ever read here.
|
|
469
|
+
*/
|
|
470
|
+
export function provenanceForRange(ledger: ProvenanceLedger, start: number, endInclusive: number): readonly ProvenanceLedgerRow[] {
|
|
471
|
+
return ledger.rows.filter((row) => row.start <= endInclusive && row.endInclusive >= start).sort((a, b) => a.start - b.start);
|
|
472
|
+
}
|
package/anno-register.ts
CHANGED
|
@@ -207,6 +207,165 @@ export const ANNO_VERB_REGISTER: readonly AnnoVerbRegisterEntry[] = Object.freez
|
|
|
207
207
|
"instead is what lets MCP-01's zero-caller clause hold without losing a capability.",
|
|
208
208
|
},
|
|
209
209
|
|
|
210
|
+
{
|
|
211
|
+
verb: "anno_exclude_range",
|
|
212
|
+
kind: "unclassified",
|
|
213
|
+
consumers: [
|
|
214
|
+
{ path: "src/mcp/vice/anno-store.ts", symbol: "addExcludedRange" },
|
|
215
|
+
{ path: "src/mcp/vice/anno-tools.ts", symbol: "anno_exclude_range" },
|
|
216
|
+
],
|
|
217
|
+
requirements: ["BUILD-05", "BUILD-07"],
|
|
218
|
+
rationale:
|
|
219
|
+
"BUILD-05 requires that any exclusion be one the user asked for, made explicit and recorded rather than " +
|
|
220
|
+
"silently applied. BUILD-07 requires a user-requested exclusion be emitted as a recorded excluded range " +
|
|
221
|
+
"rather than a hole. Both clauses presuppose a route by which the user STATES the request. Without this " +
|
|
222
|
+
"verb the store persists a table no caller on this surface can write -- a stored capability with no " +
|
|
223
|
+
"route, which is the same shape anno_add_scope's own entry was written against. The surface is the right " +
|
|
224
|
+
"home rather than the CLI because an exclusion is a small bounded piece of state a session sets while " +
|
|
225
|
+
"looking at a range, not a filesystem artefact.",
|
|
226
|
+
note:
|
|
227
|
+
"This verb's existence is what makes the requirement's 'never the tool's' clause CHECKABLE rather than " +
|
|
228
|
+
"aspirational. As long as the only way a range can be excluded is a user calling this verb, there is no " +
|
|
229
|
+
"code path by which the tool excludes anything on its own judgement. The alternative rejected: a " +
|
|
230
|
+
"confidence or verdict threshold inside the exporter would have 'implemented' the same user-visible " +
|
|
231
|
+
"outcome and would have made the tool the decider -- the failure mode the 2026-09-10 rewording of " +
|
|
232
|
+
"BUILD-05 removed.",
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
verb: "anno_include_range",
|
|
236
|
+
kind: "unclassified",
|
|
237
|
+
consumers: [
|
|
238
|
+
{ path: "src/mcp/vice/anno-store.ts", symbol: "removeExcludedRange" },
|
|
239
|
+
{ path: "src/mcp/vice/anno-tools.ts", symbol: "anno_include_range" },
|
|
240
|
+
],
|
|
241
|
+
requirements: ["BUILD-07"],
|
|
242
|
+
rationale:
|
|
243
|
+
"The inverse of the overlap refusal, on the same terms anno_remove_scope's entry states: the store " +
|
|
244
|
+
"refuses any exclusion overlapping an existing one, so a single transposed end would otherwise make a " +
|
|
245
|
+
"whole region permanently unexcludable, with the only route back being a revert through the bounded " +
|
|
246
|
+
"snapshot ring. A write verb whose mistakes cannot be undone is a data-loss surface even when every " +
|
|
247
|
+
"individual refusal is correct, so the inverse ships in the same phase as the refusal.",
|
|
248
|
+
note:
|
|
249
|
+
"The measured mechanism, not an asserted risk: the snapshot ring is bounded at MAX_SNAPSHOT_REVISIONS, so " +
|
|
250
|
+
"a mistake that is not directly undoable becomes permanently undoable after that many further writes.",
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
verb: "anno_import_ghidra_export",
|
|
254
|
+
kind: "unclassified",
|
|
255
|
+
consumers: [
|
|
256
|
+
{ path: "src/mcp/vice/anno-import.ts", symbol: "importGhidraExport" },
|
|
257
|
+
{ path: "src/mcp/vice/anno-tools.ts", symbol: "anno_import_ghidra_export" },
|
|
258
|
+
],
|
|
259
|
+
requirements: ["IMP-01", "IMP-02"],
|
|
260
|
+
rationale:
|
|
261
|
+
"The manifest's five absorbed upstream procedures predate this store having any Ghidra-derived import path " +
|
|
262
|
+
"at all, so the derivation is silent about a verb that did not exist when the manifest was written. IMP-01 " +
|
|
263
|
+
"requires a container-side importer reading a host-written transfer file through the store's single seam, " +
|
|
264
|
+
"and IMP-02 requires that transfer file to be transient evidence -- digested, consumed and deleted in the " +
|
|
265
|
+
"same call, never a second on-disk model. This verb is the only route either requirement has: without it, " +
|
|
266
|
+
"a Ghidra export sits on disk as a file no curated verb ever reads.",
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
verb: "anno_join_memmap",
|
|
270
|
+
kind: "unclassified",
|
|
271
|
+
consumers: [
|
|
272
|
+
{ path: "src/mcp/vice/anno-join.ts", symbol: "runMemmapJoin" },
|
|
273
|
+
{ path: "src/mcp/vice/anno-tools.ts", symbol: "anno_join_memmap" },
|
|
274
|
+
],
|
|
275
|
+
requirements: ["AUTO-01"],
|
|
276
|
+
rationale:
|
|
277
|
+
"AUTO-01 requires machine addresses to annotate themselves into the store mechanically -- no agent call, no " +
|
|
278
|
+
"queue walk and no skill invocation anywhere in the loop -- by joining stored cross-references against " +
|
|
279
|
+
"c64-memory-mapping's memmap.json. The manifest's five procedures never describe this join at all, because " +
|
|
280
|
+
"it answers a question this project's own store creates (what does a stored cross-reference's target " +
|
|
281
|
+
"address MEAN against the published memory map), not one upstream's absorbed procedures ever asked. Without " +
|
|
282
|
+
"this verb the annotation the join produces has no route onto the surface at all.",
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
verb: "anno_evid_ingest",
|
|
286
|
+
kind: "unclassified",
|
|
287
|
+
consumers: [
|
|
288
|
+
{ path: "src/mcp/vice/evid-ingest.ts", symbol: "ingestAccessMap" },
|
|
289
|
+
{ path: "src/mcp/vice/anno-tools.ts", symbol: "anno_evid_ingest" },
|
|
290
|
+
],
|
|
291
|
+
requirements: ["EVID-01", "EVID-04"],
|
|
292
|
+
rationale:
|
|
293
|
+
"EVID-01 requires what the emulator observed executing to become durable, accumulating rows in `.annostore`, " +
|
|
294
|
+
"keyed by run identity, so a later session queries the evidence instead of re-running the program. EVID-04 " +
|
|
295
|
+
"requires the write path be unable to state, imply or render that an address is data on the strength of " +
|
|
296
|
+
"never having been observed executing. The manifest's five absorbed upstream procedures predate this store " +
|
|
297
|
+
"having any runtime-evidence table at all, so the derivation is silent about a verb that did not exist when " +
|
|
298
|
+
"the manifest was written. This verb is the ONE place a parsed memmapshow access map becomes store rows: " +
|
|
299
|
+
"without it, an observed execute bit has no route onto the surface at all, and EVID-01's own goal (query the " +
|
|
300
|
+
"evidence instead of re-running the program) is unreachable.",
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
verb: "anno_evid_disagreements",
|
|
304
|
+
kind: "unclassified",
|
|
305
|
+
consumers: [
|
|
306
|
+
{ path: "src/mcp/vice/evid-reconcile.ts", symbol: "reconcileObservedExecution" },
|
|
307
|
+
{ path: "src/mcp/vice/anno-tools.ts", symbol: "anno_evid_disagreements" },
|
|
308
|
+
],
|
|
309
|
+
requirements: ["EVID-03", "EVID-04"],
|
|
310
|
+
rationale:
|
|
311
|
+
"EVID-03 requires a user be able to ask where the byte-derived block classification and the observed-execution " +
|
|
312
|
+
"evidence disagree, and get the disagreements first, with the block table never silently overwritten and " +
|
|
313
|
+
"agreement reported as a count rather than a wall of rows. EVID-04 requires that no summary this surface " +
|
|
314
|
+
"produces can state, imply or render that a never-observed address is data, and that every count carries the " +
|
|
315
|
+
"denominator it is a fraction of. This verb is the ONE place `reconcileObservedExecution()`'s pure join is " +
|
|
316
|
+
"reached from the MCP surface: without it, the reconciliation module a prior plan built has no caller at all, " +
|
|
317
|
+
"and EVID-03's own goal (a user can ask) is unreachable.",
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
verb: "anno_evid_runs",
|
|
321
|
+
kind: "unclassified",
|
|
322
|
+
consumers: [
|
|
323
|
+
{ path: "src/mcp/vice/anno-store.ts", symbol: "listObservedRuns" },
|
|
324
|
+
{ path: "src/mcp/vice/anno-tools.ts", symbol: "anno_evid_runs" },
|
|
325
|
+
],
|
|
326
|
+
requirements: ["EVID-04"],
|
|
327
|
+
rationale:
|
|
328
|
+
"EVID-04 requires that any summary carry the denominator it is a fraction of and that a union across however " +
|
|
329
|
+
"many runs never be presented as exhaustive. This verb is the ONE surface route to `listObservedRuns()`'s " +
|
|
330
|
+
"per-run observation counts and their shared denominator, so a later session can see what evidence a store " +
|
|
331
|
+
"already holds -- across however many runs contributed -- without re-running the program and without ever " +
|
|
332
|
+
"reading the counts as coverage of the image.",
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
verb: "anno_evid_reset",
|
|
336
|
+
kind: "unclassified",
|
|
337
|
+
consumers: [
|
|
338
|
+
{ path: "src/mcp/vice/anno-store.ts", symbol: "deleteExecObservationsForRun" },
|
|
339
|
+
{ path: "src/mcp/vice/anno-tools.ts", symbol: "anno_evid_reset" },
|
|
340
|
+
],
|
|
341
|
+
requirements: ["EVID-05"],
|
|
342
|
+
rationale:
|
|
343
|
+
"EVID-05 requires that a bracket can be reset and re-measured without a previous run's observations leaking " +
|
|
344
|
+
"into it. This verb is the store-side half of that reset (beside plan 43-03's emulator-side `vice_memmap_zap`): " +
|
|
345
|
+
"it derives the run identity through the same `runIdentityFrom()` `anno_evid_ingest` uses and deletes only " +
|
|
346
|
+
"that identity's rows, leaving every other identity and the byte-derived block table untouched. Without it, " +
|
|
347
|
+
"`deleteExecObservationsForRun()` -- the store function EVID-05's own requirement asks for -- has no route " +
|
|
348
|
+
"onto the surface at all.",
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
verb: "anno_hazard_report",
|
|
352
|
+
kind: "unclassified",
|
|
353
|
+
consumers: [
|
|
354
|
+
{ path: "src/mcp/vice/anno-hazard-report.ts", symbol: "buildHazardReport" },
|
|
355
|
+
{ path: "src/mcp/vice/anno-tools.ts", symbol: "anno_hazard_report" },
|
|
356
|
+
],
|
|
357
|
+
requirements: ["BUILD-04"],
|
|
358
|
+
rationale:
|
|
359
|
+
"A movement-blocking construction must be askable and enumerable, with its detection mechanism and its " +
|
|
360
|
+
"detection strength reported beside it, and with a third undecided outcome that is never rendered as " +
|
|
361
|
+
"clean. This verb is the ONE place the pure hazard-report function is reached from the surface: without " +
|
|
362
|
+
"it, the module a prior task built has no caller at all, and the goal it exists for (a user can ask what " +
|
|
363
|
+
"blocks a program's code from being moved) is unreachable. The manifest's five absorbed upstream " +
|
|
364
|
+
"procedures predate any movement-hazard concept in this store at all, so the derivation is silent about a " +
|
|
365
|
+
"verb that answers a question upstream never asked. This verb opens no new store table and writes " +
|
|
366
|
+
"nothing -- it is a derived query over tables the surface already writes through other verbs.",
|
|
367
|
+
},
|
|
368
|
+
|
|
210
369
|
// --- manifest-deviation: classified, routed, and answering differently ---
|
|
211
370
|
{
|
|
212
371
|
verb: "anno_save_project",
|