@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/lift.mjs
ADDED
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
// IntentLift: Code-to-Intent (deterministic, no AI). Lifts source code into an
|
|
2
|
+
// INFERRED ThunderLang draft. Generated intent is useful but humble: it carries
|
|
3
|
+
// evidence, confidence, unknowns, and needs_review, and is never marked verified.
|
|
4
|
+
//
|
|
5
|
+
// Pipeline: source -> Language Adapter -> CodeFactsIR -> Inference -> LiftedIntent -> .intent text
|
|
6
|
+
// P0 ships a TypeScript adapter. Other languages (Rust, Perl, ...) plug in as
|
|
7
|
+
// adapters that emit the same CodeFactsIR, so they share this inference engine.
|
|
8
|
+
|
|
9
|
+
import { slug } from './parse.mjs';
|
|
10
|
+
import { COMPILER_VERSION } from './emit.mjs';
|
|
11
|
+
|
|
12
|
+
const IR_SCHEMA_VERSION = '0.1.0';
|
|
13
|
+
const SEMANTIC_TYPES = new Set([
|
|
14
|
+
'Email', 'Money', 'Currency', 'Url', 'UserId', 'AccountId', 'OrderId', 'InvoiceId',
|
|
15
|
+
'PaymentId', 'Secret', 'Token', 'Jwt', 'IdempotencyKey', 'Date', 'DateTime',
|
|
16
|
+
'Duration', 'Percentage', 'TraceId', 'CorrelationId',
|
|
17
|
+
]);
|
|
18
|
+
const SENSITIVE = /password|token|jwt|secret|payment|credential|ssn|pii|email/i;
|
|
19
|
+
|
|
20
|
+
const lineOf = (source, index) => source.slice(0, index).split('\n').length;
|
|
21
|
+
|
|
22
|
+
// ── Seeded lift (OT's intent-ir-v1 grounding) ────────────────────────────────
|
|
23
|
+
// OT passes { seeds } so the lifted .intent references OT's EXACT node ids instead
|
|
24
|
+
// of lift's own function refs , no divergent second reading of the repo. The
|
|
25
|
+
// load-bearing pair is { nodeId, evidenceRef.sourceLocations }; the rest is
|
|
26
|
+
// enrichment. Additive: with no seeds, liftSource output is byte-identical to before.
|
|
27
|
+
// The machine-readable contract OT keys on.
|
|
28
|
+
export const SEED_SCHEMA = {
|
|
29
|
+
$id: 'intent-seed-v1',
|
|
30
|
+
title: 'IntentSeed',
|
|
31
|
+
description: 'An OT intent-ir-v1 node handed to liftSource so the lifted draft references it.',
|
|
32
|
+
type: 'object',
|
|
33
|
+
required: ['nodeId', 'evidenceRef'],
|
|
34
|
+
properties: {
|
|
35
|
+
nodeId: { type: 'string', description: 'OT stable intent-ir-v1 node id, e.g. "cap:auth"' },
|
|
36
|
+
nodeType: { type: 'string', description: 'intent-ir-v1 node type (Capability | SystemContract | ...)' },
|
|
37
|
+
title: { type: 'string', description: "OT's deterministic title (ground truth)" },
|
|
38
|
+
confidence: { type: 'string', description: 'observed|derived|inferred|speculative|conflicted|confirmed' },
|
|
39
|
+
evidenceRef: {
|
|
40
|
+
type: 'object',
|
|
41
|
+
required: ['signals'],
|
|
42
|
+
properties: {
|
|
43
|
+
signals: { type: 'array', items: { type: 'string' } },
|
|
44
|
+
sourceLocations: {
|
|
45
|
+
type: 'array',
|
|
46
|
+
items: { type: 'object', required: ['file'], properties: { file: { type: 'string' }, line: { type: 'integer' } } },
|
|
47
|
+
},
|
|
48
|
+
ledgerRef: { type: 'object', required: ['seq', 'hash'], properties: { seq: { type: 'integer' }, hash: { type: 'string' } } },
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Defensively normalize OT's seeds: drop malformed entries, keep input order (deterministic),
|
|
55
|
+
// and coerce evidenceRef to a stable shape. A seed with no string nodeId is dropped (it is
|
|
56
|
+
// the one load-bearing field). Never throws , a bad seed is skipped, not fatal.
|
|
57
|
+
export function normalizeSeeds(seeds) {
|
|
58
|
+
if (!Array.isArray(seeds)) return [];
|
|
59
|
+
const out = [];
|
|
60
|
+
for (const s of seeds) {
|
|
61
|
+
if (!s || typeof s !== 'object') continue;
|
|
62
|
+
const nodeId = typeof s.nodeId === 'string' ? s.nodeId.trim() : '';
|
|
63
|
+
if (!nodeId) continue;
|
|
64
|
+
const ev = s.evidenceRef && typeof s.evidenceRef === 'object' ? s.evidenceRef : {};
|
|
65
|
+
const signals = Array.isArray(ev.signals)
|
|
66
|
+
? ev.signals.filter((x) => typeof x === 'string' && x.trim()).map((x) => x.trim()) : [];
|
|
67
|
+
const sourceLocations = Array.isArray(ev.sourceLocations)
|
|
68
|
+
? ev.sourceLocations
|
|
69
|
+
.filter((l) => l && typeof l === 'object' && typeof l.file === 'string' && l.file.trim())
|
|
70
|
+
.map((l) => ({ file: l.file.trim(), ...(Number.isInteger(l.line) ? { line: l.line } : {}) }))
|
|
71
|
+
: [];
|
|
72
|
+
let ledgerRef;
|
|
73
|
+
if (ev.ledgerRef && typeof ev.ledgerRef === 'object'
|
|
74
|
+
&& Number.isInteger(ev.ledgerRef.seq) && typeof ev.ledgerRef.hash === 'string') {
|
|
75
|
+
ledgerRef = { seq: ev.ledgerRef.seq, hash: ev.ledgerRef.hash };
|
|
76
|
+
}
|
|
77
|
+
out.push({
|
|
78
|
+
nodeId,
|
|
79
|
+
nodeType: typeof s.nodeType === 'string' ? s.nodeType : null,
|
|
80
|
+
title: typeof s.title === 'string' ? s.title : null,
|
|
81
|
+
confidence: typeof s.confidence === 'string' ? s.confidence : null,
|
|
82
|
+
evidenceRef: { signals, sourceLocations, ...(ledgerRef ? { ledgerRef } : {}) },
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Turn create_invoice / createInvoice -> "create invoice"; -> PascalCase for a mission name.
|
|
89
|
+
function words(name) {
|
|
90
|
+
return String(name)
|
|
91
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
92
|
+
.replace(/[_-]+/g, ' ')
|
|
93
|
+
.toLowerCase().trim();
|
|
94
|
+
}
|
|
95
|
+
function pascal(name) {
|
|
96
|
+
return words(name).split(/\s+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join('');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function parseParams(raw) {
|
|
100
|
+
if (!raw.trim()) return [];
|
|
101
|
+
return raw.split(',').map((p) => p.trim()).filter(Boolean).map((p) => {
|
|
102
|
+
const m = p.match(/^([A-Za-z_$][\w$]*)\s*:?\s*([^=]+)?/);
|
|
103
|
+
const name = m ? m[1] : p;
|
|
104
|
+
let type = m && m[2] ? m[2].trim() : null;
|
|
105
|
+
return { name, type };
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── TypeScript / JavaScript adapter -> CodeFactsIR ───────────────────────────
|
|
110
|
+
export function extractFactsTypeScript(source, file = 'input.ts') {
|
|
111
|
+
const functions = [];
|
|
112
|
+
const seen = new Set();
|
|
113
|
+
const addFn = (name, paramsRaw, ret, idx) => {
|
|
114
|
+
if (!name || seen.has(name)) return;
|
|
115
|
+
seen.add(name);
|
|
116
|
+
functions.push({
|
|
117
|
+
name, file, line: lineOf(source, idx),
|
|
118
|
+
parameters: parseParams(paramsRaw || ''),
|
|
119
|
+
returnType: ret ? ret.trim().replace(/[{=].*$/, '').trim() : null,
|
|
120
|
+
evidence: [{ kind: 'function_signature', file, line: lineOf(source, idx) }],
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
let m;
|
|
125
|
+
const named = /(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(([^)]*)\)\s*(?::\s*([^{]+))?/g;
|
|
126
|
+
while ((m = named.exec(source))) addFn(m[1], m[2], m[3], m.index);
|
|
127
|
+
const arrow = /(?:export\s+)?const\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*(?:async\s*)?\(([^)]*)\)\s*(?::\s*([^=]+?))?\s*=>/g;
|
|
128
|
+
while ((m = arrow.exec(source))) addFn(m[1], m[2], m[3], m.index);
|
|
129
|
+
|
|
130
|
+
const tests = [];
|
|
131
|
+
const testRe = /\b(?:test|it)\s*\(\s*["'`]([^"'`]+)["'`]/g;
|
|
132
|
+
while ((m = testRe.exec(source))) tests.push({ name: m[1], file, line: lineOf(source, m.index) });
|
|
133
|
+
|
|
134
|
+
const errors = [];
|
|
135
|
+
const errSeen = new Set();
|
|
136
|
+
const addErr = (name, idx) => { if (name && !errSeen.has(name)) { errSeen.add(name); errors.push({ name, file, line: lineOf(source, idx) }); } };
|
|
137
|
+
const classErr = /class\s+([A-Za-z_$][\w$]*(?:Error|Exception))\b/g;
|
|
138
|
+
while ((m = classErr.exec(source))) addErr(m[1], m.index);
|
|
139
|
+
const thrown = /throw\s+new\s+([A-Za-z_$][\w$]*)\s*\(/g;
|
|
140
|
+
while ((m = thrown.exec(source))) addErr(m[1], m.index);
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
schemaVersion: IR_SCHEMA_VERSION, sourceLanguage: 'typescript', sourceRoot: file,
|
|
144
|
+
functions, tests, errors,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Split on a top-level delimiter, ignoring ones inside <>, (), [], {}.
|
|
149
|
+
function splitTopLevel(str, delim) {
|
|
150
|
+
const out = [];
|
|
151
|
+
let depth = 0, cur = '';
|
|
152
|
+
for (const ch of str) {
|
|
153
|
+
if ('<([{'.includes(ch)) depth++;
|
|
154
|
+
else if ('>)]}'.includes(ch)) depth = Math.max(0, depth - 1);
|
|
155
|
+
if (ch === delim && depth === 0) { out.push(cur); cur = ''; }
|
|
156
|
+
else cur += ch;
|
|
157
|
+
}
|
|
158
|
+
if (cur.trim()) out.push(cur);
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function parseRustParams(raw) {
|
|
163
|
+
return splitTopLevel(raw, ',').map((p) => p.trim()).filter(Boolean)
|
|
164
|
+
.filter((p) => !/^&?\s*(mut\s+)?self$/.test(p))
|
|
165
|
+
.map((p) => {
|
|
166
|
+
const m = p.replace(/^mut\s+/, '').match(/^([A-Za-z_]\w*)\s*:\s*(.+)$/);
|
|
167
|
+
if (!m) return { name: p, type: null };
|
|
168
|
+
return { name: m[1], type: m[2].trim().replace(/^&(mut\s+)?/, '') };
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── Rust adapter -> CodeFactsIR (strong types, Result<T,E>, error enums) ─────
|
|
173
|
+
export function extractFactsRust(source, file = 'input.rs') {
|
|
174
|
+
let m;
|
|
175
|
+
// tests first: #[test] / #[tokio::test] fn <name>
|
|
176
|
+
const tests = [];
|
|
177
|
+
const testNames = new Set();
|
|
178
|
+
const testRe = /#\[\s*(?:tokio::)?test\s*\][\s\S]*?fn\s+(\w+)/g;
|
|
179
|
+
while ((m = testRe.exec(source))) { tests.push({ name: m[1], file, line: lineOf(source, m.index) }); testNames.add(m[1]); }
|
|
180
|
+
|
|
181
|
+
const functions = [];
|
|
182
|
+
const seen = new Set();
|
|
183
|
+
const fnRe = /(?:pub\s+)?(?:async\s+)?fn\s+(\w+)\s*(?:<[^>]*>)?\s*\(([^)]*)\)\s*(?:->\s*([^{;]+))?/g;
|
|
184
|
+
while ((m = fnRe.exec(source))) {
|
|
185
|
+
const name = m[1];
|
|
186
|
+
if (seen.has(name) || testNames.has(name)) continue;
|
|
187
|
+
seen.add(name);
|
|
188
|
+
functions.push({
|
|
189
|
+
name, file, line: lineOf(source, m.index),
|
|
190
|
+
parameters: parseRustParams(m[2] || ''),
|
|
191
|
+
returnType: m[3] ? m[3].trim() : null,
|
|
192
|
+
evidence: [{ kind: 'function_signature', file, line: lineOf(source, m.index) }],
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// error enum variants: `enum <Name>Error { DuplicateInvoice, Unauthorized(..) }`
|
|
197
|
+
const errors = [];
|
|
198
|
+
const seenErr = new Set();
|
|
199
|
+
const enumRe = /enum\s+(\w*Error)\s*\{([^}]*)\}/g;
|
|
200
|
+
while ((m = enumRe.exec(source))) {
|
|
201
|
+
for (const raw of splitTopLevel(m[2], ',')) {
|
|
202
|
+
const v = raw.trim().replace(/[({].*$/s, '').trim();
|
|
203
|
+
if (v && /^[A-Z]/.test(v) && !seenErr.has(v)) { seenErr.add(v); errors.push({ name: v, source: m[1], file, line: lineOf(source, m.index) }); }
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return { schemaVersion: IR_SCHEMA_VERSION, sourceLanguage: 'rust', sourceRoot: file, functions, tests, errors };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── Perl adapter -> CodeFactsIR (dynamic: conservative, Unknown types) ───────
|
|
211
|
+
export function extractFactsPerl(source, file = 'input.pl') {
|
|
212
|
+
let m;
|
|
213
|
+
const functions = [];
|
|
214
|
+
const seen = new Set();
|
|
215
|
+
const subRe = /sub\s+(\w+)\s*(?:\(([^)]*)\))?\s*\{/g;
|
|
216
|
+
while ((m = subRe.exec(source))) {
|
|
217
|
+
const name = m[1];
|
|
218
|
+
if (seen.has(name)) continue;
|
|
219
|
+
seen.add(name);
|
|
220
|
+
let params = [];
|
|
221
|
+
if (m[2] && m[2].trim()) {
|
|
222
|
+
params = m[2].split(',').map((s) => s.trim().replace(/^[$@%]/, '')).filter(Boolean).map((n) => ({ name: n, type: 'Unknown' }));
|
|
223
|
+
} else {
|
|
224
|
+
const after = source.slice(m.index, m.index + 300);
|
|
225
|
+
const mm = after.match(/my\s*\(([^)]*)\)\s*=\s*@_/);
|
|
226
|
+
if (mm) params = mm[1].split(',').map((s) => s.trim().replace(/^\$/, '')).filter(Boolean).map((n) => ({ name: n, type: 'Unknown' }));
|
|
227
|
+
}
|
|
228
|
+
functions.push({ name, file, line: lineOf(source, m.index), parameters: params, returnType: null, evidence: [{ kind: 'sub', file, line: lineOf(source, m.index) }] });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const errors = [];
|
|
232
|
+
const seenErr = new Set();
|
|
233
|
+
const dieRe = /\b(?:die|croak|confess)\s+["']([^"']+)["']/g;
|
|
234
|
+
while ((m = dieRe.exec(source))) {
|
|
235
|
+
const s = m[1].replace(/\\n$/, '').trim().slice(0, 60);
|
|
236
|
+
const key = s.toLowerCase();
|
|
237
|
+
if (s && !seenErr.has(key)) { seenErr.add(key); errors.push({ name: s, file, line: lineOf(source, m.index) }); }
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const tests = [];
|
|
241
|
+
const seenT = new Set();
|
|
242
|
+
const addTest = (name, idx) => { const k = name.toLowerCase(); if (name && !seenT.has(k)) { seenT.add(k); tests.push({ name, file, line: lineOf(source, idx) }); } };
|
|
243
|
+
const subtestRe = /subtest\s+["']([^"']+)["']/g;
|
|
244
|
+
while ((m = subtestRe.exec(source))) addTest(m[1], m.index);
|
|
245
|
+
const okRe = /\b(?:ok|is|isnt|like)\s*\([^;]*?,\s*["']([^"']+)["']\s*\)/g;
|
|
246
|
+
while ((m = okRe.exec(source))) addTest(m[1], m.index);
|
|
247
|
+
|
|
248
|
+
return { schemaVersion: IR_SCHEMA_VERSION, sourceLanguage: 'perl', sourceRoot: file, functions, tests, errors };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Params written "Type name" (Java, C#, Go, C++). Strips annotations/qualifiers; the last
|
|
252
|
+
// token is the name, the rest is the type. Handles varargs and array brackets best-effort.
|
|
253
|
+
function parseTypeFirstParams(raw) {
|
|
254
|
+
return splitTopLevel(raw, ',').map((p) => p.trim()).filter(Boolean).map((p) => {
|
|
255
|
+
const cleaned = p.replace(/@\w+(\([^)]*\))?/g, '').replace(/\b(final|const|readonly|in|out|ref|params)\b/g, '').replace(/=.*$/, '').trim();
|
|
256
|
+
const parts = cleaned.split(/\s+/).filter(Boolean);
|
|
257
|
+
if (parts.length >= 2) {
|
|
258
|
+
const name = parts[parts.length - 1].replace(/[[\]]/g, '').replace(/^[*&]+/, '');
|
|
259
|
+
const type = parts.slice(0, -1).join(' ').replace(/^[*&]+/, '');
|
|
260
|
+
return { name, type };
|
|
261
|
+
}
|
|
262
|
+
return { name: cleaned.replace(/[[\]*&]/g, ''), type: null };
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
const addErrOf = (errors, seen, source) => (name, idx) => {
|
|
266
|
+
if (name && !seen.has(name)) { seen.add(name); errors.push({ name, file: source._file, line: lineOf(source._src, idx) }); }
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// ── Python adapter (dynamic: types when annotated, else Unknown) ─────────────
|
|
270
|
+
export function extractFactsPython(source, file = 'input.py') {
|
|
271
|
+
let m; const functions = []; const tests = []; const seen = new Set();
|
|
272
|
+
const fnRe = /^[ \t]*(?:async\s+)?def\s+(\w+)\s*\(([\s\S]*?)\)\s*(?:->\s*([^:]+))?:/gm;
|
|
273
|
+
while ((m = fnRe.exec(source))) {
|
|
274
|
+
const name = m[1];
|
|
275
|
+
if (/^test_/.test(name)) { tests.push({ name, file, line: lineOf(source, m.index) }); continue; }
|
|
276
|
+
if (seen.has(name)) continue; seen.add(name);
|
|
277
|
+
const params = splitTopLevel(m[2] || '', ',').map((p) => p.trim()).filter((p) => p && !/^(self|cls)$/.test(p) && !p.startsWith('*'))
|
|
278
|
+
.map((p) => { const nd = p.split('=')[0].trim(); const mm = nd.match(/^(\w+)\s*:\s*(.+)$/); return mm ? { name: mm[1], type: mm[2].trim() } : { name: nd, type: null }; });
|
|
279
|
+
functions.push({ name, file, line: lineOf(source, m.index), indent: (m[0].match(/^[ \t]*/) || [''])[0].length, parameters: params, returnType: m[3] ? m[3].trim() : null, evidence: [{ kind: 'def', file, line: lineOf(source, m.index) }] });
|
|
280
|
+
}
|
|
281
|
+
const errors = []; const addErr = addErrOf(errors, new Set(), { _src: source, _file: file });
|
|
282
|
+
const classErr = /class\s+(\w*(?:Error|Exception))\s*[(:]/g; while ((m = classErr.exec(source))) addErr(m[1], m.index);
|
|
283
|
+
const raiseRe = /raise\s+(\w+)\s*\(/g; while ((m = raiseRe.exec(source))) addErr(m[1], m.index);
|
|
284
|
+
return { schemaVersion: IR_SCHEMA_VERSION, sourceLanguage: 'python', sourceRoot: file, functions, tests, errors };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ── Java adapter (Type-first params, @Test methods, *Exception) ──────────────
|
|
288
|
+
export function extractFactsJava(source, file = 'input.java') {
|
|
289
|
+
let m; const functions = []; const tests = []; const seen = new Set();
|
|
290
|
+
const testMethods = new Set(); const ta = /@Test\b[\s\S]{0,120}?\b(\w+)\s*\(/g; while ((m = ta.exec(source))) testMethods.add(m[1]);
|
|
291
|
+
const methodRe = /(?:public|private|protected|static|final|abstract|synchronized|default)\s+(?:[\w$.<>,\[\]\s]*?\s+)?([A-Za-z_$][\w$.<>,\[\]]*?)\s+([A-Za-z_$]\w*)\s*\(([^)]*)\)\s*(?:throws[\w\s,.]+)?\{/g;
|
|
292
|
+
while ((m = methodRe.exec(source))) {
|
|
293
|
+
const ret = m[1].trim(); const name = m[2];
|
|
294
|
+
if (['if', 'for', 'while', 'switch', 'catch', 'return'].includes(name)) continue;
|
|
295
|
+
if (['class', 'interface', 'enum', 'new', 'return'].includes(ret)) continue;
|
|
296
|
+
if (testMethods.has(name)) { if (!tests.some((t) => t.name === name)) tests.push({ name, file, line: lineOf(source, m.index) }); continue; }
|
|
297
|
+
if (seen.has(name)) continue; seen.add(name);
|
|
298
|
+
functions.push({ name, file, line: lineOf(source, m.index), parameters: parseTypeFirstParams(m[3] || ''), returnType: ret, evidence: [{ kind: 'method', file, line: lineOf(source, m.index) }] });
|
|
299
|
+
}
|
|
300
|
+
const errors = []; const addErr = addErrOf(errors, new Set(), { _src: source, _file: file });
|
|
301
|
+
let mm; const ce = /class\s+(\w*(?:Exception|Error))\b/g; while ((mm = ce.exec(source))) addErr(mm[1], mm.index);
|
|
302
|
+
const th = /throw\s+new\s+(\w+)\s*\(/g; while ((mm = th.exec(source))) addErr(mm[1], mm.index);
|
|
303
|
+
return { schemaVersion: IR_SCHEMA_VERSION, sourceLanguage: 'java', sourceRoot: file, functions, tests, errors };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ── C# adapter (Type-first, [Fact]/[Test], *Exception) ───────────────────────
|
|
307
|
+
export function extractFactsCSharp(source, file = 'input.cs') {
|
|
308
|
+
let m; const functions = []; const tests = []; const seen = new Set();
|
|
309
|
+
const testMethods = new Set(); const ta = /\[(?:Fact|Test|TestMethod|Theory)\][\s\S]{0,160}?\b(\w+)\s*\(/g; while ((m = ta.exec(source))) testMethods.add(m[1]);
|
|
310
|
+
const methodRe = /(?:public|private|protected|internal|static|async|virtual|override|sealed)\s+(?:[\w.<>,\[\]\s]*?\s+)?([A-Za-z_][\w.<>,\[\]]*?)\s+([A-Za-z_]\w*)\s*\(([^)]*)\)\s*\{/g;
|
|
311
|
+
while ((m = methodRe.exec(source))) {
|
|
312
|
+
const ret = m[1].trim(); const name = m[2];
|
|
313
|
+
if (['if', 'for', 'while', 'switch', 'catch', 'return', 'using', 'lock'].includes(name)) continue;
|
|
314
|
+
if (testMethods.has(name)) { if (!tests.some((t) => t.name === name)) tests.push({ name, file, line: lineOf(source, m.index) }); continue; }
|
|
315
|
+
if (seen.has(name)) continue; seen.add(name);
|
|
316
|
+
functions.push({ name, file, line: lineOf(source, m.index), parameters: parseTypeFirstParams(m[3] || ''), returnType: ret, evidence: [{ kind: 'method', file, line: lineOf(source, m.index) }] });
|
|
317
|
+
}
|
|
318
|
+
const errors = []; const addErr = addErrOf(errors, new Set(), { _src: source, _file: file });
|
|
319
|
+
let mm; const ce = /class\s+(\w*(?:Exception|Error))\b/g; while ((mm = ce.exec(source))) addErr(mm[1], mm.index);
|
|
320
|
+
const th = /throw\s+new\s+(\w+)\s*\(/g; while ((mm = th.exec(source))) addErr(mm[1], mm.index);
|
|
321
|
+
return { schemaVersion: IR_SCHEMA_VERSION, sourceLanguage: 'csharp', sourceRoot: file, functions, tests, errors };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ── Go adapter (name-Type params, TestXxx, error values) ─────────────────────
|
|
325
|
+
export function extractFactsGo(source, file = 'input.go') {
|
|
326
|
+
let m; const functions = []; const tests = []; const seen = new Set();
|
|
327
|
+
const fnRe = /func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)\s*\(([^)]*)\)\s*([^{]*?)\{/g;
|
|
328
|
+
while ((m = fnRe.exec(source))) {
|
|
329
|
+
const name = m[1]; const params = m[2] || '';
|
|
330
|
+
if (/^Test/.test(name) && /testing\.[TBM]/.test(params)) { tests.push({ name, file, line: lineOf(source, m.index) }); continue; }
|
|
331
|
+
if (seen.has(name)) continue; seen.add(name);
|
|
332
|
+
const parameters = splitTopLevel(params, ',').map((p) => p.trim()).filter(Boolean).map((p) => { const parts = p.split(/\s+/); return parts.length >= 2 ? { name: parts[0], type: parts.slice(1).join(' ').replace(/^[*&]+/, '') } : { name: p, type: null }; });
|
|
333
|
+
functions.push({ name, file, line: lineOf(source, m.index), parameters, returnType: (m[3] || '').trim() || null, evidence: [{ kind: 'func', file, line: lineOf(source, m.index) }] });
|
|
334
|
+
}
|
|
335
|
+
const errors = []; const seenErr = new Set();
|
|
336
|
+
const addErr = (n, idx) => { const k = String(n).toLowerCase(); if (n && !seenErr.has(k)) { seenErr.add(k); errors.push({ name: n, file, line: lineOf(source, idx) }); } };
|
|
337
|
+
let mm; const es = /(?:errors\.New|fmt\.Errorf)\(\s*"([^"]+)"/g; while ((mm = es.exec(source))) addErr(mm[1].slice(0, 60), mm.index);
|
|
338
|
+
const et = /type\s+(\w*Error)\s+struct/g; while ((mm = et.exec(source))) addErr(mm[1], mm.index);
|
|
339
|
+
return { schemaVersion: IR_SCHEMA_VERSION, sourceLanguage: 'go', sourceRoot: file, functions, tests, errors };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ── C / C++ adapter (Type-first, gtest TEST, throw/exception) ────────────────
|
|
343
|
+
export function extractFactsCpp(source, file = 'input.cpp') {
|
|
344
|
+
let m; const functions = []; const tests = []; const seen = new Set();
|
|
345
|
+
const tr = /\bTEST(?:_F|_P)?\s*\(\s*\w+\s*,\s*(\w+)\s*\)/g; while ((m = tr.exec(source))) tests.push({ name: m[1], file, line: lineOf(source, m.index) });
|
|
346
|
+
const CTRL = new Set(['if', 'for', 'while', 'switch', 'catch', 'return', 'sizeof', 'else', 'do']);
|
|
347
|
+
const fnRe = /(?:^|\n)[ \t]*((?:[A-Za-z_][\w:]*(?:\s*<[^;{>]*>)?[\s*&]+)+)([A-Za-z_]\w*)\s*\(([^)]*)\)\s*(?:const|noexcept|override)?\s*\{/g;
|
|
348
|
+
while ((m = fnRe.exec(source))) {
|
|
349
|
+
const ret = m[1].trim(); const name = m[2];
|
|
350
|
+
if (CTRL.has(name)) continue;
|
|
351
|
+
if (seen.has(name)) continue; seen.add(name);
|
|
352
|
+
functions.push({ name, file, line: lineOf(source, m.index), parameters: parseTypeFirstParams(m[3] || ''), returnType: ret, evidence: [{ kind: 'function', file, line: lineOf(source, m.index) }] });
|
|
353
|
+
}
|
|
354
|
+
const errors = []; const addErr = addErrOf(errors, new Set(), { _src: source, _file: file });
|
|
355
|
+
let mm; const ce = /(?:class|struct)\s+(\w*(?:Exception|Error))\b/g; while ((mm = ce.exec(source))) addErr(mm[1], mm.index);
|
|
356
|
+
const th = /throw\s+(?:std::)?(\w+)\s*[({]/g; while ((mm = th.exec(source))) addErr(mm[1], mm.index);
|
|
357
|
+
return { schemaVersion: IR_SCHEMA_VERSION, sourceLanguage: 'cpp', sourceRoot: file, functions, tests, errors };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ── PHP adapter (Type $name params, test* methods, *Exception) ───────────────
|
|
361
|
+
export function extractFactsPhp(source, file = 'input.php') {
|
|
362
|
+
let m; const functions = []; const tests = []; const seen = new Set();
|
|
363
|
+
const fnRe = /(?:public|private|protected|static|final|abstract)?\s*function\s+(\w+)\s*\(([^)]*)\)\s*(?::\s*\??([\w\\]+))?/g;
|
|
364
|
+
while ((m = fnRe.exec(source))) {
|
|
365
|
+
const name = m[1];
|
|
366
|
+
if (/^test/i.test(name)) { tests.push({ name, file, line: lineOf(source, m.index) }); continue; }
|
|
367
|
+
if (seen.has(name)) continue; seen.add(name);
|
|
368
|
+
const parameters = splitTopLevel(m[2] || '', ',').map((p) => p.trim()).filter(Boolean).map((p) => { const mm = p.match(/^(?:\??([\w\\|]+)\s+)?[&.]*\$(\w+)/); return mm ? { name: mm[2], type: mm[1] || null } : { name: p.replace(/[^\w]/g, ''), type: null }; });
|
|
369
|
+
functions.push({ name, file, line: lineOf(source, m.index), parameters, returnType: m[3] ? m[3].trim() : null, evidence: [{ kind: 'function', file, line: lineOf(source, m.index) }] });
|
|
370
|
+
}
|
|
371
|
+
const errors = []; const addErr = addErrOf(errors, new Set(), { _src: source, _file: file });
|
|
372
|
+
let mm; const ce = /class\s+(\w*(?:Exception|Error))\b/g; while ((mm = ce.exec(source))) addErr(mm[1], mm.index);
|
|
373
|
+
const th = /throw\s+new\s+(\w+)\s*\(/g; while ((mm = th.exec(source))) addErr(mm[1], mm.index);
|
|
374
|
+
return { schemaVersion: IR_SCHEMA_VERSION, sourceLanguage: 'php', sourceRoot: file, functions, tests, errors };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ── Ruby adapter (dynamic, def methods, it/test blocks, *Error) ──────────────
|
|
378
|
+
export function extractFactsRuby(source, file = 'input.rb') {
|
|
379
|
+
let m; const functions = []; const tests = []; const seen = new Set();
|
|
380
|
+
const fnRe = /^[ \t]*def\s+(?:self\.)?([a-z_]\w*[!?=]?)\s*(?:\(([^)]*)\))?/gm;
|
|
381
|
+
while ((m = fnRe.exec(source))) {
|
|
382
|
+
const name = m[1];
|
|
383
|
+
if (/^test_/.test(name)) { tests.push({ name, file, line: lineOf(source, m.index) }); continue; }
|
|
384
|
+
if (seen.has(name)) continue; seen.add(name);
|
|
385
|
+
const parameters = splitTopLevel(m[2] || '', ',').map((p) => p.trim()).filter(Boolean).map((p) => ({ name: p.split(/[:=]/)[0].trim().replace(/^[*&]+/, ''), type: null }));
|
|
386
|
+
functions.push({ name, file, line: lineOf(source, m.index), indent: (m[0].match(/^[ \t]*/) || [''])[0].length, parameters, returnType: null, evidence: [{ kind: 'def', file, line: lineOf(source, m.index) }] });
|
|
387
|
+
}
|
|
388
|
+
const seenT = new Set(); const addTest = (n, idx) => { const k = String(n).toLowerCase(); if (n && !seenT.has(k)) { seenT.add(k); tests.push({ name: n, file, line: lineOf(source, idx) }); } };
|
|
389
|
+
const itRe = /\b(?:it|test|describe|context|specify)\s+["']([^"']+)["']/g; while ((m = itRe.exec(source))) addTest(m[1], m.index);
|
|
390
|
+
const errors = []; const seenErr = new Set(); const addErr = (n, idx) => { if (n && !seenErr.has(n)) { seenErr.add(n); errors.push({ name: n, file, line: lineOf(source, idx) }); } };
|
|
391
|
+
const ce = /class\s+(\w*(?:Error|Exception))\s*</g; while ((m = ce.exec(source))) addErr(m[1], m.index);
|
|
392
|
+
const raiseRe = /raise\s+(\w+)/g; while ((m = raiseRe.exec(source))) { if (/^[A-Z]/.test(m[1])) addErr(m[1], m.index); }
|
|
393
|
+
return { schemaVersion: IR_SCHEMA_VERSION, sourceLanguage: 'ruby', sourceRoot: file, functions, tests, errors };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const ADAPTERS = {
|
|
397
|
+
typescript: extractFactsTypeScript, ts: extractFactsTypeScript,
|
|
398
|
+
javascript: extractFactsTypeScript, js: extractFactsTypeScript,
|
|
399
|
+
rust: extractFactsRust, rs: extractFactsRust,
|
|
400
|
+
perl: extractFactsPerl, pl: extractFactsPerl,
|
|
401
|
+
python: extractFactsPython, py: extractFactsPython,
|
|
402
|
+
java: extractFactsJava,
|
|
403
|
+
csharp: extractFactsCSharp, cs: extractFactsCSharp, 'c#': extractFactsCSharp,
|
|
404
|
+
go: extractFactsGo, golang: extractFactsGo,
|
|
405
|
+
cpp: extractFactsCpp, 'c++': extractFactsCpp, c: extractFactsCpp, cc: extractFactsCpp,
|
|
406
|
+
php: extractFactsPhp,
|
|
407
|
+
ruby: extractFactsRuby, rb: extractFactsRuby,
|
|
408
|
+
};
|
|
409
|
+
export const SUPPORTED_LANGUAGES = ['typescript', 'javascript', 'python', 'java', 'csharp', 'go', 'rust', 'cpp', 'php', 'ruby', 'perl'];
|
|
410
|
+
const DYNAMIC_LANGUAGES = new Set(['perl', 'javascript', 'python', 'ruby', 'php']);
|
|
411
|
+
|
|
412
|
+
const LANG_DISPLAY = {
|
|
413
|
+
typescript: 'TypeScript', javascript: 'JavaScript', python: 'Python', java: 'Java',
|
|
414
|
+
csharp: 'C#', go: 'Go', rust: 'Rust', cpp: 'C++', php: 'PHP', ruby: 'Ruby', perl: 'Perl',
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
// Unwrap Result<T, E> / Promise<T> / T -> { output, error }
|
|
418
|
+
function unwrapReturn(ret) {
|
|
419
|
+
if (!ret) return { output: null, error: null };
|
|
420
|
+
let r = ret.trim();
|
|
421
|
+
const promise = r.match(/^Promise<(.+)>$/);
|
|
422
|
+
if (promise) r = promise[1].trim();
|
|
423
|
+
const result = r.match(/^(?:Result|Either)<\s*([^,]+?)\s*,\s*([^>]+?)\s*>$/);
|
|
424
|
+
if (result) return { output: result[1].trim(), error: result[2].trim() };
|
|
425
|
+
return { output: r, error: null };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ── Inference: CodeFactsIR -> LiftedIntent ──────────────────────────────────
|
|
429
|
+
export function inferIntent(facts) {
|
|
430
|
+
const primary = facts.functions.find((f) => f.parameters.length && f.returnType) || facts.functions[0];
|
|
431
|
+
if (!primary) return null;
|
|
432
|
+
|
|
433
|
+
const missionName = pascal(primary.name);
|
|
434
|
+
const { output, error } = unwrapReturn(primary.returnType);
|
|
435
|
+
|
|
436
|
+
const inputs = primary.parameters.map((p) => ({
|
|
437
|
+
name: p.name,
|
|
438
|
+
type: p.type && SEMANTIC_TYPES.has(p.type.replace(/<.*/, '')) ? p.type : (p.type || 'Unknown'),
|
|
439
|
+
evidence: 'function parameter',
|
|
440
|
+
sensitive: SENSITIVE.test(`${p.name} ${p.type || ''}`),
|
|
441
|
+
}));
|
|
442
|
+
|
|
443
|
+
const guarantees = facts.tests.map((t) => ({
|
|
444
|
+
statement: words(t.name), evidence: `test ${t.name}`, confidence: 'high',
|
|
445
|
+
sourceSpan: { file: t.file, line: t.line },
|
|
446
|
+
}));
|
|
447
|
+
|
|
448
|
+
// Prefer specific error-enum variants; fall back to the Result error type.
|
|
449
|
+
const variantErrors = facts.errors.map((e) => e.name);
|
|
450
|
+
const errorNames = [...new Set(variantErrors.length ? variantErrors : (error ? [error] : []))]
|
|
451
|
+
.filter((n) => n && !/^(string|number|boolean|void|Error)$/i.test(n));
|
|
452
|
+
const neverRules = errorNames.map((n) => ({
|
|
453
|
+
statement: `cause ${words(n)}`, evidence: `${n} error`, confidence: 'medium',
|
|
454
|
+
}));
|
|
455
|
+
|
|
456
|
+
const hasSensitive = inputs.some((i) => i.sensitive);
|
|
457
|
+
const unknown = ['why', 'owner', 'customer impact', 'PM notes', ...(hasSensitive ? [] : ['security never rules'])];
|
|
458
|
+
const needsReview = ['goal wording', 'why', 'never rules', ...(hasSensitive ? ['security rules'] : []), 'verification evidence'];
|
|
459
|
+
|
|
460
|
+
// Overall confidence: high if tests + typed signature; low if only a signature.
|
|
461
|
+
const overall = guarantees.length && inputs.every((i) => i.type !== 'Unknown') ? 'high'
|
|
462
|
+
: guarantees.length || inputs.some((i) => i.type !== 'Unknown') ? 'medium' : 'low';
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
mission: missionName,
|
|
466
|
+
from: LANG_DISPLAY[facts.sourceLanguage] || facts.sourceLanguage,
|
|
467
|
+
confidence: overall,
|
|
468
|
+
reviewed: false,
|
|
469
|
+
mapsTo: [
|
|
470
|
+
`function ${primary.file}:${primary.name}`,
|
|
471
|
+
...facts.tests.map((t) => `test ${t.file}:${t.name}`),
|
|
472
|
+
],
|
|
473
|
+
evidence: [
|
|
474
|
+
`function signature ${primary.name}`,
|
|
475
|
+
...facts.tests.slice(0, 5).map((t) => `test ${t.name}`),
|
|
476
|
+
...errorNames.slice(0, 5).map((n) => `error ${n}`),
|
|
477
|
+
],
|
|
478
|
+
goal: `${words(primary.name)} (inferred from the ${primary.name} signature)`,
|
|
479
|
+
inputs,
|
|
480
|
+
output: output && !/^(void|undefined|null)$/i.test(output) ? { name: 'result', type: output, evidence: 'return type' } : null,
|
|
481
|
+
guarantees,
|
|
482
|
+
neverRules,
|
|
483
|
+
unknown,
|
|
484
|
+
needsReview,
|
|
485
|
+
hasSensitive,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// ── LiftedIntent -> humble, source-mapped .intent draft ─────────────────────
|
|
490
|
+
export function renderLiftedIntent(lift) {
|
|
491
|
+
const L = [];
|
|
492
|
+
L.push(`# Inferred by IntentLift from ${lift.from}. Draft, unverified, needs human review.`);
|
|
493
|
+
L.push(`mission ${lift.mission}`, '');
|
|
494
|
+
L.push('inferred', ` from ${lift.from}`, ` confidence ${lift.confidence}`, ` reviewed false`, ` generated_by SkillsTech Compiler ${COMPILER_VERSION}`, '');
|
|
495
|
+
L.push('maps_to', ...lift.mapsTo.map((m) => ` ${m}`), '');
|
|
496
|
+
if (lift.seeds && lift.seeds.length) {
|
|
497
|
+
// OT intent-ir-v1 grounding. Comments (never verification), so the draft still parses.
|
|
498
|
+
L.push('# Seeded by OT intent-ir-v1 nodes , grounding, not verification:');
|
|
499
|
+
for (const s of lift.seeds) {
|
|
500
|
+
const t = s.nodeType ? ` ${s.nodeType}` : '';
|
|
501
|
+
const c = s.confidence ? ` confidence:${s.confidence}` : '';
|
|
502
|
+
L.push(`# ${s.nodeId}${t}${c}${s.title ? ` , ${s.title}` : ''}`);
|
|
503
|
+
for (const loc of s.evidenceRef.sourceLocations) L.push(`# at ${loc.file}${loc.line ? `:${loc.line}` : ''}`);
|
|
504
|
+
for (const sig of s.evidenceRef.signals.slice(0, 5)) L.push(`# signal: ${sig}`);
|
|
505
|
+
if (s.evidenceRef.ledgerRef) L.push(`# ledger: seq ${s.evidenceRef.ledgerRef.seq} ${s.evidenceRef.ledgerRef.hash}`);
|
|
506
|
+
}
|
|
507
|
+
L.push('');
|
|
508
|
+
}
|
|
509
|
+
L.push('evidence', ...lift.evidence.map((e) => ` ${e}`), '');
|
|
510
|
+
L.push('goal', ` ${lift.goal}`, '');
|
|
511
|
+
if (lift.inputs.length) {
|
|
512
|
+
L.push('input');
|
|
513
|
+
for (const i of lift.inputs) L.push(` ${i.name}: ${i.type}`);
|
|
514
|
+
L.push('');
|
|
515
|
+
}
|
|
516
|
+
if (lift.output) L.push('output', ` ${lift.output.name}: ${lift.output.type}`, '');
|
|
517
|
+
if (lift.guarantees.length) {
|
|
518
|
+
L.push('guarantees');
|
|
519
|
+
for (const g of lift.guarantees) L.push(` ${g.statement}`);
|
|
520
|
+
L.push('');
|
|
521
|
+
}
|
|
522
|
+
if (lift.neverRules.length) {
|
|
523
|
+
L.push('never');
|
|
524
|
+
for (const n of lift.neverRules) L.push(` ${n.statement}`);
|
|
525
|
+
L.push('');
|
|
526
|
+
}
|
|
527
|
+
L.push('unknown', ...lift.unknown.map((u) => ` ${u}`), '');
|
|
528
|
+
L.push('needs_review', ...lift.needsReview.map((r) => ` ${r}`), '');
|
|
529
|
+
return L.join('\n') + '\n';
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// ── Diagnostics specific to lifted drafts (all advisory) ────────────────────
|
|
533
|
+
function liftDiagnostics(lift, facts) {
|
|
534
|
+
const d = [];
|
|
535
|
+
const warn = (code, message) => d.push({ level: 'warning', code, message });
|
|
536
|
+
warn('INTENT_LIFT_NEEDS_HUMAN_REVIEW', 'This intent was inferred from code. A human must review goal, why, never rules, and verification.');
|
|
537
|
+
if (DYNAMIC_LANGUAGES.has(facts.sourceLanguage)) warn('INTENT_LIFT_DYNAMIC_LANGUAGE_LIMITATION', `${facts.sourceLanguage} is dynamically typed, so types and outputs are often Unknown and confidence is lower. Review carefully.`);
|
|
538
|
+
if (lift.confidence === 'low') warn('INTENT_LIFT_LOW_CONFIDENCE', 'Low confidence: inferred mostly from names, with little test or type evidence.');
|
|
539
|
+
if (!facts.tests.length) warn('INTENT_LIFT_NO_TEST_EVIDENCE', 'No tests found. Guarantees could not be grounded in verification evidence.');
|
|
540
|
+
if (lift.inputs.some((i) => i.type === 'Unknown')) warn('INTENT_LIFT_UNKNOWN_SEMANTIC_TYPE', 'Some fields could not be resolved to a semantic type. Review and annotate them.');
|
|
541
|
+
if (lift.hasSensitive) warn('INTENT_LIFT_SECURITY_REVIEW_NEEDED', 'Sensitive field names detected. Mark them Secret/Token/PII and add never-log rules.');
|
|
542
|
+
return d;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Lift a set of source files (a repo) into inferred ThunderLang drafts, one per
|
|
547
|
+
* file that yields a mission. `files` is [{ file, source }] (the CLI reads the
|
|
548
|
+
* filesystem; this core function stays pure). Returns per-mission drafts + a
|
|
549
|
+
* repo-level summary matching the `intent lift --from repo --json` contract.
|
|
550
|
+
*/
|
|
551
|
+
export function languageForFile(file) {
|
|
552
|
+
if (/\.rs$/i.test(file)) return 'rust';
|
|
553
|
+
if (/\.(pl|pm|t)$/i.test(file)) return 'perl';
|
|
554
|
+
if (/\.pyi?$/i.test(file)) return 'python';
|
|
555
|
+
if (/\.java$/i.test(file)) return 'java';
|
|
556
|
+
if (/\.cs$/i.test(file)) return 'csharp';
|
|
557
|
+
if (/\.go$/i.test(file)) return 'go';
|
|
558
|
+
if (/\.(cpp|cc|cxx|hpp|hh|c|h)$/i.test(file)) return 'cpp';
|
|
559
|
+
if (/\.php$/i.test(file)) return 'php';
|
|
560
|
+
if (/\.rb$/i.test(file)) return 'ruby';
|
|
561
|
+
if (/\.(mjs|cjs|jsx?)$/i.test(file)) return 'javascript';
|
|
562
|
+
return 'typescript';
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Lift EVERY function in a source file into its own inferred mission (not just the primary).
|
|
567
|
+
* This is the Intent Atlas view of a file , each operation becomes an intent you can read, so a
|
|
568
|
+
* whole module's behavior is legible as intent. Deterministic, humble (each draft is unverified).
|
|
569
|
+
*/
|
|
570
|
+
// Is a function part of a project's PUBLIC surface? Cuts internal-helper noise from the Atlas.
|
|
571
|
+
// - Go: exported names are Capitalized; `main`/`init` are not public API.
|
|
572
|
+
// - Python/Ruby: top-level (or one-level) names that are not underscore-private, and not deep
|
|
573
|
+
// nested closures (indent > 4).
|
|
574
|
+
// - Otherwise keep everything but drop underscore-private names.
|
|
575
|
+
function isPublicFn(fn, language) {
|
|
576
|
+
const name = fn.name || '';
|
|
577
|
+
// Common private-helper naming conventions, across languages.
|
|
578
|
+
if (/(?:Internal|Impl|_impl|_helper|_test|Helper)$/.test(name)) return false;
|
|
579
|
+
if (language === 'go' || language === 'golang') return /^[A-Z]/.test(name) && name !== 'Test';
|
|
580
|
+
if (language === 'python' || language === 'ruby') return !name.startsWith('_') && (fn.indent == null || fn.indent <= 4);
|
|
581
|
+
return !name.startsWith('_') && name !== 'init' && name !== 'constructor';
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
export function liftAll(source, { language = 'typescript', file = '', publicOnly = true } = {}) {
|
|
585
|
+
const key = String(language).toLowerCase();
|
|
586
|
+
const adapter = ADAPTERS[key];
|
|
587
|
+
if (!adapter) return { ok: false, error: `Unsupported language "${language}". Supported: ${SUPPORTED_LANGUAGES.join(', ')}.` };
|
|
588
|
+
const resolvedFile = file || `input.${LANG_EXT[key] || 'txt'}`;
|
|
589
|
+
const codeFacts = adapter(source, resolvedFile);
|
|
590
|
+
let fns = codeFacts.functions;
|
|
591
|
+
if (publicOnly) { const pub = fns.filter((f) => isPublicFn(f, key)); if (pub.length) fns = pub; }
|
|
592
|
+
if (!fns.length) return { ok: false, error: 'No functions found to infer intent from.', codeFacts };
|
|
593
|
+
const missions = [];
|
|
594
|
+
const seen = new Map();
|
|
595
|
+
for (const fn of fns) {
|
|
596
|
+
const lifted = inferIntent({ ...codeFacts, functions: [fn] });
|
|
597
|
+
if (!lifted) continue;
|
|
598
|
+
const base = slug(lifted.mission);
|
|
599
|
+
const n = (seen.get(base) || 0) + 1;
|
|
600
|
+
seen.set(base, n);
|
|
601
|
+
missions.push({ mission: lifted.mission, fn: fn.name, line: fn.line, confidence: lifted.confidence, intentText: renderLiftedIntent(lifted) });
|
|
602
|
+
}
|
|
603
|
+
return { ok: true, schemaVersion: IR_SCHEMA_VERSION, sourceLanguage: codeFacts.sourceLanguage, count: missions.length, missions };
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
export function liftRepo(files, { language } = {}) {
|
|
607
|
+
const missions = [];
|
|
608
|
+
const confidenceSummary = { high: 0, medium: 0, low: 0 };
|
|
609
|
+
const detected = new Set();
|
|
610
|
+
const usedNames = new Map();
|
|
611
|
+
let unknowns = 0;
|
|
612
|
+
|
|
613
|
+
for (const { file, source } of files) {
|
|
614
|
+
const lang = language || languageForFile(file);
|
|
615
|
+
const r = liftSource(source, { language: lang, file });
|
|
616
|
+
if (!r.ok) continue;
|
|
617
|
+
detected.add(lang);
|
|
618
|
+
const conf = r.lifted.confidence;
|
|
619
|
+
confidenceSummary[conf] = (confidenceSummary[conf] || 0) + 1;
|
|
620
|
+
unknowns += r.lifted.unknown.length;
|
|
621
|
+
const base = slug(r.lifted.mission);
|
|
622
|
+
const n = (usedNames.get(base) || 0) + 1;
|
|
623
|
+
usedNames.set(base, n);
|
|
624
|
+
const outName = n === 1 ? `${base}.intent` : `${base}-${n}.intent`;
|
|
625
|
+
missions.push({
|
|
626
|
+
mission: r.lifted.mission, sourceFile: file, outName,
|
|
627
|
+
intentText: r.intentText, summary: r.summary, diagnostics: r.diagnostics,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
return {
|
|
632
|
+
ok: missions.length > 0,
|
|
633
|
+
schemaVersion: IR_SCHEMA_VERSION,
|
|
634
|
+
languagesDetected: [...detected].sort(),
|
|
635
|
+
missionsGenerated: missions.length,
|
|
636
|
+
confidenceSummary,
|
|
637
|
+
unknowns,
|
|
638
|
+
missions,
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Default source file extension per language (for accurate source-map evidence in the draft).
|
|
643
|
+
const LANG_EXT = {
|
|
644
|
+
typescript: 'ts', javascript: 'js', python: 'py', java: 'java', csharp: 'cs',
|
|
645
|
+
go: 'go', rust: 'rs', cpp: 'cpp', php: 'php', ruby: 'rb', perl: 'pl',
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Lift source into an inferred ThunderLang draft. Deterministic, no AI.
|
|
650
|
+
* `seeds` (optional, OT's intent-ir-v1 nodes , see SEED_SCHEMA) make the draft reference OT's
|
|
651
|
+
* EXACT node ids instead of lift's own function refs, so there is no divergent second reading.
|
|
652
|
+
* Additive: with no seeds the output is byte-identical to before.
|
|
653
|
+
*/
|
|
654
|
+
export function liftSource(source, { language = 'typescript', file = '', seeds } = {}) {
|
|
655
|
+
const key = String(language).toLowerCase();
|
|
656
|
+
const adapter = ADAPTERS[key];
|
|
657
|
+
if (!adapter) {
|
|
658
|
+
return { ok: false, error: `Unsupported language "${language}". Supported: ${SUPPORTED_LANGUAGES.join(', ')}.` };
|
|
659
|
+
}
|
|
660
|
+
const resolvedFile = file || `input.${LANG_EXT[key] || 'txt'}`;
|
|
661
|
+
const codeFacts = adapter(source, resolvedFile);
|
|
662
|
+
const lifted = inferIntent(codeFacts);
|
|
663
|
+
if (!lifted) {
|
|
664
|
+
return { ok: false, error: 'No functions found to infer intent from.', codeFacts };
|
|
665
|
+
}
|
|
666
|
+
const normSeeds = normalizeSeeds(seeds);
|
|
667
|
+
if (normSeeds.length) {
|
|
668
|
+
lifted.seeds = normSeeds;
|
|
669
|
+
// Reference OT's exact node ids in maps_to (parseable), so downstream reads OT's ids, not a fork.
|
|
670
|
+
lifted.mapsTo = [
|
|
671
|
+
...normSeeds.map((s) => `node ${s.nodeId}${s.nodeType ? ` (${s.nodeType})` : ''}`),
|
|
672
|
+
...lifted.mapsTo,
|
|
673
|
+
];
|
|
674
|
+
// Fold OT's evidence signals into the draft's evidence (bounded, deterministic order).
|
|
675
|
+
const seedSignals = normSeeds.flatMap((s) => s.evidenceRef.signals.map((sig) => `seed ${s.nodeId}: ${sig}`));
|
|
676
|
+
lifted.evidence = [...lifted.evidence, ...seedSignals.slice(0, 8)];
|
|
677
|
+
}
|
|
678
|
+
const intentText = renderLiftedIntent(lifted);
|
|
679
|
+
const diagnostics = liftDiagnostics(lifted, codeFacts);
|
|
680
|
+
const summary = {
|
|
681
|
+
schemaVersion: IR_SCHEMA_VERSION,
|
|
682
|
+
sourceLanguage: codeFacts.sourceLanguage,
|
|
683
|
+
mission: lifted.mission,
|
|
684
|
+
confidence: lifted.confidence,
|
|
685
|
+
reviewed: false,
|
|
686
|
+
evidenceCount: lifted.evidence.length,
|
|
687
|
+
unknowns: lifted.unknown,
|
|
688
|
+
functions: codeFacts.functions.length,
|
|
689
|
+
tests: codeFacts.tests.length,
|
|
690
|
+
seeds: normSeeds.map((s) => s.nodeId),
|
|
691
|
+
};
|
|
692
|
+
return { ok: true, codeFacts, lifted, intentText, diagnostics, summary, seeds: normSeeds };
|
|
693
|
+
}
|