@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/cli.mjs
ADDED
|
@@ -0,0 +1,1534 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ThunderLang CLI (MVP, deterministic). Commands: check | graph | proof | build.
|
|
3
|
+
//
|
|
4
|
+
// The emit stage writes the artifacts the ecosystem consumes to `.intent/<mission>/` by DEFAULT,
|
|
5
|
+
// NOT `dist/` , OpenThunder's scanner excludes dist/node_modules, so proof artifacts must live in a
|
|
6
|
+
// committed, scannable location. `.intent/` mirrors the ecosystem's dot-dir convention (.openthunder/).
|
|
7
|
+
//
|
|
8
|
+
// intent check <file> parse + semantic diagnostics (exit 1 on error)
|
|
9
|
+
// intent graph <file> [--out .intent] contract-graph.json + architecture-graph.json
|
|
10
|
+
// intent proof <file> [--out .intent] .intent-proof.json
|
|
11
|
+
// intent build <file> [--out .intent] [--no-ai] all artifacts + docs + mermaid + testplan
|
|
12
|
+
//
|
|
13
|
+
// --no-ai is the default and only mode today; the flag is accepted for forward-compatibility.
|
|
14
|
+
|
|
15
|
+
import { readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, existsSync } from 'node:fs';
|
|
16
|
+
import { basename, join, relative, dirname } from 'node:path';
|
|
17
|
+
import { parseIntent, slug, subjectName, KNOWN_LENSES } from './parse.mjs';
|
|
18
|
+
import {
|
|
19
|
+
buildContractGraph, buildArchitectureGraph, buildImplementationPlan,
|
|
20
|
+
semanticDiagnostics, buildProof, sha256, COMPILER_VERSION,
|
|
21
|
+
} from './emit.mjs';
|
|
22
|
+
import { toSarif } from './sarif.mjs';
|
|
23
|
+
import { renderMarkdown, renderLensDoc, renderMermaid, renderTestplan } from './compile.mjs';
|
|
24
|
+
import { getCompletions, getHover, getCodeActions, autocorrectSource } from './intellisense.mjs';
|
|
25
|
+
import { startLspServer } from './lsp.mjs';
|
|
26
|
+
import { startMcpServer } from './mcp.mjs';
|
|
27
|
+
import { formatSource } from './format.mjs';
|
|
28
|
+
import { applyEdits } from './patch.mjs';
|
|
29
|
+
import { buildReport } from './report.mjs';
|
|
30
|
+
import { scanProject } from './scan.mjs';
|
|
31
|
+
import { VIEWS } from './scan-queries.mjs';
|
|
32
|
+
import { guardianReport } from './guardian.mjs';
|
|
33
|
+
import { simulateChange } from './simulate.mjs';
|
|
34
|
+
import { verifyLedger, explain as ledgerExplain } from './ledger.mjs';
|
|
35
|
+
import { verifyDiff } from './verify-diff.mjs';
|
|
36
|
+
import { guardSummary } from './guard.mjs';
|
|
37
|
+
import { draftIntent } from './draft.mjs';
|
|
38
|
+
import { liftSource, liftAll, liftRepo, languageForFile } from './lift.mjs';
|
|
39
|
+
import { approveIntent, checkDrift, buildDriftHandoff } from './drift.mjs';
|
|
40
|
+
import { buildMissionIndex } from './atlas.mjs';
|
|
41
|
+
import { parseSelection, regionMetrics, selectCandidate } from './select.mjs';
|
|
42
|
+
import { buildIntentGraph } from './intent-graph.mjs';
|
|
43
|
+
import { buildAtlas, searchAtlas, expandNode } from './intent-atlas.mjs';
|
|
44
|
+
import { buildFocusGraph, intentBrief, makeScope } from './focus.mjs';
|
|
45
|
+
import { comprehensionLevel, comprehensionReport, LEVELS as COMPREHENSION_LEVELS } from './comprehension.mjs';
|
|
46
|
+
import { twelveFactorReport } from './twelve-factor.mjs';
|
|
47
|
+
import { GENERATORS } from './codegen.mjs';
|
|
48
|
+
import { changeReport } from './changes.mjs';
|
|
49
|
+
import { execSync } from 'node:child_process';
|
|
50
|
+
import { diffGraphs, mergeGraphs } from './semantic-diff.mjs';
|
|
51
|
+
import { applyWaivers, governanceDiagnostics } from './governance.mjs';
|
|
52
|
+
import { exportIntent, EXPORT_FORMATS } from './exporters.mjs';
|
|
53
|
+
import { evaluateDecision, simulateLifecycle } from './runtime.mjs';
|
|
54
|
+
import { importIntent, importReport, detectFormat, IMPORT_FORMATS } from './importers.mjs';
|
|
55
|
+
import { runTests } from './testing.mjs';
|
|
56
|
+
import { evaluateOutcomes } from './outcome.mjs';
|
|
57
|
+
import { analyzeStyle } from './style.mjs';
|
|
58
|
+
import { intentProofJsonSchema, validateProof } from './proof-schema.mjs';
|
|
59
|
+
import { graphToSource } from './graph-source.mjs';
|
|
60
|
+
import { migrateGraph, validateGraph } from './migrate.mjs';
|
|
61
|
+
import { SCHEMA_VERSION, NODE_TYPES, RELATIONSHIP_TYPES, DIAGNOSTIC_RULES, ALL_DIAGNOSTICS, intentGraphJsonSchema } from './intent-schema.mjs';
|
|
62
|
+
import { CLASSIFICATIONS } from './classification.mjs';
|
|
63
|
+
import {
|
|
64
|
+
buildManifest, buildImplementationPrompt, resolveState, productionGate, adoptRegion, parseMarkers,
|
|
65
|
+
contractHash, implementationHash, recordDecision, approvalFor, emptyApprovals, makeEvent,
|
|
66
|
+
} from './ai.mjs';
|
|
67
|
+
import { parseEventLog, serializeEventLog, recordEvent, timeline } from './ai-events.mjs';
|
|
68
|
+
|
|
69
|
+
// Recursively collect supported source files, skipping vendored / build dirs.
|
|
70
|
+
const LIFT_EXTS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.rs', '.pl', '.pm'];
|
|
71
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', '.next', 'build', '.intent', 'coverage', '.vercel']);
|
|
72
|
+
function collectFiles(root, acc = []) {
|
|
73
|
+
for (const name of readdirSync(root)) {
|
|
74
|
+
if (SKIP_DIRS.has(name)) continue;
|
|
75
|
+
const full = join(root, name);
|
|
76
|
+
const st = statSync(full);
|
|
77
|
+
if (st.isDirectory()) collectFiles(full, acc);
|
|
78
|
+
else if (LIFT_EXTS.some((e) => name.endsWith(e)) && !name.endsWith('.d.ts')) acc.push(full);
|
|
79
|
+
}
|
|
80
|
+
return acc;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Walk a directory for authored .intent files (skips the .intent/ output dir).
|
|
84
|
+
function collectIntents(root, acc = []) {
|
|
85
|
+
const st = statSync(root);
|
|
86
|
+
if (!st.isDirectory()) return root.endsWith('.intent') ? [root] : acc;
|
|
87
|
+
for (const name of readdirSync(root)) {
|
|
88
|
+
if (SKIP_DIRS.has(name)) continue;
|
|
89
|
+
const full = join(root, name);
|
|
90
|
+
if (statSync(full).isDirectory()) collectIntents(full, acc);
|
|
91
|
+
else if (name.endsWith('.intent')) acc.push(full);
|
|
92
|
+
}
|
|
93
|
+
return acc;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function parseArgs(argv) {
|
|
97
|
+
const args = { _: [], out: '.intent', noAi: false };
|
|
98
|
+
for (let i = 0; i < argv.length; i++) {
|
|
99
|
+
const a = argv[i];
|
|
100
|
+
if (a === '--out') { args.out = argv[++i]; args.outExplicit = true; }
|
|
101
|
+
else if (a === '--no-ai') args.noAi = true;
|
|
102
|
+
else if (a === '--position') args.position = argv[++i];
|
|
103
|
+
else if (a === '--from') args.from = argv[++i];
|
|
104
|
+
else if (a === '--intent') args.intent = argv[++i];
|
|
105
|
+
else if (a === '--by') args.by = argv[++i];
|
|
106
|
+
else if (a === '--at') args.at = argv[++i];
|
|
107
|
+
else if (a === '--json') args.json = true;
|
|
108
|
+
else if (a === '--targets') args.targets = (argv[++i] || '').split(',').filter(Boolean);
|
|
109
|
+
else if (a === '--product') args.product = argv[++i];
|
|
110
|
+
else if (a === '--allow-pending') args.allowPending = true;
|
|
111
|
+
else if (a === '--mode') args.mode = argv[++i];
|
|
112
|
+
else if (a === '--role') args.role = argv[++i];
|
|
113
|
+
else if (a === '--note') args.note = argv[++i];
|
|
114
|
+
else if (a === '--search') args.search = argv[++i];
|
|
115
|
+
else if (a === '--expand') args.expand = argv[++i];
|
|
116
|
+
else if (a === '--now') args.now = argv[++i];
|
|
117
|
+
else if (a === '--format') args.format = argv[++i];
|
|
118
|
+
else if (a === '--inputs') args.inputs = argv[++i];
|
|
119
|
+
else if (a === '--events') args.events = (argv[++i] || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
120
|
+
else if (a === '--decision') args.decision = argv[++i];
|
|
121
|
+
else if (a === '--to') args.to = argv[++i];
|
|
122
|
+
else if (a === '--force') args.force = true;
|
|
123
|
+
else if (a === '--write' || a === '-w') args.write = true;
|
|
124
|
+
else if (a === '--check') args.check = true;
|
|
125
|
+
else if (a === '--schema') args.schema = true;
|
|
126
|
+
else if (a === '--all') args.all = true;
|
|
127
|
+
else if (a === '--ir') args.ir = argv[++i];
|
|
128
|
+
else if (a === '--subject') args.subject = argv[++i];
|
|
129
|
+
else if (a === '--lens') args.lens = argv[++i];
|
|
130
|
+
else if (a === '--nodes') args.nodes = argv[++i];
|
|
131
|
+
else if (a === '--depth') args.depth = argv[++i];
|
|
132
|
+
else if (a === '--dir') args.dir = argv[++i];
|
|
133
|
+
else if (a === '--observed') args.observed = true;
|
|
134
|
+
else if (a === '--learning') args.learning = true;
|
|
135
|
+
else if (a === '--governed') args.governed = true;
|
|
136
|
+
else if (a === '--target') args.target = argv[++i];
|
|
137
|
+
else if (a === '--brief') args.brief = argv[++i];
|
|
138
|
+
else if (a === '--after') args.after = argv[++i];
|
|
139
|
+
else if (a === '--before') args.before = argv[++i];
|
|
140
|
+
else if (a === '--edits') args.edits = argv[++i];
|
|
141
|
+
else if (a === '--set-goal') args.setGoal = argv[++i];
|
|
142
|
+
else if (a === '--add-guarantee') (args.addGuarantee ||= []).push(argv[++i]);
|
|
143
|
+
else if (a === '--add-never') (args.addNever ||= []).push(argv[++i]);
|
|
144
|
+
else if (a === '--remove-guarantee') (args.removeGuarantee ||= []).push(argv[++i]);
|
|
145
|
+
else if (a === '--remove-never') (args.removeNever ||= []).push(argv[++i]);
|
|
146
|
+
else args._.push(a);
|
|
147
|
+
}
|
|
148
|
+
return args;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const writeJson = (dir, name, obj) => {
|
|
152
|
+
mkdirSync(dir, { recursive: true });
|
|
153
|
+
writeFileSync(join(dir, name), JSON.stringify(obj, null, 2) + '\n');
|
|
154
|
+
return join(dir, name);
|
|
155
|
+
};
|
|
156
|
+
const writeText = (dir, name, text) => {
|
|
157
|
+
mkdirSync(dir, { recursive: true });
|
|
158
|
+
writeFileSync(join(dir, name), text);
|
|
159
|
+
return join(dir, name);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Persist an intent-ai-v1 event to the append-only sink at .intent/ai-events.jsonl.
|
|
163
|
+
// The durable audit trail RM / OpenThunder / Studio can replay. Returns the file path.
|
|
164
|
+
const AI_EVENT_LOG = 'ai-events.jsonl';
|
|
165
|
+
const sinkEvent = (root, event) => {
|
|
166
|
+
const dir = join(root, '.intent');
|
|
167
|
+
const path = join(dir, AI_EVENT_LOG);
|
|
168
|
+
const log = existsSync(path) ? parseEventLog(readFileSync(path, 'utf8')) : undefined;
|
|
169
|
+
mkdirSync(dir, { recursive: true });
|
|
170
|
+
writeFileSync(path, serializeEventLog(recordEvent(log, event)));
|
|
171
|
+
return path;
|
|
172
|
+
};
|
|
173
|
+
const readEventLog = (root) => {
|
|
174
|
+
const path = join(root, '.intent', AI_EVENT_LOG);
|
|
175
|
+
return existsSync(path) ? parseEventLog(readFileSync(path, 'utf8')) : parseEventLog('');
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
function load(file) {
|
|
179
|
+
const source = readFileSync(file, 'utf8');
|
|
180
|
+
const ast = parseIntent(source);
|
|
181
|
+
return { source, ast, sourceHash: sha256(source), sourceFile: basename(file) };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function printDiagnostics(diags) {
|
|
185
|
+
for (const d of diags) {
|
|
186
|
+
const tag = d.waived ? ' (WAIVED)' : '';
|
|
187
|
+
console.log(` [${d.level}]${tag} ${d.code}: ${d.message}`);
|
|
188
|
+
if (d.waived) console.log(` waived by: ${d.waiver.approvedBy} , ${d.waiver.reason}`);
|
|
189
|
+
else if (d.why) console.log(` why: ${d.why}`);
|
|
190
|
+
if (d.fix && d.fix.length) console.log(` fix: ${d.fix[0].label}`);
|
|
191
|
+
}
|
|
192
|
+
// A waived diagnostic is on the record but does not fail the build (governed exception).
|
|
193
|
+
const errors = diags.filter((d) => d.level === 'error' && !d.waived).length;
|
|
194
|
+
const warnings = diags.filter((d) => d.level === 'warning' && !d.waived).length;
|
|
195
|
+
const waived = diags.filter((d) => d.waived).length;
|
|
196
|
+
console.log(` ${errors} error(s), ${warnings} warning(s)${waived ? `, ${waived} waived` : ''}`);
|
|
197
|
+
return errors;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const HELP = `intent , the deterministic ThunderLang compiler (no AI required)
|
|
201
|
+
|
|
202
|
+
usage: intent <command> <file> [options]
|
|
203
|
+
|
|
204
|
+
Author & check
|
|
205
|
+
init [Name] scaffold a runnable starter mission (Name.intent)
|
|
206
|
+
draft --brief <json|-> scaffold a rigorous intent draft + gap checklist from a brief
|
|
207
|
+
check <file|dir> [--json|--format sarif] diagnostics for one file, or gate a whole dir
|
|
208
|
+
report [dir] [--json] repo-wide intent health: severity + area counts, coverage
|
|
209
|
+
scan [dir] [--json] [--ir <path>] Scanner: intent -> Intent IR -> Fable findings -> risk themes
|
|
210
|
+
risks | gaps | unverified | coverage | unknowns | contradictions [dir] [--json] focused scan queries
|
|
211
|
+
focus <mission|query|--nodes a,b> [--depth N] [--dir <d>] [--json] Intent Lens: focused graph + brief
|
|
212
|
+
comprehension <file|dir> [--observed] [--learning] [--governed] [--json] the C0..C7 understanding level
|
|
213
|
+
twelve-factor <file> [--json] score an intent against the 13 humanlayer/12-factor-agents principles
|
|
214
|
+
gen <file> [--target typescript|csharp|java] [--out <dir>] deterministic code scaffold (types + decision logic + TODOs)
|
|
215
|
+
changes [<base>..<head> | <base>] [--json] Change Lens: what a branch/PR changed by meaning
|
|
216
|
+
guardian <before> <after> drift: what changed, what risk, what to reverify, what learning is stale
|
|
217
|
+
impact <base> <proposed> Simulator: estimate a change's blast radius + risk BEFORE building it
|
|
218
|
+
ledger <file.json> [--subject <id>] verify the tamper-evident history + explain why/who/what changed
|
|
219
|
+
guard <file> [--json] preview the runtime guard (redacted fields, enforced decisions)
|
|
220
|
+
fmt <file|dir> [--write|--check] canonical formatting (whitespace only; comments kept)
|
|
221
|
+
edit <file> [--edits <json|->] [--set-goal ..] [--add-guarantee ..] [--write] structural edits, comments kept
|
|
222
|
+
lsp start the Language Server (LSP over stdio, for editors)
|
|
223
|
+
mcp start the MCP server (for AI coding agents; stdio)
|
|
224
|
+
build <file> docs, contract graph, test plan, and .intent-proof.json
|
|
225
|
+
graph <file> the canonical Intent Graph (intent-graph-v1)
|
|
226
|
+
proof <file> the .intent-proof.json artifact
|
|
227
|
+
proof --schema emit the canonical proof envelope JSON Schema (intent-proof-v1)
|
|
228
|
+
verify <proof.json> [src] confirm a proof is well-formed and still matches its source
|
|
229
|
+
schema emit the canonical graph schema + diagnostic catalog
|
|
230
|
+
explain <IL-CODE> explain a diagnostic code (area, severity, what it blocks)
|
|
231
|
+
rules [--json] list the whole canonical diagnostic catalog
|
|
232
|
+
notes <file> [--lens <lens>] [--json] IntentLens: the compiled note blocks by lens (not verification)
|
|
233
|
+
docs <file> [--lens <lens>] [--out <dir>] render a mission as Markdown docs (per-audience with --lens)
|
|
234
|
+
code-actions <file> [--json] available quick-fixes, safety-graded (safe | reviewable)
|
|
235
|
+
apply-fix <file> [--write] apply the SAFE autocorrects (header aliases, stray colons)
|
|
236
|
+
|
|
237
|
+
Execute (no AI, no generated code)
|
|
238
|
+
run <file> --inputs '<json>' evaluate the decision(s) against inputs
|
|
239
|
+
simulate <file> --events a,b,c walk the lifecycle(s) over events
|
|
240
|
+
test <file> run the in-file test blocks (case/scenario)
|
|
241
|
+
outcomes <file> evaluate outcome contracts vs delivery results
|
|
242
|
+
style <file> resolve style intents vs the canonical token space
|
|
243
|
+
|
|
244
|
+
Interop
|
|
245
|
+
export <file> --format <dmn|bpmn|smv|jsonschema|openapi|tokens|mermaid|css|playwright> render to a standard format
|
|
246
|
+
import <file> [--format dmn|bpmn] [--json] lift DMN/BPMN into intent
|
|
247
|
+
source <file|graph.json> regenerate .intent from a graph
|
|
248
|
+
migrate <graph.json> [--to <version>] upgrade a persisted graph
|
|
249
|
+
validate <graph.json> [--json] check a graph is canonical (anti-fork)
|
|
250
|
+
|
|
251
|
+
Navigate & compare (over many missions)
|
|
252
|
+
atlas <dir> [--search q | --expand id] the whole-system Atlas
|
|
253
|
+
index <dir> the Mission Atlas inventory
|
|
254
|
+
diff <before> <after> semantic diff (by meaning)
|
|
255
|
+
merge <base> <ours> <theirs> deterministic 3-way semantic merge
|
|
256
|
+
|
|
257
|
+
Code <-> intent
|
|
258
|
+
lift <file> [--from <lang>] lift source code into inferred intent
|
|
259
|
+
approve <file> --by <name> approve intent (drift baseline)
|
|
260
|
+
drift <file> --from <code> check intent vs code drift
|
|
261
|
+
verify-diff <intent> --after <code> [--before <code>] gate a code change against its intent
|
|
262
|
+
handoff <file> the OpenThunder drift handoff
|
|
263
|
+
|
|
264
|
+
Common options: --out <dir>, --json, --no-ai. See https://thunderlang.dev/docs`;
|
|
265
|
+
|
|
266
|
+
function main() {
|
|
267
|
+
const [cmd, ...restArgv] = process.argv.slice(2);
|
|
268
|
+
const args = parseArgs(restArgv);
|
|
269
|
+
const file = args._[0];
|
|
270
|
+
if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
271
|
+
console.log(HELP);
|
|
272
|
+
process.exit(cmd ? 0 : 2);
|
|
273
|
+
}
|
|
274
|
+
// `schema` takes no file (it emits the canonical Intent Graph schema).
|
|
275
|
+
if (cmd === 'schema') {
|
|
276
|
+
const out = {
|
|
277
|
+
schemaVersion: SCHEMA_VERSION, nodeTypes: NODE_TYPES, relationshipTypes: RELATIONSHIP_TYPES,
|
|
278
|
+
classifications: CLASSIFICATIONS, diagnostics: DIAGNOSTIC_RULES, jsonSchema: intentGraphJsonSchema(),
|
|
279
|
+
};
|
|
280
|
+
if (args.out && args.out !== '.intent') { const p = writeJson(args.out, 'intent-graph.schema.json', out); console.log(`wrote ${p.replace(process.cwd() + '/', '')}`); }
|
|
281
|
+
else console.log(JSON.stringify(out, null, 2));
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
// `intent proof --schema` emits the canonical proof envelope JSON Schema (no file needed).
|
|
285
|
+
// This is the "shared envelope" schema siblings sign (STW) and re-verify (RM/OT) against.
|
|
286
|
+
if (cmd === 'proof' && args.schema) {
|
|
287
|
+
console.log(JSON.stringify(intentProofJsonSchema(), null, 2));
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
// Verify a .intent-proof.json against its source: the source hash still matches (no
|
|
291
|
+
// drift / tampering) and the proof's claims re-derive from the source.
|
|
292
|
+
if (cmd === 'verify') {
|
|
293
|
+
const proofPath = args._[0];
|
|
294
|
+
if (!proofPath) { console.error('usage: intent verify <proof.json> [<source.intent>]'); process.exit(2); return; }
|
|
295
|
+
let proof;
|
|
296
|
+
try { proof = JSON.parse(readFileSync(proofPath, 'utf8')); } catch { console.error('intent verify: proof is not valid JSON'); process.exit(2); return; }
|
|
297
|
+
const srcPath = args._[1] || proof.sourceFile;
|
|
298
|
+
if (!srcPath || !existsSync(srcPath)) { console.error(`intent verify: source not found (${srcPath || 'none'}). Pass it: intent verify <proof.json> <source.intent>`); process.exit(2); return; }
|
|
299
|
+
const src = readFileSync(srcPath, 'utf8');
|
|
300
|
+
const ast = parseIntent(src);
|
|
301
|
+
const diags = semanticDiagnostics(ast);
|
|
302
|
+
const hashMatch = sha256(src) === proof.sourceHash;
|
|
303
|
+
const semanticNow = !diags.some((d) => d.level === 'error');
|
|
304
|
+
const semanticMatch = proof.verification ? semanticNow === !!proof.verification.semanticPassed : true;
|
|
305
|
+
const guaranteesMatch = !Array.isArray(proof.guarantees) || proof.guarantees.length === (ast.guarantees || []).length;
|
|
306
|
+
const neverMatch = !Array.isArray(proof.neverRules) || proof.neverRules.length === (ast.neverRules || []).length;
|
|
307
|
+
// Structural gate: the envelope must be a well-formed intent-proof-v1 document first.
|
|
308
|
+
const structure = validateProof(proof);
|
|
309
|
+
const valid = structure.valid && hashMatch && semanticMatch && guaranteesMatch && neverMatch;
|
|
310
|
+
const result = { schema: 'intent-verify-v1', proof: proofPath, source: srcPath, valid, checks: { wellFormed: structure.valid, hashMatch, semanticMatch, guaranteesMatch, neverMatch }, structureErrors: structure.errors };
|
|
311
|
+
if (args.json) { console.log(JSON.stringify(result, null, 2)); process.exit(valid ? 0 : 1); return; }
|
|
312
|
+
console.log(`intent verify ${basename(proofPath)}: ${valid ? 'VALID' : 'FAILED'} (source ${basename(srcPath)})`);
|
|
313
|
+
if (!structure.valid) { console.log(` X proof is not a well-formed intent-proof-v1 document:`); for (const e of structure.errors) console.log(` ${e.path || '(root)'}: ${e.message}`); }
|
|
314
|
+
if (!hashMatch) console.log(' X source hash does not match , the source has changed since the proof was generated (drift or tampering).');
|
|
315
|
+
if (!semanticMatch) console.log(' X the proof claims a different semantic result than the source produces now.');
|
|
316
|
+
if (!guaranteesMatch) console.log(' X guarantee count differs from the proof.');
|
|
317
|
+
if (!neverMatch) console.log(' X never-rule count differs from the proof.');
|
|
318
|
+
if (valid) console.log(` ok proof matches source (hash + ${(proof.guarantees || []).length} guarantee(s), ${(proof.neverRules || []).length} never-rule(s)).`);
|
|
319
|
+
process.exit(valid ? 0 : 1);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Explain a diagnostic code from the canonical catalog. `intent explain IL-DEC-001`.
|
|
324
|
+
if (cmd === 'explain') {
|
|
325
|
+
const code = file;
|
|
326
|
+
if (!code) { console.error('usage: intent explain <IL-CODE>'); process.exit(2); return; }
|
|
327
|
+
const rule = ALL_DIAGNOSTICS.find((r) => r.ruleId.toLowerCase() === code.toLowerCase());
|
|
328
|
+
if (args.json) { console.log(JSON.stringify(rule || { ruleId: code, found: false }, null, 2)); process.exit(rule ? 0 : 1); return; }
|
|
329
|
+
if (!rule) { console.error(`intent explain: "${code}" is not in the diagnostic catalog. Run "intent rules" for the full list.`); process.exit(1); return; }
|
|
330
|
+
console.log(`${rule.ruleId} (area: ${rule.area})`);
|
|
331
|
+
console.log(` ${rule.summary}`);
|
|
332
|
+
console.log(` severity: ${rule.severity}${rule.blocks && rule.blocks.length ? ` | blocks: ${rule.blocks.join(', ')}` : ' | does not block a phase'}`);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// The whole canonical diagnostic catalog (IL owns it; editors/CI/OT consume it).
|
|
337
|
+
if (cmd === 'rules') {
|
|
338
|
+
if (args.json) { console.log(JSON.stringify(ALL_DIAGNOSTICS, null, 2)); return; }
|
|
339
|
+
const byArea = {};
|
|
340
|
+
for (const r of ALL_DIAGNOSTICS) (byArea[r.area] ||= []).push(r);
|
|
341
|
+
console.log(`intent rules: ${ALL_DIAGNOSTICS.length} diagnostics in ${Object.keys(byArea).length} areas\n`);
|
|
342
|
+
for (const area of Object.keys(byArea).sort()) {
|
|
343
|
+
console.log(`${area}`);
|
|
344
|
+
for (const r of byArea[area]) {
|
|
345
|
+
const blocks = r.blocks && r.blocks.length ? `blocks ${r.blocks.join(', ')}` : 'non-blocking';
|
|
346
|
+
console.log(` ${r.ruleId.padEnd(16)} [${r.severity}, ${blocks}] ${r.summary}`);
|
|
347
|
+
}
|
|
348
|
+
console.log();
|
|
349
|
+
}
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// `intent notes <file> [--lens <lens>] [--json]` , the IntentLens report: the compiled
|
|
354
|
+
// semantic comments (`note <lens>:`) grouped by lens, each with its target and source
|
|
355
|
+
// line. Notes explain meaning for a reader; they are NEVER verification.
|
|
356
|
+
if (cmd === 'notes') {
|
|
357
|
+
if (!file) { console.error('usage: intent notes <file> [--lens <lens>] [--json]'); process.exit(2); return; }
|
|
358
|
+
const ast = parseIntent(readFileSync(file, 'utf8'));
|
|
359
|
+
let notes = ast.notes || [];
|
|
360
|
+
if (args.lens) notes = notes.filter((n) => n.lens === args.lens);
|
|
361
|
+
if (args.json) {
|
|
362
|
+
console.log(JSON.stringify({ schema: 'intent-notes-v1', mission: ast.mission || null, lens: args.lens || null, count: notes.length, notes }, null, 2));
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const scope = args.lens ? ` (lens: ${args.lens})` : '';
|
|
366
|
+
console.log(`intent notes ${basename(file)}${scope}: ${notes.length} note${notes.length === 1 ? '' : 's'}`);
|
|
367
|
+
const known = new Set(KNOWN_LENSES);
|
|
368
|
+
const byLens = {};
|
|
369
|
+
for (const n of notes) (byLens[n.lens] ||= []).push(n);
|
|
370
|
+
for (const lens of Object.keys(byLens).sort()) {
|
|
371
|
+
console.log(`\n${lens}${known.has(lens) ? '' : ' (unknown lens)'}`);
|
|
372
|
+
for (const n of byLens[lens]) {
|
|
373
|
+
const label = n.targetKind === 'mission' ? (ast.mission || 'mission') : n.targetPath.split('.').slice(3).join('.');
|
|
374
|
+
console.log(` [${n.targetKind}] ${label} , line ${n.sourceSpan.line}`);
|
|
375
|
+
for (const line of String(n.text).split('\n')) console.log(` ${line.trim()}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// `intent docs <file> [--lens <lens>] [--out <dir>]` , render a mission as Markdown docs.
|
|
382
|
+
// With --lens, produce an audience-specific doc with that lens's notes woven inline.
|
|
383
|
+
if (cmd === 'docs') {
|
|
384
|
+
if (!file) { console.error('usage: intent docs <file> [--lens <lens>] [--out <dir>]'); process.exit(2); return; }
|
|
385
|
+
const ast = parseIntent(readFileSync(file, 'utf8'));
|
|
386
|
+
const md = args.lens ? renderLensDoc(ast, args.lens) : renderMarkdown(ast);
|
|
387
|
+
if (args.outExplicit) {
|
|
388
|
+
const suffix = args.lens ? `.${args.lens}` : '';
|
|
389
|
+
const p = writeText(args.out, `${slug(ast.mission)}${suffix}.md`, md.endsWith('\n') ? md : md + '\n');
|
|
390
|
+
console.log(`wrote ${p.replace(process.cwd() + '/', '')}`);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
console.log(md);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// `intent code-actions <file> [--json]` , the available quick-fixes, each safety-graded
|
|
398
|
+
// (safe autocorrects + reviewable diagnostic fixes). The IDE lightbulb's data source.
|
|
399
|
+
if (cmd === 'code-actions') {
|
|
400
|
+
if (!file) { console.error('usage: intent code-actions <file> [--json]'); process.exit(2); return; }
|
|
401
|
+
const source = readFileSync(file, 'utf8');
|
|
402
|
+
const actions = getCodeActions(source, semanticDiagnostics(parseIntent(source)));
|
|
403
|
+
if (args.json) { console.log(JSON.stringify({ schema: 'intent-code-actions-v1', count: actions.length, actions }, null, 2)); return; }
|
|
404
|
+
console.log(`intent code-actions ${basename(file)}: ${actions.length} action${actions.length === 1 ? '' : 's'}`);
|
|
405
|
+
for (const a of actions) console.log(` [${a.safety}] ${a.kind}${a.line ? ` (line ${a.line})` : ''} ${a.title}`);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// `intent apply-fix <file> [--write]` , apply the SAFE autocorrects only (header aliases,
|
|
410
|
+
// stray colons). Reviewable quick-fixes are reported, never applied blindly.
|
|
411
|
+
if (cmd === 'apply-fix') {
|
|
412
|
+
if (!file) { console.error('usage: intent apply-fix <file> [--write]'); process.exit(2); return; }
|
|
413
|
+
const source = readFileSync(file, 'utf8');
|
|
414
|
+
const { fixed, changes } = autocorrectSource(source);
|
|
415
|
+
const reviewable = getCodeActions(source, semanticDiagnostics(parseIntent(source))).filter((a) => a.safety !== 'safe');
|
|
416
|
+
if (args.json) { console.log(JSON.stringify({ applied: changes, reviewableRemaining: reviewable.length, changed: fixed !== source }, null, 2)); }
|
|
417
|
+
else {
|
|
418
|
+
console.log(`intent apply-fix ${basename(file)}: ${changes.length} safe fix${changes.length === 1 ? '' : 'es'}${args.write ? ' applied' : ' (dry run; pass --write)'}`);
|
|
419
|
+
for (const c of changes) console.log(` line ${c.line}: "${c.from}" -> "${c.to}" [${c.rule}]`);
|
|
420
|
+
if (reviewable.length) console.log(` ${reviewable.length} reviewable quick-fix(es) left for a human (run: intent code-actions ${basename(file)})`);
|
|
421
|
+
}
|
|
422
|
+
if (args.write && fixed !== source) { writeFileSync(file, fixed); if (!args.json) console.log(` wrote ${basename(file)}`); }
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// `intent focus <mission|query|--nodes a,b> [--depth N] [--dir <d>] [--json]` , Intent Lens:
|
|
427
|
+
// a focused Focus Graph + Intent Brief around a selected scope, built over the Atlas.
|
|
428
|
+
if (cmd === 'focus') {
|
|
429
|
+
const dir = args.dir || '.';
|
|
430
|
+
const files = existsSync(dir) && statSync(dir).isDirectory() ? collectIntents(dir) : (file && existsSync(file) ? [file] : []);
|
|
431
|
+
if (!files.length) { console.error(`intent focus: no .intent files under ${dir}`); process.exit(2); return; }
|
|
432
|
+
const atlas = buildAtlas(files.map((f) => buildIntentGraph(parseIntent(readFileSync(f, 'utf8')))));
|
|
433
|
+
// Resolve seeds: --nodes ids, or a mission/feature query, or (default) all missions.
|
|
434
|
+
let seeds = [];
|
|
435
|
+
let scopeType = 'custom';
|
|
436
|
+
let scopeTitle = null;
|
|
437
|
+
if (args.nodes) { seeds = args.nodes.split(',').map((s) => s.trim()).filter(Boolean); scopeType = 'custom'; scopeTitle = `${seeds.length} selected node(s)`; }
|
|
438
|
+
else if (file && !existsSync(file)) {
|
|
439
|
+
const hit = searchAtlas(atlas, file)[0];
|
|
440
|
+
if (!hit) { console.error(`intent focus: no Atlas node matches "${file}"`); process.exit(1); return; }
|
|
441
|
+
seeds = [hit.id]; scopeType = hit.type === 'Mission' ? 'mission' : 'feature'; scopeTitle = hit.title || hit.id;
|
|
442
|
+
} else { seeds = atlas.missions.map((m) => m.id); scopeType = 'capability'; scopeTitle = 'whole project'; }
|
|
443
|
+
const scope = makeScope({ type: scopeType, title: scopeTitle, seeds, createdAt: null });
|
|
444
|
+
const focus = buildFocusGraph(atlas, { seeds, depth: args.depth ? Number(args.depth) : 2, scope });
|
|
445
|
+
const brief = intentBrief(focus);
|
|
446
|
+
if (args.json) { console.log(JSON.stringify({ scope, brief, focus }, null, 2)); return; }
|
|
447
|
+
console.log(`intent focus , ${scope.title} [${scope.type}] (${scope.scopeId})`);
|
|
448
|
+
console.log(` what: ${brief.what || '(unnamed)'}${brief.confidence ? ` confidence: ${brief.confidence}` : ''}`);
|
|
449
|
+
if (brief.who.length) console.log(` who: ${brief.who.join(', ')}`);
|
|
450
|
+
console.log(` focus graph: ${focus.overview.nodes} node(s), ${focus.overview.relationships} edge(s), depth ${focus.depth}`);
|
|
451
|
+
const br = focus.overview.byReason;
|
|
452
|
+
console.log(` by reason: ${Object.entries(br).map(([k, v]) => `${v} ${k}`).join(', ')}`);
|
|
453
|
+
if (brief.guarantees.length) console.log(` guarantees: ${brief.guarantees.length} prohibitions: ${brief.prohibitions.length} verification: ${brief.verification}`);
|
|
454
|
+
if (brief.risks) console.log(` risks in scope: ${brief.risks}`);
|
|
455
|
+
if (brief.unknowns.length) console.log(` unknowns: ${brief.unknowns.join('; ')}`);
|
|
456
|
+
if (brief.needsReview) console.log(' note: scope includes low-confidence inferred intent , review before trusting.');
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// `intent comprehension <file|dir> [--observed] [--learning] [--governed] [--json]` , the C0..C7
|
|
461
|
+
// understanding level of each mission (Comprehension Contract). IL scores the intent side (C1..C4);
|
|
462
|
+
// --observed (OpenThunder/runtime), --learning (Skills Tech Talk), --governed (Guardian) lift the
|
|
463
|
+
// joint level to C5/C6/C7 when a sibling attaches its evidence.
|
|
464
|
+
if (cmd === 'comprehension') {
|
|
465
|
+
const root = file || '.';
|
|
466
|
+
const files = existsSync(root) && statSync(root).isDirectory() ? collectIntents(root) : (existsSync(root) ? [root] : []);
|
|
467
|
+
if (!files.length) { console.error(`intent comprehension: no .intent files under ${root}`); process.exit(2); return; }
|
|
468
|
+
const opts = { observed: !!args.observed, learningPath: !!args.learning, governed: !!args.governed };
|
|
469
|
+
const asts = files.map((f) => parseIntent(readFileSync(f, 'utf8')));
|
|
470
|
+
const report = comprehensionReport(asts, opts);
|
|
471
|
+
if (args.json) { console.log(JSON.stringify(report, null, 2)); return; }
|
|
472
|
+
console.log(`intent comprehension ${root}: ${report.count} mission(s)`);
|
|
473
|
+
console.log(` distribution: ${COMPREHENSION_LEVELS.map((l) => `${l.level}:${report.byLevel[l.level]}`).join(' ')}`);
|
|
474
|
+
for (const m of report.missions) {
|
|
475
|
+
console.log(`\n ${m.mission || '(unnamed)'} , ${m.level} ${m.levelName}`);
|
|
476
|
+
console.log(` ${m.means}`);
|
|
477
|
+
if (m.missing.length) {
|
|
478
|
+
const next = m.missing[0];
|
|
479
|
+
console.log(` next: reach ${next.level} ${next.name} , ${next.need} [${next.owner}]`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// `intent twelve-factor <file> [--json]` , score an intent against the 13 principles of
|
|
486
|
+
// humanlayer/12-factor-agents (deterministic conformance lens). Per-factor verdict + score.
|
|
487
|
+
if (cmd === 'twelve-factor' || cmd === '12factor') {
|
|
488
|
+
if (!file) { console.error('usage: intent twelve-factor <file> [--json]'); process.exit(2); return; }
|
|
489
|
+
const ast = parseIntent(readFileSync(file, 'utf8'));
|
|
490
|
+
const report = twelveFactorReport(ast);
|
|
491
|
+
if (args.json) { console.log(JSON.stringify(report, null, 2)); return; }
|
|
492
|
+
const mark = { satisfied: 'ok ', partial: '~ ', absent: 'MISS' };
|
|
493
|
+
console.log(`12-factor conformance: ${report.subject || '(unnamed)'} , ${report.score}/100 (${report.grade})`);
|
|
494
|
+
console.log(` ${report.counts.satisfied} satisfied · ${report.counts.partial} partial · ${report.counts.absent} absent\n`);
|
|
495
|
+
for (const f of report.factors) {
|
|
496
|
+
console.log(` [${mark[f.verdict]}] F${String(f.factor).padStart(2)} ${f.name}`);
|
|
497
|
+
console.log(` ${f.evidence}`);
|
|
498
|
+
if (f.fix) console.log(` fix: ${f.fix}`);
|
|
499
|
+
}
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// `intent gen <file> [--target typescript|csharp|java] [--out <dir>]` , deterministic code scaffold from
|
|
504
|
+
// intent. Generates the typed contract + the decision logic (already executable) and leaves
|
|
505
|
+
// honest TODO markers for business logic. No AI. Prints, or writes with --out.
|
|
506
|
+
if (cmd === 'gen') {
|
|
507
|
+
if (!file) { console.error(`usage: intent gen <file> [--target ${Object.keys(GENERATORS).join('|')}] [--out <dir>]`); process.exit(2); return; }
|
|
508
|
+
const target = (args.target || 'typescript').toLowerCase();
|
|
509
|
+
const generate = GENERATORS[target];
|
|
510
|
+
if (!generate) { console.error(`intent gen: unknown target "${target}" (have: ${Object.keys(GENERATORS).join(', ')})`); process.exit(2); return; }
|
|
511
|
+
const ast = parseIntent(readFileSync(file, 'utf8'));
|
|
512
|
+
const code = generate(ast);
|
|
513
|
+
if (args.outExplicit) {
|
|
514
|
+
const ext = target.startsWith('ts') || target === 'typescript' ? 'ts' : target;
|
|
515
|
+
const p = writeText(args.out, `${slug(subjectName(ast) || 'intent')}.${ext}`, code.endsWith('\n') ? code : code + '\n');
|
|
516
|
+
console.log(`wrote ${p.replace(process.cwd() + '/', '')}`);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
console.log(code);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// `intent changes [<base>..<head> | <base>] [--json]` , Change Lens: what a branch / PR / commit
|
|
524
|
+
// range changed BY MEANING. git-diffs the .intent files, semantic-diffs each, and reports the
|
|
525
|
+
// behavior-level changes + regression risk. Default: HEAD vs the working tree.
|
|
526
|
+
if (cmd === 'changes') {
|
|
527
|
+
const git = (c) => { try { return execSync(`git ${c}`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }); } catch { return null; } };
|
|
528
|
+
if (git('rev-parse --is-inside-work-tree') === null) { console.error('intent changes: not a git repository'); process.exit(2); return; }
|
|
529
|
+
const range = file || '';
|
|
530
|
+
let base, head; // head === null means "the working tree"
|
|
531
|
+
if (range.includes('..')) { [base, head] = range.split('..'); head = head || 'HEAD'; }
|
|
532
|
+
else if (range) { base = range; head = null; }
|
|
533
|
+
else { base = 'HEAD'; head = null; }
|
|
534
|
+
const diffSpec = head ? `${base} ${head}` : base;
|
|
535
|
+
const names = (git(`diff --name-only ${diffSpec} -- "*.intent"`) || '').split('\n').map((s) => s.trim()).filter(Boolean);
|
|
536
|
+
if (!names.length) { console.log(`intent changes ${range || `${base}..working-tree`}: no .intent files changed`); return; }
|
|
537
|
+
const readAt = (ref, p) => (ref === null ? (existsSync(p) ? readFileSync(p, 'utf8') : null) : git(`show ${ref}:${p}`));
|
|
538
|
+
const toGraph = (src) => (src != null ? buildIntentGraph(parseIntent(src)) : null);
|
|
539
|
+
const pairs = names.map((p) => ({ path: p, before: toGraph(readAt(base, p)), after: toGraph(readAt(head, p)) }));
|
|
540
|
+
const report = changeReport(pairs);
|
|
541
|
+
if (args.json) { console.log(JSON.stringify(report, null, 2)); process.exit(report.verdict === 'review' ? 1 : 0); return; }
|
|
542
|
+
const t = report.totals;
|
|
543
|
+
console.log(`intent changes ${range || `${base}..working-tree`}: ${report.verdict.toUpperCase()}`);
|
|
544
|
+
console.log(` ${t.files} file(s) , +${t.added} / -${t.removed} / ~${t.changed} nodes${t.invalidatedApprovals ? `, ${t.invalidatedApprovals} approval(s) invalidated` : ''}`);
|
|
545
|
+
if (report.regressions.length) {
|
|
546
|
+
console.log(' regression risk (a promise or its proof was removed or weakened):');
|
|
547
|
+
for (const r of report.regressions) console.log(` - ${r.kind === 'weakened' ? 'weakened' : 'removed'} ${r.thing}: ${r.title}`);
|
|
548
|
+
}
|
|
549
|
+
const nonReg = report.highlights.filter((h) => !(h.kind === 'removed' && report.regressions.includes(h)));
|
|
550
|
+
for (const h of nonReg.slice(0, 12)) console.log(` ${h.kind === 'added' ? '+' : h.kind === 'removed' ? '-' : '~'} ${h.kind} ${h.thing}: ${h.title}`);
|
|
551
|
+
process.exit(report.verdict === 'review' ? 1 : 0);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Language Server (LSP over stdio) for editors. Long-running; no file argument.
|
|
556
|
+
if (cmd === 'lsp') {
|
|
557
|
+
startLspServer();
|
|
558
|
+
return; // keep the process alive on stdin
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// MCP server (Model Context Protocol over stdio) , makes ThunderLang a native tool for AI
|
|
562
|
+
// coding agents. Long-running; no file argument. Point an MCP client at `intent mcp`.
|
|
563
|
+
if (cmd === 'mcp') {
|
|
564
|
+
startMcpServer();
|
|
565
|
+
return; // keep the process alive on stdin
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Scaffold a runnable starter mission (deterministic, no AI). `intent init [Name]`.
|
|
569
|
+
if (cmd === 'init') {
|
|
570
|
+
const name = (file || 'Mission').replace(/\.intent$/i, '');
|
|
571
|
+
const target = join(args.out && args.out !== '.intent' ? args.out : '.', `${name}.intent`);
|
|
572
|
+
if (existsSync(target) && !args.force) {
|
|
573
|
+
console.error(`intent init: ${target} already exists (use --force to overwrite).`);
|
|
574
|
+
process.exit(1); return;
|
|
575
|
+
}
|
|
576
|
+
const starter = `mission ${name}
|
|
577
|
+
use product
|
|
578
|
+
|
|
579
|
+
goal
|
|
580
|
+
Describe what this mission must achieve.
|
|
581
|
+
|
|
582
|
+
guarantee an example property that must always hold
|
|
583
|
+
because state why it matters
|
|
584
|
+
verify a test that proves it
|
|
585
|
+
|
|
586
|
+
never
|
|
587
|
+
do something this mission must never do
|
|
588
|
+
|
|
589
|
+
# A runnable decision. Try: intent run ${name}.intent --inputs '{"age":20}'
|
|
590
|
+
decision Example
|
|
591
|
+
inputs
|
|
592
|
+
age
|
|
593
|
+
rule adult
|
|
594
|
+
when age >= 18
|
|
595
|
+
return Allowed
|
|
596
|
+
default
|
|
597
|
+
return Blocked
|
|
598
|
+
|
|
599
|
+
# Tests live in the file. Try: intent test ${name}.intent
|
|
600
|
+
test Example
|
|
601
|
+
case adult
|
|
602
|
+
given age 20
|
|
603
|
+
expect Allowed
|
|
604
|
+
case minor
|
|
605
|
+
given age 10
|
|
606
|
+
expect Blocked
|
|
607
|
+
`;
|
|
608
|
+
if (target.includes('/')) mkdirSync(dirname(target), { recursive: true });
|
|
609
|
+
writeFileSync(target, starter);
|
|
610
|
+
console.log(`intent init: wrote ${target}`);
|
|
611
|
+
console.log(` next: intent check ${target} | intent run ${target} --inputs '{"age":20}' | intent test ${target}`);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (!file && cmd !== 'draft') {
|
|
616
|
+
console.error(`intent ${cmd}: missing a file argument. Run "intent help" for usage.`);
|
|
617
|
+
process.exit(2);
|
|
618
|
+
}
|
|
619
|
+
// IntentLift: lift source CODE into inferred .intent drafts (not intent parsing).
|
|
620
|
+
if (cmd === 'lift') {
|
|
621
|
+
// Repo mode: walk a directory, lift each file, emit drafts + a repo summary.
|
|
622
|
+
if (args.from === 'repo') {
|
|
623
|
+
const root = file;
|
|
624
|
+
const files = collectFiles(root).map((f) => ({ file: relative(root, f), source: readFileSync(f, 'utf8') }));
|
|
625
|
+
// Per-file language auto-detection (unless --from overrides). "repo" is not a language.
|
|
626
|
+
const override = args.from && args.from !== 'repo' ? args.from : undefined;
|
|
627
|
+
const res = liftRepo(files, { language: override });
|
|
628
|
+
const outputs = res.missions.map((m) => ({
|
|
629
|
+
mission: m.mission,
|
|
630
|
+
file: args.out ? join(args.out, m.outName) : null,
|
|
631
|
+
confidence: m.summary.confidence, reviewed: false,
|
|
632
|
+
evidenceCount: m.summary.evidenceCount, unknowns: m.summary.unknowns,
|
|
633
|
+
}));
|
|
634
|
+
if (args.json) {
|
|
635
|
+
const { missions, ...rest } = res; void missions;
|
|
636
|
+
console.log(JSON.stringify({ sourceRoot: root, ...rest, outputs }, null, 2));
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
if (args.out) {
|
|
640
|
+
for (const m of res.missions) writeText(args.out, m.outName, m.intentText);
|
|
641
|
+
console.log(`intent lift repo ${root} -> ${res.missionsGenerated} mission(s) in ${args.out}`);
|
|
642
|
+
console.log(` confidence: ${JSON.stringify(res.confidenceSummary)} | ${res.unknowns} unknown(s) total`);
|
|
643
|
+
} else {
|
|
644
|
+
console.log(`intent lift repo ${root}: ${res.missionsGenerated} mission(s)`);
|
|
645
|
+
for (const m of res.missions) console.log(` ${m.mission} (${m.summary.confidence}) <- ${m.sourceFile}`);
|
|
646
|
+
}
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// --all: lift EVERY function into its own mission (the Intent Atlas view of a file).
|
|
651
|
+
if (args.all) {
|
|
652
|
+
const src = readFileSync(file, 'utf8');
|
|
653
|
+
const res = liftAll(src, { language: args.from || languageForFile(file), file: basename(file) });
|
|
654
|
+
if (!res.ok) { console.error(res.error); process.exit(1); }
|
|
655
|
+
if (args.json) { console.log(JSON.stringify(res, null, 2)); return; }
|
|
656
|
+
console.log(`intent lift --all ${basename(file)}: ${res.count} mission(s) inferred`);
|
|
657
|
+
for (const m of res.missions) console.log(` ${m.mission} (${m.fn}, confidence ${m.confidence})`);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Single-file mode.
|
|
662
|
+
const src = readFileSync(file, 'utf8');
|
|
663
|
+
const res = liftSource(src, { language: args.from || 'typescript', file: basename(file) });
|
|
664
|
+
if (!res.ok) { console.error(res.error); process.exit(1); }
|
|
665
|
+
if (args.json) { console.log(JSON.stringify(res.summary, null, 2)); return; }
|
|
666
|
+
if (args.out) {
|
|
667
|
+
const p = writeText(args.out, `${slug(res.lifted.mission)}.intent`, res.intentText);
|
|
668
|
+
console.log(`intent lift ${basename(file)} -> ${p.replace(process.cwd() + '/', '')}`);
|
|
669
|
+
} else {
|
|
670
|
+
console.log(res.intentText);
|
|
671
|
+
}
|
|
672
|
+
printDiagnostics(res.diagnostics);
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Approve an inferred/reviewed intent: reviewed:true + source hash + reviewer.
|
|
677
|
+
if (cmd === 'approve') {
|
|
678
|
+
const text = readFileSync(file, 'utf8');
|
|
679
|
+
const res = approveIntent(text, { approvedBy: args.by || null, approvedAt: args.at || null });
|
|
680
|
+
// args.out defaults to '.intent'; approve writes back to the file unless a real --out was given.
|
|
681
|
+
const target = args.out && args.out !== '.intent' ? args.out : file;
|
|
682
|
+
writeFileSync(target, res.text);
|
|
683
|
+
console.log(`intent approve ${basename(file)} -> reviewed: true (${res.approval.source_hash.slice(0, 24)}...)`);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Handoff: emit the il-to-ot-drift-v1 pack OpenThunder consumes for deep verification.
|
|
688
|
+
if (cmd === 'handoff') {
|
|
689
|
+
const text = readFileSync(file, 'utf8');
|
|
690
|
+
const pack = buildDriftHandoff(text, { generatedAt: args.at || null });
|
|
691
|
+
const out = JSON.stringify(pack, null, 2);
|
|
692
|
+
if (args.out && args.out !== '.intent') {
|
|
693
|
+
const p = writeText(args.out, `${slug(pack.mission)}.drift-handoff.json`, out);
|
|
694
|
+
console.log(`intent handoff ${basename(file)} -> ${p.replace(process.cwd() + '/', '')} (kind ${pack.kind}, approved ${pack.approved})`);
|
|
695
|
+
} else {
|
|
696
|
+
console.log(out);
|
|
697
|
+
}
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Drift: does the implementation still satisfy the approved intent?
|
|
702
|
+
if (cmd === 'drift') {
|
|
703
|
+
if (!args.intent) { console.error('usage: intent drift <codeFile> --intent <file.intent> [--from <lang>] [--json]'); process.exit(2); }
|
|
704
|
+
const intentText = readFileSync(args.intent, 'utf8');
|
|
705
|
+
const codeText = readFileSync(file, 'utf8');
|
|
706
|
+
const language = args.from && args.from !== 'repo' ? args.from : languageForFile(file);
|
|
707
|
+
const res = checkDrift(intentText, codeText, { language });
|
|
708
|
+
if (args.json) { console.log(JSON.stringify(res, null, 2)); }
|
|
709
|
+
else {
|
|
710
|
+
console.log(`intent drift: ${res.status.toUpperCase()} (${res.summary.blocking} blocking)`);
|
|
711
|
+
for (const f of res.findings) console.log(` [${f.level}] ${f.code}: ${f.message}`);
|
|
712
|
+
}
|
|
713
|
+
process.exit(res.status === 'drift' ? 1 : 0);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Intent AI implementations (intent-ai-v1): declare + list + manifest + prompt handoff.
|
|
717
|
+
if (cmd === 'ai') {
|
|
718
|
+
const sub = args._[0];
|
|
719
|
+
const target = args._[1] || '.';
|
|
720
|
+
if (sub === 'list') {
|
|
721
|
+
const root = target;
|
|
722
|
+
const parsed = collectIntents(root).map((f) => {
|
|
723
|
+
const source = readFileSync(f, 'utf8');
|
|
724
|
+
return { path: relative(root, f), source, ast: parseIntent(source) };
|
|
725
|
+
});
|
|
726
|
+
const manifest = buildManifest(parsed, { projectId: args.product });
|
|
727
|
+
if (args.json) { console.log(JSON.stringify(manifest, null, 2)); return; }
|
|
728
|
+
console.log(`intent ai list ${root}: ${manifest.summary.total} AI implementation(s)`);
|
|
729
|
+
for (const im of manifest.implementations) {
|
|
730
|
+
console.log(` ${im.id.padEnd(28)} ${im.status.padEnd(9)} risk:${im.risk} approval:${im.approval} scope:${im.scope}`);
|
|
731
|
+
}
|
|
732
|
+
console.log(` ${JSON.stringify(manifest.summary.byStatus)} | ${manifest.summary.approvalRequired} require approval`);
|
|
733
|
+
console.log(' note: PENDING = declared, no target region yet. OpenThunder verifies + advances state.');
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
if (sub === 'generate') {
|
|
737
|
+
// Provider-neutral: emit the structured handoff prompt for one mission's .intent file.
|
|
738
|
+
const file = args._[1];
|
|
739
|
+
if (!file) { console.error('usage: intent ai generate <file.intent> [--from <lang>]'); process.exit(2); return; }
|
|
740
|
+
const ast = parseIntent(readFileSync(file, 'utf8'));
|
|
741
|
+
if (!ast.implementation) { console.error(`intent ai generate: ${basename(file)} has no "implement with ai" block.`); process.exit(2); return; }
|
|
742
|
+
const prompt = buildImplementationPrompt(ast, { language: args.from || 'typescript' });
|
|
743
|
+
if (args.out && args.out !== '.intent') { const p = writeText(args.out, `${slug(ast.mission)}.prompt.md`, prompt); console.log(`wrote ${p.replace(process.cwd() + '/', '')}`); }
|
|
744
|
+
else console.log(prompt);
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
if (sub === 'gate') {
|
|
748
|
+
// Production gate: resolve each implementation's real state (declaration + region + proof).
|
|
749
|
+
const root = target;
|
|
750
|
+
const parsed = collectIntents(root).map((f) => ({ file: f, ast: parseIntent(readFileSync(f, 'utf8')) }));
|
|
751
|
+
const manifest = buildManifest(parsed.map((p) => ({ path: relative(root, p.file), source: '', ast: p.ast })), { projectId: args.product });
|
|
752
|
+
const apf = join(root, '.intent', 'ai-approvals.json');
|
|
753
|
+
const approvals = existsSync(apf) ? JSON.parse(readFileSync(apf, 'utf8')) : emptyApprovals();
|
|
754
|
+
const resolved = manifest.implementations.map((im) => {
|
|
755
|
+
const ast = parsed.find((p) => (p.ast.implementation?.id || slug(p.ast.mission || '')) === im.id)?.ast;
|
|
756
|
+
let region = null;
|
|
757
|
+
if (im.targetLocation) {
|
|
758
|
+
const tp = join(root, im.targetLocation);
|
|
759
|
+
if (existsSync(tp)) region = parseMarkers(readFileSync(tp, 'utf8')).regions.find((r) => r.id === im.id) || null;
|
|
760
|
+
}
|
|
761
|
+
const pf = join(root, im.proofLocation);
|
|
762
|
+
const proof = existsSync(pf) ? JSON.parse(readFileSync(pf, 'utf8')) : null;
|
|
763
|
+
const st = resolveState({ ast, region, proof, approval: approvalFor(approvals, im.id) });
|
|
764
|
+
return { id: im.id, status: st.status, approvalRequired: im.approval !== 'none', reasons: st.reasons };
|
|
765
|
+
});
|
|
766
|
+
const gate = productionGate(resolved, { allowPending: args.allowPending });
|
|
767
|
+
if (args.json) { console.log(JSON.stringify({ ...gate, mode: args.mode || 'production', resolved }, null, 2)); process.exit(gate.ok ? 0 : 1); return; }
|
|
768
|
+
console.log(`intent ai gate ${root} (${args.mode || 'production'}): ${gate.ok ? 'PASS' : 'BLOCKED'} , ${resolved.length} implementation(s)`);
|
|
769
|
+
for (const r of resolved) console.log(` ${r.id.padEnd(28)} ${r.status}${r.reasons?.[0] ? ` , ${r.reasons[0].code}: ${r.reasons[0].message}` : ''}`);
|
|
770
|
+
if (!gate.ok) console.log(` ${gate.blocking.length} implementation(s) block production. Use --allow-pending for dev builds.`);
|
|
771
|
+
process.exit(gate.ok ? 0 : 1);
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
if (sub === 'adopt') {
|
|
775
|
+
// Rewrite an AI-managed region to human-owned, preserving provenance.
|
|
776
|
+
const file = args._[1]; const id = args._[2];
|
|
777
|
+
if (!file || !id) { console.error('usage: intent ai adopt <targetFile> <id> [--from <lang>]'); process.exit(2); return; }
|
|
778
|
+
const code = readFileSync(file, 'utf8');
|
|
779
|
+
const res = adoptRegion(code, id, args.from || languageForFile(file));
|
|
780
|
+
if (!res) { console.error(`intent ai adopt: no AI-managed region "${id}" in ${basename(file)}.`); process.exit(2); return; }
|
|
781
|
+
writeFileSync(file, res.code);
|
|
782
|
+
console.log(`intent ai adopt ${id} -> human-owned (origin="ai" ownership="human") in ${basename(file)}`);
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
if (sub === 'approve' || sub === 'reject') {
|
|
786
|
+
// Record a human decision bound to the reviewed hashes. Refuses stale/unverified.
|
|
787
|
+
const root = args._[1] || '.'; const id = args._[2];
|
|
788
|
+
if (!id) { console.error(`usage: intent ai ${sub} <dir> <id> --by <reviewer> [--role <role>] [--note <note>]`); process.exit(2); return; }
|
|
789
|
+
const parsed = collectIntents(root).map((f) => ({ file: f, ast: parseIntent(readFileSync(f, 'utf8')) }));
|
|
790
|
+
const manifest = buildManifest(parsed.map((p) => ({ path: relative(root, p.file), source: '', ast: p.ast })), {});
|
|
791
|
+
const im = manifest.implementations.find((x) => x.id === id);
|
|
792
|
+
const ast = parsed.find((p) => (p.ast.implementation?.id || slug(p.ast.mission || '')) === id)?.ast;
|
|
793
|
+
if (!im || !ast) { console.error(`intent ai ${sub}: no implementation "${id}" found under ${root}.`); process.exit(2); return; }
|
|
794
|
+
let region = null;
|
|
795
|
+
if (im.targetLocation && existsSync(join(root, im.targetLocation))) region = parseMarkers(readFileSync(join(root, im.targetLocation), 'utf8')).regions.find((r) => r.id === id) || null;
|
|
796
|
+
const pf = join(root, im.proofLocation);
|
|
797
|
+
const proof = existsSync(pf) ? JSON.parse(readFileSync(pf, 'utf8')) : null;
|
|
798
|
+
const state = resolveState({ ast, region, proof });
|
|
799
|
+
if (!region) { console.error(`INTENT-AI-501: cannot ${sub} "${id}" , no generated region yet (state ${state.status}).`); process.exit(1); return; }
|
|
800
|
+
if (sub === 'approve' && !['VERIFIED', 'VERIFIED_AWAITING_APPROVAL'].includes(state.status)) {
|
|
801
|
+
console.error(`INTENT-AI-502: cannot approve "${id}" in state ${state.status}${state.reasons?.[0] ? ` (${state.reasons[0].code})` : ''}. Approve only verified, non-stale work.`);
|
|
802
|
+
process.exit(1); return;
|
|
803
|
+
}
|
|
804
|
+
const apf = join(root, '.intent', 'ai-approvals.json');
|
|
805
|
+
const store = existsSync(apf) ? JSON.parse(readFileSync(apf, 'utf8')) : emptyApprovals();
|
|
806
|
+
const at = new Date().toISOString();
|
|
807
|
+
const { store: next, error } = recordDecision(store, id, {
|
|
808
|
+
decision: sub === 'approve' ? 'approved' : 'rejected',
|
|
809
|
+
by: args.by, role: args.role, note: args.note,
|
|
810
|
+
contractHash: contractHash(ast), implementationHash: implementationHash(region.code), at,
|
|
811
|
+
});
|
|
812
|
+
if (error) { console.error(`intent ai ${sub}: ${error}`); process.exit(2); return; }
|
|
813
|
+
writeJson(join(root, '.intent'), 'ai-approvals.json', next);
|
|
814
|
+
const event = makeEvent(sub === 'approve' ? 'IntentAiImplementationApproved' : 'IntentAiImplementationRejected', {
|
|
815
|
+
implementationId: id, missionId: ast.mission, contractHash: contractHash(ast), implementationHash: implementationHash(region.code),
|
|
816
|
+
timestamp: at, toolVersion: 'thunderlang', actorType: 'human', actorId: args.by || null, previousStatus: state.status,
|
|
817
|
+
newStatus: sub === 'approve' ? 'APPROVED' : 'REJECTED',
|
|
818
|
+
});
|
|
819
|
+
const logPath = sinkEvent(root, event);
|
|
820
|
+
console.log(`intent ai ${sub} ${id} by ${args.by || '(anonymous)'}${args.role ? ` [${args.role}]` : ''} -> ${sub === 'approve' ? 'APPROVED' : 'REJECTED'} (bound to current hashes)`);
|
|
821
|
+
console.log(` logged ${event.type} to ${logPath.replace(process.cwd() + '/', '')}`);
|
|
822
|
+
if (args.json) console.log(JSON.stringify(event, null, 2));
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
// `intent ai events [dir] [--subject <implId>] [--json]` , read the append-only audit log.
|
|
826
|
+
if (sub === 'events') {
|
|
827
|
+
const root = args._[1] || '.';
|
|
828
|
+
const log = readEventLog(root);
|
|
829
|
+
const events = args.subject ? log.events.filter((e) => e.implementationId === args.subject) : log.events;
|
|
830
|
+
if (args.json) { console.log(JSON.stringify({ ...log, events }, null, 2)); return; }
|
|
831
|
+
console.log(`intent ai events ${root}: ${events.length} event${events.length === 1 ? '' : 's'}${args.subject ? ` for ${args.subject}` : ''}`);
|
|
832
|
+
for (const e of events) {
|
|
833
|
+
const who = e.actorId ? ` by ${e.actorId}` : '';
|
|
834
|
+
const move = e.previousStatus || e.newStatus ? ` (${e.previousStatus || '?'} -> ${e.newStatus || '?'})` : '';
|
|
835
|
+
console.log(` ${e.timestamp || '(no time)'} ${e.type}${who}${e.implementationId ? ` [${e.implementationId}]` : ''}${move}`);
|
|
836
|
+
}
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
if (sub === 'select') {
|
|
840
|
+
// Deterministic candidate selection: AI generates N candidates in
|
|
841
|
+
// .intent/candidates/<id>/; IL picks by the mission's selection policy.
|
|
842
|
+
const root = args._[1] || '.'; const id = args._[2];
|
|
843
|
+
if (!id) { console.error('usage: intent ai select <dir> <id> [--json]'); process.exit(2); return; }
|
|
844
|
+
const ast = collectIntents(root).map((f) => parseIntent(readFileSync(f, 'utf8')))
|
|
845
|
+
.find((a) => (a.implementation?.id || slug(a.mission || '')) === id);
|
|
846
|
+
const policy = parseSelection(ast?.selection || []);
|
|
847
|
+
const cdir = join(root, '.intent', 'candidates', id);
|
|
848
|
+
if (!existsSync(cdir)) { console.error(`intent ai select: no candidates at ${relative(process.cwd(), cdir)}.`); process.exit(2); return; }
|
|
849
|
+
const candidates = readdirSync(cdir).filter((n) => !n.endsWith('.proof.json') && !n.endsWith('.json')).map((name) => {
|
|
850
|
+
const code = readFileSync(join(cdir, name), 'utf8');
|
|
851
|
+
const region = parseMarkers(code).regions.find((r) => r.id === id);
|
|
852
|
+
const proofFile = join(cdir, name.replace(/\.[^.]+$/, '') + '.proof.json');
|
|
853
|
+
let checksPassed;
|
|
854
|
+
if (existsSync(proofFile)) { const pr = JSON.parse(readFileSync(proofFile, 'utf8')); checksPassed = Object.values(pr.checks || {}).every((v) => v !== 'failed'); }
|
|
855
|
+
return { id: name, metrics: regionMetrics(region ? region.code : code), checksPassed };
|
|
856
|
+
});
|
|
857
|
+
const result = selectCandidate(candidates, policy);
|
|
858
|
+
if (args.json) { console.log(JSON.stringify({ ...result, policy }, null, 2)); return; }
|
|
859
|
+
console.log(`intent ai select ${id}: ${result.winner ? result.winner.id : '(none eligible)'} wins (${result.eligibleCount}/${candidates.length} eligible)`);
|
|
860
|
+
console.log(` policy: prefer ${result.prefer.map((p) => `${p.direction} ${p.metric}`).join(', ')}${policy.requireAllChecks ? '; require all checks' : ''}`);
|
|
861
|
+
for (const c of result.ranking) console.log(` ${c.id.padEnd(20)} ${JSON.stringify(c.metrics)}${c.checksPassed === false ? ' [checks FAILED]' : ''}`);
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
console.error(`intent ai ${sub || ''}: IL supports list | generate | gate | adopt | approve | reject | select. OpenThunder runs verification.`);
|
|
865
|
+
process.exit(2);
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Semantic diff: compare two snapshots (dirs or .intent files) by meaning.
|
|
870
|
+
if (cmd === 'diff') {
|
|
871
|
+
const b = args._[0]; const a = args._[1];
|
|
872
|
+
if (!b || !a) { console.error('usage: intent diff <before-dir|file> <after-dir|file> [--json]'); process.exit(2); return; }
|
|
873
|
+
const snap = (p) => {
|
|
874
|
+
if (statSync(p).isDirectory()) return buildAtlas(collectIntents(p).map((f) => buildIntentGraph(parseIntent(readFileSync(f, 'utf8')))));
|
|
875
|
+
return buildIntentGraph(parseIntent(readFileSync(p, 'utf8')));
|
|
876
|
+
};
|
|
877
|
+
const diff = diffGraphs(snap(b), snap(a));
|
|
878
|
+
if (args.json) { console.log(JSON.stringify(diff, null, 2)); return; }
|
|
879
|
+
console.log(`intent diff ${b} -> ${a}: +${diff.summary.added} / -${diff.summary.removed} / ~${diff.summary.changed} node(s), +${diff.summary.relationshipsAdded} / -${diff.summary.relationshipsRemoved} edge(s)`);
|
|
880
|
+
if (Object.keys(diff.summary.addedByType).length) console.log(` added: ${JSON.stringify(diff.summary.addedByType)}`);
|
|
881
|
+
if (Object.keys(diff.summary.removedByType).length) console.log(` removed: ${JSON.stringify(diff.summary.removedByType)}`);
|
|
882
|
+
for (const c of diff.changedNodes.slice(0, 8)) console.log(` ~ ${c.type} ${c.id}`);
|
|
883
|
+
if (diff.invalidatedApprovals.length) console.log(` approvals invalidated by the change: ${diff.invalidatedApprovals.join(', ')}`);
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Semantic merge: 3-way merge of two concurrent Intent versions over a common base.
|
|
888
|
+
if (cmd === 'merge') {
|
|
889
|
+
const [bp, op, tp] = [args._[0], args._[1], args._[2]];
|
|
890
|
+
if (!bp || !op || !tp) { console.error('usage: intent merge <base> <ours> <theirs> [--json]'); process.exit(2); return; }
|
|
891
|
+
const snap = (p) => statSync(p).isDirectory()
|
|
892
|
+
? buildAtlas(collectIntents(p).map((f) => buildIntentGraph(parseIntent(readFileSync(f, 'utf8')))))
|
|
893
|
+
: buildIntentGraph(parseIntent(readFileSync(p, 'utf8')));
|
|
894
|
+
const res = mergeGraphs(snap(bp), snap(op), snap(tp));
|
|
895
|
+
if (args.json) { console.log(JSON.stringify(res, null, 2)); process.exit(res.clean ? 0 : 1); return; }
|
|
896
|
+
console.log(`intent merge: ${res.clean ? 'CLEAN' : 'CONFLICTS'} , ${res.summary.nodes} node(s), ${res.summary.conflicts} conflict(s)`);
|
|
897
|
+
for (const c of res.conflicts) console.log(` conflict: ${c.type} ${c.id} (changed differently on both sides)`);
|
|
898
|
+
process.exit(res.clean ? 0 : 1);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// Intent Runtime: EXECUTE intent , evaluate decisions against concrete inputs.
|
|
903
|
+
if (cmd === 'run') {
|
|
904
|
+
const ast = parseIntent(readFileSync(file, 'utf8'));
|
|
905
|
+
let inputs = {};
|
|
906
|
+
if (args.inputs) { try { inputs = JSON.parse(args.inputs); } catch { console.error('intent run: --inputs must be JSON'); process.exit(2); return; } }
|
|
907
|
+
let decisions = ast.decisions || [];
|
|
908
|
+
if (args.decision) decisions = decisions.filter((d) => slug(d.name) === slug(args.decision));
|
|
909
|
+
if (!decisions.length) { console.error('intent run: no decision to run' + (args.decision ? ` matching "${args.decision}"` : '')); process.exit(2); return; }
|
|
910
|
+
const runs = decisions.map((d) => evaluateDecision(d, inputs));
|
|
911
|
+
if (args.json) { console.log(JSON.stringify(runs.length === 1 ? runs[0] : runs, null, 2)); return; }
|
|
912
|
+
for (const r of runs) {
|
|
913
|
+
console.log(`decision ${r.decision}: ${r.result === null ? '(undecided)' : r.result}${r.matched ? ` [rule: ${r.matched}]` : ''}`);
|
|
914
|
+
for (const t of r.trace) console.log(` ${t.matched ? '>' : ' '} ${t.rule || '(rule)'}${t.when ? `: when ${t.when}` : ''}${t.error ? ` !! ${t.error}` : t.matched ? ' (matched)' : ''}`);
|
|
915
|
+
}
|
|
916
|
+
process.exit(runs.some((r) => r.undecided || !r.ok) ? 1 : 0);
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// Outcome contracts: evaluate each commitment against the delivery result that measures it.
|
|
921
|
+
if (cmd === 'outcomes') {
|
|
922
|
+
const ast = parseIntent(readFileSync(file, 'utf8'));
|
|
923
|
+
const r = evaluateOutcomes(ast);
|
|
924
|
+
if (args.json) { console.log(JSON.stringify(r, null, 2)); process.exit(r.missed > 0 ? 1 : 0); return; }
|
|
925
|
+
if (r.total === 0) { console.log(`intent outcomes ${basename(file)}: no outcome contracts found.`); return; }
|
|
926
|
+
console.log(`intent outcomes ${basename(file)}: ${r.met} met, ${r.missed} missed, ${r.pending} pending`);
|
|
927
|
+
for (const e of r.evaluations) {
|
|
928
|
+
const tag = e.status === 'met' ? 'MET ' : e.status === 'missed' ? 'MISSED' : 'PENDING';
|
|
929
|
+
const detail = e.comparable ? `actual ${e.actual} vs target ${e.target} (${e.direction})${e.improvement != null ? `, ${e.improvement >= 0 ? '+' : ''}${e.improvement} vs baseline` : ''}` : `no measured result yet (target ${e.target})`;
|
|
930
|
+
console.log(` ${tag} ${e.contract} , ${detail}`);
|
|
931
|
+
}
|
|
932
|
+
process.exit(r.missed > 0 ? 1 : 0);
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Style intent: resolve brand/visual language against the canonical token address space.
|
|
937
|
+
if (cmd === 'style') {
|
|
938
|
+
const ast = parseIntent(readFileSync(file, 'utf8'));
|
|
939
|
+
const r = analyzeStyle(ast);
|
|
940
|
+
if (args.json) { console.log(JSON.stringify(r, null, 2)); process.exit(r.diagnostics.some((d) => d.severity === 'blocker') ? 1 : 0); return; }
|
|
941
|
+
if (r.styleIntents.length === 0) { console.log(`intent style ${basename(file)}: no style intents found.`); return; }
|
|
942
|
+
console.log(`intent style ${basename(file)}: ${r.styleIntents.length} style intent(s)`);
|
|
943
|
+
for (const s of r.styleIntents) {
|
|
944
|
+
const a11y = s.accessibility ? `${s.accessibility.target} (${s.accessibility.classification}, verified=${s.accessibility.verified})` : 'no target';
|
|
945
|
+
console.log(` ${s.name} , a11y ${a11y}, ${s.tokens.length} token(s)${s.appliesTo ? `, applies_to ${s.appliesTo}` : ''}`);
|
|
946
|
+
for (const t of s.tokens) console.log(` ${t.canonical ? ' ' : '?'} ${t.path} = ${t.value ?? '(unset)'}`);
|
|
947
|
+
}
|
|
948
|
+
for (const d of r.diagnostics) console.log(` [${d.severity}] ${d.ruleId}: ${d.message}`);
|
|
949
|
+
process.exit(0);
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// Self-verifying intent: run the `test` blocks in a .intent file (decisions + lifecycles).
|
|
954
|
+
if (cmd === 'test') {
|
|
955
|
+
const ast = parseIntent(readFileSync(file, 'utf8'));
|
|
956
|
+
const r = runTests(ast);
|
|
957
|
+
if (args.json) { console.log(JSON.stringify(r, null, 2)); process.exit(r.ok ? 0 : 1); return; }
|
|
958
|
+
if (r.total === 0) { console.log(`intent test ${basename(file)}: no test blocks found.`); return; }
|
|
959
|
+
console.log(`intent test ${basename(file)}: ${r.passed}/${r.total} passed`);
|
|
960
|
+
for (const c of r.results) {
|
|
961
|
+
const detail = c.error ? ` ${c.error}`
|
|
962
|
+
: c.kind === 'lifecycle' ? `expected ${c.expected ?? '(any)'}, got ${c.actual} (valid=${c.valid})`
|
|
963
|
+
: `expected ${c.expected}, got ${c.actual}`;
|
|
964
|
+
console.log(` ${c.pass ? 'PASS' : 'FAIL'} ${c.target} / ${c.case}${c.pass ? '' : ` , ${detail}`}`);
|
|
965
|
+
}
|
|
966
|
+
process.exit(r.ok ? 0 : 1);
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// Intent Runtime: SIMULATE a lifecycle against a sequence of events.
|
|
971
|
+
if (cmd === 'simulate') {
|
|
972
|
+
const ast = parseIntent(readFileSync(file, 'utf8'));
|
|
973
|
+
const lcs = ast.lifecycles || [];
|
|
974
|
+
if (!lcs.length) { console.error('intent simulate: no lifecycle in this mission'); process.exit(2); return; }
|
|
975
|
+
const events = args.events || [];
|
|
976
|
+
const sims = lcs.map((lc) => simulateLifecycle(lc, events));
|
|
977
|
+
if (args.json) { console.log(JSON.stringify(sims.length === 1 ? sims[0] : sims, null, 2)); return; }
|
|
978
|
+
for (const s of sims) {
|
|
979
|
+
console.log(`lifecycle ${s.lifecycle}: ${s.path.join(' -> ')} (${s.valid ? 'valid' : 'INVALID'}${s.endedTerminal ? ', terminal' : ''})`);
|
|
980
|
+
for (const st of s.steps) console.log(` ${st.ok ? 'ok ' : 'X '} ${st.from} --${st.event}--> ${st.to}${st.reason ? ` (${st.reason})` : ''}`);
|
|
981
|
+
}
|
|
982
|
+
process.exit(sims.some((s) => !s.valid) ? 1 : 0);
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// Schema migration: upgrade a persisted Intent Graph JSON to the current (or a target)
|
|
987
|
+
// schema version, then validate the result against the canonical vocabulary.
|
|
988
|
+
// Validate an Intent Graph against the canonical vocabulary (consumer anti-fork self-check).
|
|
989
|
+
if (cmd === 'validate') {
|
|
990
|
+
const raw = readFileSync(file, 'utf8');
|
|
991
|
+
let graph;
|
|
992
|
+
try { graph = JSON.parse(raw); } catch { console.error('intent validate: input is not valid JSON'); process.exit(2); return; }
|
|
993
|
+
if (!graph || !Array.isArray(graph.nodes)) { console.error('intent validate: not an Intent Graph (no nodes[])'); process.exit(2); return; }
|
|
994
|
+
const v = validateGraph(graph);
|
|
995
|
+
if (args.json) { console.log(JSON.stringify(v, null, 2)); process.exit(v.valid ? 0 : 1); return; }
|
|
996
|
+
console.log(`intent validate ${basename(file)}: ${v.valid ? 'VALID' : `${v.issues.length} issue(s)`} (${v.version})`);
|
|
997
|
+
for (const i of v.issues) console.log(` [${i.code}] ${i.message}${i.id ? ` (${i.id})` : ''}`);
|
|
998
|
+
process.exit(v.valid ? 0 : 1);
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
if (cmd === 'migrate') {
|
|
1003
|
+
const raw = readFileSync(file, 'utf8');
|
|
1004
|
+
let graph;
|
|
1005
|
+
try { graph = JSON.parse(raw); } catch { console.error('intent migrate: input is not valid JSON'); process.exit(2); return; }
|
|
1006
|
+
if (!graph || !Array.isArray(graph.nodes)) { console.error('intent migrate: not an Intent Graph (no nodes[])'); process.exit(2); return; }
|
|
1007
|
+
let result;
|
|
1008
|
+
try { result = migrateGraph(graph, args.to ? { to: args.to } : {}); }
|
|
1009
|
+
catch (e) { console.error(`intent migrate: ${e instanceof Error ? e.message : e}`); process.exit(2); return; }
|
|
1010
|
+
const v = validateGraph(result.graph);
|
|
1011
|
+
if (args.json) { console.log(JSON.stringify({ ...result, validation: v }, null, 2)); process.exit(v.valid ? 0 : 1); return; }
|
|
1012
|
+
console.log(`intent migrate: ${result.from} -> ${result.to} (${result.migrated ? result.applied.length + ' step(s)' : 'already current'})`);
|
|
1013
|
+
for (const a of result.applied) console.log(` applied ${a.from} -> ${a.to}: ${a.description}`);
|
|
1014
|
+
console.log(` validation: ${v.valid ? 'OK' : `${v.issues.length} issue(s)`}`);
|
|
1015
|
+
for (const i of v.issues.slice(0, 10)) console.log(` [${i.code}] ${i.message}`);
|
|
1016
|
+
if (args.out && args.out !== '.intent') console.log(` wrote ${writeText(args.out, `${slug((graph.nodes.find((n) => n.type === 'Mission')?.title) || 'graph')}.graph.json`, JSON.stringify(result.graph, null, 2))}`);
|
|
1017
|
+
process.exit(v.valid ? 0 : 1);
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// Graph -> source: regenerate .intent from an Intent Graph (a graph JSON, or an .intent
|
|
1022
|
+
// file which is parsed + built first , a normalizing round-trip).
|
|
1023
|
+
if (cmd === 'source') {
|
|
1024
|
+
const raw = readFileSync(file, 'utf8');
|
|
1025
|
+
let graph;
|
|
1026
|
+
if (file.endsWith('.json')) {
|
|
1027
|
+
try { graph = JSON.parse(raw); } catch { console.error('intent source: input .json is not valid JSON'); process.exit(2); return; }
|
|
1028
|
+
if (!graph || !Array.isArray(graph.nodes)) { console.error('intent source: JSON is not an Intent Graph (no nodes[])'); process.exit(2); return; }
|
|
1029
|
+
} else {
|
|
1030
|
+
graph = buildIntentGraph(parseIntent(raw));
|
|
1031
|
+
}
|
|
1032
|
+
const src = graphToSource(graph);
|
|
1033
|
+
if (args.out && args.out !== '.intent') {
|
|
1034
|
+
const base = (graph.nodes.find((n) => n.type === 'Mission')?.title) || basename(file).replace(/\.[^.]+$/, '');
|
|
1035
|
+
console.log(`intent source: wrote ${writeText(args.out, `${slug(base)}.intent`, src)}`);
|
|
1036
|
+
} else {
|
|
1037
|
+
process.stdout.write(src);
|
|
1038
|
+
}
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// Import adapters: lift an external DMN / BPMN document back into ThunderLang source.
|
|
1043
|
+
if (cmd === 'import') {
|
|
1044
|
+
const xml = readFileSync(file, 'utf8');
|
|
1045
|
+
const fmt = args.format || detectFormat(xml);
|
|
1046
|
+
if (!fmt || !IMPORT_FORMATS.includes(fmt)) {
|
|
1047
|
+
console.error(`intent import: could not detect format; pass --format <${IMPORT_FORMATS.join('|')}>`);
|
|
1048
|
+
process.exit(2); return;
|
|
1049
|
+
}
|
|
1050
|
+
const report = importReport(xml, fmt);
|
|
1051
|
+
if (report == null) { console.error(`intent import: unsupported format "${fmt}"`); process.exit(2); return; }
|
|
1052
|
+
if (args.json) { console.log(JSON.stringify(report, null, 2)); return; }
|
|
1053
|
+
const src = report.source;
|
|
1054
|
+
if (args.out && args.out !== '.intent') {
|
|
1055
|
+
const base = basename(file).replace(/\.[^.]+$/, '');
|
|
1056
|
+
const p = writeText(args.out, `${slug(base)}.intent`, src);
|
|
1057
|
+
console.log(`intent import: wrote ${p}`);
|
|
1058
|
+
} else {
|
|
1059
|
+
process.stdout.write(src.endsWith('\n') ? src : src + '\n');
|
|
1060
|
+
}
|
|
1061
|
+
// Fidelity warnings go to stderr, so stdout stays clean for piping the source.
|
|
1062
|
+
for (const w of report.warnings) console.error(`intent import: [${w.code}] ${w.message}`);
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// Export adapters: render decisions/lifecycles to DMN / BPMN / NuSMV (interop).
|
|
1067
|
+
if (cmd === 'export') {
|
|
1068
|
+
const fmt = args.format;
|
|
1069
|
+
if (!fmt || !EXPORT_FORMATS.includes(fmt)) {
|
|
1070
|
+
console.error(`usage: intent export <file> --format <${EXPORT_FORMATS.join('|')}> [--out <dir>]`);
|
|
1071
|
+
process.exit(2); return;
|
|
1072
|
+
}
|
|
1073
|
+
const ast = parseIntent(readFileSync(file, 'utf8'));
|
|
1074
|
+
const res = exportIntent(ast, fmt);
|
|
1075
|
+
if (args.out && args.out !== '.intent') {
|
|
1076
|
+
const name = `${slug(ast.mission || basename(file, '.intent'))}.${res.ext}`;
|
|
1077
|
+
const p = writeText(args.out, name, res.content);
|
|
1078
|
+
console.log(`intent export: wrote ${p}`);
|
|
1079
|
+
} else {
|
|
1080
|
+
process.stdout.write(res.content);
|
|
1081
|
+
}
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// Intent Atlas: the navigable/searchable whole-system map over the Intent Graph.
|
|
1086
|
+
if (cmd === 'atlas') {
|
|
1087
|
+
const root = file || '.';
|
|
1088
|
+
const graphs = collectIntents(root).map((f) => buildIntentGraph(parseIntent(readFileSync(f, 'utf8'))));
|
|
1089
|
+
const atlas = buildAtlas(graphs, { product: args.product });
|
|
1090
|
+
if (args.search) {
|
|
1091
|
+
const hits = searchAtlas(atlas, args.search, { type: args.from });
|
|
1092
|
+
if (args.json) { console.log(JSON.stringify(hits, null, 2)); return; }
|
|
1093
|
+
console.log(`intent atlas search "${args.search}": ${hits.length} hit(s)`);
|
|
1094
|
+
for (const h of hits) console.log(` ${h.type.padEnd(18)} ${h.id}${h.title ? ` , ${h.title}` : ''}`);
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
if (args.expand) {
|
|
1098
|
+
const ex = expandNode(atlas, args.expand);
|
|
1099
|
+
if (!ex) { console.error(`intent atlas: no node "${args.expand}".`); process.exit(2); return; }
|
|
1100
|
+
if (args.json) { console.log(JSON.stringify(ex, null, 2)); return; }
|
|
1101
|
+
console.log(`${ex.node.type} ${ex.node.id}${ex.node.title ? ` , ${ex.node.title}` : ''}`);
|
|
1102
|
+
for (const e of ex.out) console.log(` -> ${e.rel.padEnd(16)} ${e.node.id}`);
|
|
1103
|
+
for (const e of ex.inbound) console.log(` <- ${e.rel.padEnd(16)} ${e.node.id}`);
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
if (args.json) { console.log(JSON.stringify(atlas, null, 2)); return; }
|
|
1107
|
+
console.log(`intent atlas ${root}: ${atlas.overview.missions} mission(s), ${atlas.overview.nodes} node(s), ${atlas.overview.relationships} edge(s)`);
|
|
1108
|
+
console.log(` ${JSON.stringify(atlas.overview.byType)}`);
|
|
1109
|
+
for (const m of atlas.missions) console.log(` mission ${m.id}${m.title ? ` , ${m.title}` : ''}`);
|
|
1110
|
+
console.log(' expand a node: intent atlas . --expand <id> | search: --search <query>');
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// Mission Atlas index: aggregate every .intent under a directory into one inventory.
|
|
1115
|
+
if (cmd === 'index') {
|
|
1116
|
+
const root = file;
|
|
1117
|
+
let intentFiles;
|
|
1118
|
+
try {
|
|
1119
|
+
intentFiles = collectIntents(root).map((f) => ({ path: relative(root, f), source: readFileSync(f, 'utf8') }));
|
|
1120
|
+
} catch (e) {
|
|
1121
|
+
console.error(`intent index: cannot read "${root}": ${e instanceof Error ? e.message : e}`);
|
|
1122
|
+
process.exit(2);
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
const index = buildMissionIndex(intentFiles, { product: args.product });
|
|
1126
|
+
if (args.json) { console.log(JSON.stringify(index, null, 2)); return; }
|
|
1127
|
+
console.log(`intent index ${root}: ${index.summary.missions} mission(s)`);
|
|
1128
|
+
console.log(` ${JSON.stringify(index.summary.byArea)}`);
|
|
1129
|
+
for (const m of index.missions) {
|
|
1130
|
+
console.log(` ${m.mission.padEnd(24)} ${String(m.risk).padEnd(7)} G:${m.guarantees} N:${m.neverRules} verify:${m.verification}${m.reviewed ? ' reviewed' : ''}`);
|
|
1131
|
+
}
|
|
1132
|
+
console.log(` ${index.summary.declaredFull} declared-full, ${index.summary.declaredPartial} partial, ${index.summary.unverified} unverified, ${index.summary.highRisk} high-risk`);
|
|
1133
|
+
console.log(' note: verification is DECLARED, not proven. Test/drift status needs OpenThunder.');
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// `intent verify-diff <intent> --after <code> [--before <code>]` , the AI-loop gate: prove
|
|
1138
|
+
// deterministically which of the intent's guarantees/never-rules a code change upholds or breaks.
|
|
1139
|
+
if (cmd === 'verify-diff') {
|
|
1140
|
+
const intentText = readFileSync(file, 'utf8');
|
|
1141
|
+
if (!args.after) { console.error('usage: intent verify-diff <intent> --after <codeFile> [--before <codeFile>] [--from <lang>]'); process.exit(2); return; }
|
|
1142
|
+
const after = readFileSync(args.after, 'utf8');
|
|
1143
|
+
const before = args.before ? readFileSync(args.before, 'utf8') : null;
|
|
1144
|
+
const language = args.from || languageForFile(args.after);
|
|
1145
|
+
const r = verifyDiff(intentText, { before, after, language });
|
|
1146
|
+
if (args.json) { console.log(JSON.stringify(r, null, 2)); process.exit(r.ok ? 0 : 1); return; }
|
|
1147
|
+
console.log(`intent verify-diff ${basename(file)} vs ${basename(args.after)}: ${r.verdict} (${r.blocking} blocking, ${r.summary.regressions} regression(s))`);
|
|
1148
|
+
for (const f of r.findings) {
|
|
1149
|
+
const tag = f.code === 'INTENT_VERIFY_NEVER_VIOLATED' ? 'VIOLATION' : f.regression ? 'REGRESSION' : f.level.toUpperCase();
|
|
1150
|
+
console.log(` [${tag}] ${f.message}${f.line ? ` (line ${f.line})` : ''}`);
|
|
1151
|
+
}
|
|
1152
|
+
if (r.ok) console.log(' ok the change upholds the declared contract (deterministic checks; tests + humans still own correctness).');
|
|
1153
|
+
process.exit(r.ok ? 0 : 1);
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// `intent draft --brief <json|->` , scaffold a rigorous intent draft from a structured brief,
|
|
1158
|
+
// plus a review checklist of what a human must still fill in. Prints the draft; --write saves it.
|
|
1159
|
+
if (cmd === 'draft') {
|
|
1160
|
+
const briefPath = args.brief || (cmd === 'draft' && file && file.endsWith('.json') ? file : null);
|
|
1161
|
+
if (!briefPath) { console.error('usage: intent draft --brief <brief.json|-> [--write <out.intent>]'); process.exit(2); return; }
|
|
1162
|
+
const raw = briefPath === '-' ? readFileSync(0, 'utf8') : readFileSync(briefPath, 'utf8');
|
|
1163
|
+
let brief;
|
|
1164
|
+
try { brief = JSON.parse(raw); } catch { console.error('intent draft: --brief is not valid JSON'); process.exit(2); return; }
|
|
1165
|
+
const r = draftIntent(brief);
|
|
1166
|
+
if (args.json) { console.log(JSON.stringify(r, null, 2)); return; }
|
|
1167
|
+
if (args.write) { writeFileSync(args.write, r.source); console.error(`intent draft: wrote ${args.write}`); }
|
|
1168
|
+
else process.stdout.write(r.source);
|
|
1169
|
+
if (r.review.length) { console.error('\nreview (fill these in , the draft is a proposal, not verified):'); for (const x of r.review) console.error(` - ${x.message}`); }
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// `intent guard <file>` , preview what a runtime guard compiled from this intent enforces:
|
|
1174
|
+
// which fields it redacts (secrets/PII) and which decisions it can gate at runtime.
|
|
1175
|
+
if (cmd === 'guard') {
|
|
1176
|
+
const ast = parseIntent(readFileSync(file, 'utf8'));
|
|
1177
|
+
const g = guardSummary(ast);
|
|
1178
|
+
if (args.json) { console.log(JSON.stringify(g, null, 2)); return; }
|
|
1179
|
+
console.log(`intent guard ${basename(file)}:`);
|
|
1180
|
+
console.log(` redacts fields ${g.redactsFields.length ? g.redactsFields.join(', ') : '(none declared secret/PII)'}`);
|
|
1181
|
+
console.log(` enforces decisions ${g.enforcesDecisions.length ? g.enforcesDecisions.join(', ') : '(none)'}`);
|
|
1182
|
+
if (g.neverRules.length) { console.log(' never rules:'); for (const n of g.neverRules) console.log(` - ${n}`); }
|
|
1183
|
+
console.log(' use: import { compileGuard } from "@skillstech/thunderlang/core"');
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// `intent ledger <file.json>` , verify the tamper-evident chain, and explain a subject's history
|
|
1188
|
+
// (why it was built, who approved it, what was assumed/corrected/verified, which risks accepted).
|
|
1189
|
+
if (cmd === 'ledger') {
|
|
1190
|
+
if (!file) { console.error('usage: intent ledger <ledger.json> [--subject <id>] [--json]'); process.exit(2); return; }
|
|
1191
|
+
let ledger;
|
|
1192
|
+
try { ledger = JSON.parse(readFileSync(file, 'utf8')); } catch { console.error('intent ledger: not valid JSON'); process.exit(2); return; }
|
|
1193
|
+
const chain = verifyLedger(ledger);
|
|
1194
|
+
if (args.subject) {
|
|
1195
|
+
const ex = ledgerExplain(ledger, args.subject);
|
|
1196
|
+
if (args.json) { console.log(JSON.stringify({ chain, ...ex }, null, 2)); process.exit(chain.valid ? 0 : 1); return; }
|
|
1197
|
+
console.log(`intent ledger ${basename(file)} , ${args.subject} (chain ${chain.valid ? 'VALID' : 'BROKEN'})`);
|
|
1198
|
+
if (ex.why.length) { console.log(' why built:'); for (const w of ex.why) console.log(` - ${w}`); }
|
|
1199
|
+
if (ex.approvedBy.length) console.log(` approved by: ${ex.approvedBy.join(', ')}`);
|
|
1200
|
+
if (ex.assumptions.length) console.log(` assumptions: ${ex.assumptions.length}`);
|
|
1201
|
+
if (ex.corrections.length) console.log(` corrections (inferred intent fixed): ${ex.corrections.length}`);
|
|
1202
|
+
if (ex.acceptedRisks.length) console.log(` accepted risks: ${ex.acceptedRisks.length}`);
|
|
1203
|
+
if (ex.verifications.length) console.log(` verifications: ${ex.verifications.length}`);
|
|
1204
|
+
console.log(` change history: ${ex.changeCount} entries`);
|
|
1205
|
+
process.exit(chain.valid ? 0 : 1);
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
if (args.json) { console.log(JSON.stringify(chain, null, 2)); process.exit(chain.valid ? 0 : 1); return; }
|
|
1209
|
+
const n = (ledger.entries || []).length;
|
|
1210
|
+
console.log(`intent ledger ${basename(file)}: ${n} entr${n === 1 ? 'y' : 'ies'}, chain ${chain.valid ? 'VALID (tamper-evident)' : `BROKEN at #${chain.brokenAt} , ${chain.reason}`}`);
|
|
1211
|
+
process.exit(chain.valid ? 0 : 1);
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// `intent impact <base> <proposed>` , the Simulator: estimate a change's impact BEFORE building it
|
|
1216
|
+
// , the deterministic blast radius, the risk it would introduce, contradictions, release risk.
|
|
1217
|
+
if (cmd === 'impact') {
|
|
1218
|
+
const baseArg = args._[0]; const propArg = args._[1];
|
|
1219
|
+
if (!baseArg || !propArg) { console.error('usage: intent impact <base.intent|dir> <proposed.intent|dir> [--json]'); process.exit(2); return; }
|
|
1220
|
+
const collect = (p) => (existsSync(p) && statSync(p).isDirectory() ? collectIntents(p) : [p]).map((f) => ({ file: relative(process.cwd(), f) || f, source: readFileSync(f, 'utf8') }));
|
|
1221
|
+
const r = simulateChange(collect(baseArg), collect(propArg));
|
|
1222
|
+
if (args.json) { console.log(JSON.stringify(r, null, 2)); process.exit(r.summary.safe ? 0 : 1); return; }
|
|
1223
|
+
console.log(`intent impact: ${r.summary.safe ? 'SAFE' : 'REVIEW'} (${baseArg} -> ${propArg})`);
|
|
1224
|
+
console.log(` change touches ${r.changedNodes} node(s); ripples to ${r.deterministicImpact.total} dependent(s)`);
|
|
1225
|
+
const bt = Object.entries(r.deterministicImpact.byType);
|
|
1226
|
+
if (bt.length) console.log(' deterministic impact by type: ' + bt.map(([t, ns]) => `${ns.length} ${t}`).join(', '));
|
|
1227
|
+
if (r.ruleDerivedRisk.length) { console.log(` risk it would introduce (${r.ruleDerivedRisk.length}):`); for (const f of r.ruleDerivedRisk.slice(0, 6)) console.log(` [${f.severity}] ${f.ruleId} , ${f.detected}`); }
|
|
1228
|
+
if (r.contradictions.length) console.log(` contradictions: ${r.contradictions.length}`);
|
|
1229
|
+
if (r.releaseRisks.length) console.log(` release risk: ${r.releaseRisks.length} blocking finding(s)`);
|
|
1230
|
+
if (r.unknownImpact.length) console.log(` unknown impact: ${r.unknownImpact.length} node(s) where the ripple can't be traced deterministically`);
|
|
1231
|
+
process.exit(r.summary.safe ? 0 : 1);
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// `intent guardian <before> <after>` , drift detection: what a change did to the intent , which
|
|
1236
|
+
// intent it affects, what risk it introduced, what must be reverified, what learning is stale.
|
|
1237
|
+
if (cmd === 'guardian') {
|
|
1238
|
+
const beforeArg = args._[0]; const afterArg = args._[1];
|
|
1239
|
+
if (!beforeArg || !afterArg) { console.error('usage: intent guardian <before.intent|dir> <after.intent|dir> [--json]'); process.exit(2); return; }
|
|
1240
|
+
const collect = (p) => (existsSync(p) && statSync(p).isDirectory() ? collectIntents(p) : [p]).map((f) => ({ file: relative(process.cwd(), f) || f, source: readFileSync(f, 'utf8') }));
|
|
1241
|
+
const r = guardianReport(collect(beforeArg), collect(afterArg));
|
|
1242
|
+
if (args.json) { console.log(JSON.stringify(r, null, 2)); process.exit(r.verdict === 'needs-attention' ? 1 : 0); return; }
|
|
1243
|
+
const c = r.changed;
|
|
1244
|
+
console.log(`intent guardian: ${r.verdict.toUpperCase()} (${beforeArg} -> ${afterArg})`);
|
|
1245
|
+
console.log(` changed +${c.nodesAdded} / -${c.nodesRemoved} / ~${c.nodesChanged} nodes, +${c.relationshipsAdded} / -${c.relationshipsRemoved} relationships`);
|
|
1246
|
+
if (r.affectedIntent.length) console.log(` affected ${r.affectedIntent.map((n) => n.title || n.id).join(', ')}`);
|
|
1247
|
+
if (r.introducedRisk.length) { console.log(` introduced risk (${r.introducedRisk.length}):`); for (const f of r.introducedRisk.slice(0, 6)) console.log(` [${f.severity}] ${f.ruleId} , ${f.detected}`); }
|
|
1248
|
+
if (r.resolvedRisk.length) console.log(` resolved risk: ${r.resolvedRisk.length}`);
|
|
1249
|
+
if (r.mustReverify.length) { console.log(` must reverify (${r.mustReverify.length}):`); for (const m of r.mustReverify.slice(0, 6)) console.log(` ${m.type} ${m.title || m.id} , ${m.reason}`); }
|
|
1250
|
+
if (r.staleLearning.length) { console.log(' learning to refresh:'); for (const l of r.staleLearning.slice(0, 6)) console.log(` ${l.scope} , ${l.reason}`); }
|
|
1251
|
+
process.exit(r.verdict === 'needs-attention' ? 1 : 0);
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// `intent scan [dir]` , the Scanner spine: intent -> Intent IR -> explainable Fable findings ->
|
|
1256
|
+
// risk themes. Deterministic, no AI. --json for the machine report; --ir writes the merged IR.
|
|
1257
|
+
// Part 3 focused scan queries , one question each over the Intent IR + Fable findings:
|
|
1258
|
+
// `intent risks | gaps | unverified | coverage | unknowns | contradictions [dir] [--json]`.
|
|
1259
|
+
if (VIEWS[cmd]) {
|
|
1260
|
+
const root = file || '.';
|
|
1261
|
+
const targets = existsSync(root) && statSync(root).isDirectory() ? collectIntents(root) : [root];
|
|
1262
|
+
if (!targets.length || !existsSync(targets[0])) { console.error(`intent ${cmd}: no .intent files under ${root}`); process.exit(2); return; }
|
|
1263
|
+
const scan = scanProject(targets.map((f) => ({ file: relative(process.cwd(), f) || f, source: readFileSync(f, 'utf8') })));
|
|
1264
|
+
const v = VIEWS[cmd](scan);
|
|
1265
|
+
if (args.json) { console.log(JSON.stringify(v, null, 2)); return; }
|
|
1266
|
+
if (cmd === 'coverage') {
|
|
1267
|
+
console.log(`intent coverage ${root}: ${v.verified}/${v.total} claims verified (${v.coverage}%)`);
|
|
1268
|
+
for (const c of v.unverified) console.log(` unverified [${c.type}] ${c.title}`);
|
|
1269
|
+
process.exit(v.coverage === 100 ? 0 : 1); return;
|
|
1270
|
+
}
|
|
1271
|
+
if (cmd === 'risks') {
|
|
1272
|
+
const s = v.bySeverity;
|
|
1273
|
+
console.log(`intent risks ${root}: ${v.count} risk theme(s) , ${s.blocker || 0} blocker, ${s.error || 0} error, ${s.warning || 0} warning, ${s.info || 0} info`);
|
|
1274
|
+
for (const t of v.themes) console.log(` ${String(t.count).padStart(3)} ${t.category}${t.blocker ? ` (${t.blocker} blocker)` : ''}`);
|
|
1275
|
+
for (const r of v.remediationSequence.slice(0, 5)) console.log(` fix [${r.severity}] ${r.ruleId} (${r.count}x) , ${r.remediation}`);
|
|
1276
|
+
process.exit((s.blocker || 0) + (s.error || 0) === 0 ? 0 : 1); return;
|
|
1277
|
+
}
|
|
1278
|
+
// gaps / unverified / unknowns / contradictions , a uniform list
|
|
1279
|
+
console.log(`intent ${cmd} ${root}: ${v.count}`);
|
|
1280
|
+
for (const g of v.gaps || []) console.log(` [${g.severity}] ${g.ruleId} , ${g.detected}`);
|
|
1281
|
+
for (const c of v.claims || []) console.log(` [${c.type}] ${c.title}`);
|
|
1282
|
+
for (const u of v.unknowns || []) console.log(` [${u.type}] ${u.title}${u.confidence ? ` (${u.confidence})` : ''}`);
|
|
1283
|
+
for (const c of v.conflicts || []) console.log(` [Conflict] ${c.title}`);
|
|
1284
|
+
for (const l of v.links || []) console.log(` ${l.from} ${l.type} ${l.to}`);
|
|
1285
|
+
for (const f of v.findings || []) console.log(` ${f.ruleId} , ${f.detected}`);
|
|
1286
|
+
process.exit(v.count === 0 ? 0 : 1);
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
if (cmd === 'scan') {
|
|
1291
|
+
const root = file || '.';
|
|
1292
|
+
const targets = existsSync(root) && statSync(root).isDirectory() ? collectIntents(root) : [root];
|
|
1293
|
+
const result = scanProject(targets.map((f) => ({ file: relative(process.cwd(), f) || f, source: readFileSync(f, 'utf8') })));
|
|
1294
|
+
if (args.ir) { writeFileSync(args.ir, JSON.stringify(result.ir, null, 2)); console.error(`intent scan: wrote Intent IR (${result.ir.nodes.length} nodes) to ${args.ir}`); }
|
|
1295
|
+
if (args.json) { console.log(JSON.stringify(result, null, 2)); process.exit(result.ok ? 0 : 1); return; }
|
|
1296
|
+
const s = result.bySeverity;
|
|
1297
|
+
console.log(`intent scan ${root}: ${result.totals.findings} finding(s) across ${result.totals.missions} mission(s) in ${result.totals.files} file(s)`);
|
|
1298
|
+
console.log(` severity ${s.blocker} blocker, ${s.error} error, ${s.warning} warning, ${s.info} info , Intent IR: ${result.ir.nodes.length} nodes`);
|
|
1299
|
+
if (result.risks.length) {
|
|
1300
|
+
console.log(' risk themes:');
|
|
1301
|
+
for (const r of result.risks) console.log(` ${String(r.count).padStart(3)} ${r.category}${r.blocker ? ` (${r.blocker} blocker)` : ''}`);
|
|
1302
|
+
}
|
|
1303
|
+
if (result.remediationSequence.length) {
|
|
1304
|
+
console.log(' highest-impact remediation first:');
|
|
1305
|
+
for (const r of result.remediationSequence.slice(0, 5)) console.log(` [${r.severity}] ${r.ruleId} (${r.count}x) , ${r.remediation}`);
|
|
1306
|
+
}
|
|
1307
|
+
process.exit(result.ok ? 0 : 1);
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// `intent report [dir]` , a repo-wide intent health summary (aggregates every .intent file).
|
|
1312
|
+
// Distinct from `check` (pass/fail gate): counts by severity + area, top codes, and coverage.
|
|
1313
|
+
if (cmd === 'report') {
|
|
1314
|
+
const root = file || '.';
|
|
1315
|
+
const targets = existsSync(root) && statSync(root).isDirectory() ? collectIntents(root) : [root];
|
|
1316
|
+
const rep = buildReport(targets.map((f) => ({ file: relative(process.cwd(), f) || f, source: readFileSync(f, 'utf8') })));
|
|
1317
|
+
if (args.json) { console.log(JSON.stringify(rep, null, 2)); process.exit(0); return; }
|
|
1318
|
+
const c = rep.coverage;
|
|
1319
|
+
console.log(`intent report ${root}: ${rep.totals.missions} mission(s) in ${rep.totals.files} file(s), ${rep.totals.diagnostics} diagnostic(s)`);
|
|
1320
|
+
console.log(` severity ${rep.bySeverity.blocker} blocker, ${rep.bySeverity.error} error, ${rep.bySeverity.warning} warning, ${rep.bySeverity.info} info`);
|
|
1321
|
+
console.log(' coverage '
|
|
1322
|
+
+ `guarantees verified ${c.guaranteesVerified}/${c.guarantees}${c.guaranteeVerifyRate != null ? ` (${c.guaranteeVerifyRate}%)` : ''}, `
|
|
1323
|
+
+ `missions with tests ${c.missionsWithTests}/${rep.totals.missions}${c.testCoverageRate != null ? ` (${c.testCoverageRate}%)` : ''}, `
|
|
1324
|
+
+ `outcomes contracted ${c.outcomeContracts}/${c.outcomes}${c.outcomeContractRate != null ? ` (${c.outcomeContractRate}%)` : ''}`);
|
|
1325
|
+
if (rep.topCodes.length) {
|
|
1326
|
+
console.log(' top codes ' + rep.topCodes.slice(0, 5).map((t) => `${t.code} (${t.count})`).join(', '));
|
|
1327
|
+
}
|
|
1328
|
+
const worst = rep.files.filter((f) => f.error > 0 || f.warning > 0).slice(0, 8);
|
|
1329
|
+
if (worst.length) {
|
|
1330
|
+
console.log(' files needing attention:');
|
|
1331
|
+
for (const f of worst) console.log(` ${f.error ? 'ERR' : 'warn'} ${f.file} , ${f.error} error(s), ${f.warning} warning(s)`);
|
|
1332
|
+
}
|
|
1333
|
+
process.exit(0);
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
// `intent check <file|dir> --format sarif` emits a SARIF 2.1.0 log so ThunderLang
|
|
1338
|
+
// diagnostics show up natively in GitHub/GitLab code scanning and SARIF-aware IDEs.
|
|
1339
|
+
// This is a REPORT (exit 0); gate the build with a plain `intent check .` step.
|
|
1340
|
+
if (cmd === 'check' && args.format === 'sarif') {
|
|
1341
|
+
const targets = existsSync(file) && statSync(file).isDirectory() ? collectIntents(file) : [file];
|
|
1342
|
+
const reports = targets.map((f) => {
|
|
1343
|
+
const a = parseIntent(readFileSync(f, 'utf8'));
|
|
1344
|
+
let diags = semanticDiagnostics(a);
|
|
1345
|
+
if (a.waivers && a.waivers.length) {
|
|
1346
|
+
const now = args.now || null;
|
|
1347
|
+
diags = [...applyWaivers(diags, a.waivers, { now }).diagnostics, ...governanceDiagnostics(a.waivers, diags, { now })];
|
|
1348
|
+
}
|
|
1349
|
+
return { file: relative(process.cwd(), f) || f, diagnostics: diags.filter((d) => !d.waived) };
|
|
1350
|
+
});
|
|
1351
|
+
console.log(JSON.stringify(toSarif(reports, { version: COMPILER_VERSION }), null, 2));
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
// `intent check <dir>` recurses and gates every .intent file (self-contained CI, no
|
|
1356
|
+
// wrapper script needed). Errors fail the run; warnings do not.
|
|
1357
|
+
if (cmd === 'check' && existsSync(file) && statSync(file).isDirectory()) {
|
|
1358
|
+
const files = collectIntents(file);
|
|
1359
|
+
const reports = files.map((f) => {
|
|
1360
|
+
const a = parseIntent(readFileSync(f, 'utf8'));
|
|
1361
|
+
let diags = semanticDiagnostics(a);
|
|
1362
|
+
if (a.waivers && a.waivers.length) {
|
|
1363
|
+
const now = args.now || null;
|
|
1364
|
+
diags = [...applyWaivers(diags, a.waivers, { now }).diagnostics, ...governanceDiagnostics(a.waivers, diags, { now })];
|
|
1365
|
+
}
|
|
1366
|
+
const errors = diags.filter((d) => d.level === 'error' && !d.waived).length;
|
|
1367
|
+
return { file: relative(file, f) || f, mission: a.mission || null, errors, warnings: diags.filter((d) => d.level === 'warning' && !d.waived).length, ok: errors === 0 };
|
|
1368
|
+
});
|
|
1369
|
+
const failed = reports.filter((r) => !r.ok);
|
|
1370
|
+
if (args.json) {
|
|
1371
|
+
console.log(JSON.stringify({ schema: 'intent-check-batch-v1', root: file, total: reports.length, failed: failed.length, ok: failed.length === 0, files: reports }, null, 2));
|
|
1372
|
+
process.exit(failed.length ? 1 : 0);
|
|
1373
|
+
}
|
|
1374
|
+
console.log(`intent check ${file}: ${reports.length - failed.length}/${reports.length} passed`);
|
|
1375
|
+
for (const r of reports) console.log(` ${r.ok ? 'ok ' : 'ERR'} ${r.file}${r.errors ? ` , ${r.errors} error(s)` : ''}${r.warnings ? ` (${r.warnings} warning(s))` : ''}`);
|
|
1376
|
+
process.exit(failed.length ? 1 : 0);
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// `intent edit <file>` , apply structural field edits (intent-patch-v1) to the source,
|
|
1380
|
+
// preserving comments + formatting. Edits come from --edits <json|-> and/or convenience
|
|
1381
|
+
// flags. Prints the result to stdout, or --write applies it in place.
|
|
1382
|
+
if (cmd === 'edit') {
|
|
1383
|
+
const src = readFileSync(file, 'utf8');
|
|
1384
|
+
const edits = [];
|
|
1385
|
+
if (args.edits) {
|
|
1386
|
+
const raw = args.edits === '-' ? readFileSync(0, 'utf8') : readFileSync(args.edits, 'utf8');
|
|
1387
|
+
let parsed;
|
|
1388
|
+
try { parsed = JSON.parse(raw); } catch { console.error('intent edit: --edits is not valid JSON'); process.exit(2); return; }
|
|
1389
|
+
if (!Array.isArray(parsed)) { console.error('intent edit: --edits must be a JSON array of edit ops'); process.exit(2); return; }
|
|
1390
|
+
edits.push(...parsed);
|
|
1391
|
+
}
|
|
1392
|
+
if (args.setGoal) edits.push({ op: 'setField', field: 'goal', value: args.setGoal });
|
|
1393
|
+
for (const s of args.addGuarantee || []) edits.push({ op: 'addGuarantee', statement: s });
|
|
1394
|
+
for (const s of args.addNever || []) edits.push({ op: 'addNever', statement: s });
|
|
1395
|
+
for (const m of args.removeGuarantee || []) edits.push({ op: 'removeGuarantee', match: m });
|
|
1396
|
+
for (const m of args.removeNever || []) edits.push({ op: 'removeNever', match: m });
|
|
1397
|
+
if (!edits.length) { console.error('intent edit: no edits given. Use --edits <file|-> or --set-goal/--add-guarantee/...'); process.exit(2); return; }
|
|
1398
|
+
|
|
1399
|
+
const result = applyEdits(src, edits);
|
|
1400
|
+
if (args.json) { console.log(JSON.stringify({ ...result, source: undefined, applied: result.applied.length, skipped: result.skipped }, null, 2)); }
|
|
1401
|
+
for (const s of result.skipped) console.error(` skipped: ${s.reason}`);
|
|
1402
|
+
if (args.write) {
|
|
1403
|
+
if (result.source !== src.replace(/\r\n?/g, '\n')) writeFileSync(file, result.source);
|
|
1404
|
+
console.error(`intent edit ${basename(file)}: applied ${result.applied.length}, skipped ${result.skipped.length}.`);
|
|
1405
|
+
} else if (!args.json) {
|
|
1406
|
+
process.stdout.write(result.source);
|
|
1407
|
+
}
|
|
1408
|
+
process.exit(0);
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
// `intent fmt <file|dir>` , canonical formatting (whitespace only; content + comments
|
|
1413
|
+
// preserved). Prints to stdout, or --write in place, or --check for CI (exit 1 if any
|
|
1414
|
+
// file is not already formatted).
|
|
1415
|
+
if (cmd === 'fmt') {
|
|
1416
|
+
const targets = statSync(file).isDirectory() ? collectIntents(file) : [file];
|
|
1417
|
+
const unformatted = [];
|
|
1418
|
+
let changed = 0;
|
|
1419
|
+
for (const f of targets) {
|
|
1420
|
+
const src = readFileSync(f, 'utf8');
|
|
1421
|
+
const formatted = formatSource(src);
|
|
1422
|
+
const same = src.replace(/\r\n?/g, '\n') === formatted;
|
|
1423
|
+
if (args.check) { if (!same) unformatted.push(relative('.', f) || f); continue; }
|
|
1424
|
+
if (args.write) { if (!same) { writeFileSync(f, formatted); changed++; } continue; }
|
|
1425
|
+
process.stdout.write(formatted);
|
|
1426
|
+
}
|
|
1427
|
+
if (args.check) {
|
|
1428
|
+
if (unformatted.length) { console.error(`intent fmt --check: ${unformatted.length} file(s) not formatted:`); for (const u of unformatted) console.error(` ${u}`); process.exit(1); }
|
|
1429
|
+
console.log('intent fmt --check: all formatted.');
|
|
1430
|
+
process.exit(0);
|
|
1431
|
+
}
|
|
1432
|
+
if (args.write) console.log(`intent fmt: formatted ${changed} file(s), ${targets.length - changed} already clean.`);
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
const { source, ast, sourceHash, sourceFile } = load(file);
|
|
1437
|
+
const generatedAt = new Date().toISOString();
|
|
1438
|
+
const diagnostics = semanticDiagnostics(ast);
|
|
1439
|
+
const outDir = join(args.out, slug(ast.mission || basename(file, '.intent')));
|
|
1440
|
+
|
|
1441
|
+
// Production gate: a build --mode production refuses to proceed while an AI
|
|
1442
|
+
// implementation is not shippable. --allow-pending is for dev builds only.
|
|
1443
|
+
if (cmd === 'build' && args.mode === 'production' && ast.implementation) {
|
|
1444
|
+
const id = ast.implementation.id || slug(ast.mission || '');
|
|
1445
|
+
const pf = join(args.out, 'proofs', `${id}.json`);
|
|
1446
|
+
const proof = existsSync(pf) ? JSON.parse(readFileSync(pf, 'utf8')) : null;
|
|
1447
|
+
const st = resolveState({ ast, region: null, proof });
|
|
1448
|
+
const approvalRequired = !!ast.implementation.approval && ast.implementation.approval !== 'none';
|
|
1449
|
+
const gate = productionGate([{ ...st, id, approvalRequired }], { allowPending: args.allowPending });
|
|
1450
|
+
if (!gate.ok) {
|
|
1451
|
+
const r = gate.blocking[0];
|
|
1452
|
+
console.error(`INTENT-AI-501: production build blocked , implementation "${id}" is ${r.status}${r.reasons?.[0] ? ` (${r.reasons[0].code})` : ''}. Verify + approve, or use --allow-pending for a dev build.`);
|
|
1453
|
+
process.exit(1);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
if (cmd === 'completions' || cmd === 'hover') {
|
|
1458
|
+
const [ln, coln] = (args.position || '1:1').split(':').map(Number);
|
|
1459
|
+
const out = cmd === 'completions'
|
|
1460
|
+
? getCompletions(source, { line: ln, column: coln })
|
|
1461
|
+
: getHover(source, { line: ln, column: coln });
|
|
1462
|
+
console.log(JSON.stringify(out, null, 2));
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
if (cmd === 'check') {
|
|
1467
|
+
// Governance (Gap 5): waivers downgrade matching blockers to on-the-record exceptions.
|
|
1468
|
+
let diags = diagnostics;
|
|
1469
|
+
if (ast.waivers && ast.waivers.length) {
|
|
1470
|
+
const now = args.now || null;
|
|
1471
|
+
const applied = applyWaivers(diagnostics, ast.waivers, { now });
|
|
1472
|
+
const gov = governanceDiagnostics(ast.waivers, diagnostics, { now });
|
|
1473
|
+
diags = [...applied.diagnostics, ...gov];
|
|
1474
|
+
}
|
|
1475
|
+
const errors = diags.filter((d) => d.level === 'error' && !d.waived).length;
|
|
1476
|
+
if (args.json) {
|
|
1477
|
+
// Machine-readable diagnostics for editors, CI, and OpenThunder.
|
|
1478
|
+
const out = {
|
|
1479
|
+
schema: 'intent-check-v1',
|
|
1480
|
+
file: sourceFile,
|
|
1481
|
+
mission: ast.mission || null,
|
|
1482
|
+
ok: errors === 0,
|
|
1483
|
+
summary: {
|
|
1484
|
+
errors,
|
|
1485
|
+
warnings: diags.filter((d) => d.level === 'warning' && !d.waived).length,
|
|
1486
|
+
info: diags.filter((d) => d.level === 'info').length,
|
|
1487
|
+
waived: diags.filter((d) => d.waived).length,
|
|
1488
|
+
},
|
|
1489
|
+
diagnostics: diags.map((d) => ({
|
|
1490
|
+
level: d.level, code: d.code, message: d.message,
|
|
1491
|
+
...(d.why ? { why: d.why } : {}),
|
|
1492
|
+
...(d.severity ? { severity: d.severity } : {}),
|
|
1493
|
+
...(Array.isArray(d.blocks) && d.blocks.length ? { blocks: d.blocks } : {}),
|
|
1494
|
+
...(d.waived ? { waived: true, waiver: d.waiver } : {}),
|
|
1495
|
+
})),
|
|
1496
|
+
};
|
|
1497
|
+
console.log(JSON.stringify(out, null, 2));
|
|
1498
|
+
process.exit(errors > 0 ? 1 : 0);
|
|
1499
|
+
}
|
|
1500
|
+
console.log(`intent check ${sourceFile} (mission: ${ast.mission})`);
|
|
1501
|
+
process.exit(printDiagnostics(diags) > 0 ? 1 : 0);
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
const generated = [];
|
|
1505
|
+
if (cmd === 'graph' || cmd === 'build') {
|
|
1506
|
+
generated.push(writeJson(outDir, 'contract-graph.json', buildContractGraph(ast, generatedAt)));
|
|
1507
|
+
generated.push(writeJson(outDir, 'architecture-graph.json', buildArchitectureGraph(ast, generatedAt)));
|
|
1508
|
+
generated.push(writeJson(outDir, 'implementation-plan.json', buildImplementationPlan(ast, generatedAt)));
|
|
1509
|
+
generated.push(writeJson(outDir, 'intent-graph.json', buildIntentGraph(ast)));
|
|
1510
|
+
}
|
|
1511
|
+
if (cmd === 'build') {
|
|
1512
|
+
generated.push(writeText(outDir, `${slug(ast.mission)}.md`, renderMarkdown(ast)));
|
|
1513
|
+
generated.push(writeText(outDir, `${slug(ast.mission)}.mmd`, renderMermaid(ast)));
|
|
1514
|
+
generated.push(writeText(outDir, `${slug(ast.mission)}.testplan.md`, renderTestplan(ast)));
|
|
1515
|
+
}
|
|
1516
|
+
if (cmd === 'proof' || cmd === 'build' || cmd === 'graph') {
|
|
1517
|
+
const proof = buildProof(ast, {
|
|
1518
|
+
sourceFile, sourceHash, generatedAt,
|
|
1519
|
+
targetsRequested: args.targets || ast.targets,
|
|
1520
|
+
targetsGenerated: generated.map((p) => p.replace(process.cwd() + '/', '')),
|
|
1521
|
+
diagnostics,
|
|
1522
|
+
});
|
|
1523
|
+
generated.push(writeJson(outDir, '.intent-proof.json', proof));
|
|
1524
|
+
}
|
|
1525
|
+
if (!['graph', 'proof', 'build'].includes(cmd)) {
|
|
1526
|
+
console.error(`unknown command: ${cmd}`);
|
|
1527
|
+
process.exit(2);
|
|
1528
|
+
}
|
|
1529
|
+
console.log(`intent ${cmd} ${sourceFile} -> ${outDir}`);
|
|
1530
|
+
for (const p of generated) console.log(` wrote ${p.replace(process.cwd() + '/', '')}`);
|
|
1531
|
+
printDiagnostics(diagnostics);
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
main();
|