fastscript 2.0.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +669 -600
- package/node_modules/@fastscript/core-private/src/fs-error-codes.mjs +2 -2
- package/node_modules/@fastscript/core-private/src/fs-normalize.mjs +125 -91
- package/node_modules/@fastscript/core-private/src/fs-parser.mjs +197 -57
- package/node_modules/@fastscript/core-private/src/typecheck.mjs +1466 -1462
- package/package.json +41 -5
- package/src/benchmark-discipline.mjs +39 -0
- package/src/cli.mjs +37 -2
- package/src/compatibility-governance.mjs +257 -0
- package/src/conversion-manifest.mjs +101 -0
- package/src/diagnostics.mjs +100 -0
- package/src/fs-error-codes.mjs +2 -2
- package/src/fs-normalize.mjs +39 -7
- package/src/generated/compatibility-registry-report.mjs +815 -0
- package/src/generated/docs-search-index.mjs +1137 -275
- package/src/migrate-rollback.mjs +144 -0
- package/src/migrate.mjs +1275 -47
- package/src/migration-wizard.mjs +37 -11
- package/src/module-loader.mjs +13 -1
- package/src/permissions-cli.mjs +112 -0
- package/src/profile.mjs +95 -0
- package/src/regression-guard.mjs +245 -0
- package/src/runtime-permissions.mjs +299 -0
- package/src/trace.mjs +95 -0
- package/src/validate.mjs +10 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_POLICY_PATH = resolve("fastscript.permissions.json");
|
|
6
|
+
|
|
7
|
+
const DEFAULT_POLICY = Object.freeze({
|
|
8
|
+
version: 1,
|
|
9
|
+
preset: "compat",
|
|
10
|
+
audit: {
|
|
11
|
+
enabled: true,
|
|
12
|
+
path: ".fastscript/permissions-audit.jsonl",
|
|
13
|
+
},
|
|
14
|
+
fileAccess: {
|
|
15
|
+
mode: "allow",
|
|
16
|
+
allow: ["**"],
|
|
17
|
+
deny: [],
|
|
18
|
+
},
|
|
19
|
+
envAccess: {
|
|
20
|
+
mode: "allow",
|
|
21
|
+
allow: ["*"],
|
|
22
|
+
deny: [],
|
|
23
|
+
},
|
|
24
|
+
networkAccess: {
|
|
25
|
+
mode: "allow",
|
|
26
|
+
allowHosts: ["*"],
|
|
27
|
+
denyHosts: [],
|
|
28
|
+
},
|
|
29
|
+
subprocessExecution: {
|
|
30
|
+
mode: "allow",
|
|
31
|
+
allowPrefixes: [],
|
|
32
|
+
denyPrefixes: [],
|
|
33
|
+
},
|
|
34
|
+
dynamicImports: {
|
|
35
|
+
mode: "allow",
|
|
36
|
+
allowKinds: ["relative", "absolute", "package", "fileUrl", "dataUrl", "httpUrl", "httpsUrl"],
|
|
37
|
+
denyKinds: [],
|
|
38
|
+
},
|
|
39
|
+
pluginAccess: {
|
|
40
|
+
mode: "allow",
|
|
41
|
+
allow: ["*"],
|
|
42
|
+
deny: [],
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function cloneDefault() {
|
|
47
|
+
return JSON.parse(JSON.stringify(DEFAULT_POLICY));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeSlashes(value) {
|
|
51
|
+
return String(value || "").replace(/\\/g, "/");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readJson(path, fallback) {
|
|
55
|
+
if (!existsSync(path)) return fallback;
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
58
|
+
} catch {
|
|
59
|
+
return fallback;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function wildcardToRegex(pattern) {
|
|
64
|
+
const escaped = String(pattern || "")
|
|
65
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
66
|
+
.replace(/\*\*/g, "::DOUBLE_STAR::")
|
|
67
|
+
.replace(/\*/g, "[^/]*")
|
|
68
|
+
.replace(/::DOUBLE_STAR::/g, ".*");
|
|
69
|
+
return new RegExp(`^${escaped}$`, "i");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function matchesAny(patterns, value) {
|
|
73
|
+
if (!Array.isArray(patterns) || patterns.length === 0) return false;
|
|
74
|
+
const input = normalizeSlashes(value);
|
|
75
|
+
for (const pattern of patterns) {
|
|
76
|
+
if (String(pattern || "") === "*") return true;
|
|
77
|
+
const regex = wildcardToRegex(normalizeSlashes(pattern));
|
|
78
|
+
if (regex.test(input)) return true;
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parseCommand(resource) {
|
|
84
|
+
if (Array.isArray(resource)) return resource.map((item) => String(item || "")).filter(Boolean);
|
|
85
|
+
const text = String(resource || "").trim();
|
|
86
|
+
if (!text) return [];
|
|
87
|
+
return text.match(/"[^"]*"|'[^']*'|\S+/g)?.map((token) => token.replace(/^['"]|['"]$/g, "")) || [];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function startsWithPrefix(tokens, prefixes) {
|
|
91
|
+
if (!tokens.length || !Array.isArray(prefixes) || !prefixes.length) return false;
|
|
92
|
+
for (const prefix of prefixes) {
|
|
93
|
+
const parts = parseCommand(prefix);
|
|
94
|
+
if (!parts.length || parts.length > tokens.length) continue;
|
|
95
|
+
let ok = true;
|
|
96
|
+
for (let i = 0; i < parts.length; i += 1) {
|
|
97
|
+
if (parts[i] !== tokens[i]) {
|
|
98
|
+
ok = false;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (ok) return true;
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function classifyImport(resource) {
|
|
108
|
+
const specifier = String(resource || "");
|
|
109
|
+
if (specifier.startsWith("./") || specifier.startsWith("../")) return "relative";
|
|
110
|
+
if (specifier.startsWith("/") || /^[A-Za-z]:[\\/]/.test(specifier)) return "absolute";
|
|
111
|
+
if (specifier.startsWith("file:")) return "fileUrl";
|
|
112
|
+
if (specifier.startsWith("data:")) return "dataUrl";
|
|
113
|
+
if (specifier.startsWith("http:")) return "httpUrl";
|
|
114
|
+
if (specifier.startsWith("https:")) return "httpsUrl";
|
|
115
|
+
return "package";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function normalizeMode(mode, fallback = "allow") {
|
|
119
|
+
const value = String(mode || fallback).toLowerCase();
|
|
120
|
+
return value === "deny" ? "deny" : "allow";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function mergePolicy(base, override) {
|
|
124
|
+
const policy = {
|
|
125
|
+
...base,
|
|
126
|
+
...(override || {}),
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
for (const key of ["audit", "fileAccess", "envAccess", "networkAccess", "subprocessExecution", "dynamicImports", "pluginAccess"]) {
|
|
130
|
+
policy[key] = {
|
|
131
|
+
...(base[key] || {}),
|
|
132
|
+
...((override || {})[key] || {}),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
policy.fileAccess.mode = normalizeMode(policy.fileAccess.mode);
|
|
137
|
+
policy.envAccess.mode = normalizeMode(policy.envAccess.mode);
|
|
138
|
+
policy.networkAccess.mode = normalizeMode(policy.networkAccess.mode);
|
|
139
|
+
policy.subprocessExecution.mode = normalizeMode(policy.subprocessExecution.mode);
|
|
140
|
+
policy.dynamicImports.mode = normalizeMode(policy.dynamicImports.mode);
|
|
141
|
+
policy.pluginAccess.mode = normalizeMode(policy.pluginAccess.mode);
|
|
142
|
+
|
|
143
|
+
return policy;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function applyPreset(policy) {
|
|
147
|
+
const preset = String(process.env.FASTSCRIPT_PERMISSION_PRESET || policy.preset || "compat").toLowerCase();
|
|
148
|
+
if (preset !== "secure") return policy;
|
|
149
|
+
|
|
150
|
+
return mergePolicy(policy, {
|
|
151
|
+
preset: "secure",
|
|
152
|
+
networkAccess: { mode: "deny", allowHosts: [] },
|
|
153
|
+
subprocessExecution: { mode: "deny", allowPrefixes: [] },
|
|
154
|
+
pluginAccess: { mode: "deny", allow: [] },
|
|
155
|
+
dynamicImports: { mode: "allow", allowKinds: ["relative", "absolute", "fileUrl", "dataUrl"], denyKinds: ["httpUrl", "httpsUrl"] },
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function loadPermissionPolicy({ root = process.cwd(), policyPath = process.env.FASTSCRIPT_PERMISSION_POLICY_PATH || DEFAULT_POLICY_PATH } = {}) {
|
|
160
|
+
const base = cloneDefault();
|
|
161
|
+
const abs = resolve(root, policyPath);
|
|
162
|
+
const filePolicy = readJson(abs, {});
|
|
163
|
+
const merged = applyPreset(mergePolicy(base, filePolicy));
|
|
164
|
+
return { path: abs, policy: merged };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function evaluateRuntimePermission(policy, { kind, resource, details = {} } = {}) {
|
|
168
|
+
const p = policy || cloneDefault();
|
|
169
|
+
const event = { kind: String(kind || ""), resource: String(resource || ""), details };
|
|
170
|
+
|
|
171
|
+
if (event.kind === "fileAccess") {
|
|
172
|
+
const cfg = p.fileAccess || {};
|
|
173
|
+
if (cfg.mode === "deny") return { allowed: false, reason: "file access denied by policy", boundary: "fileAccess" };
|
|
174
|
+
if (matchesAny(cfg.deny, event.resource)) return { allowed: false, reason: "file path denied", boundary: "fileAccess" };
|
|
175
|
+
if (!matchesAny(cfg.allow, event.resource)) return { allowed: false, reason: "file path not allowlisted", boundary: "fileAccess" };
|
|
176
|
+
return { allowed: true, reason: "file access allowed", boundary: "fileAccess" };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (event.kind === "envAccess") {
|
|
180
|
+
const cfg = p.envAccess || {};
|
|
181
|
+
if (cfg.mode === "deny") return { allowed: false, reason: "env access denied by policy", boundary: "envAccess" };
|
|
182
|
+
if (matchesAny(cfg.deny, event.resource)) return { allowed: false, reason: "env key denied", boundary: "envAccess" };
|
|
183
|
+
if (!matchesAny(cfg.allow, event.resource)) return { allowed: false, reason: "env key not allowlisted", boundary: "envAccess" };
|
|
184
|
+
return { allowed: true, reason: "env access allowed", boundary: "envAccess" };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (event.kind === "networkAccess") {
|
|
188
|
+
const cfg = p.networkAccess || {};
|
|
189
|
+
if (cfg.mode === "deny") return { allowed: false, reason: "network access denied by policy", boundary: "networkAccess" };
|
|
190
|
+
let host = event.resource;
|
|
191
|
+
try {
|
|
192
|
+
host = new URL(event.resource).host || host;
|
|
193
|
+
} catch {}
|
|
194
|
+
if (matchesAny(cfg.denyHosts, host)) return { allowed: false, reason: "host denied", boundary: "networkAccess" };
|
|
195
|
+
if (!matchesAny(cfg.allowHosts, host)) return { allowed: false, reason: "host not allowlisted", boundary: "networkAccess" };
|
|
196
|
+
return { allowed: true, reason: "network access allowed", boundary: "networkAccess" };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (event.kind === "subprocessExecution") {
|
|
200
|
+
const cfg = p.subprocessExecution || {};
|
|
201
|
+
if (cfg.mode === "deny") return { allowed: false, reason: "subprocess denied by policy", boundary: "subprocessExecution" };
|
|
202
|
+
const tokens = parseCommand(event.resource);
|
|
203
|
+
if (startsWithPrefix(tokens, cfg.denyPrefixes)) return { allowed: false, reason: "subprocess prefix denied", boundary: "subprocessExecution" };
|
|
204
|
+
if (Array.isArray(cfg.allowPrefixes) && cfg.allowPrefixes.length > 0 && !startsWithPrefix(tokens, cfg.allowPrefixes)) {
|
|
205
|
+
return { allowed: false, reason: "subprocess prefix not allowlisted", boundary: "subprocessExecution" };
|
|
206
|
+
}
|
|
207
|
+
return { allowed: true, reason: "subprocess allowed", boundary: "subprocessExecution" };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (event.kind === "dynamicImportAccess") {
|
|
211
|
+
const cfg = p.dynamicImports || {};
|
|
212
|
+
const importKind = classifyImport(event.resource);
|
|
213
|
+
if (cfg.mode === "deny") return { allowed: false, reason: "dynamic import denied by policy", boundary: "dynamicImports" };
|
|
214
|
+
if (Array.isArray(cfg.denyKinds) && cfg.denyKinds.includes(importKind)) {
|
|
215
|
+
return { allowed: false, reason: `dynamic import kind denied (${importKind})`, boundary: "dynamicImports" };
|
|
216
|
+
}
|
|
217
|
+
if (Array.isArray(cfg.allowKinds) && cfg.allowKinds.length > 0 && !cfg.allowKinds.includes(importKind)) {
|
|
218
|
+
return { allowed: false, reason: `dynamic import kind not allowlisted (${importKind})`, boundary: "dynamicImports" };
|
|
219
|
+
}
|
|
220
|
+
return { allowed: true, reason: "dynamic import allowed", boundary: "dynamicImports", importKind };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (event.kind === "pluginAccess") {
|
|
224
|
+
const cfg = p.pluginAccess || {};
|
|
225
|
+
if (cfg.mode === "deny") return { allowed: false, reason: "plugin access denied by policy", boundary: "pluginAccess" };
|
|
226
|
+
if (matchesAny(cfg.deny, event.resource)) return { allowed: false, reason: "plugin denied", boundary: "pluginAccess" };
|
|
227
|
+
if (!matchesAny(cfg.allow, event.resource)) return { allowed: false, reason: "plugin not allowlisted", boundary: "pluginAccess" };
|
|
228
|
+
return { allowed: true, reason: "plugin access allowed", boundary: "pluginAccess" };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return { allowed: true, reason: "permission kind not configured", boundary: "unknown" };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function appendAudit(policy, row) {
|
|
235
|
+
if (!policy?.audit?.enabled) return;
|
|
236
|
+
const out = resolve(policy.audit.path || ".fastscript/permissions-audit.jsonl");
|
|
237
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
238
|
+
appendFileSync(out, `${JSON.stringify(row)}\n`, "utf8");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function createPermissionRuntime({ root = process.cwd(), policyPath } = {}) {
|
|
242
|
+
const loaded = loadPermissionPolicy({ root, policyPath });
|
|
243
|
+
const policy = loaded.policy;
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
policy,
|
|
247
|
+
policyPath: loaded.path,
|
|
248
|
+
check(input) {
|
|
249
|
+
const decision = evaluateRuntimePermission(policy, input);
|
|
250
|
+
appendAudit(policy, {
|
|
251
|
+
ts: new Date().toISOString(),
|
|
252
|
+
allowed: decision.allowed,
|
|
253
|
+
...input,
|
|
254
|
+
decision,
|
|
255
|
+
});
|
|
256
|
+
return decision;
|
|
257
|
+
},
|
|
258
|
+
assert(input) {
|
|
259
|
+
const decision = this.check(input);
|
|
260
|
+
if (!decision.allowed) {
|
|
261
|
+
const error = new Error(`permission denied (${decision.boundary}): ${decision.reason}`);
|
|
262
|
+
error.code = "FS7101";
|
|
263
|
+
error.boundary = decision.boundary;
|
|
264
|
+
error.permission = input;
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
return decision;
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function permissionAwareEnvGet(key, { runtime = createPermissionRuntime(), fallback = undefined } = {}) {
|
|
273
|
+
runtime.assert({ kind: "envAccess", resource: String(key || "") });
|
|
274
|
+
return process.env[key] ?? fallback;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function permissionAwareReadFile(path, { runtime = createPermissionRuntime(), encoding = "utf8" } = {}) {
|
|
278
|
+
const resolved = resolve(path);
|
|
279
|
+
runtime.assert({ kind: "fileAccess", resource: resolved });
|
|
280
|
+
return readFileSync(resolved, encoding);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function permissionAwareSpawn(command, args = [], options = {}, { runtime = createPermissionRuntime() } = {}) {
|
|
284
|
+
const parts = [String(command || ""), ...(Array.isArray(args) ? args.map((item) => String(item || "")) : [])].filter(Boolean);
|
|
285
|
+
runtime.assert({ kind: "subprocessExecution", resource: parts.join(" ") });
|
|
286
|
+
return spawn(command, args, options);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export async function permissionAwareFetch(input, init = {}, { runtime = createPermissionRuntime() } = {}) {
|
|
290
|
+
const url = typeof input === "string" ? input : input?.url || String(input || "");
|
|
291
|
+
runtime.assert({ kind: "networkAccess", resource: url });
|
|
292
|
+
return fetch(input, init);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function permissionAwarePluginAccess(pluginId, { runtime = createPermissionRuntime(), onAllowed } = {}) {
|
|
296
|
+
runtime.assert({ kind: "pluginAccess", resource: String(pluginId || "") });
|
|
297
|
+
if (typeof onAllowed === "function") return onAllowed();
|
|
298
|
+
return true;
|
|
299
|
+
}
|
package/src/trace.mjs
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, relative, resolve } from "node:path";
|
|
3
|
+
import { runCheck } from "./check.mjs";
|
|
4
|
+
import { runBuild } from "./build.mjs";
|
|
5
|
+
import { runTypeCheck } from "./typecheck.mjs";
|
|
6
|
+
import { runBench } from "./bench.mjs";
|
|
7
|
+
import { createTracer } from "./observability.mjs";
|
|
8
|
+
|
|
9
|
+
function parseArgs(args = []) {
|
|
10
|
+
const options = {
|
|
11
|
+
pipeline: ["check", "typecheck", "build", "bench"],
|
|
12
|
+
out: resolve(".fastscript", "trace-latest.json"),
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
16
|
+
const arg = args[i];
|
|
17
|
+
if (arg === "--pipeline") {
|
|
18
|
+
const next = String(args[i + 1] || "").trim();
|
|
19
|
+
if (next) options.pipeline = next.split(",").map((item) => item.trim().toLowerCase()).filter(Boolean);
|
|
20
|
+
i += 1;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (arg === "--out") {
|
|
24
|
+
options.out = resolve(args[i + 1] || options.out);
|
|
25
|
+
i += 1;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return options;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function runStage(stage) {
|
|
34
|
+
switch (stage) {
|
|
35
|
+
case "check":
|
|
36
|
+
await runCheck();
|
|
37
|
+
return;
|
|
38
|
+
case "typecheck":
|
|
39
|
+
await runTypeCheck(["--mode", "pass"]);
|
|
40
|
+
return;
|
|
41
|
+
case "build":
|
|
42
|
+
await runBuild({ mode: "build" });
|
|
43
|
+
return;
|
|
44
|
+
case "bench":
|
|
45
|
+
await runBench();
|
|
46
|
+
return;
|
|
47
|
+
default:
|
|
48
|
+
throw new Error(`trace: unsupported stage '${stage}'`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function runTrace(args = []) {
|
|
53
|
+
const options = parseArgs(args);
|
|
54
|
+
const tracer = createTracer({ service: "fastscript-cli" });
|
|
55
|
+
const spans = [];
|
|
56
|
+
|
|
57
|
+
for (const stage of options.pipeline) {
|
|
58
|
+
const startedAt = Date.now();
|
|
59
|
+
const span = tracer.span(stage, { stage });
|
|
60
|
+
try {
|
|
61
|
+
await runStage(stage);
|
|
62
|
+
const durationMs = Date.now() - startedAt;
|
|
63
|
+
span.end({ status: "pass", durationMs });
|
|
64
|
+
spans.push({ stage, status: "pass", durationMs, startedAt: new Date(startedAt).toISOString() });
|
|
65
|
+
} catch (error) {
|
|
66
|
+
const durationMs = Date.now() - startedAt;
|
|
67
|
+
span.end({ status: "fail", durationMs, error: String(error?.message || error) });
|
|
68
|
+
spans.push({
|
|
69
|
+
stage,
|
|
70
|
+
status: "fail",
|
|
71
|
+
durationMs,
|
|
72
|
+
startedAt: new Date(startedAt).toISOString(),
|
|
73
|
+
error: String(error?.message || error),
|
|
74
|
+
});
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const report = {
|
|
80
|
+
generatedAt: new Date().toISOString(),
|
|
81
|
+
pipeline: options.pipeline,
|
|
82
|
+
spans,
|
|
83
|
+
status: spans.some((item) => item.status === "fail") ? "fail" : "pass",
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
mkdirSync(dirname(options.out), { recursive: true });
|
|
87
|
+
writeFileSync(options.out, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
|
88
|
+
|
|
89
|
+
if (report.status === "fail") {
|
|
90
|
+
throw new Error(`trace failed: ${spans.find((item) => item.status === "fail")?.stage || "unknown"}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log(`trace complete: ${options.pipeline.join(", ")}`);
|
|
94
|
+
console.log(`trace report: ${String(relative(resolve("."), options.out)).replace(/\\/g, "/")}`);
|
|
95
|
+
}
|
package/src/validate.mjs
CHANGED
|
@@ -6,13 +6,23 @@ import { runExport } from "./export.mjs";
|
|
|
6
6
|
import { runDbMigrate, runDbSeed } from "./db-cli.mjs";
|
|
7
7
|
import { runTypeCheck } from "./typecheck.mjs";
|
|
8
8
|
import { runLint } from "./fs-linter.mjs";
|
|
9
|
+
import { runMigrate } from "./migrate.mjs";
|
|
10
|
+
import { runPermissions } from "./permissions-cli.mjs";
|
|
11
|
+
import { runBenchmarkDiscipline } from "./benchmark-discipline.mjs";
|
|
12
|
+
import { runRegressionGuard } from "./regression-guard.mjs";
|
|
13
|
+
import { runCompatibilityGovernanceCheck } from "./compatibility-governance.mjs";
|
|
9
14
|
|
|
10
15
|
export async function runValidate() {
|
|
16
|
+
await runMigrate(["app", "--dry-run", "--fidelity-level", "full", "--fail-on-unproven-fidelity"]);
|
|
17
|
+
await runPermissions(["--mode", "report"]);
|
|
11
18
|
await runCheck();
|
|
12
19
|
await runLint(["--mode", "fail"]);
|
|
13
20
|
await runTypeCheck(["--mode", "fail"]);
|
|
14
21
|
await runBuild();
|
|
15
22
|
await runBench();
|
|
23
|
+
await runBenchmarkDiscipline();
|
|
24
|
+
await runCompatibilityGovernanceCheck();
|
|
25
|
+
await runRegressionGuard(["--mode", "all", "--auto-baseline"]);
|
|
16
26
|
await runCompat();
|
|
17
27
|
await runDbMigrate();
|
|
18
28
|
await runDbSeed();
|