@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/core.mjs ADDED
@@ -0,0 +1,92 @@
1
+ // @skillstech/thunderlang/core , the BROWSER-SAFE barrel. Every symbol here is pure ESM with
2
+ // ZERO Node.js dependencies (no fs/path/url), so it bundles cleanly into a browser app
3
+ // (SkillsTech Studio's Vite build, Repo Mastery's projections). It is a strict superset of
4
+ // the original AI-core helpers, so existing `@skillstech/thunderlang/core` imports keep
5
+ // working, plus the canonical schema/classification helpers and the pure Intent Runtime.
6
+
7
+ // AI-implementation helpers (unchanged , keeps existing /core consumers working).
8
+ export * from './ai-core.mjs';
9
+
10
+ // Classification model (observed/inferred/proposed/... + isFactual). Node-free.
11
+ export { CLASSIFICATIONS, CONFIDENCE, UNSETTLED, classify, isFactual, BLOCKABLE_PHASES } from './classification.mjs';
12
+
13
+ // Canonical Intent Graph schema constants + JSON Schema + diagnostic catalog. Node-free.
14
+ export {
15
+ SCHEMA_VERSION, NODE_TYPES, RELATIONSHIP_TYPES, NODE_STATUSES,
16
+ intentGraphJsonSchema, DIAGNOSTIC_RULES, CORE_DIAGNOSTICS, ALL_DIAGNOSTICS,
17
+ RULE_PHASES, RULE_OWNERS, RULE_NAMESPACES, VERIFICATION_RULES, ruleNamespace,
18
+ } from './intent-schema.mjs';
19
+
20
+ // Canonical proof envelope schema (intent-proof-v1) , browser-safe so a signing service or
21
+ // a cert renderer can validate the shared proof shape without a Node build.
22
+ export {
23
+ PROOF_SCHEMA, CLAIM_STATUSES, PROOF_STATUSES, intentProofJsonSchema, validateProof,
24
+ } from './proof-schema.mjs';
25
+
26
+ // Human <-> Structured <-> ThunderLang sync (browser-safe) , Studio's proposeIntent/parseToStructured.
27
+ export { parseToStructured, proposeIntent, SYNC_SCHEMA } from './sync.mjs';
28
+ // Structural source editing (browser-safe) , comment-preserving field edits for Studio.
29
+ export { applyEdits, PATCH_SCHEMA } from './patch.mjs';
30
+
31
+ // Intent IR (intent-ir-v1) , the shared ecosystem semantic representation (browser-safe).
32
+ export {
33
+ IR_SCHEMA, IR_EMBEDS, IR_NODE_TYPES, IR_RELATIONSHIP_TYPES, PROVENANCE, isFactualProvenance,
34
+ IR_CONFIDENCE, IR_CONFIDENCE_MEANING, confidenceFromClassification, SENSITIVITY, RETENTION,
35
+ NODE_FIELDS, validateIR, graphToIR,
36
+ } from './intent-ir.mjs';
37
+
38
+ // The Intent Runtime , executable intent (evaluate decisions, simulate lifecycles). Pure.
39
+ export { evaluateDecision, simulateLifecycle, checkDecisionCases, RUNTIME_SCHEMA } from './runtime.mjs';
40
+ // Runtime enforcement (browser-safe) , a guard that blocks forbidden actions + redacts secrets.
41
+ export { buildGuard, compileGuard, guardSummary, GUARD_SCHEMA } from './guard.mjs';
42
+ // Prompt -> intent (browser-safe) , scaffold a rigorous draft + gap checklist from a brief.
43
+ export { draftIntent, DRAFT_SCHEMA } from './draft.mjs';
44
+ export { compileExpr, evalExpr, ExprError } from './expr.mjs';
45
+
46
+ // Style intent , canonical token address space + accessibility vocabulary (browser-safe).
47
+ // Studio and renderers bind to these instead of forking their own design-token trees.
48
+ export {
49
+ analyzeStyle, styleDiagnostics, toDesignTokens, toCss, STYLE_SCHEMA, DESIGN_TOKENS_SCHEMA,
50
+ TOKEN_PATHS, BRAND_PATHS, STYLE_ADDRESS_SPACE, ACCESSIBILITY_TARGETS, MODE_VALUES,
51
+ ACCESSIBILITY_CLASSIFICATION,
52
+ } from './style.mjs';
53
+
54
+ // ── The compiler proper (now universal: the pure SHA-256 removed the last node:crypto dep) ──
55
+ // One source of truth for parsing, graph-building, analysis, and navigation, so OpenThunder
56
+ // (Node), the CLI (Node), SkillsTech Studio (browser), Repo Mastery (web), and SkillsTech
57
+ // Mobile (React Native) all run the SAME code, never a fork.
58
+
59
+ // The canonical SHA-256 the ecosystem keys on (intentProofHash, join keys). Node-free.
60
+ export { sha256, sha256hex } from './hash.mjs';
61
+ // Parser: `.intent` source -> Intent AST (+ slug for stable ids, KNOWN_LENSES).
62
+ export { parseIntent, slug, KNOWN_LENSES } from './parse.mjs';
63
+ // IntentLift: code -> inferred candidate intent (in-process, keyless; OT seeds + orchestrates this).
64
+ export { liftSource, liftAll, liftRepo, languageForFile, inferIntent, renderLiftedIntent, SUPPORTED_LANGUAGES, SEED_SCHEMA, normalizeSeeds } from './lift.mjs';
65
+ // Intent Graph builder (intent-graph-v1): AST -> canonical graph.
66
+ export { buildIntentGraph, INTENT_GRAPH_SCHEMA } from './intent-graph.mjs';
67
+ // In-memory compile: AST/source -> every artifact (docs, graphs, plan, proof), no filesystem.
68
+ export { compileSource, renderMarkdown, renderLensDoc, renderMermaid, renderTestplan } from './compile.mjs';
69
+ // Semantic diagnostics (the Fable/scan spine input) , now Node-free.
70
+ export { semanticDiagnostics, buildContractGraph, buildArchitectureGraph, buildImplementationPlan, buildProof } from './emit.mjs';
71
+ // Intent Scanner + Fable: project -> Intent IR + explainable findings + risk themes.
72
+ export { scanIntent, scanProject, SCAN_SCHEMA } from './scan.mjs';
73
+ export { toFinding, universalPack, RISK_CATEGORIES, FABLE_SCHEMA } from './fable.mjs';
74
+ // Focused scan queries (risks/gaps/unverified/coverage/unknowns/contradictions).
75
+ export { VIEW_SCHEMA, VIEWS, coverageView, unverifiedView, gapsView, risksView, unknownsView, contradictionsView } from './scan-queries.mjs';
76
+ // Intent Atlas (whole-system map) + navigation primitives.
77
+ export { buildAtlas, atlasNode, expandNode, searchAtlas, ATLAS_SCHEMA } from './intent-atlas.mjs';
78
+ // Intent Lens: Intent Scope + Focus Graph + Intent Brief (a focused subgraph of the Atlas).
79
+ export { FOCUS_SCHEMA, SCOPE_TYPES, FOCUS_REASONS, makeScope, buildFocusGraph, intentBrief } from './focus.mjs';
80
+ // Comprehension Contract: the C0..C7 understanding level (browser-safe; every product reads it).
81
+ export { COMPREHENSION_SCHEMA, LEVELS as COMPREHENSION_LEVELS, comprehensionLevel, comprehensionReport } from './comprehension.mjs';
82
+ // Code generation: deterministic scaffolds from intent (browser-safe, so the playground renders it).
83
+ export { CODEGEN_SCHEMA, GENERATORS, toTypeScript, toCSharp, toJava } from './codegen.mjs';
84
+ // Change Lens: what a branch/PR changed by meaning (pure; the CLI supplies the git-diffed graphs).
85
+ export { CHANGES_SCHEMA, changeReport } from './changes.mjs';
86
+ export { subjectName, intentRefId, skillRefId } from './parse.mjs';
87
+ // 12-Factor Agents conformance lens (twelve-factor-v1): score an intent against the 13 principles.
88
+ export { TWELVE_FACTOR_SCHEMA, twelveFactorReport, twelveFactorSummary } from './twelve-factor.mjs';
89
+ // Semantic diff + 3-way merge (Change Lens: diff by meaning).
90
+ export { diffGraphs, mergeGraphs } from './semantic-diff.mjs';
91
+ // Graph -> source (native round-trip) so a browser editor can regenerate .intent.
92
+ export { graphToSource, GRAPH_SOURCE_SCHEMA } from './graph-source.mjs';
@@ -0,0 +1,137 @@
1
+ // Data-shape export: turn a mission's typed input/output fields into a JSON Schema, and the
2
+ // mission itself into an OpenAPI operation. This makes ThunderLang's typed data directly
3
+ // consumable by API tooling (validators, codegen, mock servers) , the data-shape sibling of
4
+ // the DMN/BPMN decision/lifecycle exporters. Deterministic and pure.
5
+
6
+ // Known semantic types (with formats) and primitives, keyed lowercase. Module-level so the
7
+ // security/type semantic pass (`isRecognizedType`) and the JSON-Schema mapping share ONE
8
+ // source of truth , no drift between "what compiles" and "what the schema knows."
9
+ const SEMANTIC = {
10
+ email: { type: 'string', format: 'email' },
11
+ url: { type: 'string', format: 'uri' },
12
+ uri: { type: 'string', format: 'uri' },
13
+ date: { type: 'string', format: 'date' },
14
+ datetime: { type: 'string', format: 'date-time' },
15
+ timestamp: { type: 'string', format: 'date-time' },
16
+ duration: { type: 'string', format: 'duration' },
17
+ uuid: { type: 'string', format: 'uuid' },
18
+ money: { type: 'number' },
19
+ currency: { type: 'string' },
20
+ percentage: { type: 'number', minimum: 0, maximum: 100 },
21
+ secret: { type: 'string', writeOnly: true },
22
+ token: { type: 'string' },
23
+ jwt: { type: 'string' },
24
+ password: { type: 'string', writeOnly: true },
25
+ version: { type: 'string' },
26
+ environmentname: { type: 'string' },
27
+ idempotencykey: { type: 'string' },
28
+ };
29
+ const PRIMITIVE = {
30
+ string: { type: 'string' }, text: { type: 'string' },
31
+ int: { type: 'integer' }, integer: { type: 'integer' }, number: { type: 'number' }, float: { type: 'number' }, decimal: { type: 'number' },
32
+ bool: { type: 'boolean' }, boolean: { type: 'boolean' },
33
+ object: { type: 'object' }, any: {},
34
+ };
35
+
36
+ export const SEMANTIC_TYPES = Object.keys(SEMANTIC);
37
+ export const PRIMITIVE_TYPES = Object.keys(PRIMITIVE);
38
+
39
+ /**
40
+ * Is `type` something the compiler recognizes , a known semantic type or primitive, an id
41
+ * type (`UserId`), a PascalCase entity (opaque object), or a List/Array of a recognized
42
+ * type? A lowercase word that is none of these is almost always a typo (`emial`, `moeny`).
43
+ */
44
+ export function isRecognizedType(type) {
45
+ const t = String(type || '').trim();
46
+ if (!t) return true; // untyped field , not a type error here
47
+ const listMatch = t.match(/^(?:List|Array)<(.+)>$/i);
48
+ if (listMatch) return isRecognizedType(listMatch[1]);
49
+ const key = t.toLowerCase();
50
+ if (SEMANTIC[key] || PRIMITIVE[key]) return true;
51
+ if (/id$/i.test(t)) return true; // UserId / AccountId ... -> string
52
+ if (/^[A-Z]/.test(t)) return true; // PascalCase entity -> opaque object
53
+ return false;
54
+ }
55
+
56
+ // Map an ThunderLang semantic type to a JSON Schema fragment. Handles List<X> recursively,
57
+ // known semantic types (with formats), primitives, and opaque custom (PascalCase) types.
58
+ export function typeToJsonSchema(type) {
59
+ const t = String(type || '').trim();
60
+ const listMatch = t.match(/^List<(.+)>$/i) || t.match(/^Array<(.+)>$/i);
61
+ if (listMatch) return { type: 'array', items: typeToJsonSchema(listMatch[1]) };
62
+
63
+ const key = t.toLowerCase();
64
+ if (SEMANTIC[key]) return { ...SEMANTIC[key] };
65
+ if (PRIMITIVE[key]) return { ...PRIMITIVE[key] };
66
+ // Unknown PascalCase entity -> an opaque object carrying its name (title), so tooling can
67
+ // still reference it. Ids (UserId/AccountId/...) are strings.
68
+ if (/id$/i.test(t)) return { type: 'string', title: t };
69
+ return { type: 'object', title: t };
70
+ }
71
+
72
+ // Build an object schema from a list of typed fields. All declared fields are required by
73
+ // default; a field carrying an `optional` modifier is omitted from `required`.
74
+ function fieldsToSchema(fields, title) {
75
+ const properties = {};
76
+ const required = [];
77
+ for (const f of fields || []) {
78
+ if (!f.name) continue;
79
+ properties[f.name] = typeToJsonSchema(f.type);
80
+ if (!(f.modifiers || []).some((m) => /optional|nullable/i.test(m))) required.push(f.name);
81
+ }
82
+ const schema = { type: 'object', properties, additionalProperties: false };
83
+ if (required.length) schema.required = required;
84
+ if (title) schema.title = title;
85
+ return schema;
86
+ }
87
+
88
+ /**
89
+ * JSON Schema (draft 2020-12) for a mission's data shape. `which` selects 'input' (default),
90
+ * 'output', or 'both' (an object with input+output sub-schemas).
91
+ */
92
+ export function toJSONSchema(ast, { which = 'input' } = {}) {
93
+ const base = { $schema: 'https://json-schema.org/draft/2020-12/schema', $id: `https://skillstech.dev/intent/${slugish(ast.mission)}.schema.json` };
94
+ if (which === 'output') return { ...base, ...fieldsToSchema(ast.outputs, `${ast.mission || 'Mission'} output`) };
95
+ if (which === 'both') {
96
+ return { ...base, title: `${ast.mission || 'Mission'}`, type: 'object', properties: { input: fieldsToSchema(ast.inputs, 'input'), output: fieldsToSchema(ast.outputs, 'output') } };
97
+ }
98
+ return { ...base, ...fieldsToSchema(ast.inputs, `${ast.mission || 'Mission'} input`) };
99
+ }
100
+
101
+ /**
102
+ * A minimal OpenAPI 3.1 document with the mission as one operation: the input schema is the
103
+ * request body, the output schema is the 200 response, and declared `errors` become named
104
+ * error responses. Path/method are taken from a declared `api` block when present, else
105
+ * defaulted (POST /<mission-slug>).
106
+ */
107
+ export function toOpenAPI(ast) {
108
+ const api = (ast.apis || [])[0];
109
+ const method = (api && api.method ? String(api.method) : 'post').toLowerCase();
110
+ const path = api && api.path ? String(api.path) : `/${slugish(ast.mission)}`;
111
+ const opId = camelish(ast.mission || 'operation');
112
+
113
+ const responses = {
114
+ 200: { description: 'Success', content: { 'application/json': { schema: fieldsToSchema(ast.outputs, `${ast.mission} output`) } } },
115
+ };
116
+ const ERR_STATUS = (name) => (/notfound|missing|unknown/i.test(name) ? '404' : /unauthor|forbidden|denied/i.test(name) ? '403' : /duplicate|conflict|exists/i.test(name) ? '409' : /invalid|bad/i.test(name) ? '400' : '422');
117
+ for (const e of ast.errors || []) {
118
+ const status = ERR_STATUS(e.name || '');
119
+ responses[status] = { description: e.name };
120
+ }
121
+
122
+ const operation = { operationId: opId, summary: ast.title || ast.mission || '', responses };
123
+ if (ast.goal || ast.problem) operation.description = ast.goal || ast.problem;
124
+ if ((ast.inputs || []).length) operation.requestBody = { required: true, content: { 'application/json': { schema: fieldsToSchema(ast.inputs, `${ast.mission} input`) } } };
125
+
126
+ return {
127
+ openapi: '3.1.0',
128
+ info: { title: ast.title || ast.mission || 'Intent API', version: '0.1.0' },
129
+ paths: { [path]: { [method]: operation } },
130
+ };
131
+ }
132
+
133
+ const slugish = (s) => String(s || 'mission').trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'mission';
134
+ const camelish = (s) => {
135
+ const parts = String(s || 'operation').split(/[^A-Za-z0-9]+/).filter(Boolean);
136
+ return parts.map((p, i) => (i === 0 ? p[0].toLowerCase() + p.slice(1) : p[0].toUpperCase() + p.slice(1))).join('') || 'operation';
137
+ };
@@ -0,0 +1,38 @@
1
+ // Decisions, rules, process semantics (Gap 4). IL owns rule semantics + DETERMINISTIC
2
+ // conflict/coverage detection on the DECLARED decision. OpenThunder verifies the
3
+ // decision's IMPLEMENTATION (rule coverage, decision impl verification, explanation
4
+ // verification). Pure (no Node deps): browser-safe.
5
+
6
+ const norm = (s) => String(s ?? '').trim().toLowerCase().replace(/\s+/g, ' ');
7
+
8
+ /**
9
+ * Static analysis of a decision table. Returns findings (deterministic).
10
+ * - missing default (what happens when no rule matches?)
11
+ * - conflicting rules (same condition, different result)
12
+ * - redundant rules (same condition, same result)
13
+ * - no rules
14
+ */
15
+ export function analyzeDecision(dec) {
16
+ const findings = [];
17
+ const rules = dec.rules || [];
18
+
19
+ if (rules.length === 0) {
20
+ findings.push({ code: 'IL-DEC-004', message: `Decision "${dec.name}" declares no rules.` });
21
+ } else if (dec.default == null) {
22
+ findings.push({ code: 'IL-DEC-001', message: `Decision "${dec.name}" has rules but no default (undefined when no rule matches).` });
23
+ }
24
+
25
+ // Pairwise: same normalized condition -> conflict (different result) or redundant (same).
26
+ for (let i = 0; i < rules.length; i++) {
27
+ for (let j = i + 1; j < rules.length; j++) {
28
+ if (rules[i].when && rules[j].when && norm(rules[i].when) === norm(rules[j].when)) {
29
+ if (norm(rules[i].result) !== norm(rules[j].result)) {
30
+ findings.push({ code: 'IL-DEC-002', message: `Decision "${dec.name}": rules "${rules[i].name}" and "${rules[j].name}" have the same condition but different results.` });
31
+ } else {
32
+ findings.push({ code: 'IL-DEC-003', message: `Decision "${dec.name}": rules "${rules[i].name}" and "${rules[j].name}" are redundant (same condition and result).` });
33
+ }
34
+ }
35
+ }
36
+ }
37
+ return findings.sort((a, b) => `${a.code} ${a.message}`.localeCompare(`${b.code} ${b.message}`));
38
+ }
@@ -0,0 +1,48 @@
1
+ // Distributed + failure semantics (Gap 3). IL owns the failure-policy types and the
2
+ // STATIC declaration checks (a retry needs idempotency, at-least-once needs dedup, a
3
+ // permanent-failure handler needs compensation). OpenThunder verifies these hold in the
4
+ // IMPLEMENTATION (retry safety, duplicate handling, failure simulation). Pure; deterministic.
5
+
6
+ const has = (arr, re) => (arr || []).some((x) => re.test(String(x)));
7
+
8
+ /**
9
+ * Static analysis of distributed/failure declarations. Returns findings (deterministic).
10
+ * These are declaration-level guarantees; OT proves the implementation honors them.
11
+ */
12
+ export function analyzeDistributed(ast) {
13
+ const findings = [];
14
+ const handlers = ast.handlers || [];
15
+
16
+ for (const c of ast.commands || []) {
17
+ // Retry without idempotency: a retried command that is not idempotent can duplicate work.
18
+ if (c.retry && !c.idempotencyKey) {
19
+ findings.push({ code: 'IL-DIST-001', target: c.name, message: `Command "${c.name}" declares retry but no idempotency_key.` });
20
+ }
21
+ // A retried / remote command with no timeout can hang forever.
22
+ if (c.retry && !c.timeout) {
23
+ findings.push({ code: 'IL-DIST-002', target: c.name, message: `Command "${c.name}" declares retry but no timeout.` });
24
+ }
25
+ }
26
+
27
+ for (const e of ast.events || []) {
28
+ // At-least-once delivery duplicates; there must be a duplicate handler for the event.
29
+ if (e.delivery && /at_least_once/i.test(e.delivery)) {
30
+ const dedup = handlers.some((h) => /duplicate/i.test(h.trigger || '') && new RegExp(e.name, 'i').test(h.trigger || ''));
31
+ if (!dedup) findings.push({ code: 'IL-DIST-003', target: e.name, message: `Event "${e.name}" is at_least_once but has no "on duplicate ${e.name}" handler.` });
32
+ }
33
+ }
34
+
35
+ for (const h of handlers) {
36
+ // A permanent-failure handler that does not compensate leaves partial state behind.
37
+ if (/permanent_failure|permanent failure/i.test(h.trigger || '') && (h.compensate || []).length === 0) {
38
+ findings.push({ code: 'IL-DIST-004', target: h.trigger, message: 'A permanent_failure handler declares no compensation.' });
39
+ }
40
+ // A duplicate handler that references an event that is not declared.
41
+ const m = /duplicate\s+(\w+)/i.exec(h.trigger || '');
42
+ if (m && !(ast.events || []).some((e) => e.name === m[1])) {
43
+ findings.push({ code: 'IL-DIST-005', target: h.trigger, message: `Duplicate handler references undeclared event "${m[1]}".` });
44
+ }
45
+ }
46
+
47
+ return findings.sort((a, b) => `${a.code} ${a.target}`.localeCompare(`${b.code} ${b.target}`));
48
+ }
package/src/draft.mjs ADDED
@@ -0,0 +1,101 @@
1
+ // Prompt -> intent, the deterministic half (intent-draft-v1). Authoring intent by hand is
2
+ // friction most developers skip. This removes it: give a STRUCTURED brief (which an AI agent can
3
+ // produce from a free-text request, then a human approves) and IL scaffolds a rigorous,
4
+ // canonically-formatted intent draft AND a review checklist of exactly what is still missing ,
5
+ // an unverified guarantee, an unguarded secret, a goal not stated. No AI here; the draft is a
6
+ // proposal, never marked verified. Pure ESM + the formatter, so it is browser-safe.
7
+ //
8
+ // draftIntent(brief) -> { schema, source, review, diagnostics }
9
+
10
+ import { parseIntent } from './parse.mjs';
11
+ import { semanticDiagnostics } from './emit.mjs';
12
+ import { formatSource } from './format.mjs';
13
+
14
+ export const DRAFT_SCHEMA = 'intent-draft-v1';
15
+
16
+ const SECRET_TYPES = new Set(['secret', 'password', 'passwd', 'jwt', 'token', 'apikey', 'api_key', 'privatekey', 'private_key', 'credential', 'cvv']);
17
+ const SECRET_NAME = /pass(word|wd)?|secret|token|jwt|ssn|api[-_]?key|apikey|credential|cvv|private[-_]?key|card/i;
18
+ const isSecretField = (f) => f && (SECRET_TYPES.has(String(f.type || '').toLowerCase()) || SECRET_NAME.test(f.name || ''));
19
+
20
+ function pascalish(text) {
21
+ const words = String(text || 'Mission').replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/[^A-Za-z0-9]+/g, ' ').trim().split(/\s+/);
22
+ if (words.length === 1) return words[0].charAt(0).toUpperCase() + words[0].slice(1);
23
+ return words.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join('');
24
+ }
25
+
26
+ // Accept a list of strings or { statement, because?, verify? } objects.
27
+ const normRules = (list) => (Array.isArray(list) ? list : []).map((r) => (typeof r === 'string' ? { statement: r } : { ...r })).filter((r) => r.statement);
28
+
29
+ /**
30
+ * Scaffold a rigorous intent draft from a structured brief.
31
+ * @param {object} brief { mission|name, goal, actor, problem, title, profiles, guarantees[],
32
+ * neverRules[], inputs[], outputs[], decisions[] }
33
+ */
34
+ export function draftIntent(brief = {}) {
35
+ const b = brief || {};
36
+ const L = [];
37
+ const review = [];
38
+ const flag = (kind, message) => review.push({ kind, message });
39
+
40
+ const name = pascalish(b.mission || b.name || 'Mission');
41
+ L.push(`mission ${name}`);
42
+ const profiles = Array.isArray(b.profiles) && b.profiles.length ? b.profiles : ['product'];
43
+ for (const p of profiles) L.push(`use ${p}`);
44
+ L.push('');
45
+
46
+ if (b.title) L.push(`title "${String(b.title).replace(/"/g, "'")}"`);
47
+ if (b.actor) L.push(`for ${b.actor}`);
48
+ if (b.problem) L.push(`problem "${String(b.problem).replace(/"/g, "'")}"`);
49
+ if (b.goal) { L.push('goal'); L.push(` ${b.goal}`); L.push(''); }
50
+ else flag('missing-goal', 'No goal given , state the outcome this mission exists to achieve.');
51
+
52
+ const guarantees = normRules(b.guarantees);
53
+ for (const g of guarantees) {
54
+ L.push(`guarantee ${g.statement}`);
55
+ if (g.because) L.push(` because ${g.because}`);
56
+ if (g.verify) L.push(` verify ${g.verify}`);
57
+ else flag('guarantee-unverified', `Guarantee "${g.statement}" has no verification , add a test that proves it.`);
58
+ }
59
+ if (guarantees.length) L.push('');
60
+ if (!guarantees.length) flag('no-guarantees', 'No guarantees given , what must always hold?');
61
+
62
+ const nevers = normRules(b.neverRules || b.nevers);
63
+ for (const n of nevers) {
64
+ L.push(`never ${n.statement}`);
65
+ if (n.verify) L.push(` verify ${n.verify}`);
66
+ else flag('never-unverified', `Never-rule "${n.statement}" has no verification.`);
67
+ }
68
+ if (nevers.length) L.push('');
69
+
70
+ const inputs = Array.isArray(b.inputs) ? b.inputs : [];
71
+ if (inputs.length) { L.push('input'); for (const f of inputs) L.push(` ${f.name}: ${f.type || 'string'}`); L.push(''); }
72
+ const outputs = Array.isArray(b.outputs) ? b.outputs : [];
73
+ if (outputs.length) { L.push('output'); for (const f of outputs) L.push(` ${f.name}: ${f.type || 'string'}`); L.push(''); }
74
+
75
+ for (const d of Array.isArray(b.decisions) ? b.decisions : []) {
76
+ if (!d || !d.name) continue;
77
+ L.push(`decision ${d.name}`);
78
+ if (Array.isArray(d.inputs) && d.inputs.length) { L.push(' inputs'); for (const i of d.inputs) L.push(` ${typeof i === 'string' ? i : i.name}`); }
79
+ for (const r of Array.isArray(d.rules) ? d.rules : []) {
80
+ if (!r || !r.name) continue;
81
+ L.push(` rule ${r.name}`);
82
+ if (r.when) L.push(` when ${r.when}`);
83
+ if (r.return) L.push(` return ${r.return}`);
84
+ }
85
+ if (d.default) { L.push(' default'); L.push(` return ${d.default}`); }
86
+ else flag('decision-no-default', `Decision "${d.name}" has no default , what happens when no rule matches?`);
87
+ L.push('');
88
+ }
89
+
90
+ // Secret inputs with no never-rule covering them , the highest-value gap to surface.
91
+ const neverText = nevers.map((n) => n.statement.toLowerCase()).join(' ');
92
+ for (const f of inputs.filter(isSecretField)) {
93
+ if (!neverText.includes(String(f.name).toLowerCase()) && !/\b(log|expose|leak)\b/.test(neverText)) {
94
+ flag('secret-unguarded', `Input "${f.name}" is a secret , add a "never expose ${f.name} in logs" rule.`);
95
+ }
96
+ }
97
+
98
+ const source = formatSource(`${L.join('\n')}\n`);
99
+ const diagnostics = semanticDiagnostics(parseIntent(source));
100
+ return { schema: DRAFT_SCHEMA, source, review, diagnostics };
101
+ }
package/src/drift.mjs ADDED
@@ -0,0 +1,177 @@
1
+ // IntentLift round-trip: approve an inferred draft, then check whether the code
2
+ // still matches the approved intent. Deterministic, no AI. This is the
3
+ // compiler-side drift check; OpenThunder does the deeper repo-wide version later.
4
+ //
5
+ // lift code -> .intent draft -> intent approve -> (code changes) -> intent drift
6
+
7
+ import { createHash } from 'node:crypto';
8
+ import { parseIntent, slug } from './parse.mjs';
9
+ import { liftSource } from './lift.mjs';
10
+ import { COMPILER_VERSION } from './emit.mjs';
11
+
12
+ const sha256 = (s) => 'sha256:' + createHash('sha256').update(s).digest('hex');
13
+
14
+ // Remove a top-level block (header + indented body + one trailing blank).
15
+ function stripBlock(text, keyword) {
16
+ const lines = text.split('\n');
17
+ const out = [];
18
+ let i = 0;
19
+ while (i < lines.length) {
20
+ const l = lines[i];
21
+ if (!/^\s/.test(l) && (l.trim() === keyword || l.startsWith(keyword + ' '))) {
22
+ i++;
23
+ while (i < lines.length && /^\s+\S/.test(lines[i])) i++;
24
+ if (i < lines.length && lines[i].trim() === '') i++;
25
+ continue;
26
+ }
27
+ out.push(l);
28
+ i++;
29
+ }
30
+ return out.join('\n');
31
+ }
32
+
33
+ /** Hash of the intent content, excluding the approval block, so it is stable. */
34
+ export function intentHash(intentText) {
35
+ return sha256(stripBlock(intentText, 'approval').trim());
36
+ }
37
+
38
+ /**
39
+ * Approve an intent: flip `reviewed` to true, add an `approval` block with the
40
+ * source hash (of the intent content), reviewer, and time. `approvedAt` is
41
+ * passed in (never Date.now here) so approval is reproducible in tests.
42
+ */
43
+ export function approveIntent(intentText, { approvedBy, approvedAt } = {}) {
44
+ const base = stripBlock(intentText, 'approval')
45
+ .replace(/^(\s*)reviewed\s+false\b/m, '$1reviewed true')
46
+ .replace(/\s+$/, '');
47
+ // Hash the exact content that gets written (post reviewed-flip), so a later
48
+ // `intent drift` recomputes the same hash when nothing has changed.
49
+ const hash = sha256(base.trim());
50
+ const lines = ['approval', ' reviewed true'];
51
+ if (approvedBy) lines.push(` approved_by ${approvedBy}`);
52
+ if (approvedAt) lines.push(` approved_at ${approvedAt}`);
53
+ lines.push(` source_hash ${hash}`);
54
+ lines.push(` approved_with SkillsTech Compiler ${COMPILER_VERSION}`);
55
+ return {
56
+ text: `${base}\n\n${lines.join('\n')}\n`,
57
+ approval: { reviewed: true, approvedBy, approvedAt, source_hash: hash },
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Emit the drift handoff pack that OpenThunder consumes. The compiler states the
63
+ * expectations (what must be true) and the checks OpenThunder should run against
64
+ * REAL repo evidence (tests executed, routes present, logs/events scanned).
65
+ * The compiler does not perform repo-wide verification; OpenThunder does, and it
66
+ * emits `intent-drift-report-v1` in return.
67
+ */
68
+ export function buildDriftHandoff(approvedIntentText, { generatedAt = null } = {}) {
69
+ const ast = parseIntent(approvedIntentText);
70
+ const expectations = [
71
+ ...ast.guarantees.map((g) => ({
72
+ kind: 'guarantee', id: g.id, statement: g.statement,
73
+ expectedEvidence: g.verify, check: 'guarantee_has_passing_evidence',
74
+ })),
75
+ ...ast.neverRules.map((n) => ({
76
+ kind: 'never', id: n.id, statement: n.statement,
77
+ expectedEvidence: n.verify, check: 'never_rule_not_violated',
78
+ })),
79
+ ...ast.inputs.map((f) => ({
80
+ kind: 'input', name: f.name, type: f.type || 'Unknown',
81
+ check: 'input_present_in_signature',
82
+ })),
83
+ ...(ast.apis || []).map((a) => ({
84
+ kind: 'api', id: a.id, method: a.method, path: a.path,
85
+ check: 'route_present',
86
+ })),
87
+ ];
88
+ return {
89
+ schemaVersion: '0.1.0',
90
+ kind: 'il-to-ot-drift-v1',
91
+ generatedBy: `SkillsTech Compiler ${COMPILER_VERSION}`,
92
+ generatedAt,
93
+ mission: ast.mission || 'mission',
94
+ approved: !!(ast.approval && ast.approval.reviewed),
95
+ approval: ast.approval
96
+ ? {
97
+ reviewed: !!ast.approval.reviewed,
98
+ approvedBy: ast.approval.approved_by || null,
99
+ approvedAt: ast.approval.approved_at || null,
100
+ sourceHash: ast.approval.source_hash || null,
101
+ }
102
+ : null,
103
+ mapsTo: (ast.lift && ast.lift.maps_to) || [],
104
+ expectations,
105
+ handoff:
106
+ 'OpenThunder verifies these expectations against real repo evidence and emits intent-drift-report-v1. The compiler does not perform repo-wide verification.',
107
+ };
108
+ }
109
+
110
+ function verdict(findings) {
111
+ const blocking = findings.filter(
112
+ (f) => f.level === 'warning' && /UNSUPPORTED|INPUT_REMOVED|STALE/.test(f.code),
113
+ ).length;
114
+ const status = blocking > 0
115
+ ? 'drift'
116
+ : findings.some((f) => f.code === 'INTENT_DRIFT_NEW_BEHAVIOR')
117
+ ? 'review'
118
+ : 'in_sync';
119
+ return {
120
+ status,
121
+ findings,
122
+ summary: { status, findings: findings.length, blocking },
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Check whether `codeSource` still satisfies the approved `intentText`. Re-lifts
128
+ * the code and compares guarantees, never rules, and inputs by normalized slug.
129
+ */
130
+ export function checkDrift(intentText, codeSource, { language = 'typescript' } = {}) {
131
+ const ast = parseIntent(intentText);
132
+ const findings = [];
133
+ const add = (level, code, message) => findings.push({ level, code, message });
134
+
135
+ if (ast.approval?.source_hash) {
136
+ if (intentHash(intentText) !== ast.approval.source_hash) {
137
+ add('warning', 'INTENT_DRIFT_STALE_PROOF', 'The approved intent was edited after approval (hash mismatch). Re-approve it.');
138
+ }
139
+ } else {
140
+ add('info', 'INTENT_DRIFT_NOT_APPROVED', 'This intent has no approval block. Approve it first: intent approve <file>.');
141
+ }
142
+
143
+ const lift = liftSource(codeSource, { language });
144
+ if (!lift.ok) {
145
+ add('warning', 'INTENT_DRIFT_NO_CODE_EVIDENCE', lift.error || 'Could not analyze the implementation.');
146
+ return verdict(findings);
147
+ }
148
+ const li = lift.lifted;
149
+ const norm = (s) => slug(s);
150
+ const codeGuar = new Set(li.guarantees.map((g) => norm(g.statement)));
151
+ const codeNever = new Set(li.neverRules.map((n) => norm(n.statement)));
152
+ const codeInputs = new Set(li.inputs.map((i) => norm(i.name)));
153
+ const intentGuar = new Set(ast.guarantees.map((g) => norm(g.statement)));
154
+
155
+ for (const g of ast.guarantees) {
156
+ if (!codeGuar.has(norm(g.statement))) {
157
+ add('warning', 'INTENT_DRIFT_GUARANTEE_UNSUPPORTED', `Guarantee "${g.statement}" has no matching evidence in the code (test removed or renamed?).`);
158
+ }
159
+ }
160
+ for (const n of ast.neverRules) {
161
+ if (!codeNever.has(norm(n.statement))) {
162
+ add('warning', 'INTENT_DRIFT_NEVER_RULE_UNSUPPORTED', `Never rule "${n.statement}" has no matching error or guard in the code.`);
163
+ }
164
+ }
165
+ for (const f of ast.inputs) {
166
+ if (!codeInputs.has(norm(f.name))) {
167
+ add('warning', 'INTENT_DRIFT_INPUT_REMOVED', `Input "${f.name}" is declared in the intent but not found in the code signature.`);
168
+ }
169
+ }
170
+ for (const g of li.guarantees) {
171
+ if (!intentGuar.has(norm(g.statement))) {
172
+ add('info', 'INTENT_DRIFT_NEW_BEHAVIOR', `The code has behavior "${g.statement}" that the approved intent does not declare.`);
173
+ }
174
+ }
175
+
176
+ return verdict(findings);
177
+ }