@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
package/src/emit.mjs
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
// ThunderLang emit stage (deterministic, no AI). Turns an Intent AST into the artifacts the
|
|
2
|
+
// rest of the Skills Tech ecosystem consumes. Shapes match OpenThunder's confirmed consumer
|
|
3
|
+
// contract (contract-graph.json, architecture-graph.json) and the canonical .intent-proof.json.
|
|
4
|
+
// Stable IDs (slugs) let OpenThunder key Intent Drift precisely instead of fuzzy string-matching.
|
|
5
|
+
|
|
6
|
+
import { sha256, sha256hex } from './hash.mjs';
|
|
7
|
+
import { slug, KNOWN_LENSES } from './parse.mjs';
|
|
8
|
+
import { twelveFactorSummary } from './twelve-factor.mjs';
|
|
9
|
+
import { parseArchitectureRules } from './arch.mjs';
|
|
10
|
+
import { CLASSIFICATIONS } from './classification.mjs';
|
|
11
|
+
import { detectConflicts } from './conflict.mjs';
|
|
12
|
+
import { analyzeLifecycle } from './lifecycle.mjs';
|
|
13
|
+
import { analyzeDistributed } from './distributed.mjs';
|
|
14
|
+
import { analyzeDecision } from './decision.mjs';
|
|
15
|
+
import { analyzePrivacy } from './privacy.mjs';
|
|
16
|
+
import { outcomeDiagnostics } from './outcome.mjs';
|
|
17
|
+
import { styleDiagnostics } from './style.mjs';
|
|
18
|
+
import { securityDiagnostics } from './security.mjs';
|
|
19
|
+
|
|
20
|
+
// Notes metadata for proof / summaries. Notes explain meaning; they never verify.
|
|
21
|
+
export function notesSummary(ast) {
|
|
22
|
+
const notes = ast.notes || [];
|
|
23
|
+
const byLens = {};
|
|
24
|
+
for (const n of notes) byLens[n.lens] = (byLens[n.lens] || 0) + 1;
|
|
25
|
+
return {
|
|
26
|
+
included: notes.length > 0,
|
|
27
|
+
count: notes.length,
|
|
28
|
+
lenses: Object.keys(byLens).sort(),
|
|
29
|
+
byLens,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const COMPILER_VERSION = '0.1.0';
|
|
34
|
+
export const PROOF_SCHEMA_VERSION = '0.1.0';
|
|
35
|
+
// Identifies which ecosystem product emitted this proof. Consumed by SkillsTech
|
|
36
|
+
// Certified to key cert proofs to the compiler. Stable slug per the coordination bus.
|
|
37
|
+
export const SOURCE_PRODUCT = 'skillstech-compiler';
|
|
38
|
+
|
|
39
|
+
export { sha256, sha256hex }; // re-exported for the many modules that import them from emit.mjs
|
|
40
|
+
|
|
41
|
+
// ── contract-graph.json ──────────────────────────────────────────────────────
|
|
42
|
+
// missions[].guarantees / neverRules / apis / events / services , the declared contract.
|
|
43
|
+
export function buildContractGraph(ast, generatedAt) {
|
|
44
|
+
const mission = {
|
|
45
|
+
id: slug(ast.mission || 'mission'),
|
|
46
|
+
name: ast.mission,
|
|
47
|
+
title: ast.mission,
|
|
48
|
+
goal: ast.goal || null,
|
|
49
|
+
why: ast.why || null,
|
|
50
|
+
targets: ast.targets,
|
|
51
|
+
guarantees: ast.guarantees.map((g) => ({
|
|
52
|
+
id: g.id, statement: g.statement, verified: false, because: g.because, verify: g.verify,
|
|
53
|
+
})),
|
|
54
|
+
neverRules: ast.neverRules.map((n) => ({
|
|
55
|
+
id: n.id, statement: n.statement, because: n.because, verify: n.verify,
|
|
56
|
+
})),
|
|
57
|
+
apis: ast.apis.map((a) => ({ id: a.id, name: a.name, method: a.method, path: a.path })),
|
|
58
|
+
events: ast.events.map((e) => ({ id: e.id, name: e.name })),
|
|
59
|
+
services: ast.services.map((s) => ({ id: s.id, name: s.name, owner: s.owner })),
|
|
60
|
+
verify: ast.verify,
|
|
61
|
+
// Structured architecture rules OpenThunder's Architecture Lens checks against.
|
|
62
|
+
architecture: parseArchitectureRules(ast.architecture).rules,
|
|
63
|
+
};
|
|
64
|
+
return { compilerVersion: COMPILER_VERSION, generatedAt, missions: [mission] };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── architecture-graph.json ──────────────────────────────────────────────────
|
|
68
|
+
export function buildArchitectureGraph(ast, generatedAt) {
|
|
69
|
+
const dependencies = [];
|
|
70
|
+
for (const s of ast.services) {
|
|
71
|
+
for (const c of s.consumes) dependencies.push({ from: s.id, to: slug(c), kind: 'consumes' });
|
|
72
|
+
for (const p of s.publishes) dependencies.push({ from: s.id, to: slug(p), kind: 'publishes' });
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
compilerVersion: COMPILER_VERSION, generatedAt,
|
|
76
|
+
services: ast.services, apis: ast.apis, events: ast.events, databases: ast.databases,
|
|
77
|
+
dependencies,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── implementation-plan.json ─────────────────────────────────────────────────
|
|
82
|
+
// Deterministic, ordered plan derived from the contract (no AI). Foundation, not code gen.
|
|
83
|
+
export function buildImplementationPlan(ast, generatedAt) {
|
|
84
|
+
const steps = [];
|
|
85
|
+
for (const a of ast.apis) steps.push(`Add ${a.method || 'HTTP'} ${a.path || a.name} endpoint`);
|
|
86
|
+
for (const r of ast.requires) steps.push(`Validate precondition: ${r}`);
|
|
87
|
+
for (const g of ast.guarantees) steps.push(`Enforce guarantee: ${g.statement}`);
|
|
88
|
+
for (const n of ast.neverRules) steps.push(`Prevent forbidden behavior: ${n.statement}`);
|
|
89
|
+
for (const e of ast.events) steps.push(`Publish event: ${e.name}`);
|
|
90
|
+
for (const v of ast.verify) steps.push(`Add verification: ${v}`);
|
|
91
|
+
return {
|
|
92
|
+
compilerVersion: COMPILER_VERSION, generatedAt,
|
|
93
|
+
mission: ast.mission,
|
|
94
|
+
steps: steps.map((description, i) => ({ order: i + 1, description })),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── semantic diagnostics (a slice of stage 2) ────────────────────────────────
|
|
99
|
+
// Every diagnostic teaches: `message` (what), `why` (why it matters), and `fix`
|
|
100
|
+
// (how). Each fix is { label, insert?, block? }. When `insert`/`block` are set,
|
|
101
|
+
// tools (the playground) can apply the fix by inserting `insert` into `block`
|
|
102
|
+
// (`top` = a new top-level block). Advisory fixes carry only a `label`.
|
|
103
|
+
export function semanticDiagnostics(ast) {
|
|
104
|
+
const d = [...(ast.diagnostics || [])];
|
|
105
|
+
const warn = (code, message, why, fix = []) => d.push({ level: 'warning', code, message, why, fix });
|
|
106
|
+
const err = (code, message, why, fix = []) => d.push({ level: 'error', code, message, why, fix });
|
|
107
|
+
|
|
108
|
+
// The subject of an intent may be a mission or any first-class architecture
|
|
109
|
+
// kind (service, event, api, database). Only error when none is declared.
|
|
110
|
+
const hasSubject = !!ast.mission
|
|
111
|
+
|| (ast.services && ast.services.length > 0)
|
|
112
|
+
|| (ast.events && ast.events.length > 0)
|
|
113
|
+
|| (ast.apis && ast.apis.length > 0)
|
|
114
|
+
|| (ast.databases && ast.databases.length > 0)
|
|
115
|
+
|| (ast.experiences && ast.experiences.length > 0)
|
|
116
|
+
|| (ast.patterns && ast.patterns.length > 0);
|
|
117
|
+
if (!hasSubject) {
|
|
118
|
+
err('missing-subject', 'No mission, service, event, api, database, or experience declared.',
|
|
119
|
+
'Every file must declare one subject so the compiler knows what it is reasoning about.',
|
|
120
|
+
[{ label: 'Add a mission declaration', insert: 'mission MyMission', block: 'top' }]);
|
|
121
|
+
}
|
|
122
|
+
// Only missions warrant a goal; experience/pattern-only files do not.
|
|
123
|
+
if (!ast.goal && ast.mission) {
|
|
124
|
+
warn('missing-goal', 'Mission has no goal block.',
|
|
125
|
+
'The goal is the outcome the mission exists to achieve. Without it the intent is ambiguous to humans and tools.',
|
|
126
|
+
[{ label: 'Add a goal block', insert: 'goal\n Describe the outcome', block: 'top' }]);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const verifyText = [
|
|
130
|
+
...ast.verify,
|
|
131
|
+
...ast.guarantees.flatMap((g) => g.verify),
|
|
132
|
+
...ast.neverRules.flatMap((n) => n.verify),
|
|
133
|
+
].join(' ').toLowerCase();
|
|
134
|
+
|
|
135
|
+
// Guarantee that promises duplicate prevention but shows no idempotency mechanism.
|
|
136
|
+
const idempotencySignals = [
|
|
137
|
+
...ast.requires, ...ast.inputs.map((f) => `${f.name} ${f.type || ''}`),
|
|
138
|
+
].join(' ').toLowerCase();
|
|
139
|
+
for (const g of ast.guarantees) {
|
|
140
|
+
if (/duplicate/.test(g.statement.toLowerCase()) && !/idempotenc|unique|order ?reference|lookup/.test(idempotencySignals + ' ' + g.statement.toLowerCase())) {
|
|
141
|
+
warn('duplicate-without-idempotency',
|
|
142
|
+
`Guarantee "${g.statement}" declares no idempotency key, unique reference, or lookup rule to enforce it.`,
|
|
143
|
+
'Duplicate billing is a high-trust finance failure. ThunderLang expects a prevention strategy, not just a promise.',
|
|
144
|
+
[
|
|
145
|
+
{ label: 'Add idempotencyKey: IdempotencyKey to the input', insert: 'idempotencyKey: IdempotencyKey', block: 'input' },
|
|
146
|
+
{ label: 'Add a duplicate prevention test under verify', insert: 'duplicate prevention test', block: 'verify' },
|
|
147
|
+
{ label: 'Or declare a unique order reference / lookup rule' },
|
|
148
|
+
]);
|
|
149
|
+
}
|
|
150
|
+
if (g.verify.length === 0 && !verifyText.includes(slug(g.statement).replace(/-/g, ' '))) {
|
|
151
|
+
warn('guarantee-without-verification',
|
|
152
|
+
`Guarantee "${g.statement}" has no explicit verification.`,
|
|
153
|
+
'A guarantee is only trustworthy when something proves it. Unverified guarantees are exactly where Intent Drift hides.',
|
|
154
|
+
[{ label: `Attach a verify check to "${g.statement}"`, insert: `guarantee ${g.statement}\n verify ${g.statement} check`, block: 'top' }]);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
for (const n of ast.neverRules) {
|
|
158
|
+
if (n.verify.length === 0) {
|
|
159
|
+
warn('never-without-verification',
|
|
160
|
+
`Never rule "${n.statement}" has no explicit verification.`,
|
|
161
|
+
'A never rule forbids behavior, but without a check nothing enforces it in the real implementation.',
|
|
162
|
+
[{ label: 'Attach a verify check (for example a security scan)', insert: `never ${n.statement}\n verify security scan`, block: 'top' }]);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
for (const iv of ast.invariants || []) {
|
|
166
|
+
if ((iv.verify || []).length === 0) {
|
|
167
|
+
warn('invariant-without-verification',
|
|
168
|
+
`Invariant "${iv.name}" (${iv.severity}) has no explicit verification.`,
|
|
169
|
+
'A global invariant is a system-wide law , unverified, nothing stops a locally-valid change from violating it across services.',
|
|
170
|
+
[{ label: `Attach a verify check to invariant ${iv.name}`, insert: `invariant ${iv.name}\n verify ${slug(iv.name).replace(/-/g, ' ')} test`, block: 'top' }]);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Secret field with no never-log/never-return protection.
|
|
175
|
+
const neverText = ast.neverRules.map((n) => n.statement.toLowerCase()).join(' ');
|
|
176
|
+
const secretFields = [...ast.inputs, ...ast.outputs].filter(
|
|
177
|
+
(f) => /secret|token|password|jwt/i.test(`${f.name} ${f.type || ''} ${(f.modifiers || []).join(' ')}`),
|
|
178
|
+
);
|
|
179
|
+
for (const f of secretFields) {
|
|
180
|
+
if (!(neverText.includes('log') && (neverText.includes(f.name.toLowerCase()) || /secret|token|password/.test(neverText)))) {
|
|
181
|
+
warn('secret-without-never-log',
|
|
182
|
+
`Sensitive field "${f.name}" has no matching never-log / never-return rule.`,
|
|
183
|
+
'Secrets leak through logs, traces, and responses. ThunderLang expects an explicit rule that forbids it, near the field.',
|
|
184
|
+
[
|
|
185
|
+
{ label: `Add never log ${f.name}`, insert: `log ${f.name}`, block: 'never' },
|
|
186
|
+
{ label: `Add never return ${f.name} to client`, insert: `return ${f.name} to client`, block: 'never' },
|
|
187
|
+
]);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── IntentLens note checks (understanding, never verification) ──
|
|
192
|
+
for (const note of ast.notes || []) {
|
|
193
|
+
if (!KNOWN_LENSES.includes(note.lens)) {
|
|
194
|
+
warn('INTENT_NOTE_UNKNOWN_LENS',
|
|
195
|
+
`Unknown note lens "${note.lens}".`,
|
|
196
|
+
`Notes target a known reader lens so tools can group and prioritize them. Known lenses: ${KNOWN_LENSES.join(', ')}.`,
|
|
197
|
+
[{ label: 'Use a known lens (for example pm, beginner, qa, security)' }]);
|
|
198
|
+
}
|
|
199
|
+
if (!note.text || !note.text.trim()) {
|
|
200
|
+
warn('INTENT_NOTE_EMPTY',
|
|
201
|
+
`Note (${note.lens}) has no text.`,
|
|
202
|
+
'An empty note adds noise without meaning. Explain the meaning, risk, usage, or verification for that reader.',
|
|
203
|
+
[{ label: 'Add a sentence explaining the meaning for this reader' }]);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ── architecture rules: flag lines the rule parser cannot understand ──
|
|
208
|
+
for (const u of parseArchitectureRules(ast.architecture).unparsed) {
|
|
209
|
+
warn('INTENT-ARCH-001', `Architecture rule not understood: "${u}".`,
|
|
210
|
+
'Architecture rules must be dependency constraints so tools can enforce them. Supported forms: "A must not depend on B", "A may depend on B", "A may implement B [ports]".',
|
|
211
|
+
[{ label: 'Rephrase as "<layer> must not depend on <layer>"' }]);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ── AI implementation declaration (intent-ai-v1) ──
|
|
215
|
+
// An intentionally deferred AI implementation is NOT accidentally-missing code.
|
|
216
|
+
const impl = ast.implementation;
|
|
217
|
+
if (impl) {
|
|
218
|
+
const SCOPES = ['expression', 'function_body', 'method', 'test', 'adapter'];
|
|
219
|
+
const RISKS = ['low', 'medium', 'high', 'critical'];
|
|
220
|
+
const EDITS = ['managed', 'collaborative', 'adopted'];
|
|
221
|
+
d.push({
|
|
222
|
+
level: 'info', code: 'INTENT-AI-001',
|
|
223
|
+
message: `Mission declares an AI implementation "${impl.id || 'implementation'}" (risk ${impl.risk || 'low'}, ${impl.pending ? 'pending' : 'declared'}).`,
|
|
224
|
+
why: 'This is an intentionally deferred, AI-assisted implementation, not missing code. OpenThunder verifies it and gates production; the compiler tracks its state.',
|
|
225
|
+
fix: [],
|
|
226
|
+
});
|
|
227
|
+
if (impl.scope && !SCOPES.includes(impl.scope)) warn('INTENT-AI-010', `Unsupported implementation scope "${impl.scope}".`, `MVP scopes: ${SCOPES.join(', ')}.`);
|
|
228
|
+
if (impl.risk && !RISKS.includes(impl.risk)) warn('INTENT-AI-011', `Unknown risk level "${impl.risk}".`, `Use one of: ${RISKS.join(', ')}.`);
|
|
229
|
+
if (impl.editing && !EDITS.includes(impl.editing)) warn('INTENT-AI-012', `Unknown editing policy "${impl.editing}".`, `Use one of: ${EDITS.join(', ')}.`);
|
|
230
|
+
if (['high', 'critical'].includes(impl.risk) && (!impl.approval || impl.approval === 'none')) {
|
|
231
|
+
warn('INTENT-AI-013', `High-risk implementation "${impl.id || ''}" should require approval.`,
|
|
232
|
+
'High and critical risk must require human approval even after automated verification passes.',
|
|
233
|
+
[{ label: 'Add approval: required' }]);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── errors block: PascalCase failure-mode names ──
|
|
238
|
+
for (const e of ast.errors || []) {
|
|
239
|
+
if (!/^[A-Z][A-Za-z0-9]*$/.test(e.name)) {
|
|
240
|
+
warn('error-name-not-pascalcase',
|
|
241
|
+
`Failure mode "${e.name}" is not PascalCase.`,
|
|
242
|
+
'Failure modes become status/result union members and per-error tests, so they read best as PascalCase names (for example OrderNotFound).',
|
|
243
|
+
[{ label: `Rename to PascalCase (for example ${e.name.replace(/[^A-Za-z0-9]+/g, ' ').replace(/(?:^|\s)(\w)/g, (_, c) => c.toUpperCase()).replace(/\s+/g, '') || 'FailureName'})` }]);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ── Product / intent-graph diagnostics (intent-graph-v1) ──
|
|
248
|
+
// Role-aware. Kept at warning/info so `intent check` stays valid; `severity` +
|
|
249
|
+
// `blocks` drive phase gates (a valid spec can still be not-ready-to-proceed).
|
|
250
|
+
for (const m of ast.metrics || []) {
|
|
251
|
+
if (!m.window) d.push({
|
|
252
|
+
level: 'warning', code: 'IL-PM-001', severity: 'blocker', blocks: ['release'],
|
|
253
|
+
message: `Metric "${m.name}" has no measurement window.`,
|
|
254
|
+
why: 'A success metric without a measurement period cannot be evaluated after release.',
|
|
255
|
+
roles: { product: `The success metric "${m.name}" has no measurement period. Add when the team should evaluate the result.` },
|
|
256
|
+
fix: [{ label: 'Add a window (for example: window 30 days after release)' }],
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
for (const e of ast.evidence || []) {
|
|
260
|
+
if (!e.classification) d.push({
|
|
261
|
+
level: 'info', code: 'IL-EV-001', message: `Evidence "${e.name}" has no classification.`,
|
|
262
|
+
why: 'Classify evidence (observed / inferred / proposed / assumed) so AI-generated content never silently becomes fact.',
|
|
263
|
+
fix: [{ label: 'Add: classification observed' }],
|
|
264
|
+
});
|
|
265
|
+
else if (!CLASSIFICATIONS.includes(e.classification)) d.push({
|
|
266
|
+
level: 'warning', code: 'IL-EV-002', message: `Evidence "${e.name}" has an unknown classification "${e.classification}".`,
|
|
267
|
+
why: `Use one of: ${CLASSIFICATIONS.join(', ')}.`,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
for (const u of ast.unknowns || []) {
|
|
271
|
+
if (u.resolveBefore) d.push({
|
|
272
|
+
level: 'warning', code: 'IL-GRAPH-010', severity: 'blocker', blocks: [String(u.resolveBefore).toLowerCase()],
|
|
273
|
+
message: `Unknown "${u.name}" must be resolved before ${u.resolveBefore}.`,
|
|
274
|
+
why: 'An unresolved unknown that blocks a declared phase is a blocker for that phase, not an ordinary warning.',
|
|
275
|
+
roles: { product: `"${u.name}" is unresolved and blocks ${u.resolveBefore}.`, engineer: `Unknown "${u.name}" gates ${u.resolveBefore}.` },
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
for (const q of ast.questions || []) {
|
|
279
|
+
if (q.blocks) d.push({
|
|
280
|
+
level: 'warning', code: 'IL-GRAPH-011', severity: 'blocker', blocks: [String(q.blocks).toLowerCase()],
|
|
281
|
+
message: `Open question "${q.name}" blocks ${q.blocks}.`,
|
|
282
|
+
why: 'An open question that blocks a phase must be answered before that phase proceeds.',
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
for (const o of ast.outcomes || []) {
|
|
286
|
+
const hasMetric = (ast.metrics || []).length > 0;
|
|
287
|
+
if (!hasMetric) d.push({
|
|
288
|
+
level: 'warning', code: 'IL-PM-003', message: `Outcome "${o.name}" has no metric.`,
|
|
289
|
+
why: 'An outcome without a metric cannot be measured, so success cannot be proven.',
|
|
290
|
+
roles: { product: `Outcome "${o.name}" needs a metric to know whether it worked.` },
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ── Experience Contract diagnostics (intent-graph-v1) ──
|
|
295
|
+
for (const exp of ast.experiences || []) {
|
|
296
|
+
for (const st of exp.states || []) {
|
|
297
|
+
const isFailure = /(fail|error|denied|timeout|offline|reject)/i.test(st.name || '');
|
|
298
|
+
if (isFailure && !st.hasRecovery) d.push({
|
|
299
|
+
level: 'warning', code: 'IL-EXP-004', severity: 'blocker', blocks: ['experience-approval', 'release'],
|
|
300
|
+
message: `Experience "${exp.name}" state "${st.name}" is a failure state with no recovery path.`,
|
|
301
|
+
why: 'A failure state that does not explain how the user recovers strands them.',
|
|
302
|
+
roles: {
|
|
303
|
+
ux: `The ${st.name} state defines a failure, but it does not explain how the user can recover.`,
|
|
304
|
+
engineer: `Experience state \`${st.name}\` has no transition to a recoverable state.`,
|
|
305
|
+
},
|
|
306
|
+
fix: [{ label: 'Add a recovery affordance (for example: offer Retry)' }],
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
if ((exp.states || []).length === 0) d.push({
|
|
310
|
+
level: 'info', code: 'IL-EXP-001', message: `Experience "${exp.name}" declares no states.`,
|
|
311
|
+
why: 'Experiences should declare their states (empty, loading, success, failure, recovery) so completeness can be checked.',
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ── Constraint conflicts (Gap 1) , the reconciliation layer ──
|
|
316
|
+
for (const c of detectConflicts(ast)) {
|
|
317
|
+
if (c.type === 'declared' && c.status === 'resolved') continue; // a recorded human choice clears it
|
|
318
|
+
if (c.type === 'declared') d.push({
|
|
319
|
+
level: 'warning', code: 'IL-CONFLICT-001', severity: 'blocker', blocks: c.before ? [String(c.before).toLowerCase()] : ['implementation'],
|
|
320
|
+
owners: c.resolveBy || [],
|
|
321
|
+
message: `Unresolved conflict "${c.name}" between ${(c.between || []).join(' and ')}.`,
|
|
322
|
+
why: 'Roles disagree about what must be true. The conflict must be resolved before the blocked phase, or implementation will satisfy one constraint by violating another.',
|
|
323
|
+
roles: {
|
|
324
|
+
product: `"${c.name}": ${(c.between || []).join(' vs ')} conflict. Resolve with ${(c.resolveBy || ['the owners']).join(', ')} before ${c.before || 'implementation'}.`,
|
|
325
|
+
},
|
|
326
|
+
fix: (c.options || []).map((o) => ({ label: `Option: ${o}` })),
|
|
327
|
+
});
|
|
328
|
+
else if (c.type === 'scope-contradiction') d.push({
|
|
329
|
+
level: 'warning', code: 'IL-CONFLICT-010', severity: 'blocker', blocks: ['implementation'],
|
|
330
|
+
message: `Scope both includes and excludes "${c.name}".`,
|
|
331
|
+
why: 'A single item cannot be in and out of scope. One of the two declarations is wrong.',
|
|
332
|
+
});
|
|
333
|
+
else if (c.type === 'negation') d.push({
|
|
334
|
+
level: 'warning', code: 'IL-CONFLICT-012', severity: 'blocker', blocks: ['implementation'],
|
|
335
|
+
message: `Contradictory constraints: ${c.name}.`,
|
|
336
|
+
why: `${(c.between || []).join(' and ')} directly contradict each other.`,
|
|
337
|
+
});
|
|
338
|
+
else if (c.type === 'redundant') d.push({
|
|
339
|
+
level: 'info', code: 'IL-CONFLICT-011', message: `Redundant constraint "${c.name}" declared by ${(c.between || []).join(', ')}.`,
|
|
340
|
+
why: 'The same constraint is contributed by more than one role. Harmless, but consolidate for clarity.',
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ── Temporal + lifecycle (Gap 2) , static state-machine analysis ──
|
|
345
|
+
for (const lc of ast.lifecycles || []) {
|
|
346
|
+
for (const f of analyzeLifecycle(lc).findings) {
|
|
347
|
+
const isError = f.code === 'IL-LIFE-001'; // undefined-state reference is a real bug
|
|
348
|
+
d.push({
|
|
349
|
+
level: isError ? 'error' : 'warning', code: f.code,
|
|
350
|
+
message: `Lifecycle "${lc.name}": ${f.message}`,
|
|
351
|
+
why: 'The declared state machine is not well-formed. OpenThunder verifies the implementation against this same model.',
|
|
352
|
+
roles: { engineer: `Lifecycle \`${lc.name}\`: ${f.message}` },
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
for (const e of ast.eventually || []) {
|
|
357
|
+
if (!e.within) d.push({
|
|
358
|
+
level: 'warning', code: 'IL-TEMP-001', severity: 'blocker', blocks: ['verification'],
|
|
359
|
+
message: `Eventually "${e.statement}" has no time bound.`,
|
|
360
|
+
why: 'An eventual guarantee with no "within" cannot be verified or alerted on; it may never complete.',
|
|
361
|
+
fix: [{ label: 'Add a bound (for example: within 2 minutes)' }],
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ── Distributed + failure semantics (Gap 3) , static failure-policy checks ──
|
|
366
|
+
const DIST_WHY = {
|
|
367
|
+
'IL-DIST-001': 'Retrying a non-idempotent command duplicates work (double charges, duplicate records).',
|
|
368
|
+
'IL-DIST-002': 'A retried or remote command with no timeout can hang forever and exhaust resources.',
|
|
369
|
+
'IL-DIST-003': 'At-least-once delivery WILL redeliver; without duplicate handling the effect happens twice.',
|
|
370
|
+
'IL-DIST-004': 'A permanent failure with no compensation leaves partial state behind.',
|
|
371
|
+
'IL-DIST-005': 'A handler references an event that is not declared (likely a typo).',
|
|
372
|
+
};
|
|
373
|
+
// ── Decisions / rules (Gap 4) , conflict + coverage on the declared decision ──
|
|
374
|
+
for (const dec of ast.decisions || []) {
|
|
375
|
+
for (const f of analyzeDecision(dec)) {
|
|
376
|
+
const isBlocker = f.code === 'IL-DEC-001' || f.code === 'IL-DEC-002';
|
|
377
|
+
d.push({
|
|
378
|
+
level: 'warning', code: f.code,
|
|
379
|
+
severity: isBlocker ? 'blocker' : undefined, blocks: isBlocker ? ['implementation'] : undefined,
|
|
380
|
+
message: f.message,
|
|
381
|
+
why: f.code === 'IL-DEC-001' ? 'Without a default, the decision is undefined when no rule matches.'
|
|
382
|
+
: f.code === 'IL-DEC-002' ? 'Two rules fire on the same condition with different results, so the outcome is ambiguous.'
|
|
383
|
+
: f.code === 'IL-DEC-003' ? 'Two rules are identical; one is dead.'
|
|
384
|
+
: 'A decision with no rules cannot decide anything.',
|
|
385
|
+
roles: { product: f.message, engineer: f.message },
|
|
386
|
+
fix: f.code === 'IL-DEC-001' ? [{ label: 'Add a default (for example: default\\n return NotEligible)' }] : [],
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
for (const f of analyzeDistributed(ast)) {
|
|
392
|
+
const isError = f.code === 'IL-DIST-005';
|
|
393
|
+
d.push({
|
|
394
|
+
level: isError ? 'error' : 'warning', code: f.code,
|
|
395
|
+
severity: isError ? undefined : 'blocker', blocks: isError ? undefined : ['implementation'],
|
|
396
|
+
message: f.message, why: DIST_WHY[f.code],
|
|
397
|
+
roles: { engineer: f.message, product: f.message },
|
|
398
|
+
fix: f.code === 'IL-DIST-001' ? [{ label: 'Add: idempotency_key <field>' }] : f.code === 'IL-DIST-003' ? [{ label: `Add: on duplicate ${f.target} ... ignore when ...` }] : [],
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ── Data purpose + privacy (Gap 6) , purpose limitation on declared data elements ──
|
|
403
|
+
const PRIVACY_WHY = {
|
|
404
|
+
'IL-DATA-001': 'Personal data with no stated purpose cannot be limited to that purpose; purpose limitation is the core privacy duty.',
|
|
405
|
+
'IL-DATA-002': 'Personal data with no retention rule is kept indefinitely, which is a storage-limitation violation.',
|
|
406
|
+
'IL-DATA-003': 'Holding personal data requires a lawful basis (consent, contract, legitimate interest, ...).',
|
|
407
|
+
'IL-DATA-004': 'An unknown classification cannot be governed; use one of public/internal/confidential/pii/sensitive.',
|
|
408
|
+
'IL-DATA-005': 'The lawful basis is not one of the recognized GDPR Art. 6 bases.',
|
|
409
|
+
'IL-DATA-006': 'Sensitive data returned to a caller with no "never expose" guard risks over-exposure.',
|
|
410
|
+
};
|
|
411
|
+
for (const f of analyzePrivacy(ast)) {
|
|
412
|
+
const isBlocker = f.severity === 'blocker';
|
|
413
|
+
d.push({
|
|
414
|
+
level: 'warning', code: f.code,
|
|
415
|
+
severity: isBlocker ? 'blocker' : undefined, blocks: isBlocker ? ['release'] : undefined,
|
|
416
|
+
message: f.message, why: PRIVACY_WHY[f.code],
|
|
417
|
+
roles: { product: f.message, engineer: f.message, legal: f.message },
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// ── Outcome contracts , the commitment must be evaluable ──
|
|
422
|
+
const OUTCOME_WHY = {
|
|
423
|
+
'IL-OC-001': 'A contract with no target is a wish; give a target so the outcome can be judged met or missed.',
|
|
424
|
+
'IL-OC-002': 'Without a metric there is nothing to measure the outcome against.',
|
|
425
|
+
'IL-OC-003': 'A commitment with no measurement window cannot be evaluated after release.',
|
|
426
|
+
'IL-OC-004': 'The target is no better than the baseline, so meeting it proves no improvement.',
|
|
427
|
+
};
|
|
428
|
+
for (const f of outcomeDiagnostics(ast)) {
|
|
429
|
+
const isBlocker = f.severity === 'blocker';
|
|
430
|
+
d.push({
|
|
431
|
+
level: 'warning', code: f.code,
|
|
432
|
+
severity: isBlocker ? 'blocker' : undefined, blocks: isBlocker ? ['release'] : undefined,
|
|
433
|
+
message: f.message, why: OUTCOME_WHY[f.code],
|
|
434
|
+
roles: { product: f.message },
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ── Style intent , canonical token space + accessibility as a proposed claim ──
|
|
439
|
+
const STYLE_WHY = {
|
|
440
|
+
'IL-STYLE-001': 'Tokens address a canonical, lockable namespace so themes stay portable; an off-namespace path forks the design system.',
|
|
441
|
+
'IL-STYLE-002': 'The accessibility target must be a recognized standard so tools and OT agree on what is being claimed.',
|
|
442
|
+
'IL-STYLE-003': 'State the accessibility goal as a proposed claim; leaving it implicit lets "accessible" go unverified.',
|
|
443
|
+
'IL-STYLE-004': 'The mode token drives light/dark rendering; an unrecognized value has no defined behavior.',
|
|
444
|
+
'IL-STYLE-005': 'A style intent that applies to an undeclared experience will not be attached to anything in this file.',
|
|
445
|
+
};
|
|
446
|
+
for (const f of styleDiagnostics(ast)) {
|
|
447
|
+
const isBlocker = f.severity === 'blocker';
|
|
448
|
+
d.push({
|
|
449
|
+
level: f.severity === 'info' ? 'info' : 'warning', code: f.ruleId,
|
|
450
|
+
severity: isBlocker ? 'blocker' : undefined, blocks: isBlocker ? f.blocks : undefined,
|
|
451
|
+
message: f.message, why: STYLE_WHY[f.ruleId], line: f.line ?? undefined,
|
|
452
|
+
roles: { designer: f.message, product: f.message },
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ── Security + type , secrets on the bus, sensitive unauthenticated output, mistyped fields ──
|
|
457
|
+
const SECURITY_WHY = {
|
|
458
|
+
'IL-SEC-001': 'Event payloads fan out to every consumer and are often logged/persisted; a secret in one is a broad leak.',
|
|
459
|
+
'IL-SEC-002': 'Returning a secret with no auth requirement hands it to any caller; gate it behind an auth requirement.',
|
|
460
|
+
'IL-TYPE-001': 'An unrecognized lowercase type is almost always a typo, so the field silently loses its intended shape.',
|
|
461
|
+
};
|
|
462
|
+
for (const f of securityDiagnostics(ast)) {
|
|
463
|
+
const isBlocker = f.severity === 'blocker';
|
|
464
|
+
d.push({
|
|
465
|
+
level: f.severity === 'info' ? 'info' : 'warning', code: f.code,
|
|
466
|
+
severity: isBlocker ? 'blocker' : undefined, blocks: isBlocker ? ['release'] : undefined,
|
|
467
|
+
message: f.message, why: SECURITY_WHY[f.code], line: f.line ?? undefined,
|
|
468
|
+
roles: { engineer: f.message, security: f.message },
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return d;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ── .intent-proof.json ───────────────────────────────────────────────────────
|
|
476
|
+
export function buildProof(ast, { sourceFile, sourceHash, targetsRequested, targetsGenerated, diagnostics, generatedAt, origin = 'authored' }) {
|
|
477
|
+
const passedSemantic = !diagnostics.some((x) => x.level === 'error');
|
|
478
|
+
const verifiedText = ast.verify.join(' ').toLowerCase();
|
|
479
|
+
return {
|
|
480
|
+
schemaVersion: PROOF_SCHEMA_VERSION,
|
|
481
|
+
sourceProduct: SOURCE_PRODUCT,
|
|
482
|
+
// Provenance envelope: whether the intent was human-authored or recovered from code (lifted).
|
|
483
|
+
// A recovered artifact must never read as authored truth , OpenThunder stamps this so the
|
|
484
|
+
// proof carries "recovered" through the compile. Additive; defaults to authored.
|
|
485
|
+
origin,
|
|
486
|
+
missionName: ast.mission,
|
|
487
|
+
sourceFile,
|
|
488
|
+
sourceHash,
|
|
489
|
+
compilerVersion: COMPILER_VERSION,
|
|
490
|
+
generatedAt,
|
|
491
|
+
targetsRequested,
|
|
492
|
+
targetsGenerated,
|
|
493
|
+
guarantees: ast.guarantees.map((g) => ({
|
|
494
|
+
id: g.id, text: g.statement,
|
|
495
|
+
status: g.verify.length > 0 || verifiedText ? 'planned' : 'needs_verification',
|
|
496
|
+
evidence: g.verify,
|
|
497
|
+
})),
|
|
498
|
+
neverRules: ast.neverRules.map((n) => ({
|
|
499
|
+
id: n.id, text: n.statement,
|
|
500
|
+
status: n.verify.length > 0 ? 'planned' : 'needs_verification',
|
|
501
|
+
evidence: n.verify,
|
|
502
|
+
})),
|
|
503
|
+
errors: (ast.errors || []).map((e) => ({ name: e.name })),
|
|
504
|
+
examples: (ast.examples || []).map((ex) => ({ given: ex.given, expect: ex.expect })),
|
|
505
|
+
// Ownership Graph seam: the skills this intent requires (shared `skill:<slug>` ids) and the
|
|
506
|
+
// understanding a human must be able to demonstrate to own it (feeds Skills Tech Talk defense).
|
|
507
|
+
skillsRequired: (ast.skills || []).map((s) => s.id).filter(Boolean),
|
|
508
|
+
demonstrates: (ast.demonstrates || []).map((d) => d.statement),
|
|
509
|
+
// 12-Factor Agents conformance summary , makes "12-factor compliant" a claim the proof carries.
|
|
510
|
+
twelveFactor: twelveFactorSummary(ast),
|
|
511
|
+
verification: { syntaxPassed: true, semanticPassed: passedSemantic, targetsGenerated: targetsGenerated.length > 0 },
|
|
512
|
+
// Notes are understanding metadata only; they never mark a guarantee verified.
|
|
513
|
+
notes: notesSummary(ast),
|
|
514
|
+
diagnostics,
|
|
515
|
+
ai: { used: false },
|
|
516
|
+
humanApproval: { required: true, approved: false },
|
|
517
|
+
proofStatus: 'draft',
|
|
518
|
+
};
|
|
519
|
+
}
|