@nudojs/service 3.0.0 → 5.0.0-beta.0
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/README.md +19 -4
- package/dist/analysis-CSXrOVlg.d.ts +267 -0
- package/dist/analysis.d.ts +7 -0
- package/dist/analysis.js +57 -0
- package/dist/analyzer-types-JyJiCt8w.d.ts +158 -0
- package/dist/call-record-CkyCFkT9.d.ts +33 -0
- package/dist/case.d.ts +124 -0
- package/dist/case.js +18 -0
- package/dist/chunk-6IZR4ZYJ.js +1107 -0
- package/dist/chunk-E4N4A4JV.js +651 -0
- package/dist/chunk-HEU4WON5.js +2419 -0
- package/dist/chunk-K3ZPOTP2.js +199 -0
- package/dist/chunk-LFZRUXGK.js +89 -0
- package/dist/chunk-PVJIMQRI.js +246 -0
- package/dist/chunk-QAFMPM2E.js +3687 -0
- package/dist/chunk-QYRH7Z2Q.js +89 -0
- package/dist/chunk-U7JYTRIB.js +888 -0
- package/dist/{chunk-ADFCZP72.js → chunk-YLALZXTZ.js} +180 -165
- package/dist/chunk-YLVZMHWC.js +364 -0
- package/dist/{config-Cqj8zeZH.d.ts → config-DDB588oA.d.ts} +45 -48
- package/dist/dts.d.ts +142 -0
- package/dist/dts.js +30 -0
- package/dist/env-loader-fj0TSCcA.d.ts +20 -0
- package/dist/evaluator/evaluator-api.d.ts +3 -1
- package/dist/evaluator/evaluator-api.js +9 -3
- package/dist/harvest.d.ts +254 -0
- package/dist/harvest.js +75 -0
- package/dist/index.d.ts +293 -1197
- package/dist/index.js +328 -7639
- package/dist/interface-BGlP7aIk.d.ts +367 -0
- package/dist/interface.d.ts +3 -0
- package/dist/interface.js +43 -0
- package/dist/lsp.d.ts +103 -0
- package/dist/lsp.js +36 -0
- package/package.json +32 -8
|
@@ -0,0 +1,2419 @@
|
|
|
1
|
+
import {
|
|
2
|
+
unifiedDiff
|
|
3
|
+
} from "./chunk-YLVZMHWC.js";
|
|
4
|
+
import {
|
|
5
|
+
analyzeFileAsync,
|
|
6
|
+
defaultLoadModule,
|
|
7
|
+
evalAbsModuleGraph
|
|
8
|
+
} from "./chunk-QAFMPM2E.js";
|
|
9
|
+
import {
|
|
10
|
+
diskCacheRoot,
|
|
11
|
+
findProjectConfig,
|
|
12
|
+
interfaceConfig,
|
|
13
|
+
matchesEmitAllowlist
|
|
14
|
+
} from "./chunk-YLALZXTZ.js";
|
|
15
|
+
|
|
16
|
+
// src/dep-contents.ts
|
|
17
|
+
import { loadModuleDepsFingerprint } from "@nudojs/core/internal";
|
|
18
|
+
function collectLoadDepContents(filePath, source, loadModule) {
|
|
19
|
+
const fp = loadModuleDepsFingerprint(source, loadModule, filePath);
|
|
20
|
+
const depContents = fp.contents.length > 0 ? fp.contents : fp.paths.map((path) => ({ path, content: null }));
|
|
21
|
+
return { depContents, truncated: fp.truncated };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/disk-cache.ts
|
|
25
|
+
import { createHash } from "crypto";
|
|
26
|
+
import { createRequire } from "module";
|
|
27
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync, rmSync } from "fs";
|
|
28
|
+
import { join, dirname, relative, sep, isAbsolute } from "path";
|
|
29
|
+
function readServiceVersion() {
|
|
30
|
+
try {
|
|
31
|
+
const require2 = createRequire(import.meta.url);
|
|
32
|
+
for (const p of ["../package.json", "./package.json", "../../package.json"]) {
|
|
33
|
+
try {
|
|
34
|
+
const pkg = require2(p);
|
|
35
|
+
if (pkg?.name === "@nudojs/service" && pkg.version) return pkg.version;
|
|
36
|
+
if (pkg?.version && p.includes("service")) return pkg.version;
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
}
|
|
42
|
+
return "0";
|
|
43
|
+
}
|
|
44
|
+
var ANALYSIS_ABI = `nudo-check-cache-v3+${readServiceVersion()}`;
|
|
45
|
+
function sha256Hex(data) {
|
|
46
|
+
return createHash("sha256").update(data).digest("hex");
|
|
47
|
+
}
|
|
48
|
+
function relativizePath(p, root) {
|
|
49
|
+
const norm = p.split(sep).join("/");
|
|
50
|
+
if (!root) return norm;
|
|
51
|
+
const r = relative(root, p).split(sep).join("/");
|
|
52
|
+
if (!r.startsWith("..") && !isAbsolute(r)) return r;
|
|
53
|
+
return `ext:${createHash("sha256").update(norm).digest("hex").slice(0, 16)}`;
|
|
54
|
+
}
|
|
55
|
+
function sanitizeCacheNamespace(ns) {
|
|
56
|
+
return ns.replace(/[^a-z0-9_-]/gi, "_").replace(/^_+|_+$/g, "") || "cache";
|
|
57
|
+
}
|
|
58
|
+
var DiskCache = class {
|
|
59
|
+
root;
|
|
60
|
+
ns;
|
|
61
|
+
enabled = false;
|
|
62
|
+
constructor(opts) {
|
|
63
|
+
this.root = opts.root;
|
|
64
|
+
this.ns = sanitizeCacheNamespace(opts.namespace);
|
|
65
|
+
this.enabled = !!opts.root;
|
|
66
|
+
}
|
|
67
|
+
pathFor(key) {
|
|
68
|
+
if (!/^[a-f0-9]{16,128}$/i.test(key)) {
|
|
69
|
+
throw new Error("DiskCache key must be a hex digest");
|
|
70
|
+
}
|
|
71
|
+
return join(this.root, this.ns, key.slice(0, 2), `${key}.json`);
|
|
72
|
+
}
|
|
73
|
+
get(key) {
|
|
74
|
+
if (!this.enabled || !this.root) return void 0;
|
|
75
|
+
try {
|
|
76
|
+
const p = this.pathFor(key);
|
|
77
|
+
if (!existsSync(p)) return void 0;
|
|
78
|
+
const raw = readFileSync(p, "utf8");
|
|
79
|
+
const parsed = JSON.parse(raw);
|
|
80
|
+
if (parsed?.abi !== ANALYSIS_ABI) return void 0;
|
|
81
|
+
return parsed.value;
|
|
82
|
+
} catch {
|
|
83
|
+
return void 0;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
set(key, value) {
|
|
87
|
+
if (!this.enabled || !this.root) return;
|
|
88
|
+
try {
|
|
89
|
+
const p = this.pathFor(key);
|
|
90
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
91
|
+
writeFileSync(p, JSON.stringify({ abi: ANALYSIS_ABI, value }), "utf8");
|
|
92
|
+
} catch {
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
clearNamespace() {
|
|
96
|
+
if (!this.enabled || !this.root) return;
|
|
97
|
+
try {
|
|
98
|
+
rmSync(join(this.root, this.ns), { recursive: true, force: true });
|
|
99
|
+
} catch {
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
function checkCacheKey(filePath, source, opts) {
|
|
104
|
+
const rel = relativizePath(filePath, opts.projectDir);
|
|
105
|
+
const sidecarSha = opts.sidecarContent != null ? sha256Hex(opts.sidecarContent) : "nosidecar";
|
|
106
|
+
const depSeg = (opts.depContents ?? []).map(
|
|
107
|
+
(d) => `${relativizePath(d.path, opts.projectDir)}\0${d.content != null ? sha256Hex(d.content) : "miss"}`
|
|
108
|
+
).join("\n");
|
|
109
|
+
const envSeg = (opts.projectEnvNames ?? []).length > 0 ? [...opts.projectEnvNames ?? []].sort().join(",") : "-";
|
|
110
|
+
const cfgSeg = opts.analysisCfg ? `${opts.analysisCfg.mode ?? "-"}|${opts.analysisCfg.evalMissingSlot ?? "-"}|${opts.analysisCfg.callSiteBudget ?? "-"}|${opts.analysisCfg.entryThrows ?? "-"}|${opts.analysisCfg.ignoreThrows ?? "-"}` : "-";
|
|
111
|
+
return sha256Hex(
|
|
112
|
+
[
|
|
113
|
+
ANALYSIS_ABI,
|
|
114
|
+
rel,
|
|
115
|
+
opts.autoBind ? "ab1" : "ab0",
|
|
116
|
+
sha256Hex(source),
|
|
117
|
+
sidecarSha,
|
|
118
|
+
depSeg,
|
|
119
|
+
envSeg,
|
|
120
|
+
cfgSeg
|
|
121
|
+
].join("\0")
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
function ifaceCacheKey(filePath, source, opts) {
|
|
125
|
+
const rel = relativizePath(filePath, opts.projectDir);
|
|
126
|
+
const sidecarSeg = opts.autoBind && opts.sidecarSource !== void 0 ? `sc:${sha256Hex(opts.sidecarSource)}` : "sc0";
|
|
127
|
+
const depSeg = (opts.depContents ?? []).map(
|
|
128
|
+
(d) => `${relativizePath(d.path, opts.projectDir)}\0${d.content != null ? sha256Hex(d.content) : "miss"}`
|
|
129
|
+
).join("\n");
|
|
130
|
+
const envSeg = (opts.projectEnvNames ?? []).length > 0 ? [...opts.projectEnvNames ?? []].sort().join(",") : "-";
|
|
131
|
+
return sha256Hex(
|
|
132
|
+
[
|
|
133
|
+
ANALYSIS_ABI,
|
|
134
|
+
"iface",
|
|
135
|
+
rel,
|
|
136
|
+
opts.autoBind ? "ab1" : "ab0",
|
|
137
|
+
sha256Hex(source),
|
|
138
|
+
sidecarSeg,
|
|
139
|
+
depSeg,
|
|
140
|
+
envSeg
|
|
141
|
+
].join("\0")
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
function extractNudoImportSpecs(source) {
|
|
145
|
+
const specs = /* @__PURE__ */ new Set();
|
|
146
|
+
const named = /@nudo:import\s*\{[^}]*\}\s*from\s*["']([^"']+)["']/g;
|
|
147
|
+
const ns = /@nudo:import\s+\*\s+as\s+\w+\s+from\s*["']([^"']+)["']/g;
|
|
148
|
+
let m;
|
|
149
|
+
while (m = named.exec(source)) specs.add(m[1]);
|
|
150
|
+
while (m = ns.exec(source)) specs.add(m[1]);
|
|
151
|
+
return [...specs];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/interface-surface.ts
|
|
155
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
156
|
+
import { dirname as dirname2, resolve } from "path";
|
|
157
|
+
import {
|
|
158
|
+
effectiveInterface,
|
|
159
|
+
formatConstraint,
|
|
160
|
+
formatShape,
|
|
161
|
+
interfaceDiagCount,
|
|
162
|
+
localNamedExports,
|
|
163
|
+
refineDiagCount,
|
|
164
|
+
sidecarPathOf,
|
|
165
|
+
takeInterfaceDiagsSince,
|
|
166
|
+
takeRefineDiagsSince
|
|
167
|
+
} from "@nudojs/core";
|
|
168
|
+
function collectDepContents(filePath, source, loadModule) {
|
|
169
|
+
return collectLoadDepContents(filePath, source, loadModule ?? defaultLoadModule);
|
|
170
|
+
}
|
|
171
|
+
function formatInterfaceSurfaceLine(e) {
|
|
172
|
+
const params = `(${e.params.map((p) => `${p.name}: ${p.display}`).join(", ")})`;
|
|
173
|
+
let line = ` ${e.fn} [${e.source}] ${params}`;
|
|
174
|
+
if (e.returns !== void 0) line += ` \u2192 ${e.returns}`;
|
|
175
|
+
if (e.kind === "local") line += " (local)";
|
|
176
|
+
return line;
|
|
177
|
+
}
|
|
178
|
+
function absToImplicitDisplay(a) {
|
|
179
|
+
try {
|
|
180
|
+
return formatShape(a);
|
|
181
|
+
} catch {
|
|
182
|
+
return "unknown";
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function constraintToJson(c) {
|
|
186
|
+
return JSON.parse(JSON.stringify(c));
|
|
187
|
+
}
|
|
188
|
+
function cachedToEffective(e) {
|
|
189
|
+
return {
|
|
190
|
+
fnName: e.fnName,
|
|
191
|
+
params: e.params.map((p) => ({
|
|
192
|
+
param: p.param,
|
|
193
|
+
constraint: p.constraint
|
|
194
|
+
})),
|
|
195
|
+
...e.returns ? { returns: { constraint: e.returns.constraint } } : {},
|
|
196
|
+
source: e.source,
|
|
197
|
+
...e.conflict ? { conflict: e.conflict } : {}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
async function interfaceSurface(filePath, opts = {}) {
|
|
201
|
+
const abs = resolve(filePath);
|
|
202
|
+
const source = opts.source ?? readFileSync2(abs, "utf-8");
|
|
203
|
+
const fromBuffer = opts.source !== void 0;
|
|
204
|
+
const ifaceSince = interfaceDiagCount();
|
|
205
|
+
const refineSince = refineDiagCount();
|
|
206
|
+
const proj = findProjectConfig(dirname2(abs));
|
|
207
|
+
const autoBind = opts.autoBind ?? interfaceConfig(proj?.config).autoBind;
|
|
208
|
+
const loadModule = opts.loadModule ?? defaultLoadModule;
|
|
209
|
+
const exported = localNamedExports(source);
|
|
210
|
+
const kindOf = (fnName) => exported.has(fnName) ? "export" : "local";
|
|
211
|
+
const analysis = await analyzeFileAsync(abs, source, void 0, opts.records, loadModule);
|
|
212
|
+
let disk;
|
|
213
|
+
let ifaceKey;
|
|
214
|
+
let cachedTable;
|
|
215
|
+
if (!opts.loadModule && !opts.records && !fromBuffer) {
|
|
216
|
+
const cacheRoot = diskCacheRoot(proj?.config, proj?.projectDir);
|
|
217
|
+
disk = new DiskCache({ root: cacheRoot, namespace: "iface" });
|
|
218
|
+
if (disk.enabled) {
|
|
219
|
+
let sidecarSource;
|
|
220
|
+
try {
|
|
221
|
+
const sc = sidecarPathOf(abs);
|
|
222
|
+
if (autoBind !== false) {
|
|
223
|
+
const openSc = loadModule(`./${sc.slice(sc.lastIndexOf("/") + 1)}`, abs);
|
|
224
|
+
if (openSc !== void 0) sidecarSource = openSc;
|
|
225
|
+
else if (existsSync2(sc)) sidecarSource = readFileSync2(sc, "utf-8");
|
|
226
|
+
}
|
|
227
|
+
} catch {
|
|
228
|
+
sidecarSource = void 0;
|
|
229
|
+
}
|
|
230
|
+
const dep = collectDepContents(abs, source, loadModule);
|
|
231
|
+
const hasBareMiss = (dep.depContents ?? []).some((d) => d.content == null);
|
|
232
|
+
if (dep.truncated || hasBareMiss) {
|
|
233
|
+
ifaceKey = void 0;
|
|
234
|
+
cachedTable = void 0;
|
|
235
|
+
} else {
|
|
236
|
+
ifaceKey = ifaceCacheKey(abs, source, {
|
|
237
|
+
autoBind: autoBind !== false,
|
|
238
|
+
projectDir: proj?.projectDir,
|
|
239
|
+
sidecarSource,
|
|
240
|
+
depContents: dep.depContents,
|
|
241
|
+
projectEnvNames: proj?.config.env ?? []
|
|
242
|
+
});
|
|
243
|
+
cachedTable = disk.get(ifaceKey);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const entries = [];
|
|
248
|
+
const freshTable = { fns: {} };
|
|
249
|
+
const useCache = cachedTable !== void 0;
|
|
250
|
+
for (const fn of analysis.functions) {
|
|
251
|
+
let eff;
|
|
252
|
+
if (useCache) {
|
|
253
|
+
const hit = cachedTable.fns[fn.name];
|
|
254
|
+
eff = hit === null ? void 0 : hit ? cachedToEffective(hit) : void 0;
|
|
255
|
+
} else {
|
|
256
|
+
eff = effectiveInterface(source, fn.name, {
|
|
257
|
+
loadModule,
|
|
258
|
+
fromFile: abs,
|
|
259
|
+
autoBind,
|
|
260
|
+
...proj?.projectDir ? { projectDir: proj.projectDir } : {}
|
|
261
|
+
});
|
|
262
|
+
if (freshTable) {
|
|
263
|
+
freshTable.fns[fn.name] = eff ? {
|
|
264
|
+
fnName: eff.fnName,
|
|
265
|
+
params: eff.params.map((p) => ({
|
|
266
|
+
param: p.param,
|
|
267
|
+
constraint: constraintToJson(p.constraint)
|
|
268
|
+
})),
|
|
269
|
+
...eff.returns ? { returns: { constraint: constraintToJson(eff.returns.constraint) } } : {},
|
|
270
|
+
// 磁盘表保留真实分档;implicit 也可序列化(展示层用)
|
|
271
|
+
source: eff.source,
|
|
272
|
+
...eff.conflict ? { conflict: eff.conflict } : {}
|
|
273
|
+
} : null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (eff) {
|
|
277
|
+
entries.push({
|
|
278
|
+
fn: fn.name,
|
|
279
|
+
kind: kindOf(fn.name),
|
|
280
|
+
source: eff.source,
|
|
281
|
+
params: eff.params.map((p) => ({ name: p.param, display: formatConstraint(p.constraint) })),
|
|
282
|
+
returns: eff.returns ? formatConstraint(eff.returns.constraint) : void 0
|
|
283
|
+
});
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const params = fn.paramNames.map((name, i) => {
|
|
287
|
+
const seen = [];
|
|
288
|
+
for (const c of fn.cases) {
|
|
289
|
+
const absArg = c.argAbs[i];
|
|
290
|
+
if (absArg) {
|
|
291
|
+
const s = absToImplicitDisplay(absArg);
|
|
292
|
+
if (!seen.includes(s)) seen.push(s);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return { name, display: seen.length > 0 ? seen.join(" | ") : "unknown" };
|
|
296
|
+
});
|
|
297
|
+
let ret;
|
|
298
|
+
if (fn.combinedAbs) {
|
|
299
|
+
ret = absToImplicitDisplay(fn.combinedAbs);
|
|
300
|
+
} else if (fn.cases.length > 0) {
|
|
301
|
+
const last = fn.cases[fn.cases.length - 1];
|
|
302
|
+
ret = absToImplicitDisplay(last.abs);
|
|
303
|
+
}
|
|
304
|
+
entries.push({ fn: fn.name, kind: kindOf(fn.name), source: "implicit", params, returns: ret });
|
|
305
|
+
}
|
|
306
|
+
if (disk?.enabled && ifaceKey && !useCache && analysis.functions.length > 0) {
|
|
307
|
+
try {
|
|
308
|
+
disk.set(ifaceKey, freshTable);
|
|
309
|
+
} catch {
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
takeInterfaceDiagsSince(ifaceSince);
|
|
313
|
+
takeRefineDiagsSince(refineSince);
|
|
314
|
+
return entries;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// src/interface-emitter.ts
|
|
318
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, renameSync, unlinkSync } from "fs";
|
|
319
|
+
import { basename, dirname as dirname3, relative as relative2, resolve as resolve2 } from "path";
|
|
320
|
+
import {
|
|
321
|
+
execNudoModule,
|
|
322
|
+
formatConstraint as formatConstraint2,
|
|
323
|
+
interfaceDiagCount as interfaceDiagCount2,
|
|
324
|
+
isNodeModulesPath,
|
|
325
|
+
isNudoConstraint,
|
|
326
|
+
joinThenProject,
|
|
327
|
+
localNamedExports as localNamedExports2,
|
|
328
|
+
parseSource,
|
|
329
|
+
refineDiagCount as refineDiagCount2,
|
|
330
|
+
sidecarPathOf as sidecarPathOf2,
|
|
331
|
+
takeInterfaceDiagsSince as takeInterfaceDiagsSince2,
|
|
332
|
+
takeRefineDiagsSince as takeRefineDiagsSince2
|
|
333
|
+
} from "@nudojs/core";
|
|
334
|
+
import { randomBytes } from "crypto";
|
|
335
|
+
var GENERATED_HEADER = "// @generated by nudo \u2014 do not edit; regenerate with `nudo contract --emit`";
|
|
336
|
+
async function emitInterface(filePath, opts) {
|
|
337
|
+
const abs = resolve2(filePath);
|
|
338
|
+
if (isNodeModulesPath(abs) || isNodeModulesPath(sidecarPathOf2(abs))) {
|
|
339
|
+
throw new Error(
|
|
340
|
+
`emit target '${abs}' is inside node_modules; contract sidecars are never written there`
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
const proj = findProjectConfig(dirname3(abs));
|
|
344
|
+
const allow = interfaceConfig(proj?.config).emit;
|
|
345
|
+
if (!matchesEmitAllowlist(abs, proj?.projectDir, allow)) {
|
|
346
|
+
return {
|
|
347
|
+
written: [],
|
|
348
|
+
skipped: [{ fn: opts.fnNames?.[0] ?? "*", reason: "emit-denied" }],
|
|
349
|
+
changed: false,
|
|
350
|
+
issues: [
|
|
351
|
+
{
|
|
352
|
+
code: "nudo:interface-emit-denied",
|
|
353
|
+
severity: "warning",
|
|
354
|
+
message: `emit target '${relative2(process.cwd(), abs) || abs}' is outside package.json#nudo.contract.emit allowlist`
|
|
355
|
+
}
|
|
356
|
+
],
|
|
357
|
+
sidecarPath: sidecarPathOf2(abs)
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
const source = opts.source ?? readFileSync3(abs, "utf-8");
|
|
361
|
+
const ifaceSince = interfaceDiagCount2();
|
|
362
|
+
const refineSince = refineDiagCount2();
|
|
363
|
+
const sidecarPath = sidecarPathOf2(abs);
|
|
364
|
+
const sidecarSrc = existsSync3(sidecarPath) ? readFileSync3(sidecarPath, "utf-8") : "";
|
|
365
|
+
const srcRel = relative2(dirname3(sidecarPath), abs) || basename(abs);
|
|
366
|
+
const sections = collectGeneratedSections(sidecarSrc);
|
|
367
|
+
const generatedNames = new Set(sections.flatMap((s) => s.names));
|
|
368
|
+
const declared = topLevelDeclaredNames(sidecarSrc);
|
|
369
|
+
if (declared === void 0) {
|
|
370
|
+
throw new Error(
|
|
371
|
+
`sidecar '${relative2(process.cwd(), sidecarPath) || sidecarPath}' is not parseable; fix it before emitting`
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
for (const n of generatedNames) declared.delete(n);
|
|
375
|
+
const analysis = await analyzeFileAsync(
|
|
376
|
+
abs,
|
|
377
|
+
source,
|
|
378
|
+
void 0,
|
|
379
|
+
opts.records,
|
|
380
|
+
opts.loadModule
|
|
381
|
+
);
|
|
382
|
+
const exported = localNamedExports2(source);
|
|
383
|
+
const fnByName = /* @__PURE__ */ new Map();
|
|
384
|
+
for (const f of analysis.functions) {
|
|
385
|
+
if (!fnByName.has(f.name)) fnByName.set(f.name, f);
|
|
386
|
+
}
|
|
387
|
+
const fileExportOrder = analysis.functions.map((f) => f.name).filter((n) => exported.has(n));
|
|
388
|
+
let targetNames;
|
|
389
|
+
if (opts.fnNames && opts.fnNames.length > 0) {
|
|
390
|
+
targetNames = [...new Set(opts.fnNames)];
|
|
391
|
+
} else if (opts.all) {
|
|
392
|
+
targetNames = fileExportOrder;
|
|
393
|
+
} else {
|
|
394
|
+
targetNames = fileExportOrder.filter((n) => generatedNames.has(n));
|
|
395
|
+
}
|
|
396
|
+
const targetSet = new Set(targetNames);
|
|
397
|
+
const written = [];
|
|
398
|
+
const skipped = [];
|
|
399
|
+
const issues = [];
|
|
400
|
+
const accepted = [];
|
|
401
|
+
const planCache = /* @__PURE__ */ new Map();
|
|
402
|
+
const planFor = (name) => {
|
|
403
|
+
const hit = planCache.get(name);
|
|
404
|
+
if (hit) return hit;
|
|
405
|
+
const fn = fnByName.get(name);
|
|
406
|
+
const dsl = fn === void 0 ? void 0 : projectFunctionDsl(fn);
|
|
407
|
+
const roundTrip = dsl !== void 0 && roundTrips(sectionText(name, dsl, srcRel), name);
|
|
408
|
+
const rec = { dsl, roundTrip };
|
|
409
|
+
planCache.set(name, rec);
|
|
410
|
+
return rec;
|
|
411
|
+
};
|
|
412
|
+
const atomicSections = /* @__PURE__ */ new Set();
|
|
413
|
+
for (const s of sections) {
|
|
414
|
+
if (s.names.length <= 1) continue;
|
|
415
|
+
const everyRewritable = s.names.every(
|
|
416
|
+
(n) => targetSet.has(n) && exported.has(n) && !declared.has(n) && planFor(n).dsl !== void 0 && planFor(n).roundTrip
|
|
417
|
+
);
|
|
418
|
+
if (!everyRewritable) atomicSections.add(s);
|
|
419
|
+
}
|
|
420
|
+
const acceptedAtomic = /* @__PURE__ */ new Set();
|
|
421
|
+
for (const name of targetNames) {
|
|
422
|
+
if (!exported.has(name) || !fnByName.has(name)) {
|
|
423
|
+
skipped.push({
|
|
424
|
+
fn: name,
|
|
425
|
+
reason: "not-an-export"
|
|
426
|
+
});
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
if (declared.has(name)) {
|
|
430
|
+
skipped.push({ fn: name, reason: "name-clash" });
|
|
431
|
+
issues.push({
|
|
432
|
+
code: "nudo:interface-name-clash",
|
|
433
|
+
severity: "error",
|
|
434
|
+
message: `sidecar already has a handwritten binding '${name}' (${srcRel}); handwritten wins \u2014 skipping emit for it`
|
|
435
|
+
});
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
const prev = sections.find((s) => s.names.includes(name));
|
|
439
|
+
if (prev !== void 0 && atomicSections.has(prev)) {
|
|
440
|
+
if (!acceptedAtomic.has(prev)) {
|
|
441
|
+
acceptedAtomic.add(prev);
|
|
442
|
+
const keptText = normalizeSection(prev.text);
|
|
443
|
+
accepted.push({ fn: prev.names.join("+"), text: keptText, prevText: keptText });
|
|
444
|
+
issues.push({
|
|
445
|
+
code: "nudo:interface-multi-declarator",
|
|
446
|
+
severity: "warning",
|
|
447
|
+
message: `generated section '${prev.names.join(", ")}' is a hand-merged multi-declarator form; kept verbatim (split it into one export per section to re-emit)`
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
skipped.push({ fn: name, reason: "multi-declarator" });
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
const prevText = prev === void 0 ? void 0 : normalizeSection(prev.text);
|
|
454
|
+
const plan = planFor(name);
|
|
455
|
+
if (plan.dsl === void 0 || !plan.roundTrip) {
|
|
456
|
+
skipped.push({ fn: name, reason: "not-projectable" });
|
|
457
|
+
if (prevText !== void 0) {
|
|
458
|
+
accepted.push({ fn: name, text: prevText, prevText });
|
|
459
|
+
}
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
const text = sectionText(name, plan.dsl, srcRel);
|
|
463
|
+
if (opts.mode === "add" && prev !== void 0) {
|
|
464
|
+
skipped.push({ fn: name, reason: "no-change" });
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
if (prevText !== void 0 && prevText === normalizeSection(text)) {
|
|
468
|
+
skipped.push({ fn: name, reason: "no-change" });
|
|
469
|
+
accepted.push({ fn: name, text: prevText, prevText });
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
written.push(name);
|
|
473
|
+
accepted.push({ fn: name, text, prevText });
|
|
474
|
+
}
|
|
475
|
+
let finalContent;
|
|
476
|
+
if (opts.mode === "add") {
|
|
477
|
+
finalContent = accepted.length === 0 ? sidecarSrc : joinSections(sidecarSrc, accepted.map((a) => a.text));
|
|
478
|
+
} else {
|
|
479
|
+
const base = removeSections(sidecarSrc, sections);
|
|
480
|
+
const preserved = sections.filter((s) => !s.names.some((n) => targetSet.has(n))).map((s) => normalizeSection(s.text));
|
|
481
|
+
finalContent = joinSections(base, [...preserved, ...accepted.map((a) => a.text)]);
|
|
482
|
+
}
|
|
483
|
+
const changed = finalContent !== sidecarSrc;
|
|
484
|
+
const diff = changed ? unifiedDiff(sidecarSrc, finalContent, relative2(process.cwd(), sidecarPath) || sidecarPath) : void 0;
|
|
485
|
+
if (changed && !opts.dryRun) {
|
|
486
|
+
const tmp = `${sidecarPath}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
487
|
+
try {
|
|
488
|
+
writeFileSync2(tmp, finalContent, "utf-8");
|
|
489
|
+
renameSync(tmp, sidecarPath);
|
|
490
|
+
} catch (e) {
|
|
491
|
+
try {
|
|
492
|
+
if (existsSync3(tmp)) unlinkSync(tmp);
|
|
493
|
+
} catch {
|
|
494
|
+
}
|
|
495
|
+
throw e;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
takeRefineDiagsSince2(refineSince);
|
|
499
|
+
takeInterfaceDiagsSince2(ifaceSince);
|
|
500
|
+
return {
|
|
501
|
+
written,
|
|
502
|
+
skipped,
|
|
503
|
+
changed,
|
|
504
|
+
...diff !== void 0 ? { diff } : {},
|
|
505
|
+
issues,
|
|
506
|
+
sidecarPath,
|
|
507
|
+
...opts.fnNames && opts.fnNames.length > 0 ? {} : opts.all ? {} : targetNames.length === 0 ? { emptyDefaultTargets: true } : {}
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
function formatEmitSummary(sourcePath, sidecarRel, result) {
|
|
511
|
+
const lines = [];
|
|
512
|
+
if (result.changed) {
|
|
513
|
+
lines.push(`Updated ${sourcePath} \u2192 ${sidecarRel}`);
|
|
514
|
+
lines.push(` written: ${result.written.join(", ") || "(none)"}`);
|
|
515
|
+
} else {
|
|
516
|
+
lines.push(`${sourcePath}: no interface changes`);
|
|
517
|
+
if (result.emptyDefaultTargets) {
|
|
518
|
+
lines.push(
|
|
519
|
+
` tip: default --emit only refreshes existing @generated segments; pass --fn <name> or --all to create new ones`
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
for (const s of result.skipped.filter((x) => x.reason !== "no-change")) {
|
|
524
|
+
lines.push(` skipped ${s.fn} (${s.reason})`);
|
|
525
|
+
}
|
|
526
|
+
for (const i of result.issues) {
|
|
527
|
+
lines.push(` [${i.severity}] ${i.code}: ${i.message}`);
|
|
528
|
+
}
|
|
529
|
+
return lines;
|
|
530
|
+
}
|
|
531
|
+
function projectFunctionDsl(fn) {
|
|
532
|
+
const callsite = fn.cases.filter((c) => c.source === "callsite");
|
|
533
|
+
const directive = fn.cases.filter((c) => c.source === "directive");
|
|
534
|
+
const paramCases = callsite.length > 0 ? callsite : directive;
|
|
535
|
+
const returnCases = callsite.length > 0 ? callsite : directive.length > 0 ? directive : fn.cases;
|
|
536
|
+
const paramParts = [];
|
|
537
|
+
for (let i = 0; i < fn.paramNames.length; i++) {
|
|
538
|
+
const argAbs = [];
|
|
539
|
+
for (const c of paramCases) {
|
|
540
|
+
const a = c.argAbs[i];
|
|
541
|
+
if (a !== void 0) argAbs.push(a);
|
|
542
|
+
}
|
|
543
|
+
if (argAbs.length === 0) continue;
|
|
544
|
+
const constraint = joinThenProject(argAbs);
|
|
545
|
+
if (constraint === void 0) return void 0;
|
|
546
|
+
paramParts.push(`${fn.paramNames[i]}: ${formatConstraint2(constraint)}`);
|
|
547
|
+
}
|
|
548
|
+
const retAbs = [];
|
|
549
|
+
for (const c of returnCases) {
|
|
550
|
+
if (c.throwsAbs.shape.k !== "never") continue;
|
|
551
|
+
retAbs.push(c.abs);
|
|
552
|
+
}
|
|
553
|
+
const retConstraint = retAbs.length > 0 ? joinThenProject(retAbs) : void 0;
|
|
554
|
+
if (retConstraint === void 0 && paramParts.length === 0) return void 0;
|
|
555
|
+
if (retAbs.length > 0 && retConstraint === void 0) return void 0;
|
|
556
|
+
const ret = retConstraint === void 0 ? "" : `, ${formatConstraint2(retConstraint)}`;
|
|
557
|
+
return `fn({ ${paramParts.join(", ")} }${ret})`;
|
|
558
|
+
}
|
|
559
|
+
function sectionText(fn, dsl, srcRel) {
|
|
560
|
+
return `${GENERATED_HEADER}
|
|
561
|
+
// source: ${srcRel}:${fn}
|
|
562
|
+
export const ${fn} = ${dsl};
|
|
563
|
+
`;
|
|
564
|
+
}
|
|
565
|
+
function roundTrips(text, fn) {
|
|
566
|
+
const since = refineDiagCount2();
|
|
567
|
+
try {
|
|
568
|
+
const exports = execNudoModule(text);
|
|
569
|
+
const v = exports[fn];
|
|
570
|
+
return isNudoConstraint(v) && v.fn !== void 0;
|
|
571
|
+
} catch {
|
|
572
|
+
return false;
|
|
573
|
+
} finally {
|
|
574
|
+
takeRefineDiagsSince2(since);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
function collectGeneratedSections(sidecarSrc) {
|
|
578
|
+
if (sidecarSrc.trim() === "") return [];
|
|
579
|
+
let ast;
|
|
580
|
+
try {
|
|
581
|
+
ast = parseSource(sidecarSrc);
|
|
582
|
+
} catch {
|
|
583
|
+
throw new Error("sidecar is not parseable");
|
|
584
|
+
}
|
|
585
|
+
const out = [];
|
|
586
|
+
for (const stmt of ast.program.body) {
|
|
587
|
+
if (stmt.type !== "ExportNamedDeclaration" || stmt.source) continue;
|
|
588
|
+
const d = stmt.declaration;
|
|
589
|
+
if (!d || stmt.start == null || stmt.end == null) continue;
|
|
590
|
+
const names = [];
|
|
591
|
+
if (d.type === "VariableDeclaration") {
|
|
592
|
+
for (const decl of d.declarations) {
|
|
593
|
+
if (decl.id.type === "Identifier") names.push(decl.id.name);
|
|
594
|
+
}
|
|
595
|
+
} else if (d.type === "FunctionDeclaration" || d.type === "ClassDeclaration") {
|
|
596
|
+
if (d.id) names.push(d.id.name);
|
|
597
|
+
}
|
|
598
|
+
if (names.length === 0) continue;
|
|
599
|
+
const blockStart = generatedCommentBlockStart(sidecarSrc, stmt.start);
|
|
600
|
+
if (blockStart === void 0) continue;
|
|
601
|
+
out.push({ names, start: blockStart, end: stmt.end, text: sidecarSrc.slice(blockStart, stmt.end) });
|
|
602
|
+
}
|
|
603
|
+
return out;
|
|
604
|
+
}
|
|
605
|
+
function generatedCommentBlockStart(src, pos) {
|
|
606
|
+
const lines = src.slice(0, pos).split("\n");
|
|
607
|
+
let blockStartLine = -1;
|
|
608
|
+
let sawGenerated = false;
|
|
609
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
610
|
+
const line = lines[i].trim();
|
|
611
|
+
if (line === "" || line === "*/") continue;
|
|
612
|
+
if (line.startsWith("//") || line.startsWith("/*") || line.startsWith("*")) {
|
|
613
|
+
blockStartLine = i;
|
|
614
|
+
if (/@generated/.test(line)) sawGenerated = true;
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
if (!sawGenerated || blockStartLine < 0) return void 0;
|
|
620
|
+
const prefix = lines.slice(0, blockStartLine);
|
|
621
|
+
return prefix.length === 0 ? 0 : prefix.join("\n").length + 1;
|
|
622
|
+
}
|
|
623
|
+
function topLevelDeclaredNames(sidecarSrc) {
|
|
624
|
+
if (sidecarSrc.trim() === "") return /* @__PURE__ */ new Set();
|
|
625
|
+
let ast;
|
|
626
|
+
try {
|
|
627
|
+
ast = parseSource(sidecarSrc);
|
|
628
|
+
} catch {
|
|
629
|
+
return void 0;
|
|
630
|
+
}
|
|
631
|
+
const names = /* @__PURE__ */ new Set();
|
|
632
|
+
const collect = (stmt) => {
|
|
633
|
+
if (stmt.type === "VariableDeclaration") {
|
|
634
|
+
for (const d of stmt.declarations) {
|
|
635
|
+
if (d.id.type === "Identifier") names.add(d.id.name);
|
|
636
|
+
}
|
|
637
|
+
} else if (stmt.type === "FunctionDeclaration" || stmt.type === "ClassDeclaration") {
|
|
638
|
+
if (stmt.id) names.add(stmt.id.name);
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
for (const stmt of ast.program.body) {
|
|
642
|
+
if (stmt.type === "ExportNamedDeclaration") {
|
|
643
|
+
if (stmt.declaration) collect(stmt.declaration);
|
|
644
|
+
if (!stmt.source) {
|
|
645
|
+
for (const spec of stmt.specifiers) {
|
|
646
|
+
names.add(spec.exported.type === "Identifier" ? spec.exported.name : spec.exported.value);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
} else {
|
|
650
|
+
collect(stmt);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return names;
|
|
654
|
+
}
|
|
655
|
+
function removeSections(src, sections) {
|
|
656
|
+
if (sections.length === 0) return src;
|
|
657
|
+
const sorted = [...sections].sort((a, b) => a.start - b.start);
|
|
658
|
+
let out = "";
|
|
659
|
+
let pos = 0;
|
|
660
|
+
for (const s of sorted) {
|
|
661
|
+
out += src.slice(pos, s.start);
|
|
662
|
+
pos = Math.max(pos, s.end);
|
|
663
|
+
}
|
|
664
|
+
out += src.slice(pos);
|
|
665
|
+
return out;
|
|
666
|
+
}
|
|
667
|
+
function normalizeSection(text) {
|
|
668
|
+
return text.replace(/\s*$/, "") + "\n";
|
|
669
|
+
}
|
|
670
|
+
function joinSections(base, sectionTexts) {
|
|
671
|
+
const normalized = sectionTexts.map(normalizeSection).filter((t) => t.trim() !== "");
|
|
672
|
+
const tailTrimmed = base.replace(/\s+$/, "");
|
|
673
|
+
if (normalized.length === 0) return tailTrimmed === "" ? "" : `${tailTrimmed}
|
|
674
|
+
`;
|
|
675
|
+
const trimmed = tailTrimmed.replace(/^\s+/, "");
|
|
676
|
+
const lead = trimmed === "" ? "" : `${trimmed}
|
|
677
|
+
|
|
678
|
+
`;
|
|
679
|
+
return lead + normalized.join("\n");
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// src/interface-draft.ts
|
|
683
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3, realpathSync } from "fs";
|
|
684
|
+
import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute2, join as join2, relative as relative3 } from "path";
|
|
685
|
+
import {
|
|
686
|
+
effectiveInterface as effectiveInterface2,
|
|
687
|
+
formatConstraint as formatConstraint3,
|
|
688
|
+
formatShape as formatShape2,
|
|
689
|
+
generalizeFromAst,
|
|
690
|
+
isIntFlag,
|
|
691
|
+
joinThenProject as joinThenProject2,
|
|
692
|
+
localNamedExports as localNamedExports3,
|
|
693
|
+
sidecarPathOf as sidecarPathOf3
|
|
694
|
+
} from "@nudojs/core";
|
|
695
|
+
import { parse } from "@nudojs/parser";
|
|
696
|
+
function collectParamBodyAccesses(source) {
|
|
697
|
+
const out = /* @__PURE__ */ new Map();
|
|
698
|
+
let ast;
|
|
699
|
+
try {
|
|
700
|
+
ast = parse(source);
|
|
701
|
+
} catch {
|
|
702
|
+
return out;
|
|
703
|
+
}
|
|
704
|
+
const keyOf = (node) => {
|
|
705
|
+
if (node.type === "Identifier") return node.name;
|
|
706
|
+
if (node.type === "StringLiteral") return node.value;
|
|
707
|
+
return void 0;
|
|
708
|
+
};
|
|
709
|
+
const visitFn = (fnName, fnNode, paramNames) => {
|
|
710
|
+
if (paramNames.size === 0) return;
|
|
711
|
+
const byParam = /* @__PURE__ */ new Map();
|
|
712
|
+
const walk = (node, shadowed) => {
|
|
713
|
+
if (!node || typeof node !== "object") return;
|
|
714
|
+
const n = node;
|
|
715
|
+
if ((n.type === "VariableDeclarator" || n.type === "FunctionDeclaration") && n.id?.type === "Identifier") {
|
|
716
|
+
const id = n.id.name;
|
|
717
|
+
if (paramNames.has(id)) {
|
|
718
|
+
shadowed = new Set(shadowed).add(id);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
if (n.type === "MemberExpression" || n.type === "OptionalMemberExpression") {
|
|
722
|
+
const obj = n.object;
|
|
723
|
+
const prop = n.property;
|
|
724
|
+
const computed = n.computed === true;
|
|
725
|
+
if (obj?.type === "Identifier" && paramNames.has(obj.name) && !shadowed.has(obj.name) && prop && !computed) {
|
|
726
|
+
const key = keyOf(prop);
|
|
727
|
+
const pname = obj.name;
|
|
728
|
+
if (key !== void 0) {
|
|
729
|
+
if (!byParam.has(pname)) byParam.set(pname, /* @__PURE__ */ new Set());
|
|
730
|
+
byParam.get(pname).add(key);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
for (const k of Object.keys(n)) {
|
|
735
|
+
if (k === "loc" || k === "start" || k === "end") continue;
|
|
736
|
+
const child = n[k];
|
|
737
|
+
if (Array.isArray(child)) {
|
|
738
|
+
for (const item of child) walk(item, shadowed);
|
|
739
|
+
} else if (child && typeof child === "object") {
|
|
740
|
+
walk(child, shadowed);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
walk(fnNode, /* @__PURE__ */ new Set());
|
|
745
|
+
if (byParam.size > 0) out.set(fnName, byParam);
|
|
746
|
+
};
|
|
747
|
+
const paramSet = (fnNode) => {
|
|
748
|
+
const names = /* @__PURE__ */ new Set();
|
|
749
|
+
const params = fnNode.params ?? [];
|
|
750
|
+
for (const p of params) {
|
|
751
|
+
if (!p) continue;
|
|
752
|
+
if (p.type === "Identifier") names.add(p.name);
|
|
753
|
+
else if (p.type === "AssignmentPattern" && p.left?.type === "Identifier") {
|
|
754
|
+
names.add(p.left.name);
|
|
755
|
+
} else if (p.type === "RestElement" && p.argument?.type === "Identifier") {
|
|
756
|
+
names.add(p.argument.name);
|
|
757
|
+
} else if (p.type === "ObjectPattern") {
|
|
758
|
+
for (const prop of p.properties ?? []) {
|
|
759
|
+
if (prop.type === "ObjectProperty") {
|
|
760
|
+
const v = prop.value;
|
|
761
|
+
if (v.type === "Identifier") names.add(v.name);
|
|
762
|
+
else if (v.type === "AssignmentPattern" && v.left?.type === "Identifier") {
|
|
763
|
+
names.add(v.left.name);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return names;
|
|
770
|
+
};
|
|
771
|
+
const visitClassMethods = (className, classNode) => {
|
|
772
|
+
const body = classNode.body?.body ?? [];
|
|
773
|
+
for (const m of body) {
|
|
774
|
+
const mem = m;
|
|
775
|
+
const isMethod = mem.type === "MethodDefinition" || mem.type === "ClassMethod" || mem.type === "TSDeclareMethod";
|
|
776
|
+
if (!isMethod || mem.static) continue;
|
|
777
|
+
if (mem.kind && mem.kind !== "method") continue;
|
|
778
|
+
const keyName = mem.key?.type === "Identifier" ? mem.key.name : void 0;
|
|
779
|
+
if (!keyName) continue;
|
|
780
|
+
const methodNode = mem.type === "MethodDefinition" ? mem.value : mem;
|
|
781
|
+
if (!methodNode) continue;
|
|
782
|
+
visitFn(`${className}.${keyName}`, methodNode, paramSet(methodNode));
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
const considerDecl = (decl, exported) => {
|
|
786
|
+
if (!decl) return;
|
|
787
|
+
if (decl.type === "FunctionDeclaration" && decl.id) {
|
|
788
|
+
const id = decl.id;
|
|
789
|
+
visitFn(id.name, decl, paramSet(decl));
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
if (decl.type === "ClassDeclaration" && decl.id?.name) {
|
|
793
|
+
visitClassMethods(decl.id.name, decl);
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
if (decl.type === "VariableDeclaration") {
|
|
797
|
+
for (const d of decl.declarations ?? []) {
|
|
798
|
+
const id = d.id;
|
|
799
|
+
const init = d.init;
|
|
800
|
+
if (exported && id?.type === "Identifier" && init && (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression")) {
|
|
801
|
+
visitFn(id.name, init, paramSet(init));
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
const program = ast.program;
|
|
807
|
+
const bodyStmts = program?.body ?? [];
|
|
808
|
+
for (const stmt of bodyStmts) {
|
|
809
|
+
if (stmt.type === "ExportNamedDeclaration") {
|
|
810
|
+
considerDecl(stmt.declaration, true);
|
|
811
|
+
for (const spec of stmt.specifiers ?? []) {
|
|
812
|
+
const local = spec.local;
|
|
813
|
+
if (local?.type !== "Identifier") continue;
|
|
814
|
+
for (const s2 of bodyStmts) {
|
|
815
|
+
if (s2.type === "ClassDeclaration" && s2.id?.name === local.name) {
|
|
816
|
+
visitClassMethods(local.name, s2);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
} else if (stmt.type === "ExportDefaultDeclaration") {
|
|
821
|
+
considerDecl(stmt.declaration, true);
|
|
822
|
+
} else if (stmt.type === "ClassDeclaration") {
|
|
823
|
+
const id = stmt.id;
|
|
824
|
+
if (id?.name) visitClassMethods(id.name, stmt);
|
|
825
|
+
} else if (stmt.type === "FunctionDeclaration") {
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
return out;
|
|
829
|
+
}
|
|
830
|
+
function caseEvidence(fn) {
|
|
831
|
+
const callsite = fn.cases.filter((c) => c.source === "callsite");
|
|
832
|
+
const directive = fn.cases.filter((c) => c.source === "directive");
|
|
833
|
+
return {
|
|
834
|
+
paramCases: callsite.length > 0 ? callsite : directive,
|
|
835
|
+
returnCases: callsite.length > 0 ? callsite : directive.length > 0 ? directive : fn.cases,
|
|
836
|
+
paramEvidence: callsite.length > 0 ? "callsite" : directive.length > 0 ? "directive" : "none",
|
|
837
|
+
rawReturnEvidence: callsite.length > 0 ? "callsite" : directive.length > 0 ? "directive" : "none"
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
function widenDraftConstraint(c) {
|
|
841
|
+
const stripEqLits = (preds2) => preds2.filter((p) => !(p.op === "eq" && p.b?.op === "lit"));
|
|
842
|
+
if (c.members && c.members.length > 0) {
|
|
843
|
+
const widenedMembers = c.members.map(widenDraftConstraint).filter((m) => m !== void 0);
|
|
844
|
+
if (widenedMembers.length === 0) {
|
|
845
|
+
const prim = litPrimOf(c.members[0]);
|
|
846
|
+
if (!prim) return void 0;
|
|
847
|
+
return { __nudoConstraint: true, prim, preds: [] };
|
|
848
|
+
}
|
|
849
|
+
const prims = new Set(widenedMembers.map((m) => m.prim).filter(Boolean));
|
|
850
|
+
if (prims.size === 1 && widenedMembers.every((m) => !m.fields && !m.element && !m.members)) {
|
|
851
|
+
return { __nudoConstraint: true, prim: [...prims][0], preds: [] };
|
|
852
|
+
}
|
|
853
|
+
return {
|
|
854
|
+
__nudoConstraint: true,
|
|
855
|
+
...c.prim ? { prim: c.prim } : {},
|
|
856
|
+
preds: stripEqLits(c.preds ?? []),
|
|
857
|
+
members: widenedMembers
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
if (c.fields) {
|
|
861
|
+
const fields = {};
|
|
862
|
+
for (const [k, f] of Object.entries(c.fields)) {
|
|
863
|
+
const w = widenDraftConstraint(f.constraint);
|
|
864
|
+
if (!w) continue;
|
|
865
|
+
fields[k] = { constraint: w, ...f.optional ? { optional: true } : {} };
|
|
866
|
+
}
|
|
867
|
+
return {
|
|
868
|
+
__nudoConstraint: true,
|
|
869
|
+
fields,
|
|
870
|
+
preds: stripEqLits(c.preds ?? [])
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
if (c.element) {
|
|
874
|
+
const el = widenDraftConstraint(c.element);
|
|
875
|
+
return {
|
|
876
|
+
__nudoConstraint: true,
|
|
877
|
+
preds: stripEqLits(c.preds ?? []),
|
|
878
|
+
...el ? { element: el } : {}
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
if (c.fn) return void 0;
|
|
882
|
+
const preds = stripEqLits(c.preds ?? []);
|
|
883
|
+
const hasBounds = preds.length > 0;
|
|
884
|
+
if (!c.prim && !hasBounds && !isIntFlag(c)) return void 0;
|
|
885
|
+
return {
|
|
886
|
+
__nudoConstraint: true,
|
|
887
|
+
...c.prim ? { prim: c.prim } : {},
|
|
888
|
+
preds,
|
|
889
|
+
// builder 上 .int 是链式方法,truthy 恒真;必须经 isIntFlag
|
|
890
|
+
...isIntFlag(c) ? { int: true } : {}
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
function litPrimOf(c) {
|
|
894
|
+
if (!c) return void 0;
|
|
895
|
+
if (c.prim === "number" || c.prim === "string" || c.prim === "boolean") return c.prim;
|
|
896
|
+
const eq = c.preds?.find(
|
|
897
|
+
(p) => p.op === "eq" && p.b?.op === "lit"
|
|
898
|
+
);
|
|
899
|
+
const v = eq && eq.b?.op === "lit" ? eq.b.value : void 0;
|
|
900
|
+
if (typeof v === "number") return "number";
|
|
901
|
+
if (typeof v === "string") return "string";
|
|
902
|
+
if (typeof v === "boolean") return "boolean";
|
|
903
|
+
return c.members?.[0] ? litPrimOf(c.members[0]) : void 0;
|
|
904
|
+
}
|
|
905
|
+
function projectDraftParams(fn, paramCases, bodyByParam, formals) {
|
|
906
|
+
const bodyFor = (name, index) => {
|
|
907
|
+
if (!bodyByParam) return void 0;
|
|
908
|
+
const hit = bodyByParam.get(name);
|
|
909
|
+
if (hit) return hit;
|
|
910
|
+
const formal = formals?.[index];
|
|
911
|
+
if (formal && formal.kind === "pattern") {
|
|
912
|
+
for (const b of formal.bound) {
|
|
913
|
+
const boundHit = bodyByParam.get(b);
|
|
914
|
+
if (boundHit) return boundHit;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
return bodyByParam.get(`_p${index}`);
|
|
918
|
+
};
|
|
919
|
+
return fn.paramNames.map((name, i) => {
|
|
920
|
+
const bodyAccesses = bodyFor(name, i) ? [...bodyFor(name, i)].sort() : void 0;
|
|
921
|
+
const argAbs = [];
|
|
922
|
+
for (const c of paramCases) {
|
|
923
|
+
const a = c.argAbs[i];
|
|
924
|
+
if (a !== void 0) argAbs.push(a);
|
|
925
|
+
}
|
|
926
|
+
if (argAbs.length === 0) {
|
|
927
|
+
if (bodyAccesses && bodyAccesses.length > 0) {
|
|
928
|
+
return {
|
|
929
|
+
name,
|
|
930
|
+
display: `/* body-read { ${bodyAccesses.join(", ")} } \u2014 fill types when accepting */`,
|
|
931
|
+
projected: false,
|
|
932
|
+
bodyAccesses
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
return { name, display: "/* no evidence \u2014 tighten */", projected: false };
|
|
936
|
+
}
|
|
937
|
+
const raw = joinThenProject2(argAbs);
|
|
938
|
+
if (raw === void 0) {
|
|
939
|
+
return {
|
|
940
|
+
name,
|
|
941
|
+
display: `/* not projectable: ${argAbs.map((a) => formatShape2(a)).join(" | ")} */`,
|
|
942
|
+
projected: false,
|
|
943
|
+
...bodyAccesses ? { bodyAccesses } : {}
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
const constraint = widenDraftConstraint(raw);
|
|
947
|
+
if (constraint === void 0) {
|
|
948
|
+
const observed2 = formatConstraint3(raw);
|
|
949
|
+
return {
|
|
950
|
+
name,
|
|
951
|
+
display: `/* observed: ${observed2} \u2014 widen/confirm before accepting */`,
|
|
952
|
+
projected: false,
|
|
953
|
+
...bodyAccesses ? { bodyAccesses } : {}
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
let display = formatConstraint3(constraint);
|
|
957
|
+
const observed = formatConstraint3(raw);
|
|
958
|
+
if (observed !== display) {
|
|
959
|
+
display += ` /* observed: ${observed} */`;
|
|
960
|
+
}
|
|
961
|
+
if (bodyAccesses && bodyAccesses.length > 0) {
|
|
962
|
+
display += ` /* body also reads: ${bodyAccesses.join(", ")} */`;
|
|
963
|
+
}
|
|
964
|
+
return {
|
|
965
|
+
name,
|
|
966
|
+
constraint,
|
|
967
|
+
display,
|
|
968
|
+
projected: true,
|
|
969
|
+
...bodyAccesses ? { bodyAccesses } : {}
|
|
970
|
+
};
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
function projectDraftReturn(fn, returnCases, source, rawEvidence, loadModule, fromFile) {
|
|
974
|
+
const retAbs = [];
|
|
975
|
+
for (const c of returnCases) {
|
|
976
|
+
if (c.throwsAbs.shape.k !== "never") continue;
|
|
977
|
+
retAbs.push(c.abs);
|
|
978
|
+
}
|
|
979
|
+
if (retAbs.length > 0) {
|
|
980
|
+
const raw = joinThenProject2(retAbs);
|
|
981
|
+
if (raw !== void 0) {
|
|
982
|
+
if (rawEvidence === "none" || rawEvidence === "body") {
|
|
983
|
+
return {
|
|
984
|
+
display: `/* observed: ${formatConstraint3(raw)} \u2014 confirm before accepting */`,
|
|
985
|
+
projected: false,
|
|
986
|
+
evidence: rawEvidence === "none" ? "body" : rawEvidence
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
const constraint = widenDraftConstraint(raw);
|
|
990
|
+
if (constraint === void 0) {
|
|
991
|
+
return {
|
|
992
|
+
display: `/* observed: ${formatConstraint3(raw)} \u2014 widen/confirm */`,
|
|
993
|
+
projected: false,
|
|
994
|
+
evidence: rawEvidence
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
return {
|
|
998
|
+
constraint,
|
|
999
|
+
display: formatConstraint3(constraint),
|
|
1000
|
+
projected: true,
|
|
1001
|
+
evidence: rawEvidence
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
const shapeText = fn.combinedAbs ? formatShape2(fn.combinedAbs) : formatShape2(retAbs[0]);
|
|
1005
|
+
return {
|
|
1006
|
+
display: `/* not projectable: ${shapeText} */`,
|
|
1007
|
+
projected: false,
|
|
1008
|
+
evidence: rawEvidence
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
try {
|
|
1012
|
+
const g = generalizeFromAst(fn.name, source, {
|
|
1013
|
+
refine: {
|
|
1014
|
+
...loadModule ? { loadModule } : {},
|
|
1015
|
+
...fromFile ? { fromFile } : {}
|
|
1016
|
+
}
|
|
1017
|
+
});
|
|
1018
|
+
if (g?.symbolic) {
|
|
1019
|
+
return {
|
|
1020
|
+
display: `/* symbolic: ${formatShape2(g.symbolic)}${g.display ? ` \u2014 ${g.display}` : ""} */`,
|
|
1021
|
+
projected: false,
|
|
1022
|
+
evidence: "symbolic"
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
} catch {
|
|
1026
|
+
}
|
|
1027
|
+
return { display: "/* no evidence */", projected: false, evidence: "none" };
|
|
1028
|
+
}
|
|
1029
|
+
function toDraftBuilderDsl(display) {
|
|
1030
|
+
const s = display.replace(/\s*\/\*[\s\S]*?\*\/\s*/g, "").trim();
|
|
1031
|
+
return s.replace(
|
|
1032
|
+
/([A-Za-z_$][\w$]*)\?\s*:\s*([^,}\n]+)/g,
|
|
1033
|
+
(_m, name, val) => {
|
|
1034
|
+
const v = val.trim();
|
|
1035
|
+
if (v.includes(".optional()")) return `${name}: ${v}`;
|
|
1036
|
+
return `${name}: ${v}.optional()`;
|
|
1037
|
+
}
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
function draftExportName(fnName) {
|
|
1041
|
+
return fnName.includes(".") ? fnName.replace(/\./g, "_") : fnName;
|
|
1042
|
+
}
|
|
1043
|
+
function draftDsl(entry) {
|
|
1044
|
+
const parts = entry.params.filter((p) => p.projected && p.constraint !== void 0).map((p) => {
|
|
1045
|
+
const pure = toDraftBuilderDsl(formatConstraint3(p.constraint));
|
|
1046
|
+
return `${p.name}: ${pure}`;
|
|
1047
|
+
});
|
|
1048
|
+
const obj = parts.length === 0 ? "{}" : `{ ${parts.join(", ")} }`;
|
|
1049
|
+
const ret = entry.returns?.projected && entry.returns.constraint !== void 0 ? toDraftBuilderDsl(formatConstraint3(entry.returns.constraint)) : void 0;
|
|
1050
|
+
return ret === void 0 || ret === "" ? `fn(${obj})` : `fn(${obj}, ${ret})`;
|
|
1051
|
+
}
|
|
1052
|
+
function suggestedBodyDsl(fnName, params) {
|
|
1053
|
+
const withBody = params.filter((p) => p.bodyAccesses && p.bodyAccesses.length > 0 && !p.projected);
|
|
1054
|
+
if (withBody.length === 0) return void 0;
|
|
1055
|
+
const parts = withBody.map((p) => {
|
|
1056
|
+
const fields = p.bodyAccesses.map((k) => `${k}: /* TODO */`).join(", ");
|
|
1057
|
+
return `${p.name}: shape({ ${fields} })`;
|
|
1058
|
+
});
|
|
1059
|
+
return `// suggested (body-read, not a contract): ${fnName} = fn({ ${parts.join(", ")} })`;
|
|
1060
|
+
}
|
|
1061
|
+
async function draftInterface(filePath, opts = {}) {
|
|
1062
|
+
const source = opts.source ?? readFileSync4(filePath, "utf-8");
|
|
1063
|
+
const loadModule = opts.loadModule ?? defaultLoadModule;
|
|
1064
|
+
const sidecarPath = sidecarPathOf3(filePath);
|
|
1065
|
+
const wantBody = opts.bodyAccesses !== false;
|
|
1066
|
+
const analysis = await analyzeFileAsync(filePath, source, void 0, opts.records, loadModule);
|
|
1067
|
+
const exported = localNamedExports3(source);
|
|
1068
|
+
const selected = opts.fnNames && opts.fnNames.length > 0 ? new Set(opts.fnNames) : exported;
|
|
1069
|
+
const bodyMap = wantBody ? collectParamBodyAccesses(source) : /* @__PURE__ */ new Map();
|
|
1070
|
+
const entries = [];
|
|
1071
|
+
for (const fn of analysis.functions) {
|
|
1072
|
+
if (!selected.has(fn.name) && !opts.fnNames?.includes(fn.name)) continue;
|
|
1073
|
+
if (!exported.has(fn.name)) {
|
|
1074
|
+
if (opts.fnNames?.includes(fn.name)) {
|
|
1075
|
+
entries.push({
|
|
1076
|
+
fn: fn.name,
|
|
1077
|
+
params: fn.paramNames.map((n) => ({
|
|
1078
|
+
name: n,
|
|
1079
|
+
display: "/* not an export */",
|
|
1080
|
+
projected: false
|
|
1081
|
+
})),
|
|
1082
|
+
paramEvidence: "none",
|
|
1083
|
+
returnEvidence: "none",
|
|
1084
|
+
skipped: "not-an-export"
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
const eff = effectiveInterface2(source, fn.name, {
|
|
1090
|
+
loadModule,
|
|
1091
|
+
fromFile: filePath,
|
|
1092
|
+
autoBind: true
|
|
1093
|
+
});
|
|
1094
|
+
if (eff?.source === "handwritten") {
|
|
1095
|
+
entries.push({
|
|
1096
|
+
fn: fn.name,
|
|
1097
|
+
params: eff.params.map((p) => ({
|
|
1098
|
+
name: p.param,
|
|
1099
|
+
constraint: p.constraint,
|
|
1100
|
+
display: formatConstraint3(p.constraint),
|
|
1101
|
+
projected: true
|
|
1102
|
+
})),
|
|
1103
|
+
...eff.returns ? {
|
|
1104
|
+
returns: {
|
|
1105
|
+
constraint: eff.returns.constraint,
|
|
1106
|
+
display: formatConstraint3(eff.returns.constraint),
|
|
1107
|
+
projected: true
|
|
1108
|
+
}
|
|
1109
|
+
} : {},
|
|
1110
|
+
paramEvidence: "none",
|
|
1111
|
+
returnEvidence: "none",
|
|
1112
|
+
skipped: "handwritten"
|
|
1113
|
+
});
|
|
1114
|
+
continue;
|
|
1115
|
+
}
|
|
1116
|
+
const { paramCases, returnCases, paramEvidence, rawReturnEvidence } = caseEvidence(fn);
|
|
1117
|
+
const bodyByParam = bodyMap.get(fn.name);
|
|
1118
|
+
const params = projectDraftParams(fn, paramCases, bodyByParam, fn.formals);
|
|
1119
|
+
const ret = projectDraftReturn(fn, returnCases, source, rawReturnEvidence, loadModule, filePath);
|
|
1120
|
+
const { evidence: returnEvidence, ...returns } = ret;
|
|
1121
|
+
let evidence = paramEvidence;
|
|
1122
|
+
if (evidence === "none") {
|
|
1123
|
+
const anyBody = params.some((p) => p.bodyAccesses && p.bodyAccesses.length > 0);
|
|
1124
|
+
if (anyBody) evidence = "body";
|
|
1125
|
+
}
|
|
1126
|
+
entries.push({
|
|
1127
|
+
fn: fn.name,
|
|
1128
|
+
params,
|
|
1129
|
+
returns,
|
|
1130
|
+
paramEvidence: evidence,
|
|
1131
|
+
returnEvidence,
|
|
1132
|
+
dsl: draftDsl({ params, returns })
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
const draftSource = formatDraftModule(filePath, entries, sidecarPath);
|
|
1136
|
+
return { file: filePath, entries, draftSource, sidecarPath };
|
|
1137
|
+
}
|
|
1138
|
+
function formatDraftModule(filePath, entries, sidecarPath) {
|
|
1139
|
+
const target = sidecarPath ?? sidecarPathOf3(filePath);
|
|
1140
|
+
const draftName = sidecarDraftPath(filePath).split(/[/\\]/).pop() ?? "*.nudo.draft.js";
|
|
1141
|
+
const draftable = entries.filter((e) => e.dsl !== void 0 && e.skipped === void 0);
|
|
1142
|
+
const BUILDERS = ["fn", "number", "string", "boolean", "any", "shape", "array", "lit", "union"];
|
|
1143
|
+
const used = /* @__PURE__ */ new Set(["fn"]);
|
|
1144
|
+
for (const e of draftable) {
|
|
1145
|
+
const blob = `${e.dsl ?? ""}
|
|
1146
|
+
${suggestedBodyDsl(e.fn, e.params) ?? ""}`;
|
|
1147
|
+
for (const b of BUILDERS) {
|
|
1148
|
+
if (new RegExp(`\\b${b}\\b`).test(blob)) used.add(b);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
const importList = BUILDERS.filter((b) => used.has(b)).join(", ");
|
|
1152
|
+
const lines = [
|
|
1153
|
+
"// @nudo:draft",
|
|
1154
|
+
`// Generated by \`nudo contract --draft\` from ${filePath}`,
|
|
1155
|
+
`// This ${draftName} file is NOT loaded as a sidecar contract.`,
|
|
1156
|
+
`// Review each export, then copy it into ${target} to accept.`,
|
|
1157
|
+
"//",
|
|
1158
|
+
"// Evidence: callsite/directive = observed args; body = fields the",
|
|
1159
|
+
"// implementation reads (suggestion only \u2014 never a check obligation);",
|
|
1160
|
+
"// symbolic = generalize; omitted params = no evidence.",
|
|
1161
|
+
"// Handwritten contracts are never overwritten.",
|
|
1162
|
+
"",
|
|
1163
|
+
`import { ${importList} } from "@nudojs/core";`,
|
|
1164
|
+
""
|
|
1165
|
+
];
|
|
1166
|
+
if (draftable.length === 0) {
|
|
1167
|
+
lines.push("// (no draftable exports \u2014 handwritten / non-export / empty)");
|
|
1168
|
+
lines.push("");
|
|
1169
|
+
}
|
|
1170
|
+
for (const e of draftable) {
|
|
1171
|
+
lines.push(`// ${e.fn} \u2014 param: ${e.paramEvidence}, return: ${e.returnEvidence}`);
|
|
1172
|
+
for (const p of e.params) {
|
|
1173
|
+
if (!p.projected) {
|
|
1174
|
+
lines.push(`// ${p.name}: ${p.display}`);
|
|
1175
|
+
} else if (p.display.includes("/* observed") || p.display.includes("/* body also reads")) {
|
|
1176
|
+
lines.push(`// ${p.name}: ${p.display}`);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
const suggested = suggestedBodyDsl(e.fn, e.params);
|
|
1180
|
+
if (suggested) lines.push(suggested);
|
|
1181
|
+
if (e.returns && !e.returns.projected) {
|
|
1182
|
+
lines.push(`// returns: ${e.returns.display}`);
|
|
1183
|
+
} else if (e.returns?.display.includes("/* observed")) {
|
|
1184
|
+
lines.push(`// returns: ${e.returns.display}`);
|
|
1185
|
+
} else if (e.returnEvidence === "symbolic") {
|
|
1186
|
+
lines.push(`// returns: ${e.returns?.display ?? ""} (symbolic)`);
|
|
1187
|
+
}
|
|
1188
|
+
lines.push(`export const ${draftExportName(e.fn)} = ${e.dsl};`);
|
|
1189
|
+
if (e.fn.includes(".")) {
|
|
1190
|
+
lines.push(`// sidecar key may also be written as \`${e.fn}\` / nested { ${e.fn.split(".")[1]}: \u2026 }`);
|
|
1191
|
+
}
|
|
1192
|
+
lines.push("");
|
|
1193
|
+
}
|
|
1194
|
+
const skipped = entries.filter((e) => e.skipped !== void 0);
|
|
1195
|
+
if (skipped.length > 0) {
|
|
1196
|
+
lines.push("// Skipped:");
|
|
1197
|
+
for (const s of skipped) {
|
|
1198
|
+
lines.push(`// ${s.fn} (${s.skipped})`);
|
|
1199
|
+
}
|
|
1200
|
+
lines.push("");
|
|
1201
|
+
}
|
|
1202
|
+
return lines.join("\n");
|
|
1203
|
+
}
|
|
1204
|
+
function sidecarDraftPath(filePath) {
|
|
1205
|
+
return sidecarPathOf3(filePath).replace(/\.nudo\.([cm]?[jt]s)$/, ".nudo.draft.$1");
|
|
1206
|
+
}
|
|
1207
|
+
function safeRealpath(p) {
|
|
1208
|
+
try {
|
|
1209
|
+
return realpathSync(p);
|
|
1210
|
+
} catch {
|
|
1211
|
+
try {
|
|
1212
|
+
return join2(realpathSync(dirname4(p)), basename2(p));
|
|
1213
|
+
} catch {
|
|
1214
|
+
return p;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
function isDraftableEntry(entries) {
|
|
1219
|
+
return entries.some((e) => e.dsl !== void 0 && e.skipped === void 0);
|
|
1220
|
+
}
|
|
1221
|
+
function writeInterfaceDraft(filePath, draftSource, opts = {}) {
|
|
1222
|
+
const draftPath = sidecarDraftPath(filePath);
|
|
1223
|
+
const formalPath = sidecarPathOf3(filePath);
|
|
1224
|
+
if (draftPath === formalPath) {
|
|
1225
|
+
throw new Error(
|
|
1226
|
+
`draft write refused: draft path equals formal sidecar (${formalPath}); never overwrite handwritten contracts`
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
if (/[/\\]node_modules[/\\]/.test(draftPath) || /[/\\]node_modules[/\\]/.test(filePath)) {
|
|
1230
|
+
throw new Error(`draft write refused: path is inside node_modules (${draftPath})`);
|
|
1231
|
+
}
|
|
1232
|
+
if (opts.projectDir) {
|
|
1233
|
+
const rootReal = safeRealpath(opts.projectDir);
|
|
1234
|
+
const draftReal = safeRealpath(draftPath);
|
|
1235
|
+
const rel = relative3(rootReal, draftReal);
|
|
1236
|
+
if (rel.startsWith("..") || isAbsolute2(rel)) {
|
|
1237
|
+
throw new Error(`draft write refused: outside project root ${opts.projectDir}`);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
const draftable = opts.draftable !== void 0 ? opts.draftable : opts.entries !== void 0 ? isDraftableEntry(opts.entries) : /export\s+const\s+[\p{ID_Start}$_][\p{ID_Continue}$]*\s*=/u.test(draftSource);
|
|
1241
|
+
const prev = existsSync4(draftPath) ? readFileSync4(draftPath, "utf-8") : void 0;
|
|
1242
|
+
const changed = prev !== draftSource;
|
|
1243
|
+
const written = !opts.dryRun && changed && draftable;
|
|
1244
|
+
if (written) {
|
|
1245
|
+
writeFileSync3(draftPath, draftSource, "utf-8");
|
|
1246
|
+
}
|
|
1247
|
+
return {
|
|
1248
|
+
draftPath,
|
|
1249
|
+
written,
|
|
1250
|
+
changed,
|
|
1251
|
+
draftable,
|
|
1252
|
+
draftSource
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
function formatDraftSummary(sourceRel, draftRel, result, write) {
|
|
1256
|
+
const lines = [sourceRel];
|
|
1257
|
+
for (const e of result.entries) {
|
|
1258
|
+
if (e.skipped === "handwritten") {
|
|
1259
|
+
lines.push(` ${e.fn} [handwritten] skipped (draft never overwrites)`);
|
|
1260
|
+
} else if (e.skipped === "not-an-export") {
|
|
1261
|
+
lines.push(` ${e.fn} [not-an-export] skipped`);
|
|
1262
|
+
} else if (e.dsl) {
|
|
1263
|
+
lines.push(` ${e.fn} [draft ${e.paramEvidence}/${e.returnEvidence}] ${e.dsl}`);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
if (write) {
|
|
1267
|
+
if (write.changed && write.draftable) {
|
|
1268
|
+
lines.push(
|
|
1269
|
+
write.written ? `Draft written \u2192 ${draftRel}` : `[dry-run] would write \u2192 ${draftRel}`
|
|
1270
|
+
);
|
|
1271
|
+
} else if (write.changed && !write.draftable) {
|
|
1272
|
+
if (result.entries.length > 0) {
|
|
1273
|
+
const skipped = result.entries.filter((e) => e.skipped !== void 0).length;
|
|
1274
|
+
lines.push(
|
|
1275
|
+
skipped > 0 ? `Draft not written (${skipped} skipped, no writeable exports); nothing written \u2192 ${draftRel}` : `Draft not written (no writeable exports); nothing written \u2192 ${draftRel}`
|
|
1276
|
+
);
|
|
1277
|
+
} else {
|
|
1278
|
+
lines.push(`Draft empty (no draftable exports); nothing written \u2192 ${draftRel}`);
|
|
1279
|
+
}
|
|
1280
|
+
} else {
|
|
1281
|
+
lines.push(`${draftRel}: draft unchanged`);
|
|
1282
|
+
}
|
|
1283
|
+
lines.push(` review, then copy accepted exports into ${sidecarPathOf3(result.file)}`);
|
|
1284
|
+
} else {
|
|
1285
|
+
lines.push("");
|
|
1286
|
+
lines.push(result.draftSource.trimEnd());
|
|
1287
|
+
}
|
|
1288
|
+
return lines;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// src/interface-derivation-derive.ts
|
|
1292
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
1293
|
+
import { basename as basename3, dirname as dirname5, relative as relative4, resolve as resolve3 } from "path";
|
|
1294
|
+
import { parse as parse2 } from "@nudojs/parser";
|
|
1295
|
+
import {
|
|
1296
|
+
constraintToEntryAbs,
|
|
1297
|
+
effectiveInterface as effectiveInterface3,
|
|
1298
|
+
interfaceDiagCount as interfaceDiagCount3,
|
|
1299
|
+
isNodeModulesPath as isNodeModulesPath2,
|
|
1300
|
+
runTranspiled,
|
|
1301
|
+
callTranspiledExportFull,
|
|
1302
|
+
setBCallCollector,
|
|
1303
|
+
$new,
|
|
1304
|
+
$invoke,
|
|
1305
|
+
sidecarPathOf as sidecarPathOf4,
|
|
1306
|
+
takeInterfaceDiagsSince as takeInterfaceDiagsSince3,
|
|
1307
|
+
unknown as unknownAbs
|
|
1308
|
+
} from "@nudojs/core";
|
|
1309
|
+
import {
|
|
1310
|
+
beginDerivationSession,
|
|
1311
|
+
endDerivationSession,
|
|
1312
|
+
listTopFunctions,
|
|
1313
|
+
tagDerivationRoot
|
|
1314
|
+
} from "@nudojs/core/internal";
|
|
1315
|
+
|
|
1316
|
+
// src/interface-derivation-project.ts
|
|
1317
|
+
import { formatConstraint as formatConstraint4, joinThenProject as joinThenProject3 } from "@nudojs/core";
|
|
1318
|
+
import { derivationChain, getDerivation, projectDerivationDsl } from "@nudojs/core/internal";
|
|
1319
|
+
function projectParamSlot(absList, paramName) {
|
|
1320
|
+
if (absList.length === 0) return void 0;
|
|
1321
|
+
const constraint = joinThenProject3(absList);
|
|
1322
|
+
if (constraint === void 0) return void 0;
|
|
1323
|
+
if (absList.length === 1) {
|
|
1324
|
+
const node = getDerivation(absList[0]);
|
|
1325
|
+
if (node) {
|
|
1326
|
+
const proj = projectDerivationDsl(node, paramName);
|
|
1327
|
+
if (proj) {
|
|
1328
|
+
const chain = derivationChain(node);
|
|
1329
|
+
const root = chain[chain.length - 1];
|
|
1330
|
+
const shiftCount = chain.filter((n) => n.kind === "shift").length;
|
|
1331
|
+
return {
|
|
1332
|
+
constraint,
|
|
1333
|
+
dsl: proj.expr,
|
|
1334
|
+
prelude: proj.prelude,
|
|
1335
|
+
imports: proj.imports,
|
|
1336
|
+
compositional: true,
|
|
1337
|
+
...root?.kind === "root" ? { rootNodeId: root.id } : {},
|
|
1338
|
+
shiftCount
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
if (absList.length > 1) {
|
|
1344
|
+
const nodes = absList.map((a) => getDerivation(a));
|
|
1345
|
+
const projs = nodes.map((n) => n ? projectDerivationDsl(n, paramName) : void 0);
|
|
1346
|
+
const roots = nodes.map((n) => {
|
|
1347
|
+
if (!n) return void 0;
|
|
1348
|
+
const chain = derivationChain(n);
|
|
1349
|
+
return chain[chain.length - 1];
|
|
1350
|
+
});
|
|
1351
|
+
const root0 = roots[0];
|
|
1352
|
+
const sameRoot = root0 !== void 0 && root0.kind === "root" && roots.every((r) => r !== void 0 && r.id === root0.id && r.kind === "root");
|
|
1353
|
+
if (sameRoot && projs.every((p) => p !== void 0)) {
|
|
1354
|
+
const exprs = [...new Set(projs.map((p) => p.expr))];
|
|
1355
|
+
const prelude = [];
|
|
1356
|
+
const imports = [];
|
|
1357
|
+
for (const p of projs) {
|
|
1358
|
+
for (const line of p.prelude) if (!prelude.includes(line)) prelude.push(line);
|
|
1359
|
+
for (const imp of p.imports) {
|
|
1360
|
+
if (!imports.some((i) => i.name === imp.name && i.from === imp.from)) {
|
|
1361
|
+
imports.push(imp);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
const dsl = exprs.length === 1 ? exprs[0] : `union(${exprs.join(", ")})`;
|
|
1366
|
+
const shiftCounts = nodes.map(
|
|
1367
|
+
(n) => derivationChain(n).filter((x) => x.kind === "shift").length
|
|
1368
|
+
);
|
|
1369
|
+
return {
|
|
1370
|
+
constraint,
|
|
1371
|
+
dsl,
|
|
1372
|
+
prelude,
|
|
1373
|
+
imports,
|
|
1374
|
+
compositional: true,
|
|
1375
|
+
rootNodeId: root0.id,
|
|
1376
|
+
shiftCount: Math.max(...shiftCounts)
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
return {
|
|
1381
|
+
constraint,
|
|
1382
|
+
dsl: formatConstraint4(constraint),
|
|
1383
|
+
prelude: [],
|
|
1384
|
+
imports: [],
|
|
1385
|
+
compositional: false
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
function projectReturnSlot(retAbs, params, paramNames) {
|
|
1389
|
+
if (retAbs.length === 0) return void 0;
|
|
1390
|
+
const constraint = joinThenProject3(retAbs);
|
|
1391
|
+
if (constraint === void 0) return void 0;
|
|
1392
|
+
if (retAbs.length === 1) {
|
|
1393
|
+
const node = getDerivation(retAbs[0]);
|
|
1394
|
+
if (node && !derivationChain(node).some((n) => n.kind === "join" || n.kind === "opaque")) {
|
|
1395
|
+
const rel = projectReturnRelativeToParams(node, params, paramNames);
|
|
1396
|
+
if (rel) {
|
|
1397
|
+
return {
|
|
1398
|
+
constraint,
|
|
1399
|
+
dsl: rel.dsl,
|
|
1400
|
+
prelude: rel.prelude,
|
|
1401
|
+
imports: rel.imports,
|
|
1402
|
+
compositional: true
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
const proj = projectDerivationDsl(node, paramNames[0] ?? "ret");
|
|
1406
|
+
if (proj) {
|
|
1407
|
+
return {
|
|
1408
|
+
constraint,
|
|
1409
|
+
dsl: proj.expr,
|
|
1410
|
+
prelude: proj.prelude,
|
|
1411
|
+
imports: proj.imports,
|
|
1412
|
+
compositional: true
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
return {
|
|
1418
|
+
constraint,
|
|
1419
|
+
dsl: formatConstraint4(constraint),
|
|
1420
|
+
prelude: [],
|
|
1421
|
+
imports: [],
|
|
1422
|
+
compositional: false
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
function projectReturnRelativeToParams(node, params, paramNames) {
|
|
1426
|
+
const chain = derivationChain(node);
|
|
1427
|
+
const root = chain[chain.length - 1];
|
|
1428
|
+
if (!root || root.kind !== "root") return void 0;
|
|
1429
|
+
const shifts = [];
|
|
1430
|
+
for (let j = chain.length - 2; j >= 0; j--) {
|
|
1431
|
+
const n = chain[j];
|
|
1432
|
+
if (n.kind !== "shift" || n.offset === void 0) return void 0;
|
|
1433
|
+
shifts.push(n.offset);
|
|
1434
|
+
}
|
|
1435
|
+
for (let i = 0; i < params.length; i++) {
|
|
1436
|
+
const p = params[i];
|
|
1437
|
+
if (p.rootNodeId === void 0 || p.rootNodeId !== root.id) continue;
|
|
1438
|
+
const paramShiftCount = p.shiftCount ?? 0;
|
|
1439
|
+
if (shifts.length < paramShiftCount) continue;
|
|
1440
|
+
const tail = shifts.slice(paramShiftCount);
|
|
1441
|
+
if (tail.length === 0) {
|
|
1442
|
+
return { dsl: p.dsl, prelude: [], imports: [] };
|
|
1443
|
+
}
|
|
1444
|
+
let expr = p.name || paramNames[i] || "x";
|
|
1445
|
+
for (const off of tail) expr = `${expr}.shift(${off})`;
|
|
1446
|
+
return { dsl: expr, prelude: [], imports: [] };
|
|
1447
|
+
}
|
|
1448
|
+
return void 0;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
// src/interface-derivation-derive.ts
|
|
1452
|
+
function extractFnConstraintSources(sidecarSrc, fnName) {
|
|
1453
|
+
const out = { params: {} };
|
|
1454
|
+
let ast;
|
|
1455
|
+
try {
|
|
1456
|
+
ast = parse2(sidecarSrc);
|
|
1457
|
+
} catch {
|
|
1458
|
+
return out;
|
|
1459
|
+
}
|
|
1460
|
+
const imports = /* @__PURE__ */ new Map();
|
|
1461
|
+
const locals = /* @__PURE__ */ new Map();
|
|
1462
|
+
const srcOf = (node) => {
|
|
1463
|
+
const n = node;
|
|
1464
|
+
if (!n?.type) return void 0;
|
|
1465
|
+
if (n.type === "Identifier" && n.name) {
|
|
1466
|
+
const imp = imports.get(n.name);
|
|
1467
|
+
if (imp) {
|
|
1468
|
+
return { expr: imp.imported, importFrom: imp.from, importName: imp.imported };
|
|
1469
|
+
}
|
|
1470
|
+
const local = locals.get(n.name);
|
|
1471
|
+
if (local !== void 0) return { expr: local };
|
|
1472
|
+
return { expr: n.name };
|
|
1473
|
+
}
|
|
1474
|
+
if (n.start != null && n.end != null) {
|
|
1475
|
+
return { expr: sidecarSrc.slice(n.start, n.end) };
|
|
1476
|
+
}
|
|
1477
|
+
return void 0;
|
|
1478
|
+
};
|
|
1479
|
+
for (const stmt of ast.program.body) {
|
|
1480
|
+
if (stmt.type === "ImportDeclaration") {
|
|
1481
|
+
const from = stmt.source.value;
|
|
1482
|
+
for (const s of stmt.specifiers) {
|
|
1483
|
+
if (s.type === "ImportSpecifier") {
|
|
1484
|
+
const imported = s.imported.type === "Identifier" ? s.imported.name : String(s.imported);
|
|
1485
|
+
imports.set(s.local.name, { from, imported });
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
continue;
|
|
1489
|
+
}
|
|
1490
|
+
if (stmt.type !== "ExportNamedDeclaration" || stmt.source) continue;
|
|
1491
|
+
const d = stmt.declaration;
|
|
1492
|
+
if (!d || d.type !== "VariableDeclaration") continue;
|
|
1493
|
+
for (const decl of d.declarations) {
|
|
1494
|
+
if (decl.id.type !== "Identifier") continue;
|
|
1495
|
+
const name = decl.id.name;
|
|
1496
|
+
const init = decl.init;
|
|
1497
|
+
if (!init) continue;
|
|
1498
|
+
const text = init.start != null && init.end != null ? sidecarSrc.slice(init.start, init.end) : name;
|
|
1499
|
+
if (init.type !== "CallExpression") {
|
|
1500
|
+
locals.set(name, text);
|
|
1501
|
+
continue;
|
|
1502
|
+
}
|
|
1503
|
+
const calleeName = init.callee.type === "Identifier" ? init.callee.name : void 0;
|
|
1504
|
+
locals.set(name, text);
|
|
1505
|
+
if (calleeName !== "fn" || name !== fnName) continue;
|
|
1506
|
+
const paramsNode = init.arguments[0];
|
|
1507
|
+
if (paramsNode?.type === "ObjectExpression") {
|
|
1508
|
+
for (const prop of paramsNode.properties) {
|
|
1509
|
+
if (prop.type !== "ObjectProperty") continue;
|
|
1510
|
+
const key = prop.key.type === "Identifier" ? prop.key.name : prop.key.type === "StringLiteral" ? prop.key.value : void 0;
|
|
1511
|
+
if (!key) continue;
|
|
1512
|
+
const expr = srcOf(prop.value);
|
|
1513
|
+
if (expr) out.params[key] = expr;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
const retNode = init.arguments[1];
|
|
1517
|
+
if (retNode) {
|
|
1518
|
+
const expr = srcOf(retNode);
|
|
1519
|
+
if (expr) out.returns = expr;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
return out;
|
|
1524
|
+
}
|
|
1525
|
+
function functionParamNames(source, fnName) {
|
|
1526
|
+
try {
|
|
1527
|
+
const ast = parse2(source);
|
|
1528
|
+
if (fnName.includes(".")) {
|
|
1529
|
+
const [clsName, methodName] = fnName.split(".", 2);
|
|
1530
|
+
for (const stmt of ast.program.body) {
|
|
1531
|
+
let decl = stmt.type === "ExportNamedDeclaration" ? stmt.declaration : stmt;
|
|
1532
|
+
if (!decl) continue;
|
|
1533
|
+
if (decl.type === "ExportDefaultDeclaration") {
|
|
1534
|
+
decl = decl.declaration;
|
|
1535
|
+
}
|
|
1536
|
+
const c = decl;
|
|
1537
|
+
if (c.type !== "ClassDeclaration" || c.id?.name !== clsName) continue;
|
|
1538
|
+
for (const m of c.body?.body ?? []) {
|
|
1539
|
+
const mem = m;
|
|
1540
|
+
const isMethod = mem.type === "MethodDefinition" || mem.type === "ClassMethod" || mem.type === "ClassPrivateMethod";
|
|
1541
|
+
if (!isMethod) continue;
|
|
1542
|
+
if (mem.kind && mem.kind !== "method") continue;
|
|
1543
|
+
const keyName = mem.key?.type === "Identifier" ? mem.key.name : void 0;
|
|
1544
|
+
if (keyName === methodName) {
|
|
1545
|
+
return (mem.params ?? []).map(
|
|
1546
|
+
paramNameOf
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
return [];
|
|
1552
|
+
}
|
|
1553
|
+
for (const stmt of ast.program.body) {
|
|
1554
|
+
const d = stmt.type === "ExportNamedDeclaration" ? stmt.declaration ?? void 0 : stmt;
|
|
1555
|
+
if (!d) continue;
|
|
1556
|
+
if (d.type === "FunctionDeclaration" && d.id?.name === fnName) {
|
|
1557
|
+
return d.params.map(paramNameOf);
|
|
1558
|
+
}
|
|
1559
|
+
if (d.type === "VariableDeclaration") {
|
|
1560
|
+
for (const decl of d.declarations) {
|
|
1561
|
+
if (decl.id.type === "Identifier" && decl.id.name === fnName && decl.init && (decl.init.type === "ArrowFunctionExpression" || decl.init.type === "FunctionExpression")) {
|
|
1562
|
+
return decl.init.params.map(paramNameOf);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
} catch {
|
|
1568
|
+
}
|
|
1569
|
+
return [];
|
|
1570
|
+
}
|
|
1571
|
+
function paramNameOf(p) {
|
|
1572
|
+
if (p.type === "Identifier" && p.name) return p.name;
|
|
1573
|
+
if (p.type === "AssignmentPattern" && p.left?.type === "Identifier" && p.left.name) {
|
|
1574
|
+
return p.left.name;
|
|
1575
|
+
}
|
|
1576
|
+
return "_";
|
|
1577
|
+
}
|
|
1578
|
+
function topLevelFnNames(source) {
|
|
1579
|
+
try {
|
|
1580
|
+
return listTopFunctions(source);
|
|
1581
|
+
} catch {
|
|
1582
|
+
return [];
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
function importLocalMap(source, fromFile) {
|
|
1586
|
+
const out = /* @__PURE__ */ new Map();
|
|
1587
|
+
try {
|
|
1588
|
+
const ast = parse2(source);
|
|
1589
|
+
const base = dirname5(resolve3(fromFile));
|
|
1590
|
+
for (const stmt of ast.program.body) {
|
|
1591
|
+
if (stmt.type !== "ImportDeclaration") continue;
|
|
1592
|
+
const spec = stmt.source.value;
|
|
1593
|
+
if (!spec.startsWith(".") && !spec.startsWith("/")) continue;
|
|
1594
|
+
const raw = resolve3(base, spec);
|
|
1595
|
+
let modulePath = null;
|
|
1596
|
+
for (const cand of [raw, `${raw}.js`, `${raw}.ts`, `${raw}.mjs`]) {
|
|
1597
|
+
if (existsSync5(cand)) {
|
|
1598
|
+
modulePath = cand;
|
|
1599
|
+
break;
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
if (!modulePath) continue;
|
|
1603
|
+
for (const s of stmt.specifiers) {
|
|
1604
|
+
if (s.type === "ImportSpecifier") {
|
|
1605
|
+
const imported = s.imported.type === "Identifier" ? s.imported.name : String(s.imported);
|
|
1606
|
+
out.set(s.local.name, { modulePath, exportName: imported });
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
} catch {
|
|
1611
|
+
}
|
|
1612
|
+
return out;
|
|
1613
|
+
}
|
|
1614
|
+
function deriveFromRoot(filePath, opts = {}) {
|
|
1615
|
+
const abs = resolve3(filePath);
|
|
1616
|
+
const source = readFileSync5(abs, "utf-8");
|
|
1617
|
+
const since = interfaceDiagCount3();
|
|
1618
|
+
const autoBind = opts.autoBind ?? interfaceConfig(findProjectConfig(dirname5(abs))?.config).autoBind;
|
|
1619
|
+
const loadModule = opts.loadModule ?? defaultLoadModule;
|
|
1620
|
+
const sidecarPath = sidecarPathOf4(abs);
|
|
1621
|
+
const sidecarSrc = existsSync5(sidecarPath) && !isNodeModulesPath2(sidecarPath) ? readFileSync5(sidecarPath, "utf-8") : "";
|
|
1622
|
+
const fnNames = topLevelFnNames(source);
|
|
1623
|
+
const roots = [];
|
|
1624
|
+
const plans = [];
|
|
1625
|
+
for (const fn of fnNames) {
|
|
1626
|
+
const eff = effectiveInterface3(source, fn, { loadModule, fromFile: abs, autoBind });
|
|
1627
|
+
if (!eff || eff.source !== "handwritten") continue;
|
|
1628
|
+
roots.push(fn);
|
|
1629
|
+
const sources = sidecarSrc ? extractFnConstraintSources(sidecarSrc, fn) : { params: {} };
|
|
1630
|
+
const srcParams = functionParamNames(source, fn);
|
|
1631
|
+
const planParams = srcParams.map((pname) => {
|
|
1632
|
+
const hit = eff.params.find((p) => p.param === pname);
|
|
1633
|
+
if (hit) {
|
|
1634
|
+
const src = sources.params[pname];
|
|
1635
|
+
return {
|
|
1636
|
+
name: pname,
|
|
1637
|
+
constraint: hit.constraint,
|
|
1638
|
+
...src ? { src } : {}
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
return {
|
|
1642
|
+
name: pname,
|
|
1643
|
+
constraint: { __nudoConstraint: true, preds: [] }
|
|
1644
|
+
};
|
|
1645
|
+
});
|
|
1646
|
+
plans.push({ fnName: fn, params: planParams });
|
|
1647
|
+
}
|
|
1648
|
+
if (plans.length === 0) {
|
|
1649
|
+
takeInterfaceDiagsSince3(since);
|
|
1650
|
+
return { roots: [], derived: [], hasRoot: false };
|
|
1651
|
+
}
|
|
1652
|
+
let modules = {};
|
|
1653
|
+
try {
|
|
1654
|
+
modules = evalAbsModuleGraph(source, abs, { loadModule }).modules;
|
|
1655
|
+
} catch {
|
|
1656
|
+
modules = {};
|
|
1657
|
+
}
|
|
1658
|
+
const importLocals = importLocalMap(source, abs);
|
|
1659
|
+
const wanted = opts.fnNames && opts.fnNames.length > 0 ? new Set(opts.fnNames) : void 0;
|
|
1660
|
+
const derived = [];
|
|
1661
|
+
for (const plan of plans) {
|
|
1662
|
+
const relRoot = relative4(process.cwd(), abs);
|
|
1663
|
+
const label = `${relRoot === "" || relRoot.startsWith("..") ? basename3(abs) : relRoot}:${plan.fnName}`;
|
|
1664
|
+
const rows = deriveOneRoot(plan, source, abs, modules, importLocals, label);
|
|
1665
|
+
for (const row of rows) {
|
|
1666
|
+
if (wanted && !wanted.has(row.fn)) continue;
|
|
1667
|
+
derived.push(row);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
takeInterfaceDiagsSince3(since);
|
|
1671
|
+
return { roots, derived, hasRoot: true };
|
|
1672
|
+
}
|
|
1673
|
+
function deriveOneRoot(plan, source, file, modules, importLocals, label) {
|
|
1674
|
+
beginDerivationSession();
|
|
1675
|
+
const entryArgs = [];
|
|
1676
|
+
try {
|
|
1677
|
+
for (const p of plan.params) {
|
|
1678
|
+
const entry = constraintToEntryAbs(p.constraint, p.name);
|
|
1679
|
+
if (p.src) {
|
|
1680
|
+
tagDerivationRoot(entry, {
|
|
1681
|
+
expr: p.src.expr,
|
|
1682
|
+
...p.src.importFrom !== void 0 ? { importFrom: p.src.importFrom } : {},
|
|
1683
|
+
...p.src.importName !== void 0 ? { importName: p.src.importName } : {}
|
|
1684
|
+
});
|
|
1685
|
+
} else if (p.constraint.preds.length === 0 && !p.constraint.prim) {
|
|
1686
|
+
}
|
|
1687
|
+
entryArgs.push(entry);
|
|
1688
|
+
}
|
|
1689
|
+
const calls = [];
|
|
1690
|
+
const bCalls = [];
|
|
1691
|
+
const prevCall = setBCallCollector((r) => bCalls.push(r));
|
|
1692
|
+
try {
|
|
1693
|
+
{
|
|
1694
|
+
const run = runTranspiled(source, { mode: "analyze", modules });
|
|
1695
|
+
if (plan.fnName.includes(".")) {
|
|
1696
|
+
const [clsName, methodName] = plan.fnName.split(".", 2);
|
|
1697
|
+
const clsAbs = run[clsName ?? ""];
|
|
1698
|
+
if (clsAbs && typeof clsAbs === "object" && "shape" in clsAbs) {
|
|
1699
|
+
const inst = $new(clsAbs, []);
|
|
1700
|
+
$invoke(inst, methodName ?? "", entryArgs);
|
|
1701
|
+
}
|
|
1702
|
+
} else if (plan.fnName in run) {
|
|
1703
|
+
callTranspiledExportFull(run, plan.fnName, entryArgs);
|
|
1704
|
+
}
|
|
1705
|
+
calls.push(
|
|
1706
|
+
...bCalls.map((r) => ({
|
|
1707
|
+
fnName: r.fnName,
|
|
1708
|
+
args: r.args,
|
|
1709
|
+
result: r.result,
|
|
1710
|
+
callLoc: r.callLoc,
|
|
1711
|
+
threw: r.threw
|
|
1712
|
+
}))
|
|
1713
|
+
);
|
|
1714
|
+
}
|
|
1715
|
+
} catch {
|
|
1716
|
+
return [];
|
|
1717
|
+
} finally {
|
|
1718
|
+
setBCallCollector(prevCall);
|
|
1719
|
+
}
|
|
1720
|
+
const byCallee = /* @__PURE__ */ new Map();
|
|
1721
|
+
for (const call of calls) {
|
|
1722
|
+
const imp = importLocals.get(call.fnName);
|
|
1723
|
+
let targetFile;
|
|
1724
|
+
let targetExport = call.fnName;
|
|
1725
|
+
if (imp) {
|
|
1726
|
+
targetFile = imp.modulePath;
|
|
1727
|
+
targetExport = imp.exportName;
|
|
1728
|
+
} else {
|
|
1729
|
+
targetFile = file;
|
|
1730
|
+
}
|
|
1731
|
+
if (isNodeModulesPath2(targetFile)) continue;
|
|
1732
|
+
if (!existsSync5(targetFile)) continue;
|
|
1733
|
+
const key = `${targetFile}::${targetExport}`;
|
|
1734
|
+
let agg = byCallee.get(key);
|
|
1735
|
+
if (!agg) {
|
|
1736
|
+
agg = { targetFile, targetExport, argAbs: [], resultAbs: [] };
|
|
1737
|
+
byCallee.set(key, agg);
|
|
1738
|
+
}
|
|
1739
|
+
agg.argAbs.push(call.args);
|
|
1740
|
+
agg.resultAbs.push(call.threw ? unknownAbs : call.result);
|
|
1741
|
+
}
|
|
1742
|
+
const out = [];
|
|
1743
|
+
for (const agg of byCallee.values()) {
|
|
1744
|
+
let targetSource;
|
|
1745
|
+
try {
|
|
1746
|
+
targetSource = readFileSync5(agg.targetFile, "utf-8");
|
|
1747
|
+
} catch {
|
|
1748
|
+
continue;
|
|
1749
|
+
}
|
|
1750
|
+
const paramNames = functionParamNames(targetSource, agg.targetExport);
|
|
1751
|
+
if (paramNames.length === 0 && agg.argAbs.every((a) => a.length === 0)) continue;
|
|
1752
|
+
const width = Math.max(
|
|
1753
|
+
paramNames.length,
|
|
1754
|
+
...agg.argAbs.map((a) => a.length),
|
|
1755
|
+
0
|
|
1756
|
+
);
|
|
1757
|
+
const params = [];
|
|
1758
|
+
let allCompositional = true;
|
|
1759
|
+
let sawEvidence = false;
|
|
1760
|
+
for (let i = 0; i < width; i++) {
|
|
1761
|
+
const name = paramNames[i] ?? `_${i}`;
|
|
1762
|
+
const absList = [];
|
|
1763
|
+
for (const args of agg.argAbs) {
|
|
1764
|
+
const a = args[i];
|
|
1765
|
+
if (a) absList.push(a);
|
|
1766
|
+
}
|
|
1767
|
+
const slot = projectParamSlot(absList, name);
|
|
1768
|
+
if (!slot) {
|
|
1769
|
+
allCompositional = false;
|
|
1770
|
+
continue;
|
|
1771
|
+
}
|
|
1772
|
+
sawEvidence = true;
|
|
1773
|
+
if (!slot.compositional) allCompositional = false;
|
|
1774
|
+
params.push({
|
|
1775
|
+
name,
|
|
1776
|
+
constraint: slot.constraint,
|
|
1777
|
+
dsl: slot.dsl,
|
|
1778
|
+
prelude: slot.prelude,
|
|
1779
|
+
imports: slot.imports,
|
|
1780
|
+
...slot.rootNodeId !== void 0 ? { rootNodeId: slot.rootNodeId } : {},
|
|
1781
|
+
...slot.shiftCount !== void 0 ? { shiftCount: slot.shiftCount } : {}
|
|
1782
|
+
});
|
|
1783
|
+
}
|
|
1784
|
+
const retAbs = agg.resultAbs.filter(
|
|
1785
|
+
(a) => a.conf === "exact" || a.conf === "path"
|
|
1786
|
+
);
|
|
1787
|
+
const retSlot = projectReturnSlot(retAbs, params, paramNames);
|
|
1788
|
+
if (retSlot && !retSlot.compositional) allCompositional = false;
|
|
1789
|
+
if (retSlot) sawEvidence = true;
|
|
1790
|
+
if (!sawEvidence) {
|
|
1791
|
+
out.push({
|
|
1792
|
+
file: agg.targetFile,
|
|
1793
|
+
fn: agg.targetExport,
|
|
1794
|
+
paramNames,
|
|
1795
|
+
params: [],
|
|
1796
|
+
derivedFrom: label,
|
|
1797
|
+
compositional: false,
|
|
1798
|
+
underivable: true
|
|
1799
|
+
});
|
|
1800
|
+
continue;
|
|
1801
|
+
}
|
|
1802
|
+
out.push({
|
|
1803
|
+
file: agg.targetFile,
|
|
1804
|
+
fn: agg.targetExport,
|
|
1805
|
+
paramNames,
|
|
1806
|
+
params,
|
|
1807
|
+
...retSlot ? {
|
|
1808
|
+
returns: {
|
|
1809
|
+
constraint: retSlot.constraint,
|
|
1810
|
+
dsl: retSlot.dsl,
|
|
1811
|
+
prelude: retSlot.prelude,
|
|
1812
|
+
imports: retSlot.imports
|
|
1813
|
+
}
|
|
1814
|
+
} : {},
|
|
1815
|
+
derivedFrom: label,
|
|
1816
|
+
compositional: allCompositional && params.length > 0
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
return out;
|
|
1820
|
+
} finally {
|
|
1821
|
+
endDerivationSession();
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
// src/interface-derivation-rewrite.ts
|
|
1826
|
+
import { relative as relative5, resolve as resolve4 } from "path";
|
|
1827
|
+
function resolveRelImport(fromSpec, fromDir, targetDir) {
|
|
1828
|
+
const abs = resolve4(fromDir, fromSpec);
|
|
1829
|
+
let rel = relative5(targetDir, abs);
|
|
1830
|
+
if (!rel.startsWith(".")) rel = `./${rel}`;
|
|
1831
|
+
return rel.split("\\").join("/");
|
|
1832
|
+
}
|
|
1833
|
+
var NameAllocator = class {
|
|
1834
|
+
taken;
|
|
1835
|
+
constructor(initial) {
|
|
1836
|
+
this.taken = new Set(initial ?? []);
|
|
1837
|
+
}
|
|
1838
|
+
claim(preferred) {
|
|
1839
|
+
if (!this.taken.has(preferred)) {
|
|
1840
|
+
this.taken.add(preferred);
|
|
1841
|
+
return preferred;
|
|
1842
|
+
}
|
|
1843
|
+
let i = 2;
|
|
1844
|
+
while (this.taken.has(`${preferred}_${i}`)) i++;
|
|
1845
|
+
const name = `${preferred}_${i}`;
|
|
1846
|
+
this.taken.add(name);
|
|
1847
|
+
return name;
|
|
1848
|
+
}
|
|
1849
|
+
};
|
|
1850
|
+
function escapeRegExp(s) {
|
|
1851
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1852
|
+
}
|
|
1853
|
+
function rewriteIdents(src, map) {
|
|
1854
|
+
if (map.size === 0) return src;
|
|
1855
|
+
let out = src;
|
|
1856
|
+
const keys = [...map.keys()].sort((a, b) => b.length - a.length);
|
|
1857
|
+
for (const k of keys) {
|
|
1858
|
+
const v = map.get(k);
|
|
1859
|
+
if (k === v) continue;
|
|
1860
|
+
out = out.replace(new RegExp(`(?<![\\w$.])${escapeRegExp(k)}(?![\\w$])`, "g"), v);
|
|
1861
|
+
}
|
|
1862
|
+
return out;
|
|
1863
|
+
}
|
|
1864
|
+
function preludeLocalName(line) {
|
|
1865
|
+
const m = /^const\s+([A-Za-z_$][\w$]*)\s*=/.exec(line.trim());
|
|
1866
|
+
return m?.[1];
|
|
1867
|
+
}
|
|
1868
|
+
function formatDerivedSection(row, opts) {
|
|
1869
|
+
if (row.underivable || row.params.length === 0) return void 0;
|
|
1870
|
+
const importMap = /* @__PURE__ */ new Map();
|
|
1871
|
+
const preludes = [];
|
|
1872
|
+
const paramDsls = [];
|
|
1873
|
+
for (const p of row.params) {
|
|
1874
|
+
for (const imp of p.imports) {
|
|
1875
|
+
const rel = resolveRelImport(imp.from, opts.rootSidecarDir, opts.targetSidecarDir);
|
|
1876
|
+
const prev = importMap.get(imp.name);
|
|
1877
|
+
if (prev !== void 0 && prev !== rel) return void 0;
|
|
1878
|
+
importMap.set(imp.name, rel);
|
|
1879
|
+
}
|
|
1880
|
+
for (const line of p.prelude) {
|
|
1881
|
+
if (!preludes.includes(line)) preludes.push(line);
|
|
1882
|
+
}
|
|
1883
|
+
paramDsls.push(p.dsl);
|
|
1884
|
+
}
|
|
1885
|
+
let retDsl = "";
|
|
1886
|
+
if (row.returns) {
|
|
1887
|
+
for (const imp of row.returns.imports) {
|
|
1888
|
+
const rel = resolveRelImport(imp.from, opts.rootSidecarDir, opts.targetSidecarDir);
|
|
1889
|
+
const prev = importMap.get(imp.name);
|
|
1890
|
+
if (prev !== void 0 && prev !== rel) return void 0;
|
|
1891
|
+
importMap.set(imp.name, rel);
|
|
1892
|
+
}
|
|
1893
|
+
for (const line of row.returns.prelude) {
|
|
1894
|
+
if (!preludes.includes(line)) preludes.push(line);
|
|
1895
|
+
}
|
|
1896
|
+
retDsl = `, ${row.returns.dsl}`;
|
|
1897
|
+
}
|
|
1898
|
+
const namer = new NameAllocator(opts.takenNames);
|
|
1899
|
+
namer.claim(row.fn);
|
|
1900
|
+
const renames = /* @__PURE__ */ new Map();
|
|
1901
|
+
const importLocals = [];
|
|
1902
|
+
for (const [name, from] of [...importMap.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
1903
|
+
const local = namer.claim(name);
|
|
1904
|
+
if (local !== name) renames.set(name, local);
|
|
1905
|
+
importLocals.push({ original: name, local, from });
|
|
1906
|
+
}
|
|
1907
|
+
const preludeClaimed = /* @__PURE__ */ new Set();
|
|
1908
|
+
const renamedPreludes = [];
|
|
1909
|
+
for (const line of preludes) {
|
|
1910
|
+
const local = preludeLocalName(line);
|
|
1911
|
+
if (local !== void 0 && !preludeClaimed.has(local)) {
|
|
1912
|
+
preludeClaimed.add(local);
|
|
1913
|
+
if (!renames.has(local)) {
|
|
1914
|
+
const next = namer.claim(local);
|
|
1915
|
+
if (next !== local) renames.set(local, next);
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
renamedPreludes.push(rewriteIdents(line, renames));
|
|
1919
|
+
}
|
|
1920
|
+
const paramParts = row.params.map((p, i) => {
|
|
1921
|
+
const dsl = rewriteIdents(paramDsls[i], renames);
|
|
1922
|
+
const name = rewriteIdents(p.name, renames);
|
|
1923
|
+
return dsl === name ? name : `${name}: ${dsl}`;
|
|
1924
|
+
});
|
|
1925
|
+
const retPart = retDsl === "" ? "" : `, ${rewriteIdents(row.returns.dsl, renames)}`;
|
|
1926
|
+
const importLineTexts = importLocals.map(({ original, local, from }) => {
|
|
1927
|
+
const spec = original === local ? local : `${original} as ${local}`;
|
|
1928
|
+
return `import { ${spec} } from ${JSON.stringify(from)};`;
|
|
1929
|
+
}).sort((a, b) => a.localeCompare(b));
|
|
1930
|
+
const lines = [
|
|
1931
|
+
...importLineTexts,
|
|
1932
|
+
...importLineTexts.length > 0 && renamedPreludes.length > 0 ? [""] : [],
|
|
1933
|
+
...renamedPreludes,
|
|
1934
|
+
`export const ${row.fn} = fn({ ${paramParts.join(", ")} }${retPart});`
|
|
1935
|
+
];
|
|
1936
|
+
const usedNames = /* @__PURE__ */ new Set();
|
|
1937
|
+
for (const { local } of importLocals) usedNames.add(local);
|
|
1938
|
+
for (const line of renamedPreludes) {
|
|
1939
|
+
const n = preludeLocalName(line);
|
|
1940
|
+
if (n) usedNames.add(n);
|
|
1941
|
+
}
|
|
1942
|
+
usedNames.add(row.fn);
|
|
1943
|
+
return { text: lines.join("\n"), usedNames: [...usedNames] };
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
// src/interface-derivation-emit.ts
|
|
1947
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
1948
|
+
import { basename as basename4, dirname as dirname6, relative as relative6, resolve as resolve5 } from "path";
|
|
1949
|
+
import {
|
|
1950
|
+
execNudoModule as execNudoModule2,
|
|
1951
|
+
isNodeModulesPath as isNodeModulesPath3,
|
|
1952
|
+
isNudoConstraint as isNudoConstraint2,
|
|
1953
|
+
parseSource as parseSource2,
|
|
1954
|
+
refineDiagCount as refineDiagCount3,
|
|
1955
|
+
sidecarPathOf as sidecarPathOf5,
|
|
1956
|
+
takeRefineDiagsSince as takeRefineDiagsSince3
|
|
1957
|
+
} from "@nudojs/core";
|
|
1958
|
+
var DERIVED_HEADER = "// @generated by nudo \u2014 do not edit; regenerate with `nudo contract --emit`";
|
|
1959
|
+
function collectTopLevelNames(src) {
|
|
1960
|
+
const names = /* @__PURE__ */ new Set();
|
|
1961
|
+
if (src.trim() === "") return names;
|
|
1962
|
+
let ast;
|
|
1963
|
+
try {
|
|
1964
|
+
ast = parseSource2(src);
|
|
1965
|
+
} catch {
|
|
1966
|
+
return names;
|
|
1967
|
+
}
|
|
1968
|
+
const addDecl = (d) => {
|
|
1969
|
+
if (d.type === "VariableDeclaration") {
|
|
1970
|
+
for (const decl of d.declarations) {
|
|
1971
|
+
if (decl.id.type === "Identifier") names.add(decl.id.name);
|
|
1972
|
+
}
|
|
1973
|
+
} else if (d.type === "FunctionDeclaration" || d.type === "ClassDeclaration") {
|
|
1974
|
+
if (d.id) names.add(d.id.name);
|
|
1975
|
+
}
|
|
1976
|
+
};
|
|
1977
|
+
for (const stmt of ast.program.body) {
|
|
1978
|
+
if (stmt.type === "ImportDeclaration") {
|
|
1979
|
+
for (const s of stmt.specifiers) names.add(s.local.name);
|
|
1980
|
+
continue;
|
|
1981
|
+
}
|
|
1982
|
+
if (stmt.type === "ExportNamedDeclaration") {
|
|
1983
|
+
if (stmt.declaration) addDecl(stmt.declaration);
|
|
1984
|
+
for (const spec of stmt.specifiers) {
|
|
1985
|
+
names.add(
|
|
1986
|
+
spec.exported.type === "Identifier" ? spec.exported.name : spec.exported.value
|
|
1987
|
+
);
|
|
1988
|
+
}
|
|
1989
|
+
continue;
|
|
1990
|
+
}
|
|
1991
|
+
addDecl(stmt);
|
|
1992
|
+
}
|
|
1993
|
+
return names;
|
|
1994
|
+
}
|
|
1995
|
+
function sidecarAssembles(text, fromFile, loadModule) {
|
|
1996
|
+
try {
|
|
1997
|
+
parseSource2(text);
|
|
1998
|
+
execNudoModule2(text, { loadModule, fromFile });
|
|
1999
|
+
return true;
|
|
2000
|
+
} catch {
|
|
2001
|
+
return false;
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
function emitDerivedFromRoot(rootFile, opts) {
|
|
2005
|
+
const abs = resolve5(rootFile);
|
|
2006
|
+
const derive = deriveFromRoot(abs, {
|
|
2007
|
+
...opts.loadModule ? { loadModule: opts.loadModule } : {},
|
|
2008
|
+
...opts.autoBind !== void 0 ? { autoBind: opts.autoBind } : {},
|
|
2009
|
+
...opts.fnNames && opts.fnNames.length > 0 ? { fnNames: opts.fnNames } : {}
|
|
2010
|
+
});
|
|
2011
|
+
if (!derive.hasRoot) {
|
|
2012
|
+
return { sidecars: [], hasRoot: false, roots: [], entryOnly: true };
|
|
2013
|
+
}
|
|
2014
|
+
const proj = findProjectConfig(dirname6(abs));
|
|
2015
|
+
const allow = interfaceConfig(proj?.config).emit;
|
|
2016
|
+
const projectDir = proj?.projectDir;
|
|
2017
|
+
const rootSidecarDir = dirname6(sidecarPathOf5(abs));
|
|
2018
|
+
const wanted = opts.fnNames && opts.fnNames.length > 0 ? new Set(opts.fnNames) : void 0;
|
|
2019
|
+
const result = {
|
|
2020
|
+
sidecars: [],
|
|
2021
|
+
hasRoot: true,
|
|
2022
|
+
roots: derive.roots
|
|
2023
|
+
};
|
|
2024
|
+
const bySidecar = /* @__PURE__ */ new Map();
|
|
2025
|
+
for (const row of derive.derived) {
|
|
2026
|
+
if (wanted && !wanted.has(row.fn)) continue;
|
|
2027
|
+
if (!matchesEmitAllowlist(row.file, projectDir, allow)) continue;
|
|
2028
|
+
const sp = sidecarPathOf5(row.file);
|
|
2029
|
+
if (isNodeModulesPath3(sp)) continue;
|
|
2030
|
+
const list = bySidecar.get(sp) ?? [];
|
|
2031
|
+
list.push(row);
|
|
2032
|
+
bySidecar.set(sp, list);
|
|
2033
|
+
}
|
|
2034
|
+
for (const [sidecarPath, rows0] of bySidecar) {
|
|
2035
|
+
const targetFile = rows0[0].file;
|
|
2036
|
+
const targetSidecarDir = dirname6(sidecarPath);
|
|
2037
|
+
const prevSrc = existsSync6(sidecarPath) ? readFileSync6(sidecarPath, "utf-8") : "";
|
|
2038
|
+
const rows = opts.refreshExistingOnly ? rows0.filter((r) => findGeneratedSectionText(prevSrc, r.fn) !== void 0) : rows0;
|
|
2039
|
+
if (rows.length === 0) continue;
|
|
2040
|
+
const handwritten = handwrittenNames(prevSrc);
|
|
2041
|
+
const loadModule = opts.loadModule ?? defaultLoadModule;
|
|
2042
|
+
const batchFns = new Set(
|
|
2043
|
+
rows.filter((r) => !r.underivable && !handwritten.has(r.fn)).map((r) => r.fn)
|
|
2044
|
+
);
|
|
2045
|
+
const baseForNames = opts.mode === "update" ? stripGeneratedFor(prevSrc, batchFns) : prevSrc;
|
|
2046
|
+
const taken = collectTopLevelNames(baseForNames);
|
|
2047
|
+
const accepted = [];
|
|
2048
|
+
const issues = [];
|
|
2049
|
+
const written = [];
|
|
2050
|
+
let anySkip;
|
|
2051
|
+
for (const row of rows) {
|
|
2052
|
+
if (handwritten.has(row.fn)) {
|
|
2053
|
+
issues.push({
|
|
2054
|
+
code: "nudo:interface-name-clash",
|
|
2055
|
+
severity: "error",
|
|
2056
|
+
message: `sidecar already has a handwritten binding '${row.fn}' (${relative6(process.cwd(), sidecarPath) || sidecarPath}); handwritten wins \u2014 skipping emit`
|
|
2057
|
+
});
|
|
2058
|
+
anySkip = "name-clash";
|
|
2059
|
+
continue;
|
|
2060
|
+
}
|
|
2061
|
+
if (row.underivable) {
|
|
2062
|
+
issues.push({
|
|
2063
|
+
code: "nudo:interface-underivable",
|
|
2064
|
+
severity: "info",
|
|
2065
|
+
message: `${row.fn}: contract underivable from ${row.derivedFrom} (opaque / truncated / no evidence)`
|
|
2066
|
+
});
|
|
2067
|
+
anySkip = "underivable";
|
|
2068
|
+
continue;
|
|
2069
|
+
}
|
|
2070
|
+
const prevSection = findGeneratedSectionText(prevSrc, row.fn);
|
|
2071
|
+
const body = formatDerivedSection(row, {
|
|
2072
|
+
rootSidecarDir,
|
|
2073
|
+
targetSidecarDir,
|
|
2074
|
+
takenNames: taken
|
|
2075
|
+
});
|
|
2076
|
+
if (!body) {
|
|
2077
|
+
anySkip = "not-projectable";
|
|
2078
|
+
if (prevSection !== void 0) {
|
|
2079
|
+
for (const n of collectTopLevelNames(prevSection)) taken.add(n);
|
|
2080
|
+
}
|
|
2081
|
+
continue;
|
|
2082
|
+
}
|
|
2083
|
+
for (const n of body.usedNames) taken.add(n);
|
|
2084
|
+
const srcRel = relative6(targetSidecarDir, targetFile) || basename4(targetFile);
|
|
2085
|
+
const section = [
|
|
2086
|
+
DERIVED_HEADER,
|
|
2087
|
+
`// source: ${srcRel}:${row.fn}`,
|
|
2088
|
+
`// derived-from: ${row.derivedFrom}`,
|
|
2089
|
+
body.text,
|
|
2090
|
+
""
|
|
2091
|
+
].join("\n");
|
|
2092
|
+
if (!derivedRoundTrips(section, row.fn, sidecarPath, loadModule)) {
|
|
2093
|
+
anySkip = "not-projectable";
|
|
2094
|
+
if (prevSection !== void 0) {
|
|
2095
|
+
for (const n of collectTopLevelNames(prevSection)) taken.add(n);
|
|
2096
|
+
}
|
|
2097
|
+
continue;
|
|
2098
|
+
}
|
|
2099
|
+
if (opts.mode === "add" && prevSection !== void 0) {
|
|
2100
|
+
anySkip = "no-change";
|
|
2101
|
+
for (const n of collectTopLevelNames(prevSection)) taken.add(n);
|
|
2102
|
+
continue;
|
|
2103
|
+
}
|
|
2104
|
+
const norm = normalizeSectionText(section);
|
|
2105
|
+
if (prevSection !== void 0 && normalizeSectionText(prevSection) === norm) {
|
|
2106
|
+
anySkip = "no-change";
|
|
2107
|
+
accepted.push({ fn: row.fn, text: prevSection, prevText: prevSection });
|
|
2108
|
+
for (const n of collectTopLevelNames(prevSection)) taken.add(n);
|
|
2109
|
+
continue;
|
|
2110
|
+
}
|
|
2111
|
+
written.push(row.fn);
|
|
2112
|
+
accepted.push({ fn: row.fn, text: section, ...prevSection !== void 0 ? { prevText: prevSection } : {} });
|
|
2113
|
+
}
|
|
2114
|
+
let finalContent;
|
|
2115
|
+
if (accepted.length === 0) {
|
|
2116
|
+
finalContent = prevSrc;
|
|
2117
|
+
} else if (opts.mode === "add") {
|
|
2118
|
+
finalContent = joinSectionTexts(prevSrc, accepted.map((a) => a.text));
|
|
2119
|
+
} else {
|
|
2120
|
+
const acceptedFns = new Set(accepted.map((a) => a.fn));
|
|
2121
|
+
const stripped = stripGeneratedFor(prevSrc, acceptedFns);
|
|
2122
|
+
const preserved = collectGeneratedSectionsRaw(prevSrc).filter(
|
|
2123
|
+
(s) => !s.names.some((n) => acceptedFns.has(n))
|
|
2124
|
+
);
|
|
2125
|
+
finalContent = joinSectionTexts(stripped, [
|
|
2126
|
+
...preserved.map((s) => normalizeSectionText(s.text)),
|
|
2127
|
+
...accepted.map((a) => a.text)
|
|
2128
|
+
]);
|
|
2129
|
+
}
|
|
2130
|
+
if (finalContent.trim() !== "" && !sidecarAssembles(finalContent, sidecarPath, loadModule)) {
|
|
2131
|
+
issues.push({
|
|
2132
|
+
code: "nudo:interface-not-projectable",
|
|
2133
|
+
severity: "error",
|
|
2134
|
+
message: `assembled sidecar failed round-trip (${relative6(process.cwd(), sidecarPath) || sidecarPath}); refusing to write`
|
|
2135
|
+
});
|
|
2136
|
+
anySkip = "not-projectable";
|
|
2137
|
+
result.sidecars.push({
|
|
2138
|
+
file: targetFile,
|
|
2139
|
+
sidecarPath,
|
|
2140
|
+
fn: rows.map((r) => r.fn).join(","),
|
|
2141
|
+
written: false,
|
|
2142
|
+
changed: false,
|
|
2143
|
+
...anySkip ? { skipped: anySkip } : {},
|
|
2144
|
+
issues
|
|
2145
|
+
});
|
|
2146
|
+
continue;
|
|
2147
|
+
}
|
|
2148
|
+
const changed = finalContent !== prevSrc;
|
|
2149
|
+
const diff = changed ? unifiedDiff(prevSrc, finalContent, relative6(process.cwd(), sidecarPath) || sidecarPath) : void 0;
|
|
2150
|
+
if (changed && !opts.dryRun) {
|
|
2151
|
+
const tmp = `${sidecarPath}.tmp-${process.pid}-${Math.random().toString(16).slice(2, 10)}`;
|
|
2152
|
+
try {
|
|
2153
|
+
writeFileSync4(tmp, finalContent, "utf-8");
|
|
2154
|
+
renameSync2(tmp, sidecarPath);
|
|
2155
|
+
} catch (e) {
|
|
2156
|
+
try {
|
|
2157
|
+
if (existsSync6(tmp)) unlinkSync2(tmp);
|
|
2158
|
+
} catch {
|
|
2159
|
+
}
|
|
2160
|
+
throw e;
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
result.sidecars.push({
|
|
2164
|
+
file: targetFile,
|
|
2165
|
+
sidecarPath,
|
|
2166
|
+
fn: rows.map((r) => r.fn).join(","),
|
|
2167
|
+
written: written.length > 0 && changed,
|
|
2168
|
+
changed,
|
|
2169
|
+
...anySkip ? { skipped: anySkip } : {},
|
|
2170
|
+
...diff !== void 0 ? { diff } : {},
|
|
2171
|
+
issues
|
|
2172
|
+
});
|
|
2173
|
+
}
|
|
2174
|
+
return result;
|
|
2175
|
+
}
|
|
2176
|
+
function collectGeneratedSectionsRaw(src) {
|
|
2177
|
+
if (src.trim() === "") return [];
|
|
2178
|
+
let ast;
|
|
2179
|
+
try {
|
|
2180
|
+
ast = parseSource2(src);
|
|
2181
|
+
} catch {
|
|
2182
|
+
return [];
|
|
2183
|
+
}
|
|
2184
|
+
const stmts = ast.program.body;
|
|
2185
|
+
const out = [];
|
|
2186
|
+
for (const headerPos of findGeneratedHeaderOffsets(src)) {
|
|
2187
|
+
for (let i = 0; i < stmts.length; i++) {
|
|
2188
|
+
const stmt = stmts[i];
|
|
2189
|
+
if (stmt.type !== "ExportNamedDeclaration" || stmt.source) continue;
|
|
2190
|
+
if (stmt.start == null || stmt.end == null || stmt.start < headerPos) continue;
|
|
2191
|
+
const d = stmt.declaration;
|
|
2192
|
+
if (!d) continue;
|
|
2193
|
+
const names = [];
|
|
2194
|
+
if (d.type === "VariableDeclaration") {
|
|
2195
|
+
for (const decl of d.declarations) {
|
|
2196
|
+
if (decl.id.type === "Identifier") names.push(decl.id.name);
|
|
2197
|
+
}
|
|
2198
|
+
} else if (d.type === "FunctionDeclaration" || d.type === "ClassDeclaration") {
|
|
2199
|
+
if (d.id) names.push(d.id.name);
|
|
2200
|
+
}
|
|
2201
|
+
if (names.length === 0) continue;
|
|
2202
|
+
out.push({
|
|
2203
|
+
names,
|
|
2204
|
+
start: headerPos,
|
|
2205
|
+
end: stmt.end,
|
|
2206
|
+
text: src.slice(headerPos, stmt.end)
|
|
2207
|
+
});
|
|
2208
|
+
break;
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
return out;
|
|
2212
|
+
}
|
|
2213
|
+
function findGeneratedHeaderOffsets(src) {
|
|
2214
|
+
const out = [];
|
|
2215
|
+
let pos = 0;
|
|
2216
|
+
for (const line of src.split("\n")) {
|
|
2217
|
+
const t = line.trim();
|
|
2218
|
+
if ((t.startsWith("//") || t.startsWith("/*") || t.startsWith("*")) && /@generated/.test(t)) {
|
|
2219
|
+
out.push(pos);
|
|
2220
|
+
}
|
|
2221
|
+
pos += line.length + 1;
|
|
2222
|
+
}
|
|
2223
|
+
return out;
|
|
2224
|
+
}
|
|
2225
|
+
function generatedSectionNames(src) {
|
|
2226
|
+
return new Set(collectGeneratedSectionsRaw(src).flatMap((s) => s.names));
|
|
2227
|
+
}
|
|
2228
|
+
function handwrittenNames(src) {
|
|
2229
|
+
if (src.trim() === "") return /* @__PURE__ */ new Set();
|
|
2230
|
+
let ast;
|
|
2231
|
+
try {
|
|
2232
|
+
ast = parseSource2(src);
|
|
2233
|
+
} catch {
|
|
2234
|
+
return /* @__PURE__ */ new Set();
|
|
2235
|
+
}
|
|
2236
|
+
const names = /* @__PURE__ */ new Set();
|
|
2237
|
+
const collect = (stmt) => {
|
|
2238
|
+
if (stmt.type === "VariableDeclaration") {
|
|
2239
|
+
for (const d of stmt.declarations) {
|
|
2240
|
+
if (d.id.type === "Identifier") names.add(d.id.name);
|
|
2241
|
+
}
|
|
2242
|
+
} else if (stmt.type === "FunctionDeclaration" || stmt.type === "ClassDeclaration") {
|
|
2243
|
+
if (stmt.id) names.add(stmt.id.name);
|
|
2244
|
+
}
|
|
2245
|
+
};
|
|
2246
|
+
for (const stmt of ast.program.body) {
|
|
2247
|
+
if (stmt.type === "ExportNamedDeclaration") {
|
|
2248
|
+
if (stmt.declaration) collect(stmt.declaration);
|
|
2249
|
+
if (!stmt.source) {
|
|
2250
|
+
for (const spec of stmt.specifiers) {
|
|
2251
|
+
names.add(
|
|
2252
|
+
spec.exported.type === "Identifier" ? spec.exported.name : spec.exported.value
|
|
2253
|
+
);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
} else {
|
|
2257
|
+
collect(stmt);
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
for (const n of generatedSectionNames(src)) names.delete(n);
|
|
2261
|
+
return names;
|
|
2262
|
+
}
|
|
2263
|
+
function findGeneratedSectionText(src, fn) {
|
|
2264
|
+
return collectGeneratedSectionsRaw(src).find((s) => s.names.includes(fn))?.text;
|
|
2265
|
+
}
|
|
2266
|
+
function stripGeneratedFor(src, fns) {
|
|
2267
|
+
const sections = collectGeneratedSectionsRaw(src).filter(
|
|
2268
|
+
(s) => s.names.some((n) => fns.has(n))
|
|
2269
|
+
);
|
|
2270
|
+
if (sections.length === 0) return src;
|
|
2271
|
+
const sorted = [...sections].sort((a, b) => a.start - b.start);
|
|
2272
|
+
let out = "";
|
|
2273
|
+
let pos = 0;
|
|
2274
|
+
for (const s of sorted) {
|
|
2275
|
+
out += src.slice(pos, s.start);
|
|
2276
|
+
pos = Math.max(pos, s.end);
|
|
2277
|
+
}
|
|
2278
|
+
out += src.slice(pos);
|
|
2279
|
+
return out;
|
|
2280
|
+
}
|
|
2281
|
+
function normalizeSectionText(text) {
|
|
2282
|
+
return text.replace(/\s*$/, "") + "\n";
|
|
2283
|
+
}
|
|
2284
|
+
function joinSectionTexts(base, sectionTexts) {
|
|
2285
|
+
const normalized = sectionTexts.map(normalizeSectionText).filter((t) => t.trim() !== "");
|
|
2286
|
+
const tailTrimmed = base.replace(/\s+$/, "");
|
|
2287
|
+
if (normalized.length === 0) return tailTrimmed === "" ? "" : `${tailTrimmed}
|
|
2288
|
+
`;
|
|
2289
|
+
const trimmed = tailTrimmed.replace(/^\s+/, "");
|
|
2290
|
+
const lead = trimmed === "" ? "" : `${trimmed}
|
|
2291
|
+
|
|
2292
|
+
`;
|
|
2293
|
+
return lead + normalized.join("\n");
|
|
2294
|
+
}
|
|
2295
|
+
function derivedRoundTrips(text, fn, fromFile, loadModule) {
|
|
2296
|
+
const since = refineDiagCount3();
|
|
2297
|
+
try {
|
|
2298
|
+
const exports = execNudoModule2(text, { loadModule, fromFile });
|
|
2299
|
+
const v = exports[fn];
|
|
2300
|
+
return isNudoConstraint2(v) && v.fn !== void 0;
|
|
2301
|
+
} catch {
|
|
2302
|
+
return false;
|
|
2303
|
+
} finally {
|
|
2304
|
+
takeRefineDiagsSince3(since);
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
// src/what-if.ts
|
|
2309
|
+
import { parse as parse3 } from "@nudojs/parser";
|
|
2310
|
+
function splitTopLevelUnion(expr) {
|
|
2311
|
+
const members = [];
|
|
2312
|
+
let start = 0;
|
|
2313
|
+
let depth = 0;
|
|
2314
|
+
for (let i = 0; i < expr.length; i++) {
|
|
2315
|
+
const c = expr[i];
|
|
2316
|
+
if (c === "(" || c === "[" || c === "{") depth++;
|
|
2317
|
+
else if (c === ")" || c === "]" || c === "}") depth--;
|
|
2318
|
+
else if (c === "|" && depth === 0) {
|
|
2319
|
+
members.push(expr.slice(start, i));
|
|
2320
|
+
start = i + 1;
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
members.push(expr.slice(start));
|
|
2324
|
+
return members.map((m) => m.trim()).filter(Boolean);
|
|
2325
|
+
}
|
|
2326
|
+
function typeExprToDirective(expr) {
|
|
2327
|
+
const members = splitTopLevelUnion(expr);
|
|
2328
|
+
if (members.length === 0) return "any()";
|
|
2329
|
+
const mapped = members.map((m) => {
|
|
2330
|
+
if (m.startsWith("T.")) return "any()";
|
|
2331
|
+
if (m === "number" || m === "string" || m === "boolean") return `${m}()`;
|
|
2332
|
+
if (m === "unknown" || m === "any") return "any()";
|
|
2333
|
+
if (m === "null" || m === "undefined" || m === "true" || m === "false") return m;
|
|
2334
|
+
if (/^-?\d+(\.\d+)?$/.test(m)) return m;
|
|
2335
|
+
if (/^["']/.test(m)) return m;
|
|
2336
|
+
if (/[([{]|=>/.test(m) && !m.startsWith("T.")) return m;
|
|
2337
|
+
if (/^(number|string|boolean|any|array|shape|lit|union|fn)\s*\(/.test(m)) return m;
|
|
2338
|
+
return "any()";
|
|
2339
|
+
});
|
|
2340
|
+
return mapped.length === 1 ? mapped[0] : `union(${mapped.join(", ")})`;
|
|
2341
|
+
}
|
|
2342
|
+
function declaredNames(stmt, out) {
|
|
2343
|
+
if (stmt.type === "FunctionDeclaration" && stmt.id) out.add(stmt.id.name);
|
|
2344
|
+
else if (stmt.type === "ClassDeclaration" && stmt.id) out.add(stmt.id.name);
|
|
2345
|
+
else if (stmt.type === "VariableDeclaration") {
|
|
2346
|
+
for (const decl of stmt.declarations) {
|
|
2347
|
+
if (decl.id?.type === "Identifier") out.add(decl.id.name);
|
|
2348
|
+
}
|
|
2349
|
+
} else if (stmt.type === "ExportNamedDeclaration" && stmt.declaration) {
|
|
2350
|
+
declaredNames(stmt.declaration, out);
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
function injectBindings(source, bindings) {
|
|
2354
|
+
const applied = [];
|
|
2355
|
+
const unapplied = [];
|
|
2356
|
+
if (bindings.length === 0) return { source, applied, unapplied };
|
|
2357
|
+
const ast = parse3(source);
|
|
2358
|
+
const declLines = /* @__PURE__ */ new Map();
|
|
2359
|
+
for (const stmt of ast.program.body) {
|
|
2360
|
+
const names = /* @__PURE__ */ new Set();
|
|
2361
|
+
declaredNames(stmt, names);
|
|
2362
|
+
if (names.size === 0 || !stmt.loc) continue;
|
|
2363
|
+
const anchorLine = stmt.leadingComments?.[0]?.loc?.start.line ?? stmt.loc.start.line;
|
|
2364
|
+
for (const name of names) {
|
|
2365
|
+
if (!declLines.has(name)) declLines.set(name, anchorLine);
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
const byLine = /* @__PURE__ */ new Map();
|
|
2369
|
+
for (const binding of bindings) {
|
|
2370
|
+
const line = declLines.get(binding.name);
|
|
2371
|
+
if (line === void 0) {
|
|
2372
|
+
unapplied.push(binding.name);
|
|
2373
|
+
continue;
|
|
2374
|
+
}
|
|
2375
|
+
const group = byLine.get(line) ?? [];
|
|
2376
|
+
group.push(binding);
|
|
2377
|
+
byLine.set(line, group);
|
|
2378
|
+
applied.push(binding.name);
|
|
2379
|
+
}
|
|
2380
|
+
const insertions = [];
|
|
2381
|
+
for (const [line, group] of byLine) {
|
|
2382
|
+
insertions.push({
|
|
2383
|
+
index: line - 1,
|
|
2384
|
+
text: `// @nudo:as ${typeExprToDirective(group[0].type)}`
|
|
2385
|
+
});
|
|
2386
|
+
}
|
|
2387
|
+
insertions.sort((a, b) => b.index - a.index);
|
|
2388
|
+
const lines = source.split("\n");
|
|
2389
|
+
for (const ins of insertions) lines.splice(ins.index, 0, ins.text);
|
|
2390
|
+
return { source: lines.join("\n"), applied, unapplied };
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
export {
|
|
2394
|
+
collectLoadDepContents,
|
|
2395
|
+
ANALYSIS_ABI,
|
|
2396
|
+
sha256Hex,
|
|
2397
|
+
relativizePath,
|
|
2398
|
+
DiskCache,
|
|
2399
|
+
checkCacheKey,
|
|
2400
|
+
ifaceCacheKey,
|
|
2401
|
+
extractNudoImportSpecs,
|
|
2402
|
+
formatInterfaceSurfaceLine,
|
|
2403
|
+
interfaceSurface,
|
|
2404
|
+
emitInterface,
|
|
2405
|
+
formatEmitSummary,
|
|
2406
|
+
collectParamBodyAccesses,
|
|
2407
|
+
draftInterface,
|
|
2408
|
+
formatDraftModule,
|
|
2409
|
+
sidecarDraftPath,
|
|
2410
|
+
isDraftableEntry,
|
|
2411
|
+
writeInterfaceDraft,
|
|
2412
|
+
formatDraftSummary,
|
|
2413
|
+
extractFnConstraintSources,
|
|
2414
|
+
deriveFromRoot,
|
|
2415
|
+
formatDerivedSection,
|
|
2416
|
+
emitDerivedFromRoot,
|
|
2417
|
+
typeExprToDirective,
|
|
2418
|
+
injectBindings
|
|
2419
|
+
};
|