@skillstech/thunderlang 0.1.6

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.
Files changed (72) hide show
  1. package/CHANGELOG.md +375 -0
  2. package/LICENSE +21 -0
  3. package/README.md +165 -0
  4. package/dist/core.cjs +6768 -0
  5. package/dist/index.cjs +9308 -0
  6. package/intent-graph.schema.json +687 -0
  7. package/package.json +84 -0
  8. package/src/ai-core.mjs +67 -0
  9. package/src/ai-events.mjs +56 -0
  10. package/src/ai.mjs +324 -0
  11. package/src/arch.mjs +55 -0
  12. package/src/atlas.mjs +118 -0
  13. package/src/changes.mjs +74 -0
  14. package/src/classification.mjs +36 -0
  15. package/src/cli.mjs +1534 -0
  16. package/src/codegen.mjs +214 -0
  17. package/src/compile.mjs +142 -0
  18. package/src/comprehension.mjs +130 -0
  19. package/src/conflict.mjs +0 -0
  20. package/src/core.d.ts +99 -0
  21. package/src/core.mjs +92 -0
  22. package/src/data-schema.mjs +137 -0
  23. package/src/decision.mjs +38 -0
  24. package/src/distributed.mjs +48 -0
  25. package/src/draft.mjs +101 -0
  26. package/src/drift.mjs +177 -0
  27. package/src/emit.mjs +519 -0
  28. package/src/exporters.mjs +245 -0
  29. package/src/expr.mjs +245 -0
  30. package/src/fable.mjs +110 -0
  31. package/src/focus.mjs +151 -0
  32. package/src/format.mjs +55 -0
  33. package/src/governance.mjs +100 -0
  34. package/src/graph-source.mjs +292 -0
  35. package/src/guard.mjs +105 -0
  36. package/src/guardian.mjs +98 -0
  37. package/src/hash.mjs +89 -0
  38. package/src/importers.mjs +194 -0
  39. package/src/index.d.ts +725 -0
  40. package/src/index.mjs +185 -0
  41. package/src/intellisense.mjs +210 -0
  42. package/src/intent-atlas.mjs +77 -0
  43. package/src/intent-graph.mjs +346 -0
  44. package/src/intent-ir.mjs +144 -0
  45. package/src/intent-schema.mjs +215 -0
  46. package/src/ledger.mjs +109 -0
  47. package/src/lifecycle.mjs +56 -0
  48. package/src/lift.mjs +693 -0
  49. package/src/lsp.mjs +152 -0
  50. package/src/mcp.mjs +158 -0
  51. package/src/migrate.mjs +118 -0
  52. package/src/outcome.mjs +93 -0
  53. package/src/parse.mjs +733 -0
  54. package/src/patch.mjs +364 -0
  55. package/src/privacy.mjs +61 -0
  56. package/src/proof-schema.mjs +129 -0
  57. package/src/report.mjs +84 -0
  58. package/src/runtime.mjs +96 -0
  59. package/src/sarif.mjs +88 -0
  60. package/src/scan-queries.mjs +97 -0
  61. package/src/scan.mjs +87 -0
  62. package/src/security.mjs +73 -0
  63. package/src/select.mjs +80 -0
  64. package/src/semantic-diff.mjs +125 -0
  65. package/src/simulate.mjs +106 -0
  66. package/src/style.mjs +250 -0
  67. package/src/sync.mjs +103 -0
  68. package/src/testing.mjs +59 -0
  69. package/src/twelve-factor.mjs +173 -0
  70. package/src/verify-diff.mjs +105 -0
  71. package/src/xml.mjs +87 -0
  72. package/syntaxes/intent.tmLanguage.json +55 -0
package/src/focus.mjs ADDED
@@ -0,0 +1,151 @@
1
+ // Intent Lens , Intent Scope + Focus Graph (intent-focus-v1). A Focus Graph is a versioned
2
+ // subgraph of the Intent Atlas around a selected scope (a mission, a set of nodes, a change),
3
+ // with each included node tagged by WHY it is in focus. This is the shared foundation the
4
+ // vision's Intent Lens is built on: Studio renders it, OpenThunder verifies its scope,
5
+ // RepoMastery/SkillsTech Talk teach it, but the deterministic scope + subgraph live here
6
+ // because ThunderLang owns the Intent IR/Atlas. Pure and browser-safe (no Node, no AI).
7
+ //
8
+ // "Intent Atlas shows the complete system. Intent Lens helps you understand one part of it."
9
+
10
+ export const FOCUS_SCHEMA = 'intent-focus-v1';
11
+
12
+ export const SCOPE_TYPES = [
13
+ 'branch', 'pull-request', 'commit-range', 'feature', 'capability', 'requirement',
14
+ 'flow', 'service', 'module', 'release', 'incident', 'finding', 'mission', 'custom',
15
+ ];
16
+
17
+ // Why a node is in the focused graph (the vision's inclusion-reason set).
18
+ export const FOCUS_REASONS = [
19
+ 'selected', 'governing', 'dependency', 'dependent', 'implementation',
20
+ 'verification', 'risk', 'contextual',
21
+ ];
22
+
23
+ const RISK_TYPES = new Set(['Risk', 'Finding', 'Conflict', 'Threat']);
24
+ const OPEN_TYPES = new Set(['Unknown', 'Question', 'Assumption']);
25
+
26
+ // A small deterministic, browser-safe fingerprint (djb2) , for freshness comparison, not security.
27
+ function fingerprint(parts) {
28
+ let h = 5381;
29
+ const s = parts.join('');
30
+ for (let i = 0; i < s.length; i += 1) h = ((h * 33) ^ s.charCodeAt(i)) >>> 0;
31
+ return `fp_${h.toString(16)}`;
32
+ }
33
+
34
+ /** Build a typed Intent Scope. `createdAt` is supplied by the caller (deterministic/testable). */
35
+ export function makeScope({ type = 'custom', title = null, seeds = [], projectId = null, createdBy = null, createdAt = null, ...rest } = {}) {
36
+ if (!SCOPE_TYPES.includes(type)) throw new Error(`intent focus: unknown scope type "${type}"`);
37
+ return {
38
+ schema: FOCUS_SCHEMA,
39
+ scopeId: `scope.${type}.${fingerprint([type, ...seeds].map(String))}`,
40
+ projectId, type, title: title || (seeds[0] || type),
41
+ selectedNodeIds: [...seeds],
42
+ createdBy, createdAt,
43
+ provenance: 'compiler-derived',
44
+ ...rest,
45
+ };
46
+ }
47
+
48
+ // Classify why `node`, reached from `viaEdge` (or a seed), is included.
49
+ function reasonFor(node, viaEdge, seedSet) {
50
+ if (seedSet.has(node.id)) return 'selected';
51
+ if (RISK_TYPES.has(node.type)) return 'risk';
52
+ if (node.type === 'VerificationRule' || node.type === 'Verification') return 'verification';
53
+ if (!viaEdge) return 'contextual';
54
+ switch (viaEdge.type) {
55
+ case 'verified_by': return 'verification';
56
+ case 'requires': case 'constrained_by': case 'governs': case 'supported_by': return 'governing';
57
+ case 'implemented_by': case 'represented_by': return 'implementation';
58
+ case 'depends_on': return viaEdge.dir === 'out' ? 'dependency' : 'dependent';
59
+ default: return 'contextual';
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Build a Focus Graph: the subgraph of `atlas` reachable from `seeds` within `depth` hops,
65
+ * every node tagged with its focusReason and the hop distance. Deterministic.
66
+ * @param {{nodes:Array, relationships:Array}} atlas
67
+ * @param {{seeds:string[], depth?:number, scope?:object}} opts
68
+ */
69
+ export function buildFocusGraph(atlas, { seeds = [], depth = 2, scope = null } = {}) {
70
+ const nodesById = new Map((atlas?.nodes || []).map((n) => [n.id, n]));
71
+ const rels = atlas?.relationships || [];
72
+ const seedSet = new Set(seeds.filter((s) => nodesById.has(s)));
73
+ const included = new Map(); // id -> { node, reason, depth }
74
+ for (const s of seedSet) included.set(s, { node: nodesById.get(s), reason: 'selected', depth: 0 });
75
+
76
+ // Adjacency index (built once) so BFS is O(nodes + edges), not O(nodes * edges). Each entry
77
+ // records the neighbor id + the edge (with direction) that reaches it.
78
+ const adj = new Map();
79
+ const link = (from, neighborId, r, dir) => {
80
+ let list = adj.get(from);
81
+ if (!list) { list = []; adj.set(from, list); }
82
+ list.push({ neighborId, via: { type: r.type, from: r.from, to: r.to, dir } });
83
+ };
84
+ for (const r of rels) { link(r.from, r.to, r, 'out'); link(r.to, r.from, r, 'in'); }
85
+
86
+ let frontier = [...seedSet];
87
+ for (let d = 1; d <= depth && frontier.length; d += 1) {
88
+ const next = [];
89
+ for (const id of frontier) {
90
+ const edges = adj.get(id);
91
+ if (!edges) continue;
92
+ for (const { neighborId, via } of edges) {
93
+ if (included.has(neighborId)) continue;
94
+ const node = nodesById.get(neighborId) || { id: neighborId, type: 'Unknown', title: neighborId };
95
+ included.set(neighborId, { node, reason: reasonFor(node, via, seedSet), depth: d });
96
+ next.push(neighborId);
97
+ }
98
+ }
99
+ frontier = next;
100
+ }
101
+
102
+ const focusNodes = [...included.values()]
103
+ .map(({ node, reason, depth: dd }) => ({ id: node.id, type: node.type, title: node.title || node.id, focusReason: reason, depth: dd, confidence: node.confidence || null, provenance: node.provenance || null }))
104
+ .sort((a, b) => a.depth - b.depth || a.id.localeCompare(b.id));
105
+ const idSet = new Set(focusNodes.map((n) => n.id));
106
+ const focusRels = rels.filter((r) => idSet.has(r.from) && idSet.has(r.to));
107
+
108
+ const byReason = {};
109
+ for (const n of focusNodes) byReason[n.focusReason] = (byReason[n.focusReason] || 0) + 1;
110
+ const byType = {};
111
+ for (const n of focusNodes) byType[n.type] = (byType[n.type] || 0) + 1;
112
+
113
+ return {
114
+ schema: FOCUS_SCHEMA,
115
+ scope: scope || makeScope({ type: 'custom', seeds: [...seedSet] }),
116
+ depth,
117
+ freshness: fingerprint(focusNodes.map((n) => `${n.id}:${nodesById.get(n.id)?.hash || n.title}`)),
118
+ overview: { nodes: focusNodes.length, relationships: focusRels.length, byReason, byType },
119
+ nodes: focusNodes,
120
+ relationships: focusRels,
121
+ };
122
+ }
123
+
124
+ /**
125
+ * A deterministic Intent Brief for a Focus Graph: what / why / who / involves / risks /
126
+ * unknowns / confidence. Never invents; every line is derived from focused IR nodes, and the
127
+ * brief's confidence is the weakest confidence in scope (honesty).
128
+ */
129
+ export function intentBrief(focus) {
130
+ const nodes = focus?.nodes || [];
131
+ const seed = nodes.find((n) => n.focusReason === 'selected' && n.type === 'Mission')
132
+ || nodes.find((n) => n.type === 'Mission') || nodes[0] || null;
133
+ const of = (types) => nodes.filter((n) => types.includes(n.type)).map((n) => n.title);
134
+ const rank = { Confirmed: 5, Observed: 4, Derived: 3, Inferred: 2, Speculative: 1, Conflicted: 0 };
135
+ const confidences = nodes.map((n) => n.confidence).filter(Boolean);
136
+ const weakest = confidences.length ? confidences.reduce((a, b) => (rank[b] < rank[a] ? b : a)) : null;
137
+ return {
138
+ schema: FOCUS_SCHEMA,
139
+ scopeId: focus?.scope?.scopeId || null,
140
+ what: seed ? seed.title : (focus?.scope?.title || null),
141
+ who: of(['Persona', 'Actor']),
142
+ involves: focus?.overview?.byType || {},
143
+ guarantees: of(['Guarantee']),
144
+ prohibitions: of(['Never', 'ProhibitedBehavior']),
145
+ risks: nodes.filter((n) => n.focusReason === 'risk').length,
146
+ verification: nodes.filter((n) => n.focusReason === 'verification').length,
147
+ unknowns: nodes.filter((n) => OPEN_TYPES.has(n.type)).map((n) => n.title),
148
+ confidence: weakest,
149
+ needsReview: nodes.some((n) => ['Inferred', 'Speculative', 'Conflicted'].includes(n.confidence)),
150
+ };
151
+ }
package/src/format.mjs ADDED
@@ -0,0 +1,55 @@
1
+ // A deterministic formatter for .intent source , the gofmt/prettier of ThunderLang. It
2
+ // re-indents each line to its structural depth (two spaces per level), trims trailing
3
+ // whitespace, collapses runs of blank lines to one, and ensures a single trailing newline,
4
+ // while PRESERVING content and comments (only whitespace changes). Idempotent and
5
+ // semantic-preserving: format(format(x)) === format(x), and the re-parsed graph is unchanged.
6
+
7
+ export const FORMAT_INDENT = ' ';
8
+
9
+ /**
10
+ * Format ThunderLang source. Depth is taken from the source's own relative indentation
11
+ * (the same stack algorithm the parser's indent tree uses), so a file with mixed 2/4-space
12
+ * or tab indentation is normalized to consistent two-space steps.
13
+ */
14
+ export function formatSource(source) {
15
+ const lines = String(source ?? '').replace(/\r\n?/g, '\n').split('\n');
16
+ const out = [];
17
+ const stack = [-1]; // indentation widths seen so far; depth = stack.length - 1
18
+ let blankPending = false;
19
+ let inString = false; // inside an unterminated multi-line "..." string
20
+
21
+ const quoteCount = (s) => (s.match(/(?<!\\)"/g) || []).length;
22
+
23
+ for (const line of lines) {
24
+ // A line CONTINUING a multi-line string is content: preserve it byte-for-byte.
25
+ if (inString) {
26
+ if (blankPending) { out.push(''); blankPending = false; }
27
+ out.push(line.replace(/[\r]+$/, ''));
28
+ if (quoteCount(line) % 2 === 1) inString = false;
29
+ continue;
30
+ }
31
+
32
+ const trimmed = line.replace(/\s+$/, '').trimStart();
33
+ if (trimmed === '') { blankPending = true; continue; }
34
+
35
+ const indent = line.length - line.trimStart().length;
36
+ while (stack.length > 1 && indent <= stack[stack.length - 1]) stack.pop();
37
+ const depth = stack.length - 1;
38
+ stack.push(indent);
39
+
40
+ if (blankPending && out.length) out.push(''); // collapse any run of blanks to one
41
+ blankPending = false;
42
+ out.push(FORMAT_INDENT.repeat(depth) + trimmed);
43
+ if (quoteCount(trimmed) % 2 === 1) inString = true; // this line opened a string
44
+ }
45
+
46
+ // Exactly one trailing newline; no leading blank line.
47
+ while (out.length && out[0] === '') out.shift();
48
+ while (out.length && out[out.length - 1] === '') out.pop();
49
+ return out.join('\n') + '\n';
50
+ }
51
+
52
+ /** True if the source is already in canonical form. */
53
+ export function isFormatted(source) {
54
+ return String(source ?? '').replace(/\r\n?/g, '\n') === formatSource(source);
55
+ }
@@ -0,0 +1,100 @@
1
+ // Governance: waivers (founder Gap 5). A waiver is a GOVERNED EXCEPTION to a blocking
2
+ // diagnostic , a named authority signs off, with a reason and (optionally) an expiry and
3
+ // a scope, allowing a normally-blocking condition to ship WITH a full audit trail. This is
4
+ // how intent stays honest under real deadlines: you never silently drop a blocker, you
5
+ // waive it on the record. Deterministic and pure , expiry is evaluated only against a
6
+ // caller-supplied `now` (an ISO date string); with no `now`, expiry is not enforced so the
7
+ // analysis stays reproducible.
8
+
9
+ export const GOVERNANCE_SCHEMA = 'intent-governance-v1';
10
+
11
+ // ISO-date compare without Date (keeps the module pure/deterministic). Both are 'YYYY-MM-DD'
12
+ // (or ISO datetime); lexical compare is correct for that format. Returns true if a < b.
13
+ const isoBefore = (a, b) => String(a) < String(b);
14
+
15
+ /**
16
+ * Validate a set of waivers and (optionally) evaluate expiry against `now`.
17
+ * A waiver is well-formed only with a `code`, a `reason`, and an `approvedBy`.
18
+ * @returns {{level,code,message,why,waiver}[]} governance diagnostics (IL-GOV-*)
19
+ */
20
+ export function governanceDiagnostics(waivers = [], diagnostics = [], { now = null } = {}) {
21
+ const out = [];
22
+ const codesPresent = new Set(diagnostics.map((d) => d.code).filter(Boolean));
23
+ for (const w of waivers) {
24
+ if (!w.code) out.push({
25
+ level: 'error', code: 'IL-GOV-001', role: 'governance',
26
+ message: `A waiver names no diagnostic code.`,
27
+ why: 'A waiver must say exactly which diagnostic it excuses; a blanket waiver excuses nothing accountably.',
28
+ waiver: w.id,
29
+ });
30
+ if (!w.reason) out.push({
31
+ level: 'error', code: 'IL-GOV-002', role: 'governance',
32
+ message: `Waiver for ${w.code || '(no code)'} has no reason.`,
33
+ why: 'An exception without a stated reason is not governance, it is silence. Record why shipping is acceptable.',
34
+ waiver: w.id,
35
+ });
36
+ if (!w.approvedBy) out.push({
37
+ level: 'error', code: 'IL-GOV-003', role: 'governance',
38
+ message: `Waiver for ${w.code || '(no code)'} names no approver.`,
39
+ why: 'A waiver is an authority accepting risk; without a named approver there is no accountable owner.',
40
+ waiver: w.id,
41
+ });
42
+ if (w.code && !codesPresent.has(w.code)) out.push({
43
+ level: 'warning', code: 'IL-GOV-004', role: 'governance',
44
+ message: `Waiver for ${w.code} does not match any current diagnostic , it may be stale.`,
45
+ why: 'A dangling waiver hides that the condition it excused is already gone; remove it or it silently pre-approves a future regression.',
46
+ waiver: w.id,
47
+ });
48
+ if (now && w.expires && !isoBefore(now, w.expires)) out.push({
49
+ level: 'error', code: 'IL-GOV-005', role: 'governance',
50
+ message: `Waiver for ${w.code} expired on ${w.expires}.`,
51
+ why: 'An expired waiver no longer excuses anything; the diagnostic it covered is blocking again.',
52
+ waiver: w.id,
53
+ });
54
+ }
55
+ return out;
56
+ }
57
+
58
+ /**
59
+ * Apply waivers to a diagnostic set: each blocking diagnostic that matches a VALID, ACTIVE
60
+ * waiver is annotated `waived:true` (with the audit record) and no longer counts as blocking.
61
+ * Non-matching / invalid / expired waivers leave the diagnostic blocking. Deterministic.
62
+ *
63
+ * A waiver matches a diagnostic when codes are equal AND (the waiver has no scope, or the
64
+ * diagnostic's scope/target/mission/waiver field equals the waiver scope).
65
+ *
66
+ * @returns {{diagnostics, waived, blockingAfter, report}}
67
+ */
68
+ export function applyWaivers(diagnostics = [], waivers = [], { now = null } = {}) {
69
+ const valid = waivers.filter((w) => w.code && w.reason && w.approvedBy
70
+ && !(now && w.expires && !isoBefore(now, w.expires)));
71
+
72
+ const scopeOf = (d) => d.scope || d.target || d.targetPath || d.mission || null;
73
+ const matches = (w, d) => w.code === d.code && (!w.scope || w.scope === scopeOf(d));
74
+
75
+ const waived = [];
76
+ const applied = diagnostics.map((d) => {
77
+ const w = valid.find((x) => matches(x, d));
78
+ if (!w) return d;
79
+ const record = { ...d, waived: true, waiver: { id: w.id, approvedBy: w.approvedBy, reason: w.reason, expires: w.expires || null } };
80
+ waived.push(record);
81
+ return record;
82
+ });
83
+
84
+ const isBlocking = (d) => !d.waived && (d.severity === 'blocker' || (Array.isArray(d.blocks) && d.blocks.length > 0) || d.level === 'error');
85
+ const blockingAfter = applied.filter(isBlocking);
86
+
87
+ return {
88
+ schema: GOVERNANCE_SCHEMA,
89
+ diagnostics: applied,
90
+ waived,
91
+ blockingAfter,
92
+ report: {
93
+ total: diagnostics.length,
94
+ waived: waived.length,
95
+ blockingAfter: blockingAfter.length,
96
+ waivers: waivers.length,
97
+ activeWaivers: valid.length,
98
+ },
99
+ };
100
+ }
@@ -0,0 +1,292 @@
1
+ // Graph -> source: regenerate editable ThunderLang from an Intent Graph , the inverse of
2
+ // buildIntentGraph. This closes the native round-trip: a tool (SkillsTech Studio, or
3
+ // OpenThunder's discovery) can hold a graph, edit it, and emit `.intent` source that
4
+ // re-parses to an equivalent graph. Deterministic and pure.
5
+ //
6
+ // Round-trip contract: node TYPES + TITLES and the typed RELATIONSHIPS between them are
7
+ // preserved through graph -> source -> graph, and decisions round-trip by EXECUTION. A few
8
+ // compound/derived nodes are best-effort (see docs): Conflict (regenerated from role
9
+ // constraints), Journey steps, and Pattern requirement bodies.
10
+
11
+ export const GRAPH_SOURCE_SCHEMA = 'intent-graph-source-v1';
12
+
13
+ // Parse a "key value; key value" description back into an object (inverse of the joins
14
+ // buildIntentGraph uses for scalar fields packed into a node description).
15
+ function parseSegments(desc) {
16
+ const o = {};
17
+ for (const seg of String(desc || '').split(';')) {
18
+ const s = seg.trim();
19
+ if (!s) continue;
20
+ const m = s.match(/^(\w+)\s+(.+)$/);
21
+ if (m) o[m[1]] = m[2].trim();
22
+ else o[s] = true; // a bare flag like "idempotent"
23
+ }
24
+ return o;
25
+ }
26
+
27
+ export function graphToSource(graph) {
28
+ const nodes = graph?.nodes || [];
29
+ const rels = graph?.relationships || [];
30
+ const byId = new Map(nodes.map((n) => [n.id, n]));
31
+ const byType = (t) => nodes.filter((n) => n.type === t);
32
+ const first = (t) => byType(t)[0] || null;
33
+ const title = (n) => (n && n.title != null ? String(n.title) : '');
34
+ // nodes reached FROM id by relType; nodes pointing TO id by relType.
35
+ const out = (id, type) => rels.filter((r) => r.from === id && r.type === type).map((r) => byId.get(r.to)).filter(Boolean);
36
+ const inTo = (id, type) => rels.filter((r) => r.to === id && r.type === type).map((r) => byId.get(r.from)).filter(Boolean);
37
+ const phaseOut = (id) => rels.filter((r) => r.from === id && r.type === 'blocks' && String(r.to).startsWith('phase.')).map((r) => String(r.to).slice('phase.'.length));
38
+
39
+ const L = [];
40
+ const push = (s = '') => L.push(s);
41
+
42
+ const mission = first('Mission');
43
+ const mName = title(mission) || 'Unnamed';
44
+ push(`mission ${mName}`);
45
+
46
+ // Infer the profiles in play from the node types present (advisory, aids readability).
47
+ const present = new Set(nodes.map((n) => n.type));
48
+ const uses = [];
49
+ if (['Outcome', 'Metric', 'Evidence', 'Persona'].some((t) => present.has(t))) uses.push('product');
50
+ if (['ExperienceContract', 'ExperienceState', 'Pattern'].some((t) => present.has(t))) uses.push('experience');
51
+ if (['Capability', 'SystemContract'].some((t) => present.has(t))) uses.push('system');
52
+ if (['Release', 'OutcomeResult', 'LearningArtifact', 'OutcomeContract'].some((t) => present.has(t))) uses.push('delivery');
53
+ if (['DesignComponent', 'DesignArtifact'].some((t) => present.has(t))) uses.push('design');
54
+ for (const u of uses) push(`use ${u}`);
55
+ push();
56
+
57
+ if (mission?.description) { push('goal'); push(` ${mission.description}`); push(); }
58
+
59
+ // Persona / customer.
60
+ for (const p of byType('Persona')) push(`${p.description === 'customer' ? 'customer' : 'persona'} ${title(p)}`);
61
+
62
+ for (const e of byType('Evidence')) {
63
+ push(`evidence ${title(e)}`);
64
+ if (e.classification) push(` classification ${e.classification}`);
65
+ if (e.confidence) push(` confidence ${e.confidence}`);
66
+ if (e.source) push(` source ${e.source}`);
67
+ }
68
+
69
+ for (const o of byType('Outcome')) {
70
+ push(`outcome ${title(o)}`);
71
+ if (o.description) push(` "${o.description}"`);
72
+ }
73
+
74
+ for (const m of byType('Metric')) {
75
+ push(`metric ${title(m)}`);
76
+ const kv = parseSegments(m.description);
77
+ if (kv.baseline) push(` baseline ${kv.baseline}`);
78
+ if (kv.target) push(` target ${kv.target}`);
79
+ if (kv.window) push(` window ${kv.window}`);
80
+ }
81
+
82
+ const reqs = byType('Requirement');
83
+ if (reqs.length) { push('requires'); for (const r of reqs) push(` ${title(r)}`); }
84
+
85
+ // Guarantees (attached form so `verify` round-trips to VerificationRule nodes).
86
+ for (const g of byType('Guarantee')) {
87
+ push(`guarantee ${title(g)}`);
88
+ for (const v of out(g.id, 'verified_by')) push(` verify ${title(v)}`);
89
+ }
90
+
91
+ // Never-rules: attached form when a `verify` exists (so it round-trips to VerificationRule
92
+ // nodes), plain `never` block otherwise.
93
+ const nevers = byType('Never');
94
+ const neverBare = nevers.filter((n) => out(n.id, 'verified_by').length === 0);
95
+ if (neverBare.length) { push('never'); for (const n of neverBare) push(` ${title(n)}`); }
96
+ for (const n of nevers) {
97
+ const vs = out(n.id, 'verified_by');
98
+ if (!vs.length) continue;
99
+ push(`never ${title(n)}`);
100
+ for (const v of vs) push(` verify ${title(v)}`);
101
+ }
102
+
103
+ // Global invariants , regenerate name + statement + verify so they round-trip to Invariant nodes.
104
+ for (const iv of byType('Invariant')) {
105
+ const nm = (iv.id || '').replace(/^invariant\./, '')
106
+ || String(title(iv)).replace(/[^A-Za-z0-9]+/g, '-').replace(/^-+|-+$/g, '').toLowerCase() || 'invariant';
107
+ push(`invariant ${nm}`);
108
+ push(` statement ${title(iv)}`);
109
+ for (const v of out(iv.id, 'verified_by')) push(` verify ${title(v)}`);
110
+ }
111
+
112
+ // Skills the mission requires , regenerate a `requires_skill` block so they round-trip to Skill
113
+ // nodes. (Required-understanding `demonstrates` is prose in the proof/AST, not a structural graph
114
+ // node, so it is intentionally not regenerated here.)
115
+ const skillNodes = byType('Skill');
116
+ if (skillNodes.length) { push('requires_skill'); for (const s of skillNodes) push(` ${title(s)}`); }
117
+
118
+ for (const u of byType('Unknown')) {
119
+ push(`unknown ${title(u)}`);
120
+ if (u.owner) push(` owner ${u.owner}`);
121
+ // The Unknown -blocks-> phase edge is emitted from `resolve before`, not `blocks`.
122
+ for (const ph of phaseOut(u.id)) push(` resolve before ${ph}`);
123
+ }
124
+ for (const q of byType('Question')) {
125
+ push(`question ${title(q)}`);
126
+ if (q.owner) push(` asked_of ${q.owner}`);
127
+ for (const ph of phaseOut(q.id)) push(` blocks ${ph}`);
128
+ }
129
+ for (const a of byType('Assumption')) {
130
+ push(`assumption ${title(a)}`);
131
+ if (a.confidence) push(` confidence ${a.confidence}`);
132
+ }
133
+
134
+ const approvals = byType('Approval');
135
+ if (approvals.length) { push('approval required from'); for (const a of approvals) push(` ${title(a)}`); }
136
+
137
+ // Role-scoped constraints (`<role> requires`), grouped by role.
138
+ const constraintsByRole = {};
139
+ for (const c of byType('Constraint')) (constraintsByRole[c.owner || 'product'] ||= []).push(title(c));
140
+ for (const [role, stmts] of Object.entries(constraintsByRole)) {
141
+ push(`${role} requires`);
142
+ for (const s of stmts) push(` ${s}`);
143
+ }
144
+
145
+ // Experience contracts + their states.
146
+ for (const exp of byType('ExperienceContract')) {
147
+ push(`experience ${title(exp)}`);
148
+ if (exp.owner) push(` actor ${exp.owner}`);
149
+ if (exp.description) push(` goal "${exp.description}"`);
150
+ for (const p of out(exp.id, 'derived_from')) if (p.type === 'Pattern') push(` follows ${title(p)}`);
151
+ for (const st of out(exp.id, 'requires')) if (st.type === 'ExperienceState') {
152
+ push(` state ${title(st)}`);
153
+ if (st.status === 'recoverable') push(' recover available');
154
+ }
155
+ }
156
+ for (const p of byType('Pattern')) push(`pattern ${title(p)}`);
157
+
158
+ // Style intents , re-emit the block (name, the experience it constrains, a11y target).
159
+ for (const si of byType('StyleIntent')) {
160
+ push(`style_intent ${title(si)}`);
161
+ const edge = rels.find((r) => r.to === si.id && r.type === 'constrained_by');
162
+ const exp = edge ? byId.get(edge.from) : null;
163
+ if (exp && exp.type === 'ExperienceContract') push(` applies_to ${title(exp)}`);
164
+ const am = String(si.description || '').match(/a11y target (\S+)/);
165
+ if (am) push(` accessibility_target ${am[1]}`);
166
+ }
167
+
168
+ // Lifecycles.
169
+ for (const lc of byType('Lifecycle')) {
170
+ push(`lifecycle ${title(lc)}`);
171
+ const states = out(lc.id, 'requires').filter((s) => s.type === 'LifecycleState');
172
+ for (const s of states) push(` state ${title(s)}`);
173
+ let ti = 0;
174
+ for (const s of states) {
175
+ for (const r of rels.filter((r) => r.from === s.id && r.type === 'transitions_to')) {
176
+ const to = byId.get(r.to);
177
+ if (!to) continue;
178
+ ti += 1;
179
+ push(` transition ${r.name || `t${ti}`}`);
180
+ push(` from ${title(s)}`);
181
+ push(` to ${title(to)}`);
182
+ if (r.within) push(` within ${r.within}`);
183
+ }
184
+ }
185
+ const terminals = states.filter((s) => s.status === 'verified').map(title);
186
+ if (terminals.length) push(` terminal ${terminals.join(', ')}`);
187
+ }
188
+
189
+ // Decisions + rules (round-trip by execution).
190
+ for (const d of byType('Decision')) {
191
+ push(`decision ${title(d)}`);
192
+ for (const r of out(d.id, 'requires').filter((n) => n.type === 'Rule')) {
193
+ const m = String(r.description || '').match(/^when (.+?) -> (.*)$/);
194
+ push(` rule ${title(r)}`);
195
+ if (m) { push(` when ${m[1]}`); push(` return ${m[2]}`); }
196
+ }
197
+ const dm = String(d.description || '').match(/default (.+)$/);
198
+ if (dm) { push(' default'); push(` return ${dm[1]}`); }
199
+ if (d.owner) push(` owner ${d.owner}`);
200
+ }
201
+
202
+ // Commands + failure handlers.
203
+ for (const c of byType('Command')) {
204
+ push(`command ${title(c)}`);
205
+ const kv = parseSegments(c.description);
206
+ if (kv.idempotent) push(' idempotency_key id');
207
+ if (kv.timeout) push(` timeout ${kv.timeout}`);
208
+ if (kv.retry) push(` retry ${kv.retry}`);
209
+ }
210
+ for (const h of byType('FailureHandler')) {
211
+ push(`on ${title(h)}`);
212
+ const m = String(h.description || '').match(/compensate (.+)$/);
213
+ if (m) for (const step of m[1].split(',')) push(` compensate ${step.trim()}`);
214
+ }
215
+
216
+ // System profile.
217
+ for (const cap of byType('Capability')) {
218
+ push(`capability ${title(cap)}`);
219
+ if (cap.description) push(` description "${cap.description}"`);
220
+ // Capability -implemented_by-> member (members are OUT edges, unlike design components).
221
+ for (const m of out(cap.id, 'implemented_by')) push(` implements ${title(m)}`);
222
+ }
223
+ for (const iface of byType('SystemContract')) {
224
+ push(`interface ${title(iface)}`);
225
+ const kv = parseSegments(iface.description);
226
+ if (kv.provides) push(` provides ${kv.provides}`);
227
+ if (kv.requires) push(` requires ${kv.requires}`);
228
+ if (kv.slo) push(` slo "${kv.slo}"`);
229
+ }
230
+
231
+ // Delivery profile.
232
+ for (const r of byType('Release')) {
233
+ push(`release ${title(r)}`);
234
+ const kv = parseSegments(r.description);
235
+ if (kv.v) push(` version "${kv.v.replace(/^v/, '')}"`);
236
+ if (r.status) push(` status ${r.status}`);
237
+ if (kv.date) push(` date ${kv.date}`);
238
+ if (kv.includes) for (const i of kv.includes.split(',')) push(` includes ${i.trim()}`);
239
+ }
240
+ for (const res of byType('OutcomeResult')) {
241
+ push(`result ${title(res)}`);
242
+ const measured = inTo(res.id, 'resulted_in').find((n) => n.type === 'Outcome');
243
+ if (measured) push(` measures ${title(measured)}`);
244
+ const kv = parseSegments(res.description);
245
+ if (kv.metric) push(` metric ${kv.metric}`);
246
+ if (kv.value) push(` value ${kv.value}`);
247
+ if (kv.baseline) push(` baseline ${kv.baseline}`);
248
+ }
249
+ for (const l of byType('LearningArtifact')) {
250
+ push(`learning ${title(l)}`);
251
+ if (l.description) push(` description "${l.description}"`);
252
+ const from = out(l.id, 'derived_from').find((n) => n.type === 'Release');
253
+ if (from) push(` from ${title(from)}`);
254
+ }
255
+
256
+ // Outcome contracts.
257
+ for (const c of byType('OutcomeContract')) {
258
+ push(`outcome_contract ${title(c)}`);
259
+ const o = out(c.id, 'targets').find((n) => n.type === 'Outcome');
260
+ if (o) push(` outcome ${title(o)}`);
261
+ const met = out(c.id, 'measured_by').find((n) => n.type === 'Metric');
262
+ if (met) push(` metric ${title(met)}`);
263
+ const kv = parseSegments(c.description);
264
+ if (kv.baseline) push(` baseline ${kv.baseline}`);
265
+ if (kv.target) push(` target ${kv.target}`);
266
+ if (/lower is better/.test(c.description || '')) push(' direction lower');
267
+ if (kv.window) push(` window ${kv.window}`);
268
+ if (c.owner) push(` owner ${c.owner}`);
269
+ }
270
+
271
+ // Design profile.
272
+ for (const comp of byType('DesignComponent')) {
273
+ push(`component ${title(comp)}`);
274
+ const desc = String(comp.description || '');
275
+ const lead = desc.split(';')[0].trim();
276
+ if (lead && !/^(variants|tokens):/.test(lead)) push(` description "${lead}"`);
277
+ const vm = desc.match(/variants:\s*([^;]+)/);
278
+ if (vm) for (const v of vm[1].split(',')) push(` variant ${v.trim()}`);
279
+ const tm = desc.match(/tokens:\s*([^;]+)/);
280
+ if (tm) for (const t of tm[1].split(',')) push(` token ${t.trim()}`);
281
+ for (const impl of inTo(comp.id, 'implemented_by')) push(` implements ${title(impl)}`);
282
+ }
283
+ for (const art of byType('DesignArtifact')) {
284
+ push(`artifact ${title(art)}`);
285
+ const kv = parseSegments(art.description);
286
+ if (kv.kind) push(` kind ${kv.kind}`);
287
+ if (art.source) push(` ref "${art.source}"`);
288
+ for (const c of inTo(art.id, 'represented_by')) if (c.type === 'DesignComponent') push(` covers ${title(c)}`);
289
+ }
290
+
291
+ return L.join('\n') + '\n';
292
+ }