@volter/supercode-orchestrator 0.4.154 → 0.4.155
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/ci.mjs +23 -0
- package/board/cli.mjs +5 -2
- package/board/dispatch.mjs +27 -6
- package/board/ztrack.mjs +97 -16
- package/package.json +1 -1
package/board/ci.mjs
CHANGED
|
@@ -180,6 +180,29 @@ export function openReviewPullRequests(board, id) {
|
|
|
180
180
|
return { pr: prs.map((p) => p.url), pr_head: prs.map((p) => p.head), branch };
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
/** An arc that changed no code (a disposition, such as closing a PR with its reason): its work branch holds no commit
|
|
184
|
+
* beyond its repository's default branch, locally or on origin, no PR was ever opened from it, and it registered no
|
|
185
|
+
* other branch. Answers what its review reads instead of a diff, or null when the arc carries (or may carry) code. */
|
|
186
|
+
export function noCodeChange(board, id) {
|
|
187
|
+
const own = repositoryWorkspace(board, id);
|
|
188
|
+
if (!own) return null;
|
|
189
|
+
try {
|
|
190
|
+
const repository = originOf(own);
|
|
191
|
+
const task = board.db.prepare('SELECT branch_name FROM tasks WHERE id=?').get(id);
|
|
192
|
+
const branch = task?.branch_name ?? (run('git', ['branch', '--show-current'], own) || null);
|
|
193
|
+
const base = run('git', ['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'], own);
|
|
194
|
+
if (!repository || !branch || !base || branch === base.replace(/^origin\//, '')) return null;
|
|
195
|
+
if (registeredBranches(board, id).some((r) => r.repository !== repository || r.branch !== branch)) return null;
|
|
196
|
+
// An unknown local or remote commit is code this checkout cannot see: run() throws and the arc waits.
|
|
197
|
+
if (run('git', ['for-each-ref', '--format=%(refname)', `refs/heads/${branch}`], own)) run('git', ['merge-base', '--is-ancestor', `refs/heads/${branch}`, base], own);
|
|
198
|
+
const remote = run('git', ['ls-remote', 'origin', `refs/heads/${branch}`], own).split(/\s+/)[0];
|
|
199
|
+
if (remote) run('git', ['merge-base', '--is-ancestor', remote, base], own);
|
|
200
|
+
const owner = repository.split('/')[0];
|
|
201
|
+
if (github(`/repos/${repository}/pulls?state=all&head=${encodeURIComponent(`${owner}:${branch}`)}&per_page=1`).length) return null;
|
|
202
|
+
return { pr: [], pr_head: [], branch, no_code: { repository, branch, base, branch_head: remote || null } };
|
|
203
|
+
} catch { return null; }
|
|
204
|
+
}
|
|
205
|
+
|
|
183
206
|
/** The repositories this machine keeps a checkout of. */
|
|
184
207
|
function knownRepositories() {
|
|
185
208
|
try { return [...new Set(branchCheckouts(null, '*').map((row) => row.repository))]; } catch { return []; }
|
package/board/cli.mjs
CHANGED
|
@@ -756,8 +756,11 @@ export async function runWorkflow(argv) {
|
|
|
756
756
|
const params = { ...(args['failure-limit'] ? { failure_limit: Number(args['failure-limit']) } : {}), ...(args.max ? { max_spawn: Number(args.max) } : {}) };
|
|
757
757
|
if (args['dry-run']) {
|
|
758
758
|
// planned through the workflow's own dispatch (its start event, eligible, order and limits), writing nothing
|
|
759
|
-
const
|
|
760
|
-
|
|
759
|
+
const reports = await plan({ root, lanes, settings, params, slugs: [slug] });
|
|
760
|
+
const would = reports.flatMap((r) => r.would), reviews = reports.flatMap((r) => r.would_review ?? []);
|
|
761
|
+
const lines = [...reviews.map((r) => `would request review of ${r.id}: ${r.no_code ? `no code changed (${r.no_code.repository} ${r.no_code.branch} holds no commit beyond ${r.no_code.base}, no PR from it)` : r.prs.map((url, i) => `${url} at ${r.heads[i]}`).join(', ')}`),
|
|
762
|
+
...would.map((w) => `would start ${w.id} (${w.status}${w.role ? `, its ${w.role}` : ''}) as ${w.surface} for ${w.assignee}${w.machine ? ` on ${w.machine}` : ''}`)];
|
|
763
|
+
say(args, lines.length ? lines.join('\n') : 'nothing would start', args.json ? { would, would_review: reviews } : would);
|
|
761
764
|
return 0;
|
|
762
765
|
}
|
|
763
766
|
const reports = await tick({ root, lanes, settings, params });
|
package/board/dispatch.mjs
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// Every side effect follows a committed transition: a card is claimed before its session is launched, and a
|
|
16
16
|
// subscriber's cursor moves only after its notice was delivered, so two dispatchers over one board never both
|
|
17
17
|
// launch and a crash between the two steps is answered by the next tick.
|
|
18
|
-
import { inspectCompletionEvidence, openReviewPullRequests, reviewDue, withCompletionEvidence, reviewCandidate, workflowRevision, retainWorkflowTick } from './ci.mjs';
|
|
18
|
+
import { inspectCompletionEvidence, noCodeChange, openReviewPullRequests, reviewDue, withCompletionEvidence, reviewCandidate, workflowRevision, retainWorkflowTick } from './ci.mjs';
|
|
19
19
|
import { createRequire } from 'node:module';
|
|
20
20
|
import { spawn as spawnChild, spawnSync } from 'node:child_process';
|
|
21
21
|
import { existsSync, mkdirSync, openSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
@@ -1848,15 +1848,30 @@ export async function plan({ root, lanes, settings, slugs = null, params = {} })
|
|
|
1848
1848
|
for (const slug of slugs ?? listBoards(root)) {
|
|
1849
1849
|
const report = { board: slug, would: [], queued: [], unassigned: [], nonspawnable: [] };
|
|
1850
1850
|
const board = openBoard(root, slug);
|
|
1851
|
-
try {
|
|
1851
|
+
try {
|
|
1852
|
+
board.label = boardLabel(root, slug);
|
|
1853
|
+
await activatePullRequestReviews(board, entry, report, [], { dry: true });
|
|
1854
|
+
startCards(board, fleet, lanes, settings, report, entry, [], { dry: true });
|
|
1855
|
+
} finally { board.close(); }
|
|
1852
1856
|
out.push(report);
|
|
1853
1857
|
}
|
|
1854
1858
|
return out;
|
|
1855
1859
|
}
|
|
1856
1860
|
|
|
1861
|
+
/** Whether an arc with no open PR may be one that changed no code and is due its review: it filed and closed tasks
|
|
1862
|
+
* (a fresh arc with none has not started), and no review of it is pending or passed (after changes are requested,
|
|
1863
|
+
* closing the tasks that own the findings makes it due again). */
|
|
1864
|
+
function noCodeReviewDue(board, id) {
|
|
1865
|
+
return view(board, (db) => {
|
|
1866
|
+
if (!db.prepare('SELECT 1 FROM supercode_cards WHERE subtask_of = ? LIMIT 1').get(id)) return false;
|
|
1867
|
+
const last = db.prepare("SELECT kind FROM task_events WHERE task_id = ? AND kind IN ('review_requested', 'changes_requested', 'review_pass') ORDER BY id DESC LIMIT 1").get(id);
|
|
1868
|
+
return !last || last.kind === 'changes_requested';
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1857
1872
|
/** An opened PR asks for the declared review transition once the arc's tasks are closed. */
|
|
1858
1873
|
// A task written in the card's own Tasks section that did not become a subtask holds its review too.
|
|
1859
|
-
async function activatePullRequestReviews(board, entry, report, effects, { only = null } = {}) {
|
|
1874
|
+
async function activatePullRequestReviews(board, entry, report, effects, { only = null, dry = false } = {}) {
|
|
1860
1875
|
const candidates = view(board, db => db.prepare(`SELECT t.id FROM tasks t JOIN supercode_cards c ON c.task_id=t.id
|
|
1861
1876
|
WHERE c.subtask_of IS NULL AND c.every IS NULL
|
|
1862
1877
|
AND t.workspace_kind!='scratch' ${only ? 'AND t.id=?' : ''}`).all(...(only ? [only] : [])));
|
|
@@ -1879,20 +1894,26 @@ async function activatePullRequestReviews(board, entry, report, effects, { only
|
|
|
1879
1894
|
payload = openReviewPullRequests(board, id);
|
|
1880
1895
|
// The arc's open PRs ask for one review only while some head has no verdict: after changes are
|
|
1881
1896
|
// requested the next run is the implementer's, until a new head is pushed.
|
|
1897
|
+
// An arc that closed its tasks having changed no code gets its one review of the act itself (ADR 0003).
|
|
1898
|
+
if (!payload && noCodeReviewDue(board, id)) payload = noCodeChange(board, id);
|
|
1882
1899
|
if (!payload) { report.queued.push([id, 'review waits: no open PR on its branch, a wt/<card> branch or a registered checkout']); continue; }
|
|
1883
1900
|
if (payload.waits) { report.queued.push([id, `review waits: ${payload.waits}`]); payload = null; continue; }
|
|
1884
|
-
if (!reviewDue(board, id, payload.pr.map((url, index) => ({ url, head: payload.pr_head[index] })))) {
|
|
1901
|
+
if (!payload.no_code && !reviewDue(board, id, payload.pr.map((url, index) => ({ url, head: payload.pr_head[index] })))) {
|
|
1885
1902
|
report.queued.push([id, `review waits: every open PR head (${payload.pr.join(', ')}) has a verdict or predates the last one; push a commit that answers it`]);
|
|
1886
1903
|
continue;
|
|
1887
1904
|
}
|
|
1888
|
-
const evidence = inspectCompletionEvidence(board, id, entry, payload, 'request_review');
|
|
1905
|
+
const evidence = payload.no_code ? { required: false, ready: true, reason: null, phase: 'source', no_code: payload.no_code } : inspectCompletionEvidence(board, id, entry, payload, 'request_review');
|
|
1889
1906
|
if (!evidence.ready) throw new Error(evidence.reason);
|
|
1907
|
+
// a dry run names the review a tick would request and writes nothing
|
|
1908
|
+
if (dry) { (report.would_review ??= []).push({ id, prs: payload.pr, heads: payload.pr_head, no_code: payload.no_code ?? null }); continue; }
|
|
1890
1909
|
refreshCardInputs(board, id, { commits: true });
|
|
1891
1910
|
const moved = tx(board, db => {
|
|
1892
1911
|
const task = getTask(db, id);
|
|
1893
1912
|
if (eligible(db) !== true) return null;
|
|
1894
1913
|
return withCompletionEvidence(board, id, evidence, () => fire(db, board, entry, id, 'request_review', {
|
|
1895
|
-
roles: ['platform'], payload: { ...payload, run_id: task.current_run_id,
|
|
1914
|
+
roles: ['platform'], payload: { ...payload, run_id: task.current_run_id, ci: evidence, summary: payload.no_code
|
|
1915
|
+
? `No code changed: ${payload.no_code.repository} ${payload.branch} holds no commit beyond ${payload.no_code.base} and no PR was opened from it. Review the arc's act from the card: its comments, its tasks' results and what they point at.`
|
|
1916
|
+
: `Opened ${payload.pr.map((url, index) => `${url} at ${payload.pr_head[index]}`).join(', ')}` }
|
|
1896
1917
|
}));
|
|
1897
1918
|
});
|
|
1898
1919
|
if (moved) {
|
package/board/ztrack.mjs
CHANGED
|
@@ -78,7 +78,7 @@ class ZtrackBoard {
|
|
|
78
78
|
this.conn = new DatabaseSync(this.file);
|
|
79
79
|
this.refuseUnimported(legacy);
|
|
80
80
|
this.errors = new Map(); this.statements = new Map(); this.hashes = new Map();
|
|
81
|
-
this.active = false; this.overlay = false; this.seen = null;
|
|
81
|
+
this.active = false; this.overlay = false; this.seen = null; this.refused = new Set();
|
|
82
82
|
this.prepareConnection();
|
|
83
83
|
this.loadErrors();
|
|
84
84
|
const self = this;
|
|
@@ -330,6 +330,7 @@ class ZtrackBoard {
|
|
|
330
330
|
this.conn.exec('BEGIN');
|
|
331
331
|
this.active = true;
|
|
332
332
|
this.conn.exec('DELETE FROM temp.board_dirty; DELETE FROM temp.board_touched');
|
|
333
|
+
this.refused = new Set();
|
|
333
334
|
this.loadErrors();
|
|
334
335
|
this.recover();
|
|
335
336
|
if (this.seed) this.indexForSeed();
|
|
@@ -339,6 +340,7 @@ class ZtrackBoard {
|
|
|
339
340
|
}
|
|
340
341
|
if (hermes && hermes.cursor !== this.get('hermes_cursor', 0)) throw RETRY;
|
|
341
342
|
if (this.seed) this.seedRows(); else this.lift(hermes);
|
|
343
|
+
this.snapshotHeld();
|
|
342
344
|
const result = fn(this.db);
|
|
343
345
|
const files = this.render();
|
|
344
346
|
this.statement('INSERT INTO main.board_export (name, key) SELECT DISTINCT name, key FROM temp.board_dirty WHERE true ON CONFLICT(name, key) DO UPDATE SET rev = rev + 1').run();
|
|
@@ -417,8 +419,9 @@ class ZtrackBoard {
|
|
|
417
419
|
|
|
418
420
|
// What the edited documents now say, as a plan of rows; applied to the tables by a write, to
|
|
419
421
|
// connection-local copies by a read. Only the spans the edit touched are parsed.
|
|
420
|
-
|
|
421
|
-
|
|
422
|
+
// `held` names cards read again from their indexed span with no edit (see render).
|
|
423
|
+
observe(stale, held = []) {
|
|
424
|
+
const plan = { layout: new Map(), dropped: [], published: {}, cards: new Map(), rows: new Map(), subtasks: new Map(), tasks: new Map(), remove: new Set(), errors: new Map(), checked: new Set(), before: new Map() };
|
|
422
425
|
const changed = new Set();
|
|
423
426
|
for (const doc of this.docs) {
|
|
424
427
|
const entries = this.entries(doc.role);
|
|
@@ -436,9 +439,11 @@ class ZtrackBoard {
|
|
|
436
439
|
plan.dropped.push(...replaced.map((e) => e.cid));
|
|
437
440
|
plan.published[doc.role] = edit.hash;
|
|
438
441
|
for (const c of [...added, ...replaced]) if (c.id) changed.add(nativeId(c.id));
|
|
442
|
+
for (const c of replaced) if (c.id) plan.before.set(nativeId(c.id), { role: doc.role, entry: c });
|
|
439
443
|
}
|
|
440
444
|
const places = new Map();
|
|
441
445
|
for (const [role, entries] of plan.layout) for (const entry of entries) if (entry.id) places.set(nativeId(entry.id), [...(places.get(nativeId(entry.id)) ?? []), { role, entry }]);
|
|
446
|
+
for (const id of held) { changed.add(id); const at = places.get(id); if (at?.length === 1) plan.before.set(id, at[0]); }
|
|
442
447
|
for (const id of changed) {
|
|
443
448
|
plan.checked.add(id);
|
|
444
449
|
const at = places.get(id) ?? [];
|
|
@@ -469,7 +474,8 @@ class ZtrackBoard {
|
|
|
469
474
|
const keyed = new Map([...plan.tasks].map(([tid, { key }]) => [key, tid]));
|
|
470
475
|
const resolveRef = (ref) => keyed.get(ref) ?? this.one('SELECT id FROM main.board_tasks WHERE key = ?', ref)?.id ?? ref;
|
|
471
476
|
for (const rows of plan.rows.values()) rows.state.blocked_by = JSON.stringify(json(rows.state.blocked_by, []).map((b) => ({ ...b, id: resolveRef(b.id) })));
|
|
472
|
-
this.
|
|
477
|
+
if (held.length) this.keepMoved(plan);
|
|
478
|
+
this.checkControls(plan, resolveRef);
|
|
473
479
|
// A refused task line refuses its card's edit: the hand text stays in the document with the reason, never
|
|
474
480
|
// re-rendered without the line.
|
|
475
481
|
for (const [id, subtasks] of plan.subtasks) {
|
|
@@ -485,6 +491,35 @@ class ZtrackBoard {
|
|
|
485
491
|
return plan;
|
|
486
492
|
}
|
|
487
493
|
|
|
494
|
+
// The rows of every card held by a recorded refusal (with its subtasks) before this write's verb runs, so a held
|
|
495
|
+
// card read again after it keeps what the verb moved (keepMoved).
|
|
496
|
+
snapshotHeld() {
|
|
497
|
+
this.heldRows = new Map();
|
|
498
|
+
const cards = new Set();
|
|
499
|
+
for (const id of this.errors.keys()) cards.add(this.one('SELECT subtask_of FROM main.supercode_cards WHERE task_id = ?', id)?.subtask_of ?? id);
|
|
500
|
+
for (const card of cards) {
|
|
501
|
+
for (const { task_id: id } of [{ task_id: card }, ...this.statement('SELECT task_id FROM main.supercode_cards WHERE subtask_of = ?').all(card)]) {
|
|
502
|
+
this.heldRows.set(id, { task: this.one('SELECT * FROM main.tasks WHERE id = ?', id) ?? null, state: this.one('SELECT * FROM main.supercode_cards WHERE task_id = ?', id) ?? null });
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// A held card's span is older than this write: every column the write's verb moved keeps the store's value
|
|
508
|
+
// (title, body, assignee, status, ...); the span's text lands only where the store did not move.
|
|
509
|
+
keepMoved(plan) {
|
|
510
|
+
const same = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
511
|
+
for (const [id, rows] of plan.rows) {
|
|
512
|
+
const task = this.one('SELECT * FROM main.tasks WHERE id = ?', id), state = this.one('SELECT * FROM main.supercode_cards WHERE task_id = ?', id);
|
|
513
|
+
if (!task) continue;
|
|
514
|
+
const was = this.heldRows?.get(id);
|
|
515
|
+
for (const [side, now, then] of [['task', task, was?.task], ['state', state, was?.state]]) {
|
|
516
|
+
if (!now) continue;
|
|
517
|
+
// A row the write created has nothing held to keep: all of it is the store's.
|
|
518
|
+
for (const key of Object.keys(now)) if (!then || !same(now[key], then[key])) rows[side][key] = now[key];
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
488
523
|
planRemoval(plan, id) {
|
|
489
524
|
const task = this.one('SELECT current_run_id FROM main.tasks WHERE id = ?', id);
|
|
490
525
|
if (!task) return;
|
|
@@ -495,7 +530,10 @@ class ZtrackBoard {
|
|
|
495
530
|
|
|
496
531
|
// A raw edit cannot authenticate its author: the workflow's controlled fields change only through
|
|
497
532
|
// role-checked verbs. The first import of a document has nothing to compare with.
|
|
498
|
-
|
|
533
|
+
// A controlled value the edit left as the replaced span said it is no edit but a stale line (the store moved
|
|
534
|
+
// while its card was not rendered, as under a recorded refusal): it takes the store's value, and the card's
|
|
535
|
+
// render writes that back into its lines (t_f3f102db).
|
|
536
|
+
checkControls(plan, resolveRef) {
|
|
499
537
|
if (!this.one('SELECT 1 FROM main.tasks LIMIT 1')) return;
|
|
500
538
|
let fields = [];
|
|
501
539
|
try { fields = workflowFor(this.root).params.document_control_fields ?? []; } catch { /* no home policy */ }
|
|
@@ -504,13 +542,46 @@ class ZtrackBoard {
|
|
|
504
542
|
: field === 'blocked_by' ? json(state?.blocked_by, []).map((b) => [b.kind, b.id]).sort()
|
|
505
543
|
: field === 'paused' ? json(state?.paused, null)
|
|
506
544
|
: field === 'archived' ? Boolean(state?.archived) : task?.[field] ?? null;
|
|
545
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
546
|
+
// The rows a card's span read as before this edit, with its subtasks', parsed only when a field differs.
|
|
547
|
+
const before = new Map();
|
|
548
|
+
const rowsBefore = (card) => {
|
|
549
|
+
if (before.has(card)) return before.get(card);
|
|
550
|
+
const rows = new Map(); before.set(card, rows);
|
|
551
|
+
const at = plan.before.get(card);
|
|
552
|
+
if (!at) return rows;
|
|
553
|
+
try {
|
|
554
|
+
const { records, errors } = this.codec.inspectBoardDocument(this.texts([at.entry])[0].text, this.docOf(at.role).path);
|
|
555
|
+
if (errors.length || records.length !== 1 || nativeId(records[0].id) !== card) return rows;
|
|
556
|
+
const parsed = this.preset.KanbanRootSchema.parse(this.preset.parseKanban([records[0]])).issues[0];
|
|
557
|
+
rows.set(card, this.cardRows(card, parsed, null));
|
|
558
|
+
for (const [tid, taskCard] of this.taskCards(card, parsed)) rows.set(tid, this.cardRows(tid, taskCard, card));
|
|
559
|
+
for (const r of rows.values()) r.state.blocked_by = JSON.stringify(json(r.state.blocked_by, []).map((b) => ({ ...b, id: resolveRef(b.id) })));
|
|
560
|
+
} catch { rows.clear(); } // an unreadable span says nothing of what the lines said
|
|
561
|
+
return rows;
|
|
562
|
+
};
|
|
563
|
+
const adopt = (rows, task, state, field) => {
|
|
564
|
+
if (field === 'status') { rows.task.status = task.status; rows.state.stage = state?.stage ?? null; }
|
|
565
|
+
else if (field === 'blocked_by') rows.state.blocked_by = state?.blocked_by ?? '[]';
|
|
566
|
+
else if (field === 'paused') rows.state.paused = state?.paused ?? null;
|
|
567
|
+
else if (field === 'archived') rows.state.archived = state?.archived ?? 0;
|
|
568
|
+
else rows.task[field] = task[field] ?? null;
|
|
569
|
+
};
|
|
507
570
|
for (const [id, rows] of plan.rows) {
|
|
508
571
|
const task = this.one('SELECT * FROM main.tasks WHERE id = ?', id), state = this.one('SELECT * FROM main.supercode_cards WHERE task_id = ?', id);
|
|
509
572
|
// A new display author is not an authenticated creator.
|
|
510
|
-
const
|
|
573
|
+
const differ = fields.filter((field) => (task || field !== 'created_by') && !same(value(rows.task, rows.state, field), value(task, state, field)));
|
|
574
|
+
const prior = task && differ.length ? rowsBefore(rows.state.subtask_of ?? id).get(id) : null;
|
|
575
|
+
const stale = prior ? differ.filter((field) => same(value(prior.task, prior.state, field), value(rows.task, rows.state, field))) : [];
|
|
576
|
+
for (const field of stale) adopt(rows, task, state, field);
|
|
577
|
+
const forbidden = differ.filter((field) => !stale.includes(field));
|
|
511
578
|
if (forbidden.length) plan.errors.set(id, `Document edit cannot change ${forbidden.join(', ')}; use this card's role-checked workflow verbs or request-state.`);
|
|
512
579
|
}
|
|
513
580
|
for (const id of plan.remove) {
|
|
581
|
+
// A task line already absent from the replaced span was not removed by this edit: the store keeps it.
|
|
582
|
+
const parent = this.one('SELECT subtask_of FROM main.supercode_cards WHERE task_id = ?', id)?.subtask_of;
|
|
583
|
+
const prior = parent && !plan.remove.has(parent) ? rowsBefore(parent) : null;
|
|
584
|
+
if (prior?.size && !prior.has(id)) { plan.remove.delete(id); plan.checked.add(id); continue; }
|
|
514
585
|
plan.errors.set(id, "Document edit cannot change card removal; use this card's role-checked workflow verbs or request-state.");
|
|
515
586
|
}
|
|
516
587
|
for (const id of plan.errors.keys()) plan.remove.delete(id);
|
|
@@ -553,6 +624,7 @@ class ZtrackBoard {
|
|
|
553
624
|
for (const id of plan.checked) this.errors.delete(id);
|
|
554
625
|
for (const [id, reason] of plan.errors) this.errors.set(id, reason);
|
|
555
626
|
if (overlay) return;
|
|
627
|
+
for (const id of plan.errors.keys()) this.refused.add(this.one('SELECT subtask_of FROM main.supercode_cards WHERE task_id = ?', id)?.subtask_of ?? id);
|
|
556
628
|
for (const [tid, { key, sources }] of plan.tasks) this.statement('INSERT OR REPLACE INTO main.board_tasks (id, key, sources) VALUES (?, ?, ?)').run(tid, key, sources?.length ? JSON.stringify(sources) : null);
|
|
557
629
|
for (const id of [...plan.checked, ...plan.rows.keys()]) if (!plan.errors.has(id)) this.statement('DELETE FROM main.board_errors WHERE id = ?').run(id);
|
|
558
630
|
for (const [id, reason] of plan.errors) this.statement('INSERT OR REPLACE INTO main.board_errors VALUES (?, ?)').run(id, reason);
|
|
@@ -629,21 +701,30 @@ class ZtrackBoard {
|
|
|
629
701
|
// into their documents; a card whose record is unchanged keeps its exact bytes. Answers the
|
|
630
702
|
// documents to publish.
|
|
631
703
|
render() {
|
|
632
|
-
const
|
|
633
|
-
|
|
704
|
+
const cardsTouched = () => {
|
|
705
|
+
const parents = new Set();
|
|
706
|
+
for (const { id } of this.statement('SELECT DISTINCT id FROM temp.board_touched WHERE id IS NOT NULL').all()) {
|
|
707
|
+
const parent = this.one('SELECT subtask_of FROM main.supercode_cards WHERE task_id = ?', id)?.subtask_of;
|
|
708
|
+
const key = parent ? null : this.one('SELECT key FROM main.board_tasks WHERE id = ?', id)?.key;
|
|
709
|
+
parents.add(parent ?? (key && !this.one('SELECT 1 FROM main.tasks WHERE id = ?', id) ? key.slice(0, key.lastIndexOf(':')) : id));
|
|
710
|
+
}
|
|
711
|
+
return parents;
|
|
712
|
+
};
|
|
713
|
+
let parents = cardsTouched();
|
|
714
|
+
if (!parents.size) return [];
|
|
715
|
+
const heldBy = (id) => this.errors.has(id) || this.one('SELECT 1 FROM main.supercode_cards WHERE subtask_of = ? AND task_id IN (SELECT id FROM main.board_errors)', id);
|
|
716
|
+
// A card held by a recorded refusal is not rendered, so the store's verbs move its tasks past its lines. When
|
|
717
|
+
// the store moves it, its span is read again: what the store moved past is stale and takes the store's value
|
|
718
|
+
// (checkControls), and the card renders. A card refused by this write's own edit stays as it was written.
|
|
719
|
+
const held = [...parents].filter((id) => !this.refused.has(id) && heldBy(id));
|
|
720
|
+
if (held.length) { this.applyPlan(this.observe([], held), { overlay: false }); parents = cardsTouched(); }
|
|
721
|
+
if (this.seedIds) for (const id of this.seedIds) parents.add(id);
|
|
634
722
|
const layout = { open: this.entries('open'), archived: this.entries('archived') };
|
|
635
723
|
const where = new Map();
|
|
636
724
|
for (const role of ['open', 'archived']) layout[role].forEach((entry, index) => { if (entry.id) where.set(nativeId(entry.id), { role, index, entry }); });
|
|
637
|
-
const parents = new Set();
|
|
638
|
-
for (const id of touched) {
|
|
639
|
-
const parent = this.one('SELECT subtask_of FROM main.supercode_cards WHERE task_id = ?', id)?.subtask_of;
|
|
640
|
-
const key = parent ? null : this.one('SELECT key FROM main.board_tasks WHERE id = ?', id)?.key;
|
|
641
|
-
parents.add(parent ?? (key && !this.one('SELECT 1 FROM main.tasks WHERE id = ?', id) ? key.slice(0, key.lastIndexOf(':')) : id));
|
|
642
|
-
}
|
|
643
|
-
if (this.seedIds) for (const id of this.seedIds) parents.add(id);
|
|
644
725
|
const edits = { open: [], archived: [] };
|
|
645
726
|
for (const id of parents) {
|
|
646
|
-
if (
|
|
727
|
+
if (heldBy(id)) continue;
|
|
647
728
|
const task = this.one('SELECT * FROM main.tasks WHERE id = ?', id);
|
|
648
729
|
const state = this.one('SELECT * FROM main.supercode_cards WHERE task_id = ?', id) ?? {};
|
|
649
730
|
if (task && state.subtask_of) continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.155",
|
|
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": {
|