@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/package.json ADDED
@@ -0,0 +1,84 @@
1
+ {
2
+ "name": "@skillstech/thunderlang",
3
+ "version": "0.1.6",
4
+ "description": "Deterministic ThunderLang compiler, CLI, and Language Server. No AI required. Turns .intent files into the canonical Intent Graph, docs, and a proof, and EXECUTES the intent: run decisions, simulate lifecycles, and check outcome contracts. Interops with DMN, BPMN, JSON Schema, and OpenAPI.",
5
+ "type": "module",
6
+ "main": "./src/index.mjs",
7
+ "types": "./src/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.d.ts",
11
+ "import": "./src/index.mjs",
12
+ "require": "./dist/index.cjs"
13
+ },
14
+ "./core": {
15
+ "types": "./src/core.d.ts",
16
+ "import": "./src/core.mjs",
17
+ "require": "./dist/core.cjs"
18
+ },
19
+ "./package.json": "./package.json"
20
+ },
21
+ "bin": {
22
+ "intent": "src/cli.mjs",
23
+ "thunderlang": "src/cli.mjs",
24
+ "tl": "src/cli.mjs"
25
+ },
26
+ "files": [
27
+ "src",
28
+ "dist",
29
+ "intent-graph.schema.json",
30
+ "syntaxes",
31
+ "CHANGELOG.md",
32
+ "README.md",
33
+ "LICENSE"
34
+ ],
35
+ "engines": {
36
+ "node": ">=18"
37
+ },
38
+ "scripts": {
39
+ "test": "node --test",
40
+ "build:cjs": "node scripts/build-cjs.mjs",
41
+ "prepack": "node scripts/build-cjs.mjs",
42
+ "prepublishOnly": "node scripts/build-cjs.mjs && node --test",
43
+ "check:example": "node src/cli.mjs check ../examples/CreateInvoice.intent",
44
+ "build:example": "node src/cli.mjs build ../examples/CreateInvoice.intent"
45
+ },
46
+ "devDependencies": {
47
+ "esbuild": "^0.28.1"
48
+ },
49
+ "keywords": [
50
+ "intent",
51
+ "thunderlang",
52
+ "intent-oriented-programming",
53
+ "compiler",
54
+ "cli",
55
+ "language-server",
56
+ "lsp",
57
+ "runtime",
58
+ "executable",
59
+ "verification",
60
+ "contract",
61
+ "proof",
62
+ "decision",
63
+ "dmn",
64
+ "bpmn",
65
+ "openapi",
66
+ "json-schema",
67
+ "intent-graph",
68
+ "ai"
69
+ ],
70
+ "homepage": "https://thunderlang.dev",
71
+ "repository": {
72
+ "type": "git",
73
+ "url": "git+https://github.com/SkillsTechTalk/thunderlang.git",
74
+ "directory": "compiler"
75
+ },
76
+ "bugs": {
77
+ "url": "https://github.com/SkillsTechTalk/thunderlang/issues"
78
+ },
79
+ "author": "SkillsTech",
80
+ "license": "MIT",
81
+ "publishConfig": {
82
+ "access": "public"
83
+ }
84
+ }
@@ -0,0 +1,67 @@
1
+ // Pure, dependency-free core of intent-ai-v1 , NO Node built-ins (no crypto/fs/path),
2
+ // so it is safe to import in a browser bundle (e.g. Repo Mastery's Vite app).
3
+ // Published as the subpath `@skillstech/thunderlang/core`.
4
+ //
5
+ // The rest of ai.mjs (hashing, markers, manifest) needs Node crypto; these helpers
6
+ // do not. Everything here is re-exported from ai.mjs so the main API is unchanged.
7
+
8
+ // ── State model ─────────────────────────────────────────────────────────────
9
+ export const IMPLEMENTATION_STATES = [
10
+ 'PENDING', 'GENERATED', 'VERIFIED', 'VERIFIED_AWAITING_APPROVAL',
11
+ 'APPROVED', 'MODIFIED', 'INVALID', 'REJECTED', 'ADOPTED',
12
+ ];
13
+
14
+ export const RISK_LEVELS = ['low', 'medium', 'high', 'critical'];
15
+ export const HIGH_RISK = new Set(['high', 'critical']);
16
+
17
+ /** True if a state means the implementation must block a production build. */
18
+ export function blocksProduction(status, { approvalRequired = false } = {}) {
19
+ if (['PENDING', 'GENERATED', 'MODIFIED', 'INVALID', 'REJECTED'].includes(status)) return true;
20
+ if (status === 'VERIFIED_AWAITING_APPROVAL') return true;
21
+ if (status === 'VERIFIED' && approvalRequired) return true;
22
+ return false; // APPROVED and ADOPTED ship
23
+ }
24
+
25
+ // ── Comment prefixes for managed-region markers ─────────────────────────────
26
+ export const COMMENT_PREFIX = {
27
+ typescript: '//', javascript: '//', tsx: '//', jsx: '//',
28
+ csharp: '//', java: '//', go: '//', rust: '//',
29
+ python: '#', ruby: '#', perl: '#', shell: '#',
30
+ };
31
+
32
+ // ── Integration events (versioned; no shared-DB coupling) ───────────────────
33
+ export const INTENT_AI_EVENTS = [
34
+ 'IntentAiImplementationDeclared', 'IntentAiGenerationRequested', 'IntentAiCandidateImported',
35
+ 'IntentAiImplementationGenerated', 'IntentAiVerificationStarted', 'IntentAiVerificationPassed',
36
+ 'IntentAiVerificationFailed', 'IntentAiApprovalRequested', 'IntentAiImplementationApproved',
37
+ 'IntentAiImplementationRejected', 'IntentAiImplementationModified', 'IntentAiProofInvalidated',
38
+ 'IntentAiImplementationAdopted', 'IntentAiMasteryGenerated', 'IntentAiOwnershipUpdated',
39
+ ];
40
+
41
+ /** Build a versioned integration event (payload shape per contract intent-ai-v1). */
42
+ export function makeEvent(type, fields = {}) {
43
+ return {
44
+ schemaVersion: '1.0',
45
+ type,
46
+ projectId: fields.projectId ?? null,
47
+ repoId: fields.repoId ?? null,
48
+ implementationId: fields.implementationId ?? null,
49
+ missionId: fields.missionId ?? null,
50
+ contractHash: fields.contractHash ?? null,
51
+ implementationHash: fields.implementationHash ?? null,
52
+ correlationId: fields.correlationId ?? null,
53
+ timestamp: fields.timestamp ?? null,
54
+ toolVersion: fields.toolVersion ?? null,
55
+ actorType: fields.actorType ?? null,
56
+ actorId: fields.actorId ?? null,
57
+ previousStatus: fields.previousStatus ?? null,
58
+ newStatus: fields.newStatus ?? null,
59
+ };
60
+ }
61
+
62
+ // ── Canonical proof checks keys (locked; RM + OT read these exact strings) ───
63
+ // Note: "contracts" is plural (matches the proof JSON), values are passed|failed|skipped.
64
+ export const PROOF_CHECK_KEYS = [
65
+ 'regionIntegrity', 'syntax', 'types', 'contracts', 'effects',
66
+ 'architecture', 'security', 'tests', 'determinism',
67
+ ];
@@ -0,0 +1,56 @@
1
+ // Intent AI event sink (intent-ai-events-v1) , the append-only log of intent-ai-v1
2
+ // integration events (declared, generated, verified, approved, rejected, modified,
3
+ // adopted, ...). The compiler already builds these events with `makeEvent`; this is
4
+ // where they are persisted, so a project keeps a durable audit trail of every AI
5
+ // action, who took it, and how the status changed. Pure and browser-safe: the CLI
6
+ // owns the JSON-Lines file, this module owns the shape and the read/write of it.
7
+
8
+ import { INTENT_AI_EVENTS } from './ai-core.mjs';
9
+
10
+ export const EVENT_LOG_SCHEMA = 'intent-ai-events-v1';
11
+
12
+ export function emptyEventLog() {
13
+ return { schema: EVENT_LOG_SCHEMA, events: [] };
14
+ }
15
+
16
+ /** Append an event to the log. Returns a NEW log (append-only, never mutates). */
17
+ export function recordEvent(log, event) {
18
+ if (!event || !INTENT_AI_EVENTS.includes(event.type)) {
19
+ throw new Error(`intent ai events: unknown event type "${event?.type}"`);
20
+ }
21
+ const base = log && Array.isArray(log.events) ? log : emptyEventLog();
22
+ return { ...base, events: [...base.events, event] };
23
+ }
24
+
25
+ /** Parse a JSON-Lines event log (one event per line; blank lines ignored). */
26
+ export function parseEventLog(jsonl) {
27
+ const events = String(jsonl || '')
28
+ .split('\n')
29
+ .map((l) => l.trim())
30
+ .filter(Boolean)
31
+ .map((l) => JSON.parse(l));
32
+ return { schema: EVENT_LOG_SCHEMA, events };
33
+ }
34
+
35
+ /** Serialize a log back to JSON Lines (trailing newline when non-empty). */
36
+ export function serializeEventLog(log) {
37
+ const events = log?.events || [];
38
+ return events.length ? events.map((e) => JSON.stringify(e)).join('\n') + '\n' : '';
39
+ }
40
+
41
+ // ── Queries ──────────────────────────────────────────────────────────────────
42
+ const of = (log) => log?.events || [];
43
+ export const eventsFor = (log, implementationId) =>
44
+ of(log).filter((e) => !implementationId || e.implementationId === implementationId);
45
+ export const eventsOfType = (log, type) => of(log).filter((e) => e.type === type);
46
+
47
+ /** A compact status timeline for one implementation: [{ type, at, by, from, to }]. */
48
+ export function timeline(log, implementationId) {
49
+ return eventsFor(log, implementationId).map((e) => ({
50
+ type: e.type,
51
+ at: e.timestamp ?? null,
52
+ by: e.actorId ?? null,
53
+ from: e.previousStatus ?? null,
54
+ to: e.newStatus ?? null,
55
+ }));
56
+ }
package/src/ai.mjs ADDED
@@ -0,0 +1,324 @@
1
+ // Intent AI Implementations , the shared domain for intentionally deferred,
2
+ // AI-assisted implementations. This module is the single source of truth that all
3
+ // four SkillsTech products key off (contract intent-ai-v1):
4
+ // - the state model
5
+ // - the managed-region marker parser (multi-language)
6
+ // - the contract hash and implementation hash (with normalization)
7
+ // - the .intent/ai-implementations.json manifest shape
8
+ //
9
+ // ThunderLang OWNS this. OpenThunder verifies against it; Repo Mastery teaches it;
10
+ // SkillsTech orchestrates it. Deterministic, no AI required.
11
+
12
+ import { sha256 } from './emit.mjs';
13
+ import { slug } from './parse.mjs';
14
+ // Pure, browser-safe helpers live in ai-core.mjs (no Node deps). Re-exported here so
15
+ // the main API is unchanged; browser consumers can import '@skillstech/thunderlang/core'.
16
+ import { COMMENT_PREFIX, HIGH_RISK, blocksProduction } from './ai-core.mjs';
17
+ export {
18
+ IMPLEMENTATION_STATES, RISK_LEVELS, HIGH_RISK, blocksProduction,
19
+ COMMENT_PREFIX, INTENT_AI_EVENTS, makeEvent, PROOF_CHECK_KEYS,
20
+ } from './ai-core.mjs';
21
+
22
+ // ── Managed-region markers (multi-language, one shared parser) ───────────────
23
+ // The machine-readable <intent:ai-implementation ...> ... </intent:ai-implementation>
24
+ // marker is authoritative. Comment prefix varies by language; the marker token is
25
+ // the same everywhere, so we detect it regardless of prefix.
26
+ const OPEN_TOKEN = 'intent:ai-implementation'; // AI-managed
27
+ const ADOPTED_TOKEN = 'intent:implementation'; // human-owned after adoption
28
+ const attrRe = /([a-zA-Z][\w-]*)\s*=\s*"([^"]*)"/g;
29
+
30
+ function parseAttrs(text) {
31
+ const attrs = {};
32
+ let m;
33
+ attrRe.lastIndex = 0;
34
+ while ((m = attrRe.exec(text))) attrs[m[1]] = m[2];
35
+ return attrs;
36
+ }
37
+
38
+ /**
39
+ * Parse managed regions from target code. Comment-prefix agnostic.
40
+ * Returns { regions: [{ token, id, attrs, startLine, endLine, code }], findings: [] }.
41
+ * findings use INTENT-AI-1xx region-integrity codes.
42
+ */
43
+ export function parseMarkers(code) {
44
+ const lines = String(code).split('\n');
45
+ const regions = [];
46
+ const findings = [];
47
+ const stack = [];
48
+ const seenIds = new Set();
49
+
50
+ lines.forEach((line, i) => {
51
+ const ln = i + 1;
52
+ const token = line.includes(OPEN_TOKEN) ? OPEN_TOKEN : line.includes(ADOPTED_TOKEN) ? ADOPTED_TOKEN : null;
53
+ const isClose = /<\s*\//.test(line) || line.includes('</');
54
+ if (token && line.includes(`</${token}`)) {
55
+ // closing marker
56
+ const open = stack.pop();
57
+ if (!open) { findings.push({ code: 'INTENT-AI-101', line: ln, message: `Closing marker with no matching open.` }); return; }
58
+ regions.push({ ...open, endLine: ln, code: lines.slice(open.startLine, i).join('\n') });
59
+ } else if (token && line.includes(`<${token}`) && !isCloseOnly(line, token)) {
60
+ const attrs = parseAttrs(line);
61
+ if (!attrs.id) findings.push({ code: 'INTENT-AI-104', line: ln, message: 'Managed region has no id.' });
62
+ if (attrs.id && seenIds.has(attrs.id)) findings.push({ code: 'INTENT-AI-102', line: ln, message: `Duplicate implementation id "${attrs.id}".` });
63
+ if (attrs.id) seenIds.add(attrs.id);
64
+ stack.push({ token, id: attrs.id || null, attrs, startLine: ln });
65
+ }
66
+ });
67
+
68
+ for (const open of stack) {
69
+ findings.push({ code: 'INTENT-AI-101', line: open.startLine, message: `Managed region "${open.id}" is missing its closing marker.` });
70
+ }
71
+ return { regions, findings };
72
+ }
73
+
74
+ function isCloseOnly(line, token) {
75
+ return line.includes(`</${token}`) && !new RegExp(`<${token}\\b`).test(line);
76
+ }
77
+
78
+ /** Render open + close marker comment lines for a language. */
79
+ export function renderMarker(meta, language = 'typescript', { token = OPEN_TOKEN } = {}) {
80
+ const c = COMMENT_PREFIX[language] || '//';
81
+ const order = ['id', 'mission', 'contract-hash', 'implementation-hash', 'generation-id', 'status', 'editing', 'risk', 'origin', 'ownership'];
82
+ const attrs = order.filter((k) => meta[k] != null && meta[k] !== '').map((k) => `${k}="${meta[k]}"`);
83
+ const open = `${c} <${token} ${attrs.join(' ')}>`;
84
+ const close = `${c} </${token}>`;
85
+ return { open, close };
86
+ }
87
+
88
+ // ── Hashing (contract hash + implementation hash) ────────────────────────────
89
+ const norm = (s) => String(s ?? '').trim().replace(/\s+/g, ' ').toLowerCase();
90
+ const sortedNorm = (arr) => (arr || []).map(norm).filter(Boolean).sort();
91
+
92
+ /**
93
+ * Contract hash , normalized representation of the mission's declared contract.
94
+ * Order-insensitive where the language is order-insensitive; formatting-insensitive.
95
+ */
96
+ export function contractHash(ast) {
97
+ const canonical = {
98
+ mission: norm(ast.mission),
99
+ inputs: sortedNorm((ast.inputs || []).map((i) => `${i.name}:${i.type}`)),
100
+ outputs: sortedNorm((ast.outputs || []).map((o) => `${o.name}:${o.type}`)),
101
+ requires: sortedNorm(ast.requires),
102
+ guarantees: sortedNorm((ast.guarantees || []).map((g) => g.statement)),
103
+ never: sortedNorm((ast.neverRules || []).map((n) => n.statement)),
104
+ constraints: sortedNorm(ast.constraints),
105
+ errors: sortedNorm((ast.errors || []).map((e) => e.name)),
106
+ verify: sortedNorm(ast.verify),
107
+ architecture: sortedNorm(ast.architecture),
108
+ risk: norm(ast.implementation?.risk),
109
+ approval: norm(ast.implementation?.approval),
110
+ };
111
+ return sha256(JSON.stringify(canonical));
112
+ }
113
+
114
+ /**
115
+ * Implementation hash , covers only the managed region's code after deterministic
116
+ * normalization: strip marker lines, normalize line endings, trim trailing
117
+ * whitespace, drop leading/trailing blank lines. Formatting-only edits inside the
118
+ * body still change indentation/content, so this is intentionally content-sensitive
119
+ * but not byte-sensitive to trailing whitespace or line endings.
120
+ */
121
+ export function implementationHash(regionCode) {
122
+ const body = String(regionCode || '')
123
+ .replace(/\r\n/g, '\n')
124
+ .split('\n')
125
+ .filter((l) => !l.includes(OPEN_TOKEN) && !l.includes(ADOPTED_TOKEN))
126
+ .map((l) => l.replace(/[ \t]+$/g, ''))
127
+ .join('\n')
128
+ .replace(/^\n+/, '')
129
+ .replace(/\n+$/, '');
130
+ return sha256(body);
131
+ }
132
+
133
+ // ── Provider-neutral implementation prompt (Path 1: external agent handoff) ──
134
+ // Deterministic, no AI required. Produces the structured prompt a developer hands to
135
+ // Claude Code / Codex / Cursor, or the package for a BYOK / local provider.
136
+ export function buildImplementationPrompt(ast, { language = 'typescript' } = {}) {
137
+ const impl = ast.implementation || {};
138
+ const id = impl.id || slug(ast.mission || 'implementation');
139
+ const { open, close } = renderMarker({ id, mission: ast.mission, status: 'generated' }, language);
140
+ const list = (arr) => (arr && arr.length ? arr.map((x) => `- ${x}`).join('\n') : '- (none)');
141
+ return [
142
+ `# Implement: ${ast.mission} (id: ${id})`,
143
+ '',
144
+ `You are implementing one function/region for the mission "${ast.mission}". Follow the contract EXACTLY.`,
145
+ '',
146
+ '## Goal', ast.goal || '(none)',
147
+ '', '## Inputs', list((ast.inputs || []).map((i) => `${i.name}: ${i.type}`)),
148
+ '', '## Outputs', list((ast.outputs || []).map((o) => `${o.name}: ${o.type}`)),
149
+ '', '## Requires (preconditions)', list(ast.requires),
150
+ '', '## Guarantees (must always hold)', list((ast.guarantees || []).map((g) => g.statement)),
151
+ '', '## Never (prohibited behavior)', list((ast.neverRules || []).map((n) => n.statement)),
152
+ '', '## Constraints', list(ast.constraints),
153
+ '', '## Failure modes', list((ast.errors || []).map((e) => e.name)),
154
+ '', '## Verify (your code must pass)', list(ast.verify),
155
+ '', '## Architecture rules', list(ast.architecture),
156
+ '', '## Scope', `- scope: ${impl.scope || 'function_body'}`,
157
+ `- may modify:\n${list(impl.mayModify || [`${ast.mission}.body`])}`,
158
+ `- must NOT modify:\n${list(impl.mustNotModify || [`${ast.mission}.contract`, 'contract types', 'architecture'])}`,
159
+ '', '## Output requirements',
160
+ `- Return ONLY the ${language} region, wrapped in the exact machine-readable markers below.`,
161
+ '- Do not modify anything outside the region. Do not add prohibited effects (network, fs, clock, randomness).',
162
+ '- The implementation must be deterministic if the contract requires it.',
163
+ '', '## Required marker format', '```', open, '// your implementation here', close, '```',
164
+ '',
165
+ ].join('\n');
166
+ }
167
+
168
+ // ── Candidate import + scope validation ─────────────────────────────────────
169
+ /**
170
+ * Validate an imported candidate against the permitted scope of one implementation.
171
+ * A candidate may contain ONLY the region for `id`; a region for any other id is a
172
+ * scope escape (INTENT-AI-505). Reuses the marker parser's integrity findings.
173
+ * Returns { ok, findings, region }.
174
+ */
175
+ export function validateCandidate(code, id) {
176
+ const { regions, findings } = parseMarkers(code);
177
+ const out = [...findings];
178
+ const own = regions.filter((r) => r.id === id);
179
+ const foreign = regions.filter((r) => r.id && r.id !== id);
180
+ if (own.length === 0) out.push({ code: 'INTENT-AI-103', line: 1, message: `Candidate has no managed region "${id}".` });
181
+ if (own.length > 1) out.push({ code: 'INTENT-AI-102', line: own[1]?.startLine ?? 1, message: `Candidate has duplicate region "${id}".` });
182
+ for (const f of foreign) out.push({ code: 'INTENT-AI-505', line: f.startLine, message: `Candidate modifies region "${f.id}", outside the permitted scope of "${id}".` });
183
+ return { ok: out.length === 0 && own.length === 1, findings: out, region: own[0] || null };
184
+ }
185
+
186
+ // ── State resolution (declaration + marker + proof -> actual state) ──────────
187
+ // The heart of the lifecycle: given the contract, the generated region (if any),
188
+ // and the proof (if any), compute the current state per the shared rules.
189
+ export function resolveState({ ast, region = null, proof = null, approval = null }) {
190
+ const impl = ast.implementation || {};
191
+ const approvalRequired = !!impl.approval && impl.approval !== 'none';
192
+ const reasons = [];
193
+
194
+ if (region && region.token === ADOPTED_TOKEN) return { status: 'ADOPTED', approvalRequired, reasons };
195
+ if (region && region.attrs && region.attrs.status === 'rejected') return { status: 'REJECTED', approvalRequired, reasons };
196
+ if (!region) return { status: 'PENDING', approvalRequired, reasons: [{ code: 'INTENT-AI-501', message: 'No implementation region yet.' }] };
197
+ if (!proof) return { status: 'GENERATED', approvalRequired, reasons: [{ code: 'INTENT-AI-501', message: 'Generated but not verified.' }] };
198
+
199
+ const cH = contractHash(ast);
200
+ const iH = implementationHash(region.code || '');
201
+ if (proof.contractHash !== cH) { reasons.push({ code: 'INTENT-AI-503', message: 'Proof is stale: the contract changed.' }); return { status: 'MODIFIED', approvalRequired, reasons }; }
202
+ if (proof.implementationHash !== iH) { reasons.push({ code: 'INTENT-AI-504', message: 'Proof is stale: the implementation changed.' }); return { status: 'MODIFIED', approvalRequired, reasons }; }
203
+ if (proof.status === 'INVALID') return { status: 'INVALID', approvalRequired, reasons };
204
+
205
+ const hashesMatchApproval = approval && approval.contractHash === cH && approval.implementationHash === iH;
206
+ if (hashesMatchApproval && approval.decision === 'rejected') return { status: 'REJECTED', approvalRequired, reasons };
207
+ // A recorded decision defaults to 'approved' when a matching approval has no decision field.
208
+ if (hashesMatchApproval && (approval.decision === undefined || approval.decision === 'approved')) return { status: 'APPROVED', approvalRequired, reasons };
209
+ if (approvalRequired) return { status: 'VERIFIED_AWAITING_APPROVAL', approvalRequired, reasons: [{ code: 'INTENT-AI-502', message: 'Verified, but human approval is required.' }] };
210
+ return { status: 'VERIFIED', approvalRequired, reasons };
211
+ }
212
+
213
+ // ── Approvals store (.intent/ai-approvals.json) ──────────────────────────────
214
+ // A decision binds to the exact hashes it reviewed, so a later edit makes it stale
215
+ // (resolveState stops treating it as APPROVED once the hashes move).
216
+ export const APPROVALS_SCHEMA_VERSION = '1.0';
217
+
218
+ export function emptyApprovals() {
219
+ return { schemaVersion: APPROVALS_SCHEMA_VERSION, approvals: {} };
220
+ }
221
+
222
+ export function approvalFor(store, id) {
223
+ return (store && store.approvals && store.approvals[id]) || null;
224
+ }
225
+
226
+ /**
227
+ * Record an approve/reject decision bound to the reviewed hashes. Refuses to record
228
+ * against hashes that don't match the current contract + implementation (no approving
229
+ * stale work). Returns { store, record } or { error }.
230
+ */
231
+ export function recordDecision(store, id, { decision, by, role, note, contractHash: cH, implementationHash: iH, at }) {
232
+ if (!['approved', 'rejected'].includes(decision)) return { error: `Unknown decision "${decision}".` };
233
+ if (!cH || !iH) return { error: 'Cannot record a decision without current contract + implementation hashes.' };
234
+ const record = { decision, by: by || null, role: role || null, note: note || null, contractHash: cH, implementationHash: iH, at: at || null };
235
+ const next = { ...emptyApprovals(), ...store, approvals: { ...(store?.approvals || {}), [id]: record } };
236
+ return { store: next, record };
237
+ }
238
+
239
+ // (INTENT_AI_EVENTS + makeEvent live in ai-core.mjs and are re-exported at the top.)
240
+
241
+ /**
242
+ * Production gate over resolved implementations. Blocks unless every implementation
243
+ * ships (APPROVED / ADOPTED). --allow-pending lets a dev build tolerate PENDING only.
244
+ */
245
+ export function productionGate(resolved, { allowPending = false } = {}) {
246
+ const blocking = resolved.filter((r) => {
247
+ if (allowPending && r.status === 'PENDING') return false;
248
+ return blocksProduction(r.status, { approvalRequired: r.approvalRequired });
249
+ });
250
+ return { ok: blocking.length === 0, blocking, total: resolved.length };
251
+ }
252
+
253
+ // ── Adoption (AI-owned region -> human-owned) ────────────────────────────────
254
+ /**
255
+ * Rewrite a managed region's markers from AI-managed to human-adopted, preserving
256
+ * provenance (origin="ai" ownership="human"). Returns { code, adopted } or null if
257
+ * the id is not found.
258
+ */
259
+ export function adoptRegion(code, id, language = 'typescript') {
260
+ const { regions } = parseMarkers(code);
261
+ const region = regions.find((r) => r.id === id && r.token === OPEN_TOKEN);
262
+ if (!region) return null;
263
+ const lines = String(code).split('\n');
264
+ const { open, close } = renderMarker(
265
+ { id, mission: region.attrs.mission, origin: 'ai', ownership: 'human' },
266
+ language, { token: ADOPTED_TOKEN },
267
+ );
268
+ lines[region.startLine - 1] = open; // 1-indexed marker lines
269
+ lines[region.endLine - 1] = close;
270
+ return { code: lines.join('\n'), adopted: id };
271
+ }
272
+
273
+ // ── Manifest (.intent/ai-implementations.json) ───────────────────────────────
274
+ export const MANIFEST_SCHEMA_VERSION = '1.0';
275
+
276
+ /** Default risk-driven approval policy. */
277
+ function defaultApproval(impl) {
278
+ if (impl.approval) return impl.approval;
279
+ return HIGH_RISK.has(impl.risk) ? 'required' : 'none';
280
+ }
281
+
282
+ /**
283
+ * Build the AI-implementation manifest from parsed .intent files.
284
+ * @param {{path?: string, source: string, ast: object}[]} files (ast from parseIntent)
285
+ * @param {{projectId?: string}} [opts]
286
+ */
287
+ export function buildManifest(files, opts = {}) {
288
+ const implementations = [];
289
+ for (const f of files) {
290
+ const impl = f.ast?.implementation;
291
+ if (!impl) continue;
292
+ const id = impl.id || slug(f.ast.mission || 'unnamed');
293
+ implementations.push({
294
+ id,
295
+ mission: f.ast.mission || null,
296
+ sourceLocation: f.path || null,
297
+ targetLocation: impl.target || impl.targetLocation || null,
298
+ scope: impl.scope || 'function_body',
299
+ strategy: impl.strategy || 'generate_once',
300
+ editing: impl.editing || 'collaborative',
301
+ risk: impl.risk || 'low',
302
+ approval: defaultApproval(impl),
303
+ status: 'PENDING',
304
+ contractHash: contractHash(f.ast),
305
+ implementationHash: null,
306
+ generationId: null,
307
+ proofLocation: `.intent/proofs/${id}.json`,
308
+ mayModify: impl.mayModify || [`${f.ast.mission}.body`],
309
+ mustNotModify: impl.mustNotModify || [`${f.ast.mission}.contract`],
310
+ });
311
+ }
312
+ implementations.sort((a, b) => a.id.localeCompare(b.id));
313
+ return {
314
+ schemaVersion: MANIFEST_SCHEMA_VERSION,
315
+ projectId: opts.projectId || null,
316
+ generatedBy: 'thunderlang',
317
+ implementations,
318
+ summary: {
319
+ total: implementations.length,
320
+ byStatus: implementations.reduce((m, i) => ((m[i.status] = (m[i.status] || 0) + 1), m), {}),
321
+ approvalRequired: implementations.filter((i) => i.approval !== 'none').length,
322
+ },
323
+ };
324
+ }
package/src/arch.mjs ADDED
@@ -0,0 +1,55 @@
1
+ // Architecture rules , the language semantics of the `architecture` block.
2
+ //
3
+ // ThunderLang OWNS parsing architecture constraints into a structured, versioned form.
4
+ // OpenThunder's Architecture Lens checks the actual dependency graph against them
5
+ // (INTENT-ARCH-307). This module is the shared, deterministic rule model both use.
6
+ //
7
+ // architecture
8
+ // domain must not depend on infrastructure
9
+ // application may depend on domain
10
+ // infrastructure may implement application ports
11
+
12
+ const RELATIONS = [
13
+ { re: /^(.+?)\s+must\s+not\s+depend\s+on\s+(.+)$/i, relation: 'must-not-depend-on' },
14
+ { re: /^(.+?)\s+must\s+depend\s+on\s+(.+)$/i, relation: 'must-depend-on' },
15
+ { re: /^(.+?)\s+may\s+depend\s+on\s+(.+)$/i, relation: 'may-depend-on' },
16
+ { re: /^(.+?)\s+may\s+implement\s+(.+?)(?:\s+ports)?$/i, relation: 'may-implement' },
17
+ ];
18
+
19
+ const layer = (s) => String(s).trim().toLowerCase().replace(/\s+/g, ' ');
20
+
21
+ /**
22
+ * Parse architecture-block lines into structured rules.
23
+ * @param {string[]} lines
24
+ * @returns {{ rules: {from, relation, to, raw}[], unparsed: string[] }}
25
+ */
26
+ export function parseArchitectureRules(lines) {
27
+ const rules = [];
28
+ const unparsed = [];
29
+ for (const raw of lines || []) {
30
+ const text = String(raw).trim();
31
+ if (!text) continue;
32
+ let matched = false;
33
+ for (const { re, relation } of RELATIONS) {
34
+ const m = text.match(re);
35
+ if (m) { rules.push({ from: layer(m[1]), relation, to: layer(m[2]), raw: text }); matched = true; break; }
36
+ }
37
+ if (!matched) unparsed.push(text);
38
+ }
39
+ return { rules, unparsed };
40
+ }
41
+
42
+ /**
43
+ * Does a dependency `from -> to` violate a `must-not-depend-on` rule? Returns the
44
+ * violated rule or null. Layer names match by substring so "domain.billing" is caught
45
+ * by a rule about "domain". This is the reusable check OpenThunder calls per edge.
46
+ */
47
+ export function violatesArchitecture(rules, from, to) {
48
+ const f = layer(from);
49
+ const t = layer(to);
50
+ for (const r of rules || []) {
51
+ if (r.relation !== 'must-not-depend-on') continue;
52
+ if (f.includes(r.from) && t.includes(r.to)) return r;
53
+ }
54
+ return null;
55
+ }