@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.
- package/CHANGELOG.md +375 -0
- package/LICENSE +21 -0
- package/README.md +165 -0
- package/dist/core.cjs +6768 -0
- package/dist/index.cjs +9308 -0
- package/intent-graph.schema.json +687 -0
- package/package.json +84 -0
- package/src/ai-core.mjs +67 -0
- package/src/ai-events.mjs +56 -0
- package/src/ai.mjs +324 -0
- package/src/arch.mjs +55 -0
- package/src/atlas.mjs +118 -0
- package/src/changes.mjs +74 -0
- package/src/classification.mjs +36 -0
- package/src/cli.mjs +1534 -0
- package/src/codegen.mjs +214 -0
- package/src/compile.mjs +142 -0
- package/src/comprehension.mjs +130 -0
- package/src/conflict.mjs +0 -0
- package/src/core.d.ts +99 -0
- package/src/core.mjs +92 -0
- package/src/data-schema.mjs +137 -0
- package/src/decision.mjs +38 -0
- package/src/distributed.mjs +48 -0
- package/src/draft.mjs +101 -0
- package/src/drift.mjs +177 -0
- package/src/emit.mjs +519 -0
- package/src/exporters.mjs +245 -0
- package/src/expr.mjs +245 -0
- package/src/fable.mjs +110 -0
- package/src/focus.mjs +151 -0
- package/src/format.mjs +55 -0
- package/src/governance.mjs +100 -0
- package/src/graph-source.mjs +292 -0
- package/src/guard.mjs +105 -0
- package/src/guardian.mjs +98 -0
- package/src/hash.mjs +89 -0
- package/src/importers.mjs +194 -0
- package/src/index.d.ts +725 -0
- package/src/index.mjs +185 -0
- package/src/intellisense.mjs +210 -0
- package/src/intent-atlas.mjs +77 -0
- package/src/intent-graph.mjs +346 -0
- package/src/intent-ir.mjs +144 -0
- package/src/intent-schema.mjs +215 -0
- package/src/ledger.mjs +109 -0
- package/src/lifecycle.mjs +56 -0
- package/src/lift.mjs +693 -0
- package/src/lsp.mjs +152 -0
- package/src/mcp.mjs +158 -0
- package/src/migrate.mjs +118 -0
- package/src/outcome.mjs +93 -0
- package/src/parse.mjs +733 -0
- package/src/patch.mjs +364 -0
- package/src/privacy.mjs +61 -0
- package/src/proof-schema.mjs +129 -0
- package/src/report.mjs +84 -0
- package/src/runtime.mjs +96 -0
- package/src/sarif.mjs +88 -0
- package/src/scan-queries.mjs +97 -0
- package/src/scan.mjs +87 -0
- package/src/security.mjs +73 -0
- package/src/select.mjs +80 -0
- package/src/semantic-diff.mjs +125 -0
- package/src/simulate.mjs +106 -0
- package/src/style.mjs +250 -0
- package/src/sync.mjs +103 -0
- package/src/testing.mjs +59 -0
- package/src/twelve-factor.mjs +173 -0
- package/src/verify-diff.mjs +105 -0
- package/src/xml.mjs +87 -0
- package/syntaxes/intent.tmLanguage.json +55 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
// Canonical Intent Graph (intent-graph-v1, Section 4). One model all four systems
|
|
2
|
+
// consume: nodes (Mission, Evidence, Outcome, Metric, Requirement, Guarantee, Never,
|
|
3
|
+
// Unknown, Assumption, Question, Approval, ...) + typed relationships. Deterministic;
|
|
4
|
+
// pure (no Node deps): browser-safe. OT/RM/ST read this; they do not re-parse .intent.
|
|
5
|
+
|
|
6
|
+
import { slug, subjectName, skillRefId } from './parse.mjs';
|
|
7
|
+
import { detectConflicts } from './conflict.mjs';
|
|
8
|
+
import { buildLifecycle } from './lifecycle.mjs';
|
|
9
|
+
import { analyzeDistributed } from './distributed.mjs';
|
|
10
|
+
|
|
11
|
+
export const INTENT_GRAPH_SCHEMA = 'intent-graph-v1';
|
|
12
|
+
|
|
13
|
+
function node(id, type, title, extra = {}) {
|
|
14
|
+
return {
|
|
15
|
+
id, type, title: title || null,
|
|
16
|
+
description: extra.description ?? null,
|
|
17
|
+
status: extra.status ?? 'draft',
|
|
18
|
+
owner: extra.owner ?? null,
|
|
19
|
+
classification: extra.classification ?? null,
|
|
20
|
+
confidence: extra.confidence ?? null,
|
|
21
|
+
source: extra.source ?? null,
|
|
22
|
+
tags: extra.tags ?? [],
|
|
23
|
+
createdTime: null, updatedTime: null, // deterministic; stamped by consumers
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const rel = (from, type, to, extra) => (extra ? { from, type, to, ...extra } : { from, type, to });
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build the canonical Intent Graph from a parsed AST.
|
|
30
|
+
* @param {object} ast (from parseIntent)
|
|
31
|
+
* @returns {{schema, missionId, nodes, relationships, summary}}
|
|
32
|
+
*/
|
|
33
|
+
export function buildIntentGraph(ast) {
|
|
34
|
+
const nodes = [];
|
|
35
|
+
const relationships = [];
|
|
36
|
+
const subject = subjectName(ast);
|
|
37
|
+
const mId = `mission.${slug(subject || 'unnamed')}`;
|
|
38
|
+
nodes.push(node(mId, 'Mission', ast.title || subject, {
|
|
39
|
+
description: ast.problem || ast.goal || null,
|
|
40
|
+
owner: ast.owner || null,
|
|
41
|
+
status: ast.approvals && ast.approvals.length ? 'approval-required' : 'draft',
|
|
42
|
+
}));
|
|
43
|
+
// Persona / customer the mission serves (product profile).
|
|
44
|
+
for (const [kind, name] of [['persona', ast.persona], ['customer', ast.customer]]) {
|
|
45
|
+
if (!name) continue;
|
|
46
|
+
const id = `persona.${slug(name)}`;
|
|
47
|
+
nodes.push(node(id, 'Persona', name, { description: kind }));
|
|
48
|
+
relationships.push(rel(mId, 'addresses', id));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const e of ast.evidence || []) {
|
|
52
|
+
const id = `evidence.${slug(e.name || 'evidence')}`;
|
|
53
|
+
nodes.push(node(id, 'Evidence', e.name, { classification: e.classification, confidence: e.confidence, source: e.source }));
|
|
54
|
+
relationships.push(rel(mId, 'supported_by', id));
|
|
55
|
+
}
|
|
56
|
+
for (const o of ast.outcomes || []) {
|
|
57
|
+
const id = `outcome.${slug(o.name || 'outcome')}`;
|
|
58
|
+
nodes.push(node(id, 'Outcome', o.name, { description: o.description }));
|
|
59
|
+
relationships.push(rel(mId, 'targets', id));
|
|
60
|
+
}
|
|
61
|
+
for (const m of ast.metrics || []) {
|
|
62
|
+
const id = `metric.${slug(m.name || 'metric')}`;
|
|
63
|
+
nodes.push(node(id, 'Metric', m.name, { description: [m.baseline && `baseline ${m.baseline}`, m.target && `target ${m.target}`, m.window && `window ${m.window}`].filter(Boolean).join('; ') || null }));
|
|
64
|
+
// Attach a metric to an outcome by name overlap, else to the mission. Use the SAME id
|
|
65
|
+
// computation as the outcome node (with its 'outcome' fallback) so the edge never dangles.
|
|
66
|
+
const ms = slug(m.name || 'metric');
|
|
67
|
+
const o = (ast.outcomes || []).find((x) => { const xs = slug(x.name || 'outcome'); return xs.includes(ms) || ms.includes(xs); });
|
|
68
|
+
relationships.push(rel(o ? `outcome.${slug(o.name || 'outcome')}` : mId, 'measured_by', id));
|
|
69
|
+
}
|
|
70
|
+
(ast.requires || []).forEach((r, i) => {
|
|
71
|
+
const id = `requirement.${slug(ast.mission)}.${i + 1}`;
|
|
72
|
+
nodes.push(node(id, 'Requirement', r, { classification: 'observed' }));
|
|
73
|
+
relationships.push(rel(mId, 'requires', id));
|
|
74
|
+
});
|
|
75
|
+
const emittedVerifs = new Set();
|
|
76
|
+
for (const g of ast.guarantees || []) {
|
|
77
|
+
const id = `guarantee.${g.id || slug(g.statement)}`;
|
|
78
|
+
nodes.push(node(id, 'Guarantee', g.statement, { status: g.verify && g.verify.length ? 'verify-declared' : 'unverified' }));
|
|
79
|
+
relationships.push(rel(mId, 'requires', id));
|
|
80
|
+
for (const v of g.verify || []) {
|
|
81
|
+
const vid = `verification.${slug(v)}`;
|
|
82
|
+
if (!emittedVerifs.has(vid)) { nodes.push(node(vid, 'VerificationRule', v, { status: 'verify-declared' })); emittedVerifs.add(vid); }
|
|
83
|
+
relationships.push(rel(id, 'verified_by', vid));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
for (const n of ast.neverRules || []) {
|
|
87
|
+
const id = `never.${n.id || slug(n.statement)}`;
|
|
88
|
+
nodes.push(node(id, 'Never', n.statement, { status: n.verify && n.verify.length ? 'verify-declared' : 'unverified' }));
|
|
89
|
+
relationships.push(rel(mId, 'constrained_by', id));
|
|
90
|
+
for (const v of n.verify || []) {
|
|
91
|
+
const vid = `verification.${slug(v)}`;
|
|
92
|
+
if (!emittedVerifs.has(vid)) { nodes.push(node(vid, 'VerificationRule', v, { status: 'verify-declared' })); emittedVerifs.add(vid); }
|
|
93
|
+
relationships.push(rel(id, 'verified_by', vid));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Global invariants , system-wide laws. The mission is constrained by each; verify -> edge.
|
|
97
|
+
for (const iv of ast.invariants || []) {
|
|
98
|
+
const id = `invariant.${iv.id || slug(iv.name)}`;
|
|
99
|
+
nodes.push(node(id, 'Invariant', iv.statement || iv.name, {
|
|
100
|
+
status: iv.verify && iv.verify.length ? 'verify-declared' : 'unverified',
|
|
101
|
+
description: iv.because || null,
|
|
102
|
+
}));
|
|
103
|
+
relationships.push(rel(mId, 'constrained_by', id));
|
|
104
|
+
for (const v of iv.verify || []) {
|
|
105
|
+
const vid = `verification.${slug(v)}`;
|
|
106
|
+
if (!emittedVerifs.has(vid)) { nodes.push(node(vid, 'VerificationRule', v, { status: 'verify-declared' })); emittedVerifs.add(vid); }
|
|
107
|
+
relationships.push(rel(id, 'verified_by', vid));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Skills the mission requires , the Ownership Graph's skill<->intent join. Each Skill node id is
|
|
111
|
+
// the shared `skill:<slug>` namespace (IL owns the id shape; STCE the content), so every product
|
|
112
|
+
// references the same skill by the same id. The mission `requires_skill` each.
|
|
113
|
+
const emittedSkills = new Set();
|
|
114
|
+
for (const sk of ast.skills || []) {
|
|
115
|
+
const id = sk.id || skillRefId(sk.name);
|
|
116
|
+
if (!emittedSkills.has(id)) { nodes.push(node(id, 'Skill', sk.name, { status: 'required' })); emittedSkills.add(id); }
|
|
117
|
+
relationships.push(rel(mId, 'requires_skill', id));
|
|
118
|
+
}
|
|
119
|
+
for (const u of ast.unknowns || []) {
|
|
120
|
+
const id = `unknown.${slug(u.name || 'unknown')}`;
|
|
121
|
+
nodes.push(node(id, 'Unknown', u.name, { classification: 'unknown', owner: u.owner, status: 'unresolved' }));
|
|
122
|
+
relationships.push(rel(mId, 'depends_on', id));
|
|
123
|
+
if (u.resolveBefore) relationships.push(rel(id, 'blocks', `phase.${slug(u.resolveBefore)}`));
|
|
124
|
+
}
|
|
125
|
+
for (const q of ast.questions || []) {
|
|
126
|
+
const id = `question.${slug(q.name || 'question')}`;
|
|
127
|
+
nodes.push(node(id, 'Question', q.name, { classification: 'unknown', owner: q.askedOf, status: 'open' }));
|
|
128
|
+
relationships.push(rel(mId, 'depends_on', id)); // container edge (Question -> Mission via depends_on), matching Unknown
|
|
129
|
+
if (q.blocks) relationships.push(rel(id, 'blocks', `phase.${slug(q.blocks)}`));
|
|
130
|
+
}
|
|
131
|
+
for (const a of ast.assumptionDecls || []) {
|
|
132
|
+
const id = `assumption.${slug(a.name || 'assumption')}`;
|
|
133
|
+
nodes.push(node(id, 'Assumption', a.name, { classification: 'assumed', confidence: a.confidence, status: 'unvalidated' }));
|
|
134
|
+
relationships.push(rel(mId, 'depends_on', id));
|
|
135
|
+
}
|
|
136
|
+
const patternIds = new Set((ast.patterns || []).map((p) => `pattern.${slug(p.name || 'pattern')}`));
|
|
137
|
+
for (const exp of ast.experiences || []) {
|
|
138
|
+
const eId = `experience.${slug(exp.name || 'experience')}`;
|
|
139
|
+
nodes.push(node(eId, 'ExperienceContract', exp.name, { description: exp.goal || null, owner: exp.actor || null }));
|
|
140
|
+
relationships.push(rel(mId, 'represented_by', eId));
|
|
141
|
+
// Only link to a pattern the mission actually declares (a `follows` ref may not resolve).
|
|
142
|
+
for (const p of exp.follows || []) { const t = `pattern.${slug(p)}`; if (patternIds.has(t)) relationships.push(rel(eId, 'derived_from', t)); }
|
|
143
|
+
for (const j of exp.journeys || []) {
|
|
144
|
+
const jId = `journey.${slug(exp.name)}.${slug(j.name || 'journey')}`;
|
|
145
|
+
nodes.push(node(jId, 'Journey', j.name, { description: `${(j.steps || []).length} step(s)` }));
|
|
146
|
+
relationships.push(rel(eId, 'represented_by', jId));
|
|
147
|
+
}
|
|
148
|
+
for (const st of exp.states || []) {
|
|
149
|
+
const sId = `experience-state.${slug(exp.name)}.${slug(st.name || 'state')}`;
|
|
150
|
+
nodes.push(node(sId, 'ExperienceState', st.name, { status: st.hasRecovery ? 'recoverable' : 'defined' }));
|
|
151
|
+
relationships.push(rel(eId, 'requires', sId));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
for (const pat of ast.patterns || []) {
|
|
155
|
+
nodes.push(node(`pattern.${slug(pat.name || 'pattern')}`, 'Pattern', pat.name, { description: `${(pat.requires || []).length} requirement(s)` }));
|
|
156
|
+
}
|
|
157
|
+
// Style intents , brand/visual language governing an experience. The whole node is
|
|
158
|
+
// `proposed` (design intent), and its accessibility target is never emitted as verified.
|
|
159
|
+
const experienceIds = new Set((ast.experiences || []).map((e) => `experience.${slug(e.name || 'experience')}`));
|
|
160
|
+
for (const si of ast.styleIntents || []) {
|
|
161
|
+
const stId = `style.${slug(si.name || 'style')}`;
|
|
162
|
+
const desc = [
|
|
163
|
+
si.purpose,
|
|
164
|
+
si.accessibilityTarget && `a11y target ${si.accessibilityTarget} (proposed)`,
|
|
165
|
+
si.tokens && si.tokens.length ? `${si.tokens.length} token(s)` : null,
|
|
166
|
+
].filter(Boolean).join('; ') || null;
|
|
167
|
+
nodes.push(node(stId, 'StyleIntent', si.name, { description: desc, classification: 'proposed' }));
|
|
168
|
+
const target = si.appliesTo ? `experience.${slug(si.appliesTo)}` : null;
|
|
169
|
+
if (target && experienceIds.has(target)) relationships.push(rel(target, 'constrained_by', stId));
|
|
170
|
+
else relationships.push(rel(mId, 'requires', stId));
|
|
171
|
+
}
|
|
172
|
+
for (const role of ast.approvals || []) {
|
|
173
|
+
const id = `approval.${slug(ast.mission)}.${slug(role)}`;
|
|
174
|
+
nodes.push(node(id, 'Approval', role, { status: 'required', owner: role }));
|
|
175
|
+
relationships.push(rel(mId, 'approved_by', id));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
(ast.roleConstraints || []).forEach((rc, i) => {
|
|
179
|
+
const id = `constraint.${slug(rc.role)}.${i + 1}`;
|
|
180
|
+
nodes.push(node(id, 'Constraint', rc.statement, { owner: rc.role }));
|
|
181
|
+
relationships.push(rel(mId, 'requires', id));
|
|
182
|
+
});
|
|
183
|
+
// Conflict `between`/`resolveBy` entries are informational references that may or may not
|
|
184
|
+
// name an emitted node; only edge to targets that actually exist so nothing dangles.
|
|
185
|
+
const existingIds = new Set(nodes.map((n) => n.id));
|
|
186
|
+
detectConflicts(ast).forEach((c, i) => {
|
|
187
|
+
const id = `conflict.${slug(c.name || String(i + 1))}`;
|
|
188
|
+
nodes.push(node(id, 'Conflict', c.name, { status: c.status, description: c.type }));
|
|
189
|
+
for (const b of c.between || []) {
|
|
190
|
+
const target = `constraint.${slug(b)}`;
|
|
191
|
+
if (existingIds.has(target)) relationships.push(rel(id, 'contradicts', target));
|
|
192
|
+
}
|
|
193
|
+
if (c.before) relationships.push(rel(id, 'blocks', `phase.${slug(c.before)}`));
|
|
194
|
+
for (const r of c.resolveBy || []) {
|
|
195
|
+
const target = `approval.${slug(ast.mission)}.${slug(r)}`;
|
|
196
|
+
if (existingIds.has(target)) relationships.push(rel(id, 'approved_by', target));
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
for (const raw of ast.lifecycles || []) {
|
|
201
|
+
const ir = buildLifecycle(raw);
|
|
202
|
+
const lId = `lifecycle.${slug(ir.name || 'lifecycle')}`;
|
|
203
|
+
nodes.push(node(lId, 'Lifecycle', ir.name, { description: `${ir.states.length} states, ${ir.transitions.length} transitions`, status: ir.initial ? 'defined' : 'draft' }));
|
|
204
|
+
relationships.push(rel(mId, 'represented_by', lId));
|
|
205
|
+
const stateSlugs = new Set();
|
|
206
|
+
for (const s of ir.states) {
|
|
207
|
+
const ss = slug(s);
|
|
208
|
+
stateSlugs.add(ss);
|
|
209
|
+
const sId = `lifecycle-state.${slug(ir.name)}.${ss}`;
|
|
210
|
+
nodes.push(node(sId, 'LifecycleState', s, { status: ir.terminals.includes(s) ? 'verified' : 'defined' }));
|
|
211
|
+
relationships.push(rel(lId, 'requires', sId));
|
|
212
|
+
}
|
|
213
|
+
for (const t of ir.transitions) {
|
|
214
|
+
// Transition metadata (name / within) rides ON the transitions_to edge, not the state node.
|
|
215
|
+
// Only emit for transitions between DECLARED states, so a transition that references an
|
|
216
|
+
// undefined state (flagged separately as IL-LIFE-001) never leaves a dangling edge.
|
|
217
|
+
if (t.from && t.to && stateSlugs.has(slug(t.from)) && stateSlugs.has(slug(t.to))) relationships.push(rel(
|
|
218
|
+
`lifecycle-state.${slug(ir.name)}.${slug(t.from)}`, 'transitions_to', `lifecycle-state.${slug(ir.name)}.${slug(t.to)}`,
|
|
219
|
+
(t.name || t.within) ? { name: t.name || null, within: t.within || null } : undefined,
|
|
220
|
+
));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
for (const dec of ast.decisions || []) {
|
|
225
|
+
const dId = `decision.${slug(dec.name || 'decision')}`;
|
|
226
|
+
nodes.push(node(dId, 'Decision', dec.name, { owner: dec.owner || null, description: `${(dec.rules || []).length} rules${dec.default ? ', default ' + dec.default : ''}` }));
|
|
227
|
+
relationships.push(rel(mId, 'requires', dId));
|
|
228
|
+
for (const r of dec.rules || []) {
|
|
229
|
+
const rId = `rule.${slug(dec.name)}.${slug(r.name || 'rule')}`;
|
|
230
|
+
nodes.push(node(rId, 'Rule', r.name, { description: r.when ? `when ${r.when} -> ${r.result}` : null }));
|
|
231
|
+
relationships.push(rel(dId, 'requires', rId));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
for (const c of ast.commands || []) {
|
|
235
|
+
const id = `command.${slug(c.name || 'command')}`;
|
|
236
|
+
nodes.push(node(id, 'Command', c.name, { description: [c.idempotencyKey && 'idempotent', c.timeout && `timeout ${c.timeout}`, c.retry && `retry ${c.retry}`].filter(Boolean).join('; ') || null }));
|
|
237
|
+
relationships.push(rel(mId, 'requires', id));
|
|
238
|
+
}
|
|
239
|
+
for (const h of ast.handlers || []) {
|
|
240
|
+
const id = `handler.${slug(h.trigger || 'handler')}`;
|
|
241
|
+
nodes.push(node(id, 'FailureHandler', h.trigger, { description: (h.compensate || []).length ? `compensate ${h.compensate.join(', ')}` : null }));
|
|
242
|
+
relationships.push(rel(mId, 'constrained_by', id));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ── System profile: capabilities + system contracts (interfaces) ──
|
|
246
|
+
const nodeIds = new Set(nodes.map((n) => n.id));
|
|
247
|
+
for (const cap of ast.capabilities || []) {
|
|
248
|
+
const id = `capability.${slug(cap.name || 'capability')}`;
|
|
249
|
+
nodes.push(node(id, 'Capability', cap.name, { description: cap.description || null }));
|
|
250
|
+
relationships.push(rel(mId, 'requires', id));
|
|
251
|
+
// Link each declared member to the capability if it resolves to an emitted node.
|
|
252
|
+
for (const m of cap.implements || []) {
|
|
253
|
+
const target = [`command.${slug(m)}`, `decision.${slug(m)}`, `mission.${slug(m)}`].find((cand) => nodeIds.has(cand));
|
|
254
|
+
if (target) relationships.push(rel(id, 'implemented_by', target));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
for (const iface of ast.interfaces || []) {
|
|
258
|
+
const id = `system-contract.${slug(iface.name || 'interface')}`;
|
|
259
|
+
const desc = [iface.provides.length && `provides ${iface.provides.join(', ')}`, iface.requires.length && `requires ${iface.requires.join(', ')}`, iface.slo && `slo ${iface.slo}`].filter(Boolean).join('; ') || null;
|
|
260
|
+
nodes.push(node(id, 'SystemContract', iface.name, { description: desc }));
|
|
261
|
+
relationships.push(rel(mId, 'requires', id));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ── Design profile: design-system components + artifacts (mockups) ──
|
|
265
|
+
// Resolve a design reference to a Pattern / ExperienceContract / ExperienceState node.
|
|
266
|
+
const allIds = nodes.map((n) => n.id);
|
|
267
|
+
const resolveDesignTarget = (name) => {
|
|
268
|
+
const s = slug(name);
|
|
269
|
+
return [`pattern.${s}`, `experience.${s}`].find((c) => nodeIds.has(c))
|
|
270
|
+
|| allIds.find((id) => id.startsWith('experience-state.') && id.endsWith(`.${s}`))
|
|
271
|
+
|| null;
|
|
272
|
+
};
|
|
273
|
+
for (const comp of ast.components || []) {
|
|
274
|
+
const id = `design-component.${slug(comp.name || 'component')}`;
|
|
275
|
+
const desc = [comp.description, comp.variants.length && `variants: ${comp.variants.join(', ')}`, comp.tokens.length && `tokens: ${comp.tokens.join(', ')}`].filter(Boolean).join('; ') || null;
|
|
276
|
+
nodes.push(node(id, 'DesignComponent', comp.name, { description: desc }));
|
|
277
|
+
relationships.push(rel(mId, 'requires', id));
|
|
278
|
+
// Each experience state / pattern the component implements: <target> -implemented_by-> component.
|
|
279
|
+
for (const m of comp.implements || []) {
|
|
280
|
+
const target = resolveDesignTarget(m);
|
|
281
|
+
if (target) relationships.push(rel(target, 'implemented_by', id));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const componentNames = new Set((ast.components || []).map((c) => slug(c.name || '')));
|
|
285
|
+
for (const art of ast.artifacts || []) {
|
|
286
|
+
const id = `design-artifact.${slug(art.name || 'artifact')}`;
|
|
287
|
+
nodes.push(node(id, 'DesignArtifact', art.name, { source: art.ref || null, description: [art.kind && `kind ${art.kind}`, art.covers.length && `covers ${art.covers.join(', ')}`].filter(Boolean).join('; ') || null }));
|
|
288
|
+
relationships.push(rel(mId, 'represented_by', id));
|
|
289
|
+
// A covered component is represented_by this artifact (the mockup depicts it).
|
|
290
|
+
for (const c of art.covers || []) {
|
|
291
|
+
if (componentNames.has(slug(c))) relationships.push(rel(`design-component.${slug(c)}`, 'represented_by', id));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ── Delivery profile: releases, outcome results, learnings ──
|
|
296
|
+
for (const r of ast.releases || []) {
|
|
297
|
+
const id = `release.${slug(r.name || 'release')}`;
|
|
298
|
+
nodes.push(node(id, 'Release', r.name, { status: r.status || 'planned', description: [r.version && `v${r.version}`, r.date && `date ${r.date}`, r.includes.length && `includes ${r.includes.join(', ')}`].filter(Boolean).join('; ') || null }));
|
|
299
|
+
relationships.push(rel(mId, 'released_in', id));
|
|
300
|
+
}
|
|
301
|
+
for (const res of ast.results || []) {
|
|
302
|
+
const id = `outcome-result.${slug(res.name || 'result')}`;
|
|
303
|
+
nodes.push(node(id, 'OutcomeResult', res.name, { description: [res.metric && `metric ${res.metric}`, res.value && `value ${res.value}`, res.baseline && `baseline ${res.baseline}`].filter(Boolean).join('; ') || null }));
|
|
304
|
+
// A result resolves the outcome it measures (Outcome -resulted_in-> OutcomeResult).
|
|
305
|
+
const outId = res.measures ? `outcome.${slug(res.measures)}` : null;
|
|
306
|
+
relationships.push(rel(nodeIds.has(outId) ? outId : mId, 'resulted_in', id));
|
|
307
|
+
}
|
|
308
|
+
const releaseNames = new Set((ast.releases || []).map((r) => slug(r.name || '')));
|
|
309
|
+
for (const l of ast.learnings || []) {
|
|
310
|
+
const id = `learning.${slug(l.name || 'learning')}`;
|
|
311
|
+
nodes.push(node(id, 'LearningArtifact', l.name, { description: l.description || null }));
|
|
312
|
+
// A learning is derived from its source release (if resolvable) or the mission.
|
|
313
|
+
const fromRelease = l.from && releaseNames.has(slug(l.from));
|
|
314
|
+
relationships.push(rel(id, 'derived_from', fromRelease ? `release.${slug(l.from)}` : mId));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ── Outcome contracts: executable commitments binding an outcome to a target ──
|
|
318
|
+
for (const c of ast.outcomeContracts || []) {
|
|
319
|
+
const id = `outcome-contract.${slug(c.name || 'contract')}`;
|
|
320
|
+
const desc = [c.baseline && `baseline ${c.baseline}`, c.target && `target ${c.target}`, `${c.direction || 'higher'} is better`, c.window && `window ${c.window}`].filter(Boolean).join('; ') || null;
|
|
321
|
+
nodes.push(node(id, 'OutcomeContract', c.name, { owner: c.owner || null, description: desc }));
|
|
322
|
+
relationships.push(rel(mId, 'requires', id));
|
|
323
|
+
// The contract targets the outcome it governs and is measured by its metric (when they resolve).
|
|
324
|
+
const outId = c.outcome ? `outcome.${slug(c.outcome)}` : null;
|
|
325
|
+
if (nodeIds.has(outId)) relationships.push(rel(id, 'targets', outId));
|
|
326
|
+
const metId = c.metric ? `metric.${slug(c.metric)}` : null;
|
|
327
|
+
if (nodeIds.has(metId)) relationships.push(rel(id, 'measured_by', metId));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const byType = {};
|
|
331
|
+
for (const n of nodes) byType[n.type] = (byType[n.type] || 0) + 1;
|
|
332
|
+
return {
|
|
333
|
+
schema: INTENT_GRAPH_SCHEMA,
|
|
334
|
+
missionId: mId,
|
|
335
|
+
nodes,
|
|
336
|
+
relationships,
|
|
337
|
+
summary: {
|
|
338
|
+
nodes: nodes.length,
|
|
339
|
+
relationships: relationships.length,
|
|
340
|
+
byType,
|
|
341
|
+
unresolved: (ast.unknowns || []).length + (ast.questions || []).length,
|
|
342
|
+
conflicts: detectConflicts(ast).filter((c) => c.status === 'unresolved').length,
|
|
343
|
+
approvalsRequired: (ast.approvals || []).length,
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// Intent IR (intent-ir-v1) , the ONE shared semantic representation the whole SkillsTech
|
|
2
|
+
// ecosystem operates on: Intent Scanner, Intent Atlas, Intent Engine, Repo Mastery, Skills Tech
|
|
3
|
+
// Talk, and OpenThunder all normalize into this, instead of each re-modeling a project.
|
|
4
|
+
//
|
|
5
|
+
// Design rule (anti-fork): Intent IR is a strict SUPERSET of the canonical Intent Graph
|
|
6
|
+
// (intent-graph-v1). Every intent-graph-v1 node type and relationship type is included and keeps
|
|
7
|
+
// its meaning, so a graph that validates today still validates as Intent IR , RepoMastery and
|
|
8
|
+
// OpenThunder keep consuming intent-graph-v1 unchanged while new products consume the richer IR.
|
|
9
|
+
// Pure ESM, zero Node deps (browser-safe).
|
|
10
|
+
|
|
11
|
+
import { NODE_TYPES as GRAPH_NODE_TYPES, RELATIONSHIP_TYPES as GRAPH_REL_TYPES } from './intent-schema.mjs';
|
|
12
|
+
|
|
13
|
+
export const IR_SCHEMA = 'intent-ir-v1';
|
|
14
|
+
export const IR_EMBEDS = 'intent-graph-v1';
|
|
15
|
+
|
|
16
|
+
// ── Node types, organized by tier (the intent-graph-v1 set is embedded via GRAPH_NODE_TYPES) ──
|
|
17
|
+
const IR_ONLY_NODE_TYPES = [
|
|
18
|
+
// Organization / product context
|
|
19
|
+
'Organization', 'Workspace', 'Project', 'Repository', 'Application', 'System', 'Domain', 'Persona',
|
|
20
|
+
// Requirements / experience
|
|
21
|
+
'UserJourney', 'ExperienceFlow', 'Screen', 'BusinessRule', 'DomainConcept', 'GlossaryTerm', 'ProhibitedBehavior',
|
|
22
|
+
// Architecture
|
|
23
|
+
'ArchitectureDecision', 'SystemBoundary', 'TrustBoundary', 'Service', 'Component', 'Module', 'Package', 'File',
|
|
24
|
+
'Symbol', 'Class', 'Interface', 'Function', 'Method', 'Endpoint', 'Message', 'Queue',
|
|
25
|
+
// Data / infra
|
|
26
|
+
'DataModel', 'Database', 'Table', 'Schema', 'Migration', 'InfrastructureResource', 'Configuration', 'Dependency',
|
|
27
|
+
// Security / risk
|
|
28
|
+
'SecurityControl', 'Threat', 'Risk', 'Finding',
|
|
29
|
+
// Verification / delivery
|
|
30
|
+
'TestScenario', 'Verification', 'Change',
|
|
31
|
+
// Knowledge / learning
|
|
32
|
+
'LearningModule', 'LearningPath', 'Drill', 'Quiz', 'Flashcard', 'Misconception', 'LearnerConceptState',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
// The full Intent IR node vocabulary = intent-graph-v1 nodes + the IR-only additions (deduped).
|
|
36
|
+
export const IR_NODE_TYPES = [...new Set([...GRAPH_NODE_TYPES, ...IR_ONLY_NODE_TYPES])];
|
|
37
|
+
|
|
38
|
+
const IR_ONLY_REL_TYPES = [
|
|
39
|
+
'produces', 'performs', 'contains', 'defines', 'belongs_to', 'exposes', 'publishes', 'consumes',
|
|
40
|
+
'stores', 'constrains', 'mitigates', 'affects', 'produces_evidence', 'contains_change', 'teaches',
|
|
41
|
+
'derived_from_artifact', 'assesses', 'prerequisite_for', 'has_state', 'appears_in', 'covers',
|
|
42
|
+
'implies_candidate_intent', 'governs', 'satisfies', 'conflicts_with',
|
|
43
|
+
];
|
|
44
|
+
export const IR_RELATIONSHIP_TYPES = [...new Set([...GRAPH_REL_TYPES, ...IR_ONLY_REL_TYPES])];
|
|
45
|
+
|
|
46
|
+
// ── Provenance , where a fact came from. Load-bearing for "never show AI-inferred as confirmed." ──
|
|
47
|
+
export const PROVENANCE = [
|
|
48
|
+
'user-authored', 'imported', 'deterministically-discovered', 'compiler-derived', 'test-derived',
|
|
49
|
+
'runtime-observed', 'ai-proposed', 'ai-generated', 'system-generated', 'human-approved', 'human-corrected',
|
|
50
|
+
];
|
|
51
|
+
// Which provenance may be treated as fact without human confirmation.
|
|
52
|
+
export const FACTUAL_PROVENANCE = new Set([
|
|
53
|
+
'user-authored', 'deterministically-discovered', 'compiler-derived', 'test-derived', 'runtime-observed', 'human-approved', 'human-corrected',
|
|
54
|
+
]);
|
|
55
|
+
export const isFactualProvenance = (p) => FACTUAL_PROVENANCE.has(p);
|
|
56
|
+
|
|
57
|
+
// ── Confidence taxonomy (richer than a bare percentage; every value is defined + explainable) ──
|
|
58
|
+
export const IR_CONFIDENCE = ['Confirmed', 'Observed', 'Derived', 'Inferred', 'Speculative', 'Conflicted'];
|
|
59
|
+
export const IR_CONFIDENCE_MEANING = {
|
|
60
|
+
Confirmed: 'Explicit approved intent exists and maps cleanly to implementation.',
|
|
61
|
+
Observed: 'Behavior is directly visible through deterministic analysis.',
|
|
62
|
+
Derived: 'Behavior follows reliably from several observed facts.',
|
|
63
|
+
Inferred: 'A plausible interpretation that requires human review.',
|
|
64
|
+
Speculative: 'Evidence is weak or incomplete.',
|
|
65
|
+
Conflicted: 'Different parts of the project imply incompatible intent.',
|
|
66
|
+
};
|
|
67
|
+
// Map intent-graph-v1's classification enum onto the IR confidence taxonomy (compat bridge).
|
|
68
|
+
export function confidenceFromClassification(classification) {
|
|
69
|
+
switch (classification) {
|
|
70
|
+
case 'verified': case 'decided': return 'Confirmed';
|
|
71
|
+
case 'observed': return 'Observed';
|
|
72
|
+
case 'inferred': return 'Derived';
|
|
73
|
+
case 'proposed': return 'Inferred';
|
|
74
|
+
case 'assumed': return 'Speculative';
|
|
75
|
+
default: return 'Inferred';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const SENSITIVITY = ['public', 'internal', 'confidential', 'restricted', 'secret'];
|
|
80
|
+
export const RETENTION = ['ephemeral', 'short', 'standard', 'long', 'permanent'];
|
|
81
|
+
export const REVIEW_STATUS = ['unreviewed', 'in-review', 'reviewed', 'needs-revalidation'];
|
|
82
|
+
export const APPROVAL_STATUS = ['unapproved', 'proposed', 'approved', 'rejected'];
|
|
83
|
+
|
|
84
|
+
// The canonical node envelope every Intent IR node carries (all optional except id + type).
|
|
85
|
+
export const NODE_FIELDS = [
|
|
86
|
+
'id', 'type', 'title', 'summary', 'description', 'status', 'owner', 'source', 'sourceLocation',
|
|
87
|
+
'sourceType', 'version', 'hash', 'createdTime', 'updatedTime', 'confidence', 'provenance',
|
|
88
|
+
'reviewStatus', 'approvalStatus', 'permissions', 'sensitivity', 'retention', 'tags', 'evidence', 'metadata',
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
const NODE_SET = new Set(IR_NODE_TYPES);
|
|
92
|
+
const REL_SET = new Set(IR_RELATIONSHIP_TYPES);
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Deterministic structural validation of an Intent IR document. Returns { valid, errors }.
|
|
96
|
+
* Honesty guard: a node whose provenance is not factual MUST carry a confidence, and MUST NOT be
|
|
97
|
+
* marked approved without review , so AI-inferred information is never presented as confirmed.
|
|
98
|
+
*/
|
|
99
|
+
export function validateIR(ir) {
|
|
100
|
+
const errors = [];
|
|
101
|
+
const err = (path, message) => errors.push({ path, message });
|
|
102
|
+
if (!ir || typeof ir !== 'object') return { valid: false, errors: [{ path: '', message: 'IR must be an object' }] };
|
|
103
|
+
if (ir.schema && ir.schema !== IR_SCHEMA) err('schema', `schema must be "${IR_SCHEMA}"`);
|
|
104
|
+
const nodes = Array.isArray(ir.nodes) ? ir.nodes : (err('nodes', 'nodes must be an array'), []);
|
|
105
|
+
const ids = new Set();
|
|
106
|
+
for (const [i, n] of nodes.entries()) {
|
|
107
|
+
if (!n || typeof n !== 'object') { err(`nodes[${i}]`, 'node must be an object'); continue; }
|
|
108
|
+
if (!n.id) err(`nodes[${i}].id`, 'node needs a stable id');
|
|
109
|
+
else if (ids.has(n.id)) err(`nodes[${i}].id`, `duplicate node id "${n.id}"`); else ids.add(n.id);
|
|
110
|
+
if (!NODE_SET.has(n.type)) err(`nodes[${i}].type`, `unknown node type "${n.type}"`);
|
|
111
|
+
if (n.provenance && !PROVENANCE.includes(n.provenance)) err(`nodes[${i}].provenance`, `unknown provenance "${n.provenance}"`);
|
|
112
|
+
if (n.confidence && !IR_CONFIDENCE.includes(n.confidence)) err(`nodes[${i}].confidence`, `unknown confidence "${n.confidence}"`);
|
|
113
|
+
if (n.sensitivity && !SENSITIVITY.includes(n.sensitivity)) err(`nodes[${i}].sensitivity`, `unknown sensitivity "${n.sensitivity}"`);
|
|
114
|
+
// Honesty guard.
|
|
115
|
+
if (n.provenance && !isFactualProvenance(n.provenance)) {
|
|
116
|
+
if (!n.confidence) err(`nodes[${i}].confidence`, `non-factual provenance "${n.provenance}" requires a confidence`);
|
|
117
|
+
if (n.approvalStatus === 'approved' && n.reviewStatus !== 'reviewed') err(`nodes[${i}].approvalStatus`, 'AI/inferred node cannot be "approved" without reviewStatus "reviewed"');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const rels = Array.isArray(ir.relationships) ? ir.relationships : (ir.relationships === undefined ? [] : (err('relationships', 'relationships must be an array'), []));
|
|
121
|
+
const isPlaceholder = (x) => String(x || '').startsWith('phase.');
|
|
122
|
+
for (const [i, r] of rels.entries()) {
|
|
123
|
+
if (!REL_SET.has(r?.type)) err(`relationships[${i}].type`, `unknown relationship type "${r?.type}"`);
|
|
124
|
+
// Both ends must resolve to a node, unless they are phase.* placeholders (blocks-phase edges).
|
|
125
|
+
if (r && !ids.has(r.from) && !isPlaceholder(r.from)) err(`relationships[${i}].from`, `dangling from "${r.from}"`);
|
|
126
|
+
if (r && !ids.has(r.to) && !isPlaceholder(r.to)) err(`relationships[${i}].to`, `dangling to "${r.to}"`);
|
|
127
|
+
}
|
|
128
|
+
return { valid: errors.length === 0, errors };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Lift a canonical intent-graph-v1 document into Intent IR , additive, lossless. Each node keeps
|
|
133
|
+
* its id/type/title, and its classification becomes an IR confidence with compiler-derived
|
|
134
|
+
* provenance (deterministic output), so existing graphs flow into the shared IR with honest
|
|
135
|
+
* provenance and never as unverified "fact."
|
|
136
|
+
*/
|
|
137
|
+
export function graphToIR(graph, { provenance = 'compiler-derived' } = {}) {
|
|
138
|
+
const nodes = (graph?.nodes || []).map((n) => ({
|
|
139
|
+
...n,
|
|
140
|
+
provenance: n.provenance || provenance,
|
|
141
|
+
confidence: n.confidence || (n.classification ? confidenceFromClassification(n.classification) : undefined),
|
|
142
|
+
}));
|
|
143
|
+
return { schema: IR_SCHEMA, embeds: IR_EMBEDS, missionId: graph?.missionId ?? null, nodes, relationships: graph?.relationships || [] };
|
|
144
|
+
}
|