@volter/supercode-orchestrator 0.5.223 → 0.5.225

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/board/cli.mjs CHANGED
@@ -35,6 +35,7 @@ import * as slowLog from '@volter/supercode-harness-sdk/slow-log';
35
35
  import { basename, dirname, join, resolve } from 'node:path';
36
36
  import { fileURLToPath } from 'node:url';
37
37
  import { homedir, tmpdir, userInfo } from 'node:os';
38
+ import { storageContract } from './storage-contract.mjs';
38
39
  import { boardKey, cardRef, actorOf, CALLER_RUN_ADDRESS, CALLER_RUN_AGENT, SESSION_CURRENT_RUNS, SESSION_RUNS, sessionIndexed, heldConfigWatched, heldDocumentsChanged, heldDocumentsWatched, holdBoards, retireHeldBoards, releaseHeldBoards, publishMarked, view, backingPaths, layOut, backingOf, boardExists, boardCopyOf, boardPath, briefPath, DEFAULT_BOARD, event, json, listBoards, logsDir, now, openBoard, setActor, setPrincipal, principalOf, isCaller, tx } from './store.mjs';
39
40
  import { isLocalWorkspace } from './workspace.mjs';
40
41
  import { openZtrackBoard } from './ztrack.mjs';
@@ -1970,11 +1971,9 @@ export async function serveBoardGeneration(root, args = {}, argv = ['serve', '--
1970
1971
  };
1971
1972
  const disconnected = () => { stopDispatching(); process.exit(1); };
1972
1973
  process.on('message', receive); process.once('disconnect', disconnected);
1973
- // A transition cannot restore database files over committed card writes. Refuse a storage-code change
1974
- // until a compatible migration has its own protocol; retained generations must use the same storage contract.
1975
- const storage = createHash('sha256');
1976
- for (const file of ['store.mjs', 'files.mjs', 'ztrack.mjs', 'published.mjs']) storage.update(file).update(readFileSync(new URL(file, import.meta.url)));
1977
- tellOwner({ type: 'prepared', storage: storage.digest('hex') });
1974
+ // A transition cannot restore database files over committed card writes: retained generations must write the same
1975
+ // storage format (storage-contract.mjs), and a format change waits for a compatible migration with its own protocol.
1976
+ tellOwner({ type: 'prepared', ...storageContract() });
1978
1977
  try { await activation; return await runVerb(argv); }
1979
1978
  finally { process.off('message', receive); process.off('disconnect', disconnected); unwatchHealth(); unwatchWorkflow(); }
1980
1979
  }
@@ -199,7 +199,10 @@ export async function serveBoardOwner(root, args) {
199
199
  const candidate = start(selected);
200
200
  try {
201
201
  const proof = await candidate.wait('prepared'), held = active.events.get('prepared');
202
- if (!proof.storage || proof.storage !== held?.storage) throw new Error('board storage contract differs; predecessor retained without opening a writer');
202
+ // Both generations name their format (storage-contract.mjs): the formats are compared. A generation from before
203
+ // `format` names only its storage modules' source, which is then compared.
204
+ const same = proof.format && held?.format ? proof.format === held.format : Boolean(proof.storage) && proof.storage === held?.storage;
205
+ if (!same) throw new Error('board storage contract differs; predecessor retained without opening a writer');
203
206
  pending = { id, candidate, predecessor: active, older: previous, phase: 'prepared' };
204
207
  return status();
205
208
  } catch (error) { await terminate(candidate); throw error; }
@@ -0,0 +1,85 @@
1
+ // The board's storage contract, which a retained handover compares between predecessor and candidate (ADR 0080,
2
+ // generation-owner.mjs prepare). A rollback leaves the predecessor serving what the candidate wrote, so the two must
3
+ // write the same format.
4
+ // - `format` fails closed. It is the SQL every backing lays out, as SQLite records it once the layout has run on an
5
+ // empty database, and the whole source of the four storage modules except what a format cannot depend on: their
6
+ // import lines, whole-line comments, blank lines, and the members named in READS, which decide how a store is read or
7
+ // which rows a load keeps and write no format. Any other change to those modules moves it, a writer or anything a
8
+ // writer reads (the property sets, a file name, a number's form) alike.
9
+ // - `storage` is the earlier contract, the modules' whole source. An owner from before `format` compares it alone, so a
10
+ // candidate keeps sending it; an owner that knows `format` compares that when both generations send one.
11
+ // `format` is null when a member READS names occurs more than once (the owner then compares `storage`).
12
+ import { createHash } from 'node:crypto';
13
+ import fs, { readFileSync } from 'node:fs';
14
+ import { syncBuiltinESMExports } from 'node:module';
15
+ import { DatabaseSync } from 'node:sqlite';
16
+ import { layOut } from './store.mjs';
17
+
18
+ const MODULES = ['store.mjs', 'files.mjs', 'ztrack.mjs', 'published.mjs'];
19
+ // Members that only read a store or choose which rows a load keeps (VO-3578 row 22's #2328 changed these): a change to
20
+ // one keeps the format. A member joins this list only when what it writes is rows of the laid-out tables, never a file,
21
+ // a document's text or a JSON shape.
22
+ const READS = {
23
+ 'store.mjs': ['unnumberTasks'],
24
+ 'files.mjs': ['load'],
25
+ 'ztrack.mjs': ['applyPlan', 'importRuntime'],
26
+ };
27
+
28
+ const source = (file) => readFileSync(new URL(file, import.meta.url), 'utf8');
29
+ // A member's lines in its module: from its first line (` name(` in a class, `function name(` or `const name =` at the
30
+ // top) to the line that closes it at the same indent, or that line alone when it ends the member. None when absent.
31
+ function without(text, name) {
32
+ const lines = text.split('\n'), opens = new RegExp(`^(?: (?:async )?${name}\\(|(?:export )?(?:async )?function ${name}\\(|(?:export )?const ${name} = )`);
33
+ const starts = lines.flatMap((line, i) => opens.test(line) ? [i] : []);
34
+ if (starts.length > 1) throw new Error(`${name} is more than one member of its module`);
35
+ if (!starts.length) return text;
36
+ const [start] = starts, first = lines[start].trimEnd(), indent = /^ */.exec(first)[0];
37
+ const end = first.endsWith(';') ? start : lines.findIndex((line, i) => i > start && line === `${indent}}`);
38
+ if (end < 0) throw new Error(`${name} has no closing line`);
39
+ return [...lines.slice(0, start), ...lines.slice(end + 1)].join('\n');
40
+ }
41
+ // What a format cannot depend on: import statements, lines that are only a comment, and blank lines.
42
+ function code(text) {
43
+ const kept = [];
44
+ let importing = false;
45
+ for (const line of text.split('\n')) {
46
+ const trimmed = line.trim();
47
+ if (importing || /^import\s/.test(trimmed)) { importing = !/;\s*$/.test(trimmed); continue; }
48
+ if (!trimmed || /^(\/\/|\/\*|\*)/.test(trimmed)) continue;
49
+ kept.push(line);
50
+ }
51
+ return kept.join('\n');
52
+ }
53
+
54
+ // The layout's one-time migration notices (store.mjs writes them to stderr with writeSync) describe a real store; on
55
+ // this empty one they would put 'holds the owner event loop' lines in the board's log at every generation start. The
56
+ // layout is synchronous, so for its call alone a stderr line that starts 'board: ' is dropped; writeSync is restored
57
+ // before anything else runs.
58
+ function quietly(work) {
59
+ const writeSync = fs.writeSync;
60
+ fs.writeSync = (fd, text, ...rest) => (fd === 2 && typeof text === 'string' && text.startsWith('board: ') ? text.length : writeSync(fd, text, ...rest));
61
+ syncBuiltinESMExports();
62
+ try { return work(); } finally { fs.writeSync = writeSync; syncBuiltinESMExports(); }
63
+ }
64
+
65
+ function format() {
66
+ const db = new DatabaseSync(':memory:');
67
+ try {
68
+ quietly(() => layOut(db, { fresh: true }));
69
+ const ztrack = source('ztrack.mjs'), opens = ztrack.indexOf('const BOARD_TABLES = `');
70
+ if (opens < 0) throw new Error('ztrack.mjs has no BOARD_TABLES');
71
+ db.exec(ztrack.slice(ztrack.indexOf('`', opens) + 1, ztrack.indexOf('`;', opens)));
72
+ const layout = db.prepare("SELECT type, name, tbl_name, sql FROM sqlite_master WHERE name NOT LIKE 'sqlite_%' ORDER BY type, name, tbl_name").all()
73
+ .map(({ type, name, tbl_name: table, sql }) => [type, name, table, sql]);
74
+ const modules = MODULES.map((file) => [file, code((READS[file] ?? []).reduce(without, source(file)))]);
75
+ return createHash('sha256').update(JSON.stringify({ layout, modules })).digest('hex');
76
+ } finally { db.close(); }
77
+ }
78
+
79
+ export function storageContract() {
80
+ const storage = createHash('sha256');
81
+ for (const file of MODULES) storage.update(file).update(readFileSync(new URL(file, import.meta.url)));
82
+ let formatDigest = null;
83
+ try { formatDigest = format(); } catch (error) { process.stderr.write(`board storage contract: no format digest (${error.message}); the owner compares storage\n`); }
84
+ return { storage: storage.digest('hex'), format: formatDigest };
85
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.5.223",
3
+ "version": "0.5.225",
4
4
  "type": "module",
5
5
  "description": "The orchestrator runtime over the Volter Harness ontology: one typed operational model whose folder is its serialization, read and written through the harness orchestration doors (docs/ORCHESTRATOR-IR.md)",
6
6
  "exports": {
@@ -36,7 +36,7 @@
36
36
  "check-types": "node scripts/generate-types.mjs --check"
37
37
  },
38
38
  "dependencies": {
39
- "@volter/supercode-teams": "^0.3.380",
39
+ "@volter/supercode-teams": "^0.3.382",
40
40
  "@volter/supercode-terminal": "^0.3.12",
41
41
  "@volter/supercode-harness-sdk": "^0.3.106",
42
42
  "@volter/teams": "0.2.106",