explainer-video 0.0.0-stage → 0.4.0
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/.claude-plugin/plugin.json +12 -0
- package/LICENSE +21 -0
- package/README.md +152 -2
- package/cordis.patch.yml +7 -0
- package/lib/index.js +35 -0
- package/package.json +56 -4
- package/plugin.json +13 -0
- package/skills/coursework-author/.synced-from +5 -0
- package/skills/coursework-author/SKILL.md +209 -0
- package/skills/craft-codeflow-flows/SKILL.md +116 -0
- package/skills/craft-codeflow-mine/SKILL.md +207 -0
- package/skills/craft-codeflow-plan/SKILL.md +65 -0
- package/skills/craft-codeflow-scene/SKILL.md +168 -0
- package/skills/craft-video-align/SKILL.md +109 -0
- package/skills/craft-video-assemble/SKILL.md +69 -0
- package/skills/craft-video-audio/SKILL.md +59 -0
- package/skills/craft-video-enrich/SKILL.md +46 -0
- package/skills/craft-video-plan/SKILL.md +57 -0
- package/skills/craft-video-publish/SKILL.md +79 -0
- package/skills/craft-video-render/SKILL.md +68 -0
- package/skills/craft-video-scene/SKILL.md +112 -0
- package/skills/craft-video-script/SKILL.md +80 -0
- package/skills/explainer-video-from-codebase/FLOWS.md +577 -0
- package/skills/explainer-video-from-codebase/SKILL.md +659 -0
- package/skills/explainer-video-from-codebase/tools/apply-flows.mjs +531 -0
- package/skills/explainer-video-from-codebase/tools/approve-flows.mjs +56 -0
- package/skills/explainer-video-from-codebase/tools/codemap-md.mjs +116 -0
- package/skills/explainer-video-from-codebase/tools/config/dotenv.mjs +13 -0
- package/skills/explainer-video-from-codebase/tools/config/json.mjs +14 -0
- package/skills/explainer-video-from-codebase/tools/config/redact.mjs +16 -0
- package/skills/explainer-video-from-codebase/tools/config/toml-lite.mjs +13 -0
- package/skills/explainer-video-from-codebase/tools/config/yaml-lite.mjs +18 -0
- package/skills/explainer-video-from-codebase/tools/excerpt-check.mjs +186 -0
- package/skills/explainer-video-from-codebase/tools/excerpt.mjs +116 -0
- package/skills/explainer-video-from-codebase/tools/flow-diagram.mjs +168 -0
- package/skills/explainer-video-from-codebase/tools/flow-plan.mjs +129 -0
- package/skills/explainer-video-from-codebase/tools/flows-md.mjs +87 -0
- package/skills/explainer-video-from-codebase/tools/lang/c.mjs +684 -0
- package/skills/explainer-video-from-codebase/tools/lang/go.mjs +673 -0
- package/skills/explainer-video-from-codebase/tools/lang/python-config.mjs +143 -0
- package/skills/explainer-video-from-codebase/tools/lang/python-deps.mjs +62 -0
- package/skills/explainer-video-from-codebase/tools/lang/python-stdlib.mjs +24 -0
- package/skills/explainer-video-from-codebase/tools/lang/python.mjs +609 -0
- package/skills/explainer-video-from-codebase/tools/lang/typescript.mjs +963 -0
- package/skills/explainer-video-from-codebase/tools/lib/decompose.mjs +102 -0
- package/skills/explainer-video-from-codebase/tools/lib/groups.mjs +77 -0
- package/skills/explainer-video-from-codebase/tools/lib/observe-coverage.mjs +68 -0
- package/skills/explainer-video-from-codebase/tools/lib/observe-v8.mjs +232 -0
- package/skills/explainer-video-from-codebase/tools/lib/observe.mjs +162 -0
- package/skills/explainer-video-from-codebase/tools/lib/redact.mjs +28 -0
- package/skills/explainer-video-from-codebase/tools/lib/sandbox.mjs +97 -0
- package/skills/explainer-video-from-codebase/tools/lib/test-links.mjs +28 -0
- package/skills/explainer-video-from-codebase/tools/lib/toml.mjs +129 -0
- package/skills/explainer-video-from-codebase/tools/lib/trace-c.mjs +95 -0
- package/skills/explainer-video-from-codebase/tools/lib/trace-go.mjs +101 -0
- package/skills/explainer-video-from-codebase/tools/lib/trace-js.mjs +136 -0
- package/skills/explainer-video-from-codebase/tools/lib/tree-sitter.mjs +19 -0
- package/skills/explainer-video-from-codebase/tools/lib/walk.mjs +138 -0
- package/skills/explainer-video-from-codebase/tools/mine.mjs +409 -0
- package/skills/explainer-video-from-codebase/tools/propose.mjs +127 -0
- package/skills/explainer-video-from-codebase/tools/rank.mjs +119 -0
- package/skills/explainer-video-from-codebase/tools/ranking-md.mjs +76 -0
- package/skills/explainer-video-from-codebase/tools/rules/c-handlers.mjs +53 -0
- package/skills/explainer-video-from-codebase/tools/rules/celery-taskiq.mjs +48 -0
- package/skills/explainer-video-from-codebase/tools/rules/click-typer.mjs +6 -0
- package/skills/explainer-video-from-codebase/tools/rules/express.mjs +72 -0
- package/skills/explainer-video-from-codebase/tools/rules/fastapi.mjs +99 -0
- package/skills/explainer-video-from-codebase/tools/rules/go-http.mjs +173 -0
- package/skills/explainer-video-from-codebase/tools/rules/go-test.mjs +108 -0
- package/skills/explainer-video-from-codebase/tools/rules/grpc.mjs +57 -0
- package/skills/explainer-video-from-codebase/tools/rules/gtest.mjs +97 -0
- package/skills/explainer-video-from-codebase/tools/rules/haystack.mjs +55 -0
- package/skills/explainer-video-from-codebase/tools/rules/jest-vitest.mjs +100 -0
- package/skills/explainer-video-from-codebase/tools/rules/js-http-client.mjs +111 -0
- package/skills/explainer-video-from-codebase/tools/rules/nextjs.mjs +53 -0
- package/skills/explainer-video-from-codebase/tools/rules/process-spawn.mjs +49 -0
- package/skills/explainer-video-from-codebase/tools/rules/py-http-client.mjs +26 -0
- package/skills/explainer-video-from-codebase/tools/rules/pytest.mjs +107 -0
- package/skills/explainer-video-from-codebase/tools/rules/react-router.mjs +17 -0
- package/skills/explainer-video-from-codebase/tools/rules/registry.mjs +31 -0
- package/skills/explainer-video-from-codebase/tools/trace/c/fmtrace-sh +14 -0
- package/skills/explainer-video-from-codebase/tools/trace/js/fmtrace-net.cjs +33 -0
- package/skills/explainer-video-from-codebase/tools/trace/js/fmtrace-srcmap.cjs +41 -0
- package/skills/explainer-video-from-codebase/tools/trace/python/fmtrace_plugin.py +87 -0
- package/skills/explainer-video-from-codebase/tools/trace/python/sitecustomize.py +330 -0
- package/skills/explainer-video-from-codebase/tools/trace/sandbox/linux-bwrap.sh +73 -0
- package/skills/explainer-video-from-codebase/tools/trace/sandbox/macos-nonet.sb +41 -0
- package/skills/explainer-video-from-codebase/tools/trace.mjs +577 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-c.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-cpp.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-go.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-javascript.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-python.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-typescript.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/web-tree-sitter.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/MANIFEST.json +172 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-c.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-cpp.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-go.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-javascript.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-python.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-tsx.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-typescript.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/web-tree-sitter.js +4081 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/web-tree-sitter.wasm +0 -0
- package/skills/explainer-video-from-coursework/ARCHITECTURE.md +59 -0
- package/skills/explainer-video-from-coursework/PIPELINE.md +343 -0
- package/skills/explainer-video-from-coursework/ROADMAP.md +725 -0
- package/skills/explainer-video-from-coursework/SERIES.md +232 -0
- package/skills/explainer-video-from-coursework/SETUP.md +105 -0
- package/skills/explainer-video-from-coursework/SKILL.md +858 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-chart.js +12 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-equation.js +10 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-hubspoke.js +7 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-loopflow.js +7 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-sequence.js +12 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-timeline.js +11 -0
- package/skills/explainer-video-from-coursework/src/scene-code.html +127 -0
- package/skills/explainer-video-from-coursework/src/scene-code.js +379 -0
- package/skills/explainer-video-from-coursework/src/scene-core.js +1250 -0
- package/skills/explainer-video-from-coursework/src/scene-diagram.html +50 -0
- package/skills/explainer-video-from-coursework/src/scene-svg.html +50 -0
- package/skills/explainer-video-from-coursework/src/timeline.js +297 -0
- package/skills/explainer-video-from-coursework/src/timing-chart.js +160 -0
- package/skills/explainer-video-from-coursework/tools/align.mjs +419 -0
- package/skills/explainer-video-from-coursework/tools/apply-enrichment.mjs +206 -0
- package/skills/explainer-video-from-coursework/tools/assemble.mjs +223 -0
- package/skills/explainer-video-from-coursework/tools/build-audio.mjs +234 -0
- package/skills/explainer-video-from-coursework/tools/chromium.mjs +73 -0
- package/skills/explainer-video-from-coursework/tools/debug-page.mjs +75 -0
- package/skills/explainer-video-from-coursework/tools/env.mjs +30 -0
- package/skills/explainer-video-from-coursework/tools/frame-dedup.mjs +131 -0
- package/skills/explainer-video-from-coursework/tools/key-source.mjs +151 -0
- package/skills/explainer-video-from-coursework/tools/model.mjs +21 -0
- package/skills/explainer-video-from-coursework/tools/naming.mjs +38 -0
- package/skills/explainer-video-from-coursework/tools/onset-audit.mjs +51 -0
- package/skills/explainer-video-from-coursework/tools/pacing-check.mjs +350 -0
- package/skills/explainer-video-from-coursework/tools/planner.mjs +616 -0
- package/skills/explainer-video-from-coursework/tools/publish.mjs +230 -0
- package/skills/explainer-video-from-coursework/tools/render.mjs +205 -0
- package/skills/explainer-video-from-coursework/tools/resolve-beats.mjs +239 -0
- package/skills/explainer-video-from-coursework/tools/script-check.mjs +174 -0
- package/skills/explainer-video-from-coursework/tools/stamp.mjs +71 -0
- package/skills/explainer-video-from-coursework/tools/timing-report.mjs +299 -0
- package/skills/explainer-video-from-coursework/tools/transcribe-gemini.mjs +96 -0
- package/skills/explainer-video-from-coursework/tools/tts-gemini.mjs +394 -0
- package/skills/explainer-video-from-coursework/tools/verify-audio.mjs +88 -0
- package/skills/explainer-video-from-coursework/tools/workdir.mjs +124 -0
- package/skills/explainer-video-from-coursework/vendor/plotly-basic.min.js +8 -0
- package/skills/explainer-video-from-coursework/vendor/plotly-basic.min.js.LICENSE.txt +16 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* =============================================================================
|
|
3
|
+
flow-diagram.mjs — the skeleton of a flow's diagram, generated from the
|
|
4
|
+
validated flows, so its boxes and arrows are the code's and not a guess.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
node tools/flow-diagram.mjs --flows out/<name>/flows.validated.json --as map [--flow <id>]
|
|
8
|
+
node tools/flow-diagram.mjs --flows out/<name>/flows.validated.json --flow <id> --as sequence
|
|
9
|
+
node tools/flow-diagram.mjs --flows out/<name>/flows.validated.json --flow <id> --as exercises
|
|
10
|
+
|
|
11
|
+
Prints JSON (design §4.6); the agent adds the layout polish and the beats:
|
|
12
|
+
map a `diagram` host config: one box per module the hops run through
|
|
13
|
+
(every series item, or one with --flow), its hops as members, an
|
|
14
|
+
arrow per call between modules; `crossings` lists the queue, HTTP
|
|
15
|
+
and spawn hand-offs with their evidence (for banners).
|
|
16
|
+
sequence `svg` lane params for SceneCore.primitives.sequence: the caller
|
|
17
|
+
(the entry's route, queue key or command) and each module as a
|
|
18
|
+
participant, one message per hop, kind call / queue / http / spawn.
|
|
19
|
+
exercises a `diagram` host config for an eval or dev flow: its hops as the
|
|
20
|
+
harness lane on the left, each production flow it drives as one
|
|
21
|
+
named box with its video number ("see video 8"), and the crossing
|
|
22
|
+
between them; the production flow is never re-drawn.
|
|
23
|
+
|
|
24
|
+
The video number comes from plan.json beside the validated flows when it was
|
|
25
|
+
planned from this exact file, else from the same planner at the default cap.
|
|
26
|
+
|
|
27
|
+
Exit: 0 ok · 1 bad flags, unreadable inputs, an unknown flow, or exercises on a
|
|
28
|
+
flow with no `exercises` hop
|
|
29
|
+
========================================================================== */
|
|
30
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
31
|
+
import { join, resolve, dirname } from 'node:path';
|
|
32
|
+
import { createHash } from 'node:crypto';
|
|
33
|
+
import { planVideos } from './lib/decompose.mjs';
|
|
34
|
+
|
|
35
|
+
const fail = (code, msg) => { console.error(`[flow-diagram] ${msg}`); process.exit(code); };
|
|
36
|
+
|
|
37
|
+
/* ---------------- args ---------------- */
|
|
38
|
+
const argv = process.argv.slice(2);
|
|
39
|
+
const VALUED = new Set(['--flows', '--flow', '--as']);
|
|
40
|
+
const flags = new Map();
|
|
41
|
+
for (let i = 0; i < argv.length; i++) {
|
|
42
|
+
if (VALUED.has(argv[i])) { if (argv[i + 1] == null) fail(1, `${argv[i]} needs a value`); flags.set(argv[i], argv[++i]); }
|
|
43
|
+
else fail(1, `unknown argument ${argv[i]}`);
|
|
44
|
+
}
|
|
45
|
+
const AS = flags.get('--as');
|
|
46
|
+
if (!flags.has('--flows') || !['map', 'sequence', 'exercises'].includes(AS)) fail(1, 'usage: flow-diagram.mjs --flows out/<name>/flows.validated.json --as map|sequence|exercises [--flow <id>]');
|
|
47
|
+
if (AS !== 'map' && !flags.has('--flow')) fail(1, `--as ${AS} draws one flow: pass --flow <id>`);
|
|
48
|
+
const VALIDATED = resolve(flags.get('--flows')), OUT = dirname(VALIDATED);
|
|
49
|
+
const sha = t => createHash('sha256').update(t).digest('hex');
|
|
50
|
+
const readText = (f, what) => { try { return readFileSync(f, 'utf8'); } catch (e) { fail(1, `cannot read ${what} ${f}: ${e.message}`); } };
|
|
51
|
+
const text = readText(VALIDATED, 'the validated flows');
|
|
52
|
+
let v;
|
|
53
|
+
try { v = JSON.parse(text); } catch (e) { fail(1, `${VALIDATED} is not valid JSON: ${e.message}`); }
|
|
54
|
+
if (v?.schema !== 1 || v.tool?.name !== 'apply-flows.mjs' || v.ok !== true) fail(1, `${VALIDATED} is not a passing apply-flows.mjs output (schema 1)`);
|
|
55
|
+
const items = new Map([...v.flows, ...v.cores].map(x => [x.id, x]));
|
|
56
|
+
const one = flags.get('--flow');
|
|
57
|
+
if (one != null && !items.has(one)) fail(1, `no flow or core "${one}" in ${VALIDATED} (have: ${[...items.keys()].join(', ')})`);
|
|
58
|
+
|
|
59
|
+
const moduleOf = sym => sym.split(':')[0];
|
|
60
|
+
// `pkg.mod.leaf` -> `mod.leaf`; a TS/JS module (a path) -> its last two path parts
|
|
61
|
+
const shortName = mod => mod.includes('/') ? mod.split('/').slice(-2).join('/') : mod.split('.').slice(-2).join('.');
|
|
62
|
+
const COLOR = { queue: 'hub', http: 'ok', spawn: 'bad' };
|
|
63
|
+
const CROSS = new Set(Object.keys(COLOR));
|
|
64
|
+
// the call that brings a hop in: the first downward step of its connection (the walk returns up, then calls down)
|
|
65
|
+
const callIn = h => (h.connection?.steps || []).find(s => s.down) || null;
|
|
66
|
+
const crossKind = h => (h.connection?.steps || []).find(s => s.down && CROSS.has(s.kind))?.kind || null;
|
|
67
|
+
|
|
68
|
+
// the video a flow is narrated in: plan.json when planned from this file, else the planner at its default cap
|
|
69
|
+
function videoOf(id) {
|
|
70
|
+
const planPath = join(OUT, 'plan.json');
|
|
71
|
+
if (existsSync(planPath)) {
|
|
72
|
+
const plan = JSON.parse(readText(planPath, 'the plan'));
|
|
73
|
+
if (plan.inputs?.validated?.sha256 === sha(text)) return { video: plan.videos.find(x => x.group === id)?.index ?? null, videoFrom: 'plan.json' };
|
|
74
|
+
}
|
|
75
|
+
return { video: planVideos(v, {}).videos.find(x => x.group === id)?.index ?? null, videoFrom: 'estimate (no plan.json from this file)' };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// n row positions from top to bottom, one centred row when n is 1
|
|
79
|
+
const spread = (n, top, bottom) => Array.from({ length: n }, (_, r) => Math.round(n === 1 ? (top + bottom) / 2 : top + r * (bottom - top) / (n - 1)));
|
|
80
|
+
// a grid the agent will re-lay: three columns, rows as needed
|
|
81
|
+
const grid = n => {
|
|
82
|
+
const cols = Math.min(3, Math.max(1, n)), rows = Math.ceil(n / cols);
|
|
83
|
+
const xs = cols === 1 ? [960] : cols === 2 ? [600, 1320] : [340, 960, 1580];
|
|
84
|
+
return { columns: Object.fromEntries(xs.map((x, i) => [`c${i}`, { x }])), rows: spread(rows, 260, 940), at: i => ({ column: `c${i % cols}`, row: Math.floor(i / cols) }) };
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/* ---------------- map ---------------- */
|
|
88
|
+
function map() {
|
|
89
|
+
const chosen = one ? [items.get(one)] : v.series.map(s => items.get(s.id));
|
|
90
|
+
const mods = new Map(); // module -> { hops, group }
|
|
91
|
+
const add = h => { const m = moduleOf(h.symbol); if (!mods.has(m)) mods.set(m, { hops: [], group: h.group }); if (!mods.get(m).hops.includes(h.name)) mods.get(m).hops.push(h.name); };
|
|
92
|
+
for (const it of chosen) for (const h of it.hops) add(h);
|
|
93
|
+
const ids = new Map([...mods.keys()].map((m, i) => [m, `m${i}`]));
|
|
94
|
+
const g = grid(mods.size);
|
|
95
|
+
const nodes = [...mods].map(([m, x], i) => ({ id: ids.get(m), kind: 'box', ...g.at(i), label: shortName(m), stereotype: `${m} · ${x.group}`,
|
|
96
|
+
members: x.hops.length > 6 ? [...x.hops.slice(0, 5), `+${x.hops.length - 5} more`] : x.hops }));
|
|
97
|
+
const edges = new Map(), crossings = [];
|
|
98
|
+
for (const it of chosen) for (const h of it.hops) {
|
|
99
|
+
for (const s of (h.connection?.steps || []).filter(s => s.down)) {
|
|
100
|
+
const a = moduleOf(s.from), b = moduleOf(s.to);
|
|
101
|
+
if (!ids.has(a) || !ids.has(b) || a === b) continue;
|
|
102
|
+
const k = `${a}>${b}`;
|
|
103
|
+
if (!edges.has(k) || CROSS.has(s.kind)) edges.set(k, { id: `e${edges.size}`, from: ids.get(a), to: ids.get(b), arrow: true, color: COLOR[s.kind] || 'client' });
|
|
104
|
+
if (CROSS.has(s.kind)) crossings.push({ item: it.id, hop: h.index, from: s.from, to: s.to, kind: s.kind, evidence: s.evidence || null });
|
|
105
|
+
}
|
|
106
|
+
const x = h.exercises ? h.drive : null;
|
|
107
|
+
if (x && ids.has(moduleOf(x.to))) {
|
|
108
|
+
const a = moduleOf(h.symbol), b = moduleOf(x.to);
|
|
109
|
+
edges.set(`${a}>${b}`, { id: '', from: ids.get(a), to: ids.get(b), arrow: true, color: COLOR[x.kind] || 'client' });
|
|
110
|
+
crossings.push({ item: it.id, hop: h.index, from: h.symbol, to: x.to, kind: x.kind, evidence: x.evidence, exercises: h.exercises });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
[...edges.values()].forEach((e, i) => { e.id = `e${i}`; });
|
|
114
|
+
return { host: 'diagram', as: 'map', flow: one || null, items: chosen.map(x => x.id),
|
|
115
|
+
config: { layout: { columns: g.columns, rows: g.rows }, nodes, edges: [...edges.values()] }, crossings };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* ---------------- sequence ---------------- */
|
|
119
|
+
function callerLabel(it) {
|
|
120
|
+
const e = it.entry;
|
|
121
|
+
if (!e) return { label: 'caller', sub: it.kind === 'core' ? 'shared code: called from several flows' : null };
|
|
122
|
+
if (e.kind === 'http') return { label: `${e.method} ${e.path}`, sub: 'HTTP request' };
|
|
123
|
+
if (e.kind === 'ui-route') return { label: e.path, sub: 'page in the browser' };
|
|
124
|
+
if (e.kind === 'queue-consumer' || e.kind === 'scheduled') return { label: e.key, sub: e.kind === 'scheduled' ? 'schedule' : 'queue message' };
|
|
125
|
+
if (e.kind === 'main' || e.kind === 'cli') return { label: e.file, sub: 'command line' };
|
|
126
|
+
return { label: e.kind, sub: e.file };
|
|
127
|
+
}
|
|
128
|
+
function sequence() {
|
|
129
|
+
const it = items.get(one);
|
|
130
|
+
const parts = [{ id: 'caller', ...callerLabel(it) }];
|
|
131
|
+
const pid = new Map();
|
|
132
|
+
const part = mod => { if (!pid.has(mod)) { pid.set(mod, `p${pid.size}`); parts.push({ id: pid.get(mod), label: shortName(mod), sub: shortName(mod) === mod ? null : mod }); } return pid.get(mod); };
|
|
133
|
+
const messages = [];
|
|
134
|
+
const entryKind = it.entry?.kind === 'http' ? 'http' : it.entry?.kind === 'queue-consumer' ? 'queue' : 'call';
|
|
135
|
+
for (const h of it.hops) {
|
|
136
|
+
const to = part(moduleOf(h.symbol)), c = callIn(h);
|
|
137
|
+
const from = c ? part(moduleOf(c.from)) : 'caller';
|
|
138
|
+
messages.push({ from, to, label: h.name, kind: crossKind(h) || (c ? 'call' : entryKind), hop: h.index, via: Math.max(0, (h.connection?.steps || []).filter(s => s.down).length - 1) });
|
|
139
|
+
}
|
|
140
|
+
for (const h of it.hops.filter(x => x.exercises)) {
|
|
141
|
+
const tgt = items.get(h.exercises), vid = videoOf(h.exercises);
|
|
142
|
+
const id = `flow:${h.exercises}`;
|
|
143
|
+
if (!parts.some(p => p.id === id)) parts.push({ id, label: tgt.title, sub: vid.video ? `see video ${vid.video}` : 'a production flow' });
|
|
144
|
+
messages.push({ from: part(moduleOf(h.symbol)), to: id, label: `drives ${h.exercises}`, kind: h.drive.kind, hop: h.index, via: 0 });
|
|
145
|
+
}
|
|
146
|
+
return { host: 'svg', as: 'sequence', flow: it.id, primitive: 'sequence',
|
|
147
|
+
params: { x: 160, y: 170, w: 1600, h: 800, at: 0, participants: parts, messages } };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* ---------------- exercises ---------------- */
|
|
151
|
+
// how an eval or dev hop drives the production flow it names is apply-flows' `drive`: {kind, to, evidence}
|
|
152
|
+
function exercises() {
|
|
153
|
+
const it = items.get(one);
|
|
154
|
+
const drives = it.hops.filter(h => h.exercises);
|
|
155
|
+
if (!drives.length) fail(1, `${one} has no hop that exercises a production flow: --as exercises draws an eval or dev flow's hand-off`);
|
|
156
|
+
const targets = [...new Set(drives.map(h => h.exercises))];
|
|
157
|
+
const ys = spread(Math.max(it.hops.length, targets.length), 220, 900);
|
|
158
|
+
const links = drives.map(h => ({ hop: h.index, symbol: h.symbol, target: h.exercises, targetTitle: items.get(h.exercises).title, ...videoOf(h.exercises), ...h.drive }));
|
|
159
|
+
const nodes = [
|
|
160
|
+
...it.hops.map((h, i) => ({ id: `h${h.index}`, kind: 'box', column: 'harness', row: i, label: h.name, stereotype: `${it.group} · hop ${h.index}`, size: { w: 420, h: 96 } })),
|
|
161
|
+
...targets.map((t, i) => { const l = links.find(x => x.target === t); return { id: `flow:${t}`, kind: 'box', column: 'production', row: i, label: t, stereotype: 'production flow', members: [l.video ? `see video ${l.video}` : 'not in this series'], sub: items.get(t).title }; }),
|
|
162
|
+
];
|
|
163
|
+
const edges = links.map((l, i) => ({ id: `x${i}`, from: `h${l.hop}`, to: `flow:${l.target}`, arrow: true, color: COLOR[l.kind] || 'client' }));
|
|
164
|
+
return { host: 'diagram', as: 'exercises', flow: it.id, exercises: links,
|
|
165
|
+
config: { layout: { columns: { harness: { x: 440 }, production: { x: 1420 } }, rows: ys }, nodes, edges } };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log(JSON.stringify(AS === 'map' ? map() : AS === 'sequence' ? sequence() : exercises(), null, 2));
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* =============================================================================
|
|
3
|
+
flow-plan.mjs — turn an approved, validated flow list into a video plan in
|
|
4
|
+
the coursework plan's shape, so the per-video stages 3–8 run unchanged.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
node tools/flow-plan.mjs --flows out/<name>/flows.validated.json [--max-min 25] [--estimate] [--force]
|
|
8
|
+
|
|
9
|
+
Refuses (exit 5) unless flows.approved.json beside it carries the sha256 of
|
|
10
|
+
this flows.validated.json, and flows.json has not changed since it was
|
|
11
|
+
validated: no plan, and so no script, exists before the user approves the
|
|
12
|
+
list, and any later edit needs a new approval. --estimate prints the plan
|
|
13
|
+
without writing it and without an approval (the approval table's estimate).
|
|
14
|
+
|
|
15
|
+
The plan: one overview video, then each series flow and core in series order,
|
|
16
|
+
split under the cap (lib/decompose.mjs). Each video carries `group` (its flow
|
|
17
|
+
or core id), `sections` ("<file> :: <symbol> @L<start>", what a script's
|
|
18
|
+
`covers` must name) and `sectionSpans` with each hop's cited tests. The plan
|
|
19
|
+
lists `forms` (overview, flow, core) for apply-enrichment, and the approval.
|
|
20
|
+
Coverage (C4): every kept hop is assigned to exactly one video.
|
|
21
|
+
|
|
22
|
+
Exit: 0 ok · 1 bad flags or unreadable input · 2 an orphaned hop or a video over
|
|
23
|
+
the cap · 4 would overwrite the plan an enriched plan was built from ·
|
|
24
|
+
5 no matching approval
|
|
25
|
+
========================================================================== */
|
|
26
|
+
import { readFileSync, writeFileSync, existsSync, realpathSync } from 'node:fs';
|
|
27
|
+
import { join, resolve, dirname, sep, relative } from 'node:path';
|
|
28
|
+
import { createHash } from 'node:crypto';
|
|
29
|
+
import { MODEL, planVideos } from './lib/decompose.mjs';
|
|
30
|
+
|
|
31
|
+
const fail = (code, msg) => { console.error(`[flow-plan] ${msg}`); process.exit(code); };
|
|
32
|
+
|
|
33
|
+
/* ---------------- args ---------------- */
|
|
34
|
+
const argv = process.argv.slice(2);
|
|
35
|
+
const VALUED = new Set(['--flows', '--max-min']), BOOL = new Set(['--estimate', '--force']);
|
|
36
|
+
const flags = new Map();
|
|
37
|
+
for (let i = 0; i < argv.length; i++) {
|
|
38
|
+
if (VALUED.has(argv[i])) { if (argv[i + 1] == null) fail(1, `${argv[i]} needs a value`); flags.set(argv[i], argv[++i]); }
|
|
39
|
+
else if (BOOL.has(argv[i])) flags.set(argv[i], true);
|
|
40
|
+
else fail(1, `unknown argument ${argv[i]}`);
|
|
41
|
+
}
|
|
42
|
+
if (!flags.has('--flows')) fail(1, 'usage: flow-plan.mjs --flows out/<name>/flows.validated.json [--max-min 25] [--estimate] [--force]');
|
|
43
|
+
const maxMin = flags.has('--max-min') ? Number(flags.get('--max-min')) : 25, minMin = 2.5;
|
|
44
|
+
if (!(maxMin > 0)) fail(1, '--max-min must be a positive number of minutes');
|
|
45
|
+
const VALIDATED = resolve(flags.get('--flows')), OUT = dirname(VALIDATED);
|
|
46
|
+
const sha = t => createHash('sha256').update(t).digest('hex');
|
|
47
|
+
let text, v;
|
|
48
|
+
try { text = readFileSync(VALIDATED, 'utf8'); v = JSON.parse(text); } catch (e) { fail(1, `cannot read the validated flows ${VALIDATED}: ${e.message}`); }
|
|
49
|
+
if (v?.schema !== 1 || v.tool?.name !== 'apply-flows.mjs' || v.ok !== true) fail(1, `${VALIDATED} is not a passing apply-flows.mjs output (schema 1)`);
|
|
50
|
+
if (v.root && existsSync(v.root)) { const root = realpathSync(v.root), o = realpathSync(OUT); if (o === root || o.startsWith(root + sep)) fail(1, `refusing to write inside the repo: ${OUT}`); }
|
|
51
|
+
const estimate = flags.has('--estimate');
|
|
52
|
+
|
|
53
|
+
/* ---------------- the approval (design §4.4b) ---------------- */
|
|
54
|
+
const APPROVED = join(OUT, 'flows.approved.json');
|
|
55
|
+
let approval = null;
|
|
56
|
+
const refuse = msg => estimate ? console.log(`[flow-plan] estimate only: ${msg}`) : fail(5, `${msg}\n Show the user the approval table in flows.md and, on their explicit yes, run approve-flows. No plan was written.`);
|
|
57
|
+
const flowsFile = join(OUT, v.inputs.flows.file);
|
|
58
|
+
if (existsSync(flowsFile) && sha(readFileSync(flowsFile, 'utf8')) !== v.inputs.flows.sha256)
|
|
59
|
+
refuse(`${v.inputs.flows.file} changed since apply-flows validated it: rerun apply-flows; the edited list needs a new approval.`);
|
|
60
|
+
else if (!existsSync(APPROVED)) refuse('no flows.approved.json: the user has not approved this flow list.');
|
|
61
|
+
else {
|
|
62
|
+
try { approval = JSON.parse(readFileSync(APPROVED, 'utf8')); } catch (e) { refuse(`flows.approved.json is not valid JSON: ${e.message}`); }
|
|
63
|
+
if (approval && approval.flowsHash !== sha(text)) {
|
|
64
|
+
refuse(`flows.approved.json approves a different flow list (sha256 ${String(approval.flowsHash).slice(0, 12)}…, this one is ${sha(text).slice(0, 12)}…): the list changed after the approval.`);
|
|
65
|
+
approval = null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* ---------------- plan ---------------- */
|
|
70
|
+
const p = planVideos(v, { maxMin, minMin });
|
|
71
|
+
const plan = {
|
|
72
|
+
generatedFrom: flags.get('--flows'), kind: 'codebase', root: v.root, commit: v.commit, treeHash: v.treeHash,
|
|
73
|
+
model: { ...MODEL, maxVideoMin: maxMin, minVideoMin: minMin },
|
|
74
|
+
forms: ['overview', 'flow', 'core'],
|
|
75
|
+
approval: approval && { by: approval.approvedBy, at: approval.at, note: approval.note, flowsHash: approval.flowsHash },
|
|
76
|
+
inputs: { ...v.inputs, validated: { file: relative(OUT, VALIDATED), sha256: sha(text) } },
|
|
77
|
+
totals: p.totals, coverage: p.coverage, orphans: p.orphans, capViolations: p.capViolations,
|
|
78
|
+
outOfRepo: v.outOfRepo, tests: v.tests, videos: p.videos,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const cite = t => `${t.strength === 'reachable' ? 'reachable from' : 'pinned by'} \`${t.test.split(':').pop()}\` (${t.file}:${t.assertLines.join(',')}): ${t.say}${t.strength === 'reachable' ? ' (reachable only: never "pinned" or "tested")' : ''}`;
|
|
82
|
+
const L = [];
|
|
83
|
+
L.push(`# ${(v.root || '').split('/').pop()} — code-flow video plan`, '');
|
|
84
|
+
L.push(plan.approval ? `Approved by **${plan.approval.by}** at ${plan.approval.at}: "${plan.approval.note}" (flows.validated.json sha256 ${plan.approval.flowsHash.slice(0, 12)}…)`
|
|
85
|
+
: '**Estimate only: this flow list is not approved.** No plan is written until the user approves it.', '');
|
|
86
|
+
const B = MODEL.basis;
|
|
87
|
+
L.push(`> Calibrated on ${B.videos} narrated pilot video (${B.hops} hops, ${B.narratedLines} narrated lines, ${B.branches} taught branches, ` +
|
|
88
|
+
`${B.citedTests} cited tests, ${B.words} words): frame ${MODEL.frame}, hop ${MODEL.hopBase} + ${MODEL.wpl} words per narrated line (≤ ${MODEL.lineCap}), ` +
|
|
89
|
+
`taught branch ${MODEL.branch}, cited test ${MODEL.test} (≤ ${MODEL.maxTests} per video). Still **uncalibrated**: crossing ${MODEL.boundary} ` +
|
|
90
|
+
`(the pilot crossed no boundary) and the overview's ${MODEL.overview} words.`, '');
|
|
91
|
+
L.push(`## ${p.totals.videos} videos, ${p.totals.seriesMinutes} min (longest ${p.totals.longestVideoMin} min, cap ${maxMin} min)`, '');
|
|
92
|
+
L.push('| # | Title | Form | Group | Hops | Crossings | Branches | Cited tests | Words | Min |', '|---|---|---|---|---|---|---|---|---|---|');
|
|
93
|
+
for (const x of p.videos) L.push(`| ${x.index} | ${x.title} | \`${x.form}\` | ${x.group} | ${x.evidence.hops ?? '—'} | ${x.evidence.crossings ?? '—'} | ${x.evidence.branches ?? '—'} | ${x.evidence.citedTests ?? '—'} | ${x.words} | ${x.minutes} |`);
|
|
94
|
+
L.push('', '## Sections per video', '', 'A script covers each of its video\'s sections by name in `covers`.', '');
|
|
95
|
+
for (const x of p.videos) {
|
|
96
|
+
L.push(`### ${x.index}. ${x.title}`, '', `${x.form} · ${x.formWhy}`, '');
|
|
97
|
+
for (const s of x.sectionSpans) L.push(`- \`${s.id}\`${s.pinnedBy?.length ? ` — ${s.pinnedBy.map(cite).join('; ')}` : ''}`);
|
|
98
|
+
for (const t of x.teach || []) L.push(`- teach \`${t.dispatch || t.guard}\`: ${t.say}`);
|
|
99
|
+
L.push('');
|
|
100
|
+
}
|
|
101
|
+
L.push('## Coverage (C4)', '', `${p.coverage.length} hops · ${p.coverage.length - p.orphans.length} assigned to exactly one video · **${p.orphans.length} orphaned**`);
|
|
102
|
+
for (const o of p.orphans) L.push(`- ⚠️ ${o.item} hop ${o.hop}: ${o.symbol}`);
|
|
103
|
+
for (const c of p.capViolations) L.push(`- ⚠️ video ${c.index} is ${c.minutes} min, over the ${maxMin} min cap: a hop is never split; raise --max-min or narrate fewer lines`);
|
|
104
|
+
if (v.outOfRepo.length) L.push('', '## Out-of-repo claims (unverified)', '', ...v.outOfRepo.map(o => `- ${o.claim} — ${o.evidence}`));
|
|
105
|
+
L.push('');
|
|
106
|
+
const md = L.join('\n');
|
|
107
|
+
|
|
108
|
+
if (estimate) { console.log(md); process.exit(p.orphans.length || p.capViolations.length ? 2 : 0); }
|
|
109
|
+
|
|
110
|
+
/* ---------------- model guard: an enriched plan is keyed by video index ---------------- */
|
|
111
|
+
const ENRICHED = join(OUT, 'plan.enriched.json');
|
|
112
|
+
if (existsSync(ENRICHED)) {
|
|
113
|
+
let prev = null;
|
|
114
|
+
try { prev = JSON.parse(readFileSync(ENRICHED, 'utf8')); } catch { prev = null; }
|
|
115
|
+
const why = [];
|
|
116
|
+
if (prev?.treeHash !== v.treeHash) why.push('the code map tree');
|
|
117
|
+
if (prev?.inputs?.candidates?.sha256 !== v.inputs.candidates.sha256) why.push('candidates.json');
|
|
118
|
+
if (prev?.inputs?.flows?.sha256 !== v.inputs.flows.sha256) why.push('flows.json');
|
|
119
|
+
if (prev?.approval?.flowsHash !== approval.flowsHash || prev?.approval?.at !== approval.at) why.push('the approval');
|
|
120
|
+
if (prev?.model?.maxVideoMin !== maxMin) why.push(`the cap (${prev?.model?.maxVideoMin} → ${maxMin} min)`);
|
|
121
|
+
if (why.length && !flags.has('--force')) fail(4, `plan.enriched.json was built from a different plan (${why.join(', ')} changed): enrichment is keyed by video index, so re-planning would repoint every curated title. Rerun with --force, then rerun apply-enrichment.`);
|
|
122
|
+
if (why.length) console.error(`[flow-plan] --force: overwriting the plan behind plan.enriched.json (${why.join(', ')} changed); rerun apply-enrichment`);
|
|
123
|
+
}
|
|
124
|
+
writeFileSync(join(OUT, 'plan.json'), JSON.stringify(plan, null, 2));
|
|
125
|
+
writeFileSync(join(OUT, 'plan.md'), md);
|
|
126
|
+
console.log(md);
|
|
127
|
+
console.log(`[out] ${join(OUT, 'plan.json')}`);
|
|
128
|
+
console.log(`[out] ${join(OUT, 'plan.md')}`);
|
|
129
|
+
if (p.orphans.length || p.capViolations.length) { console.log(`\n⚠️ ${p.orphans.length} orphaned hop(s), ${p.capViolations.length} video(s) over the cap`); process.exitCode = 2; }
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/* flows.md: the approval table first (every series flow and core with its reason, every skipped high-ranked row,
|
|
2
|
+
the variants, the dev rows, the estimate), then the C1′ and C1 accounting, then every flow and core in full: every
|
|
3
|
+
hop with how it is reached, the rule and evidence of each edge, and the cited tests with their assertion lines.
|
|
4
|
+
No truncation: this is what the user approves. */
|
|
5
|
+
const short = id => id == null ? '' : id.endsWith(':<main>') ? id.split(':')[0].split('.').pop() + ':<main>' : id.split(':').pop();
|
|
6
|
+
const cell = c => String(c ?? '').replace(/\|/g, '\\|').replace(/\n/g, ' ');
|
|
7
|
+
const table = (head, rows) => rows.length ? [`| ${head.join(' | ')} |`, `|${head.map(() => '---').join('|')}|`, ...rows.map(r => `| ${r.map(cell).join(' | ')} |`)].join('\n') : '_none_';
|
|
8
|
+
const code = id => id ? `\`${id}\`` : '';
|
|
9
|
+
const span = h => `${h.file}:${h.start}-${h.end}${h.lines ? ` (lines ${h.lines.map(([a, b]) => a === b ? a : `${a}-${b}`).join(',')})` : ''}`;
|
|
10
|
+
|
|
11
|
+
const reached = h => {
|
|
12
|
+
const c = h.connection;
|
|
13
|
+
if (!c) return '';
|
|
14
|
+
if (c.how === 'entry' || c.how === 'start') return c.how;
|
|
15
|
+
if (c.how === 'variant') return 'starts a variant';
|
|
16
|
+
const from = c.path[0];
|
|
17
|
+
return `${c.how === 'candidate' ? 'walk' : c.how === 'edges' ? 'edges' : 'declared link'} from \`${short(from)}\`: ${c.steps.map(s => s.kind).join(' → ')}`;
|
|
18
|
+
};
|
|
19
|
+
const evidence = h => (h.connection?.steps || []).filter(s => s.rule || s.declared || (s.evidence && s.kind !== 'static')).map(s => `${s.rule || s.kind}: ${s.evidence || ''}`.trim()).join('; ');
|
|
20
|
+
const tests = h => h.pinnedBy.map(t => `\`${short(t.test)}\` (${t.file}:${t.line}-${t.end}, asserts at ${t.assertLines.join(', ') || '—'}${t.strength === 'reachable' ? ', reachable only: never "tested"' : ''}) — ${t.say}`).join('<br>');
|
|
21
|
+
|
|
22
|
+
export function renderFlows(v, problems, estimate, r) {
|
|
23
|
+
const out = [];
|
|
24
|
+
const all = [...v.flows, ...v.cores];
|
|
25
|
+
const byId = new Map(all.map(x => [x.id, x]));
|
|
26
|
+
out.push(`# Flows: ${(v.root || '').split('/').pop()}`, '',
|
|
27
|
+
`- ${v.commit ? `commit \`${v.commit.slice(0, 7)}\` · ` : ''}tree ${(v.treeHash || '').slice(0, 19)}…`,
|
|
28
|
+
`- inputs: \`${v.inputs.flows.file}\` (sha256 ${v.inputs.flows.sha256.slice(0, 12)}…), \`${v.inputs.candidates.file}\`, \`${v.inputs.ranking.file}\``,
|
|
29
|
+
`- high-ranked: the top ${v.params.explainTop} production and top ${v.params.explainTopEval} eval rows, plus every row at or above the lowest-ranked row the series includes`, '');
|
|
30
|
+
if (problems.length) out.push(`## ${problems.length} problem(s): not validated`, '', ...problems.map(p => `- ${p}`), '');
|
|
31
|
+
|
|
32
|
+
out.push('## Approval table', '',
|
|
33
|
+
problems.length ? 'Fix the problems above first: nothing here may be approved yet.'
|
|
34
|
+
: 'Show this table to the user and ask them to approve it or edit it (add, drop, reorder, promote). Run `approve-flows` only on an explicit yes; after any edit, rerun `apply-flows` and ask again.', '');
|
|
35
|
+
out.push('### The proposed series, in video order', '',
|
|
36
|
+
table(['#', 'kind', 'id', 'title', 'group', 'rank', 'size', 'why'], v.series.map((s, i) => { const x = byId.get(s.id), c = x.from && r.candidates[x.from];
|
|
37
|
+
return [i + 1, s.kind, code(s.id), x.title, x.group, s.rank || '—', `${x.hops.length} hops${c ? ` · ${c.own} own symbols` : ''}`, s.why]; })), '');
|
|
38
|
+
const highSet = new Set(Object.values(v.triage.highRanked).flatMap(h => h.rows.map(x => x.entry)));
|
|
39
|
+
out.push('### Skipped rows', '', table(['entry', 'group', 'rank', 'high-ranked', 'why'], v.skipped.map(s => [code(short(s.entry)), s.group, s.rank || '—', highSet.has(s.entry) ? 'yes' : 'no', s.why])), '');
|
|
40
|
+
out.push('### Variants', '', table(['of', 'entry', 'rank', 'why'], all.flatMap(x => x.variants.map(y => [code(x.id), code(short(y.entry)), y.rank || '—', y.why]))), '');
|
|
41
|
+
if (v.promote.length) out.push('### Promoted dev rows', '', table(['entry', 'rank', 'why'], v.promote.map(p => [code(short(p.entry)), p.rank || '—', p.why])), '');
|
|
42
|
+
out.push('### Dev rows (filtered by default; promote one in flows.json with a reason)', '',
|
|
43
|
+
table(['entry', 'kind', 'route / key', 'rank', 'status'], v.triage.entries.filter(t => t.group === 'dev').map(t => [code(short(t.id)), t.kind, t.route, t.rank ? `#${t.rank}` : '—', t.status || 'untriaged'])), '');
|
|
44
|
+
if (estimate) {
|
|
45
|
+
out.push(`### Estimate: ${estimate.videos.length} videos, ${estimate.totals.seriesMinutes} min (cap ${estimate.totals.capMinutes} min per video)`, '',
|
|
46
|
+
'From the cost model in `flow-plan --estimate`; calibrated on one pilot code video, except the crossing and overview constants.', '',
|
|
47
|
+
table(['#', 'video', 'form', 'hops', 'cited tests', 'words', 'min'], estimate.videos.map(x => [x.index, x.title, x.form, x.evidence.hops ?? '—', x.evidence.citedTests ?? '—', x.words, x.minutes])), '');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
out.push('## High-ranked rows (C1′)', '');
|
|
51
|
+
for (const [grp, h] of Object.entries(v.triage.highRanked)) {
|
|
52
|
+
if (!h.rows.length) continue;
|
|
53
|
+
const s = h.summary;
|
|
54
|
+
out.push(`${grp}: ${s.highRanked} high-ranked · ${s.series} in series · ${s.variant} variant · ${s.skipped} skipped · ${s.unexplained} unexplained`, '',
|
|
55
|
+
table(['rank', 'entry', 'decision', 'why'], h.rows.map(x => [`#${x.rank}`, code(short(x.entry)), x.status || '**unexplained**', x.why || ''])), '');
|
|
56
|
+
}
|
|
57
|
+
const t = v.triage.entries, g = grp => t.filter(x => x.group === grp), n = (xs, s) => xs.filter(x => x.status === s).length;
|
|
58
|
+
out.push('## Entry triage (C1)', '',
|
|
59
|
+
`${t.length + v.triage.testGroupEntries.length} entries · prod ${g('prod').length} · eval ${g('eval').length} · dev ${g('dev').length} (${n(g('dev'), 'dev-filtered')} filtered, ${n(g('dev'), 'continuation')} continuations) · ${t.filter(x => !x.status).length} untriaged · test-group entries: ${v.triage.testGroupEntries.length} (listed)`, '',
|
|
60
|
+
table(['entry', 'group', 'kind', 'route / key', 'rank', 'decision', 'by', 'reason'], t.map(x => [code(x.id), x.group, x.kind, x.route, x.rank ? `#${x.rank}` : '—', x.status || '**untriaged**', x.by, x.reason])), '');
|
|
61
|
+
if (v.triage.testGroupEntries.length) out.push('Entries in the test group (not flows): ' + v.triage.testGroupEntries.map(code).join(', '), '');
|
|
62
|
+
if (v.excluded.length) out.push('### Exclusion rules', '', table(['match', 'why', 'matched'], v.excluded.map(x => [code(x.match), x.why, x.matched.map(short).join(', ')])), '');
|
|
63
|
+
|
|
64
|
+
out.push('## Flows and cores', '');
|
|
65
|
+
for (const s of v.series) {
|
|
66
|
+
const x = byId.get(s.id);
|
|
67
|
+
out.push(`### ${x.kind} \`${x.id}\`: ${x.title}`, '',
|
|
68
|
+
`${x.from ? `from \`${x.from}\`${x.entry?.path ? ` (${[x.entry.method, x.entry.path].filter(Boolean).join(' ')})` : x.entry?.key ? ` (${x.entry.key})` : ''} · ` : ''}group ${x.group} · ${x.rank || 'no rank'} · why: ${s.why}`, '',
|
|
69
|
+
table(['#', 'hop', 'span', 'reached by', 'rule / evidence', 'cited tests', 'notes'], x.hops.map(h => [h.index, code(h.symbol), span(h), reached(h), evidence(h), tests(h),
|
|
70
|
+
[h.crossing ? 'crossing' : '', h.break ? 'break' : '', h.uses ? `uses ${h.uses}` : '', h.exercises ? `exercises ${h.exercises}` : '', h.selectedBy ? `selected only by an unverified out-of-repo claim: ${h.selectedBy.outOfRepo}` : ''].filter(Boolean).join(', ')])), '');
|
|
71
|
+
if (x.segments.length) out.push('Segments:', '', table(['root', 'reached from', 'via', 'own', 'decision', 'say / why'], x.segments.map(y => [code(short(y.root)), y.from ? code(short(y.from)) : '(entry)', y.kind || '', y.own, y.decision + (y.rule ? ` (${y.rule})` : ''), y.say || y.why || ''])), '');
|
|
72
|
+
if (x.teach.length) out.push('Taught branches:', '', table(['point', 'at hop', 'alternatives', 'say'], x.teach.map(y => [code(y.dispatch || y.guard), y.hop,
|
|
73
|
+
(y.alternatives || []).map(a => typeof a === 'string' ? code(short(a)) : a.cls ? `\`${short(a.cls)}\` when ${a.guards.join(' & ') || '(fallthrough)'}${a.sel ? ` (${Object.entries(a.sel).map(([p, s2]) => `${p}: ${s2}`).join(', ')})` : ''}` : `\`${short(a.to)}\` when ${a.guards.join(' & ')}`).join('; '), y.say])), '');
|
|
74
|
+
if (x.links.length) out.push('Declared links:', '', table(['from', 'to', 'kind', 'evidence'], x.links.map(l => [code(short(l.from)), code(short(l.to)), l.kind, l.evidence])), '');
|
|
75
|
+
}
|
|
76
|
+
const shared = new Map();
|
|
77
|
+
// a hop that `uses` the core holding the same symbol is that core's reference, not a second narration
|
|
78
|
+
const refersToCore = h => h.uses && byId.get(h.uses)?.hops.some(y => y.symbol === h.symbol);
|
|
79
|
+
for (const x of all) for (const h of x.hops) if (!refersToCore(h)) (shared.get(h.symbol) || shared.set(h.symbol, []).get(h.symbol)).push(x.id);
|
|
80
|
+
const twice = [...shared].filter(([, ids]) => ids.length > 1);
|
|
81
|
+
out.push('## Hops kept by two or more flows', '', 'A sequence kept by two or more flows is a core candidate; a hop listed here is narrated in each.', '',
|
|
82
|
+
table(['hop', 'kept by'], twice.map(([s, ids]) => [code(s), ids.join(', ')])), '');
|
|
83
|
+
out.push('## Out-of-repo claims (unverified)', '', table(['claim', 'evidence', 'selects'], v.outOfRepo.map(o => [o.claim, o.evidence, o.selects.map(code).join(', ')])), '');
|
|
84
|
+
out.push('## Test configuration', '', 'Mocks every test under a conftest runs with: a claim that a test exercises one of these targets is false.', '',
|
|
85
|
+
table(['target', 'scope', 'where'], [...v.tests.sessionMocks.map(x => [code(x.target), 'session', x.conftest]), ...v.tests.autouseMocks.map(x => [code(x.target), 'autouse fixture', code(x.fixture)])]), '');
|
|
86
|
+
return out.join('\n');
|
|
87
|
+
}
|