@entrorove/skill 0.1.69 → 0.1.70
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 +5 -0
- package/IDE-EXECUTION.md +5 -3
- package/SKILL.md +2 -2
- package/cloud-locale.d.mts +7 -0
- package/cloud-locale.mjs +59 -0
- package/cloud-task-client.mjs +21 -13
- package/cloud-watch-client.mjs +11 -3
- package/design-lint.mjs +20 -2
- package/ide-presentation.mjs +15 -4
- package/local-archguard-components.mjs +31 -4
- package/local-delegated-modify-analysis.mjs +16 -7
- package/local-scope-files.mjs +14 -2
- package/local-scope-read-set.mjs +66 -0
- package/local-scope-repository.mjs +70 -12
- package/local-scope-signature.mjs +48 -5
- package/local-scope-uni-build.mjs +135 -0
- package/local-scope-uni-conditional.mjs +54 -0
- package/local-scope-uni-directives.mjs +96 -0
- package/local-scope-uni-expressions.mjs +101 -0
- package/local-scope-workspaces.mjs +14 -2
- package/manifest.json +1 -1
- package/package.json +10 -2
- package/release-state.json +27 -19
- package/release.mjs +2 -2
- package/releases.json +6 -0
- package/scope-context-client.mjs +11 -27
- package/scope-context-journal.mjs +44 -0
- package/scope-context-observer.mjs +34 -9
- package/scope-context-release.mjs +1 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { promises as fs } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
const context = new AsyncLocalStorage();
|
|
7
|
+
const MAX_RECORDS = 32_768, MAX_PATH_BYTES = 4_194_304, MAX_DIRECTORY_ENTRIES = 32_768;
|
|
8
|
+
const fail = (code, message) => { throw Object.assign(new Error(message), { code }); };
|
|
9
|
+
const changed = () => fail('ENTROROVE_SCOPE_CHANGED', 'Original scope resolver observations changed before signing');
|
|
10
|
+
const fingerprint = value => createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
11
|
+
function record(root, relative, kind, expected, inspect, directoryEntries = 0) {
|
|
12
|
+
const state = context.getStore();
|
|
13
|
+
if (!state || state.sealed) return;
|
|
14
|
+
if (root !== state.root || typeof relative !== 'string' || typeof inspect !== 'function') {
|
|
15
|
+
fail('ENTROROVE_SCOPE_ATTESTATION_BINDING', 'Scope read observations require their original in-process root');
|
|
16
|
+
}
|
|
17
|
+
const key = `${kind}:${relative}`, checksum = fingerprint(expected), previous = state.records.get(key);
|
|
18
|
+
if (previous) { if (previous.checksum !== checksum) changed(); return; }
|
|
19
|
+
state.pathBytes += Buffer.byteLength(relative);
|
|
20
|
+
state.directoryEntries += directoryEntries;
|
|
21
|
+
if (state.records.size >= MAX_RECORDS || state.pathBytes > MAX_PATH_BYTES || state.directoryEntries > MAX_DIRECTORY_ENTRIES) {
|
|
22
|
+
fail('ENTROROVE_SCOPE_LIMIT', 'Scope resolver observations exceed their bounded in-process read set');
|
|
23
|
+
}
|
|
24
|
+
state.records.set(key, { checksum, inspect });
|
|
25
|
+
}
|
|
26
|
+
export function observeScopeAbsence(root, relative, inspect) {
|
|
27
|
+
record(root, relative, 'absence', false, async () => (await inspect()).exists);
|
|
28
|
+
}
|
|
29
|
+
export function observeScopeRead(root, relative, expected, inspect) {
|
|
30
|
+
record(root, relative, 'read', expected, inspect);
|
|
31
|
+
}
|
|
32
|
+
export function observeScopeTarget(root, relative, target, inspect) {
|
|
33
|
+
const kind = value => ({ exists: value.exists, file: value.stat?.isFile() ?? false, directory: value.stat?.isDirectory() ?? false });
|
|
34
|
+
record(root, relative, 'target', kind(target), async () => kind(await inspect()));
|
|
35
|
+
}
|
|
36
|
+
export function observeScopeDirectory(root, relative, names, inspect) {
|
|
37
|
+
record(root, relative, 'directory', names, inspect, names.length);
|
|
38
|
+
}
|
|
39
|
+
function sameRoot(before, after) {
|
|
40
|
+
return after.isDirectory() && !after.isSymbolicLink() && before.dev === after.dev && before.ino === after.ino;
|
|
41
|
+
}
|
|
42
|
+
export async function captureScopeReadSet(root, action) {
|
|
43
|
+
if (context.getStore() || typeof root !== 'string' || !path.isAbsolute(root) || typeof action !== 'function') {
|
|
44
|
+
fail('ENTROROVE_SCOPE_ATTESTATION_BINDING', 'Scope read capture requires one actual independent observation');
|
|
45
|
+
}
|
|
46
|
+
const original = await fs.lstat(root);
|
|
47
|
+
if (!sameRoot(original, original) || await fs.realpath(root) !== root) {
|
|
48
|
+
fail('ENTROROVE_SCOPE_ATTESTATION_BINDING', 'Scope read capture requires the original real repository root');
|
|
49
|
+
}
|
|
50
|
+
const state = { root, records: new Map(), pathBytes: 0, directoryEntries: 0, sealed: false };
|
|
51
|
+
let value;
|
|
52
|
+
try { value = await context.run(state, action); } finally { state.sealed = true; }
|
|
53
|
+
const verify = () => context.run(undefined, async () => {
|
|
54
|
+
try {
|
|
55
|
+
if (!sameRoot(original, await fs.lstat(root)) || await fs.realpath(root) !== root) changed();
|
|
56
|
+
for (const observation of state.records.values()) {
|
|
57
|
+
if (fingerprint(await observation.inspect()) !== observation.checksum) changed();
|
|
58
|
+
}
|
|
59
|
+
if (!sameRoot(original, await fs.lstat(root)) || await fs.realpath(root) !== root) changed();
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (error.code?.startsWith('ENTROROVE_')) throw error;
|
|
62
|
+
changed();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
return { value, verify };
|
|
66
|
+
}
|
|
@@ -17,8 +17,16 @@ import { createScopeImportResolver, unresolvedImportSite } from './local-scope-i
|
|
|
17
17
|
import { isTypeOnlyReference, isDynamicFunctionReference } from './local-typescript-function-reference.mjs';
|
|
18
18
|
import { createScopeRequireContexts } from './local-scope-create-require.mjs';
|
|
19
19
|
import { resolveCreatedRequire } from './local-scope-require-resolution.mjs';
|
|
20
|
+
import { uniConditionalVariants } from './local-scope-uni-conditional.mjs';
|
|
21
|
+
import { createUniBuildInspection } from './local-scope-uni-build.mjs';
|
|
20
22
|
|
|
21
23
|
const execute = promisify(execFile);
|
|
24
|
+
const inspectionFreshness = new WeakMap();
|
|
25
|
+
export async function verifyScopeInspectionFreshness(facts) {
|
|
26
|
+
const verify = facts && typeof facts === 'object' ? inspectionFreshness.get(facts.importGraph) : null;
|
|
27
|
+
if (!verify) scopeFail('ENTROROVE_SCOPE_ATTESTATION_BINDING', 'Fresh scope verification requires its original in-process graph inspection');
|
|
28
|
+
await verify();
|
|
29
|
+
}
|
|
22
30
|
async function git(root, args, allowed = [], input = { repositoryRoot: root }) {
|
|
23
31
|
await assertBudgetRunnerAllowed(input);
|
|
24
32
|
const executable = await scopeGitExecutable({ repositoryRoot: root });
|
|
@@ -93,6 +101,10 @@ export async function gitFacts(root, files, baselineRef, input = { repositoryRoo
|
|
|
93
101
|
const scriptKinds = new Map([['.ts', ts.ScriptKind.TS], ['.mts', ts.ScriptKind.TS], ['.cts', ts.ScriptKind.TS],
|
|
94
102
|
['.tsx', ts.ScriptKind.TSX], ['.js', ts.ScriptKind.JS], ['.mjs', ts.ScriptKind.JS], ['.cjs', ts.ScriptKind.JS], ['.jsx', ts.ScriptKind.JSX]]);
|
|
95
103
|
const dynamicNames = new Set(['eval', 'Function', 'createRequire']);
|
|
104
|
+
function physicalImportSite(site, context) {
|
|
105
|
+
return { ...site, line: site.line + (context.lineOffset ?? 0),
|
|
106
|
+
column: site.line === 1 ? Math.max(1, site.column + (context.columnOffset ?? 0) - (context.syntheticPrefix ?? 0)) : site.column };
|
|
107
|
+
}
|
|
96
108
|
function staticString(expression) {
|
|
97
109
|
while (expression && ts.isParenthesizedExpression(expression)) expression = expression.expression;
|
|
98
110
|
if (!expression || (!ts.isStringLiteral(expression) && !ts.isNoSubstitutionTemplateLiteral(expression))) return null;
|
|
@@ -108,7 +120,11 @@ function globalObject(expression) {
|
|
|
108
120
|
function scriptImports(source, filename, context = {}) {
|
|
109
121
|
const parsed = ts.createSourceFile(filename, source, ts.ScriptTarget.Latest, true,
|
|
110
122
|
scriptKinds.get(path.extname(filename).toLowerCase()) ?? ts.ScriptKind.TS);
|
|
111
|
-
if (parsed.parseDiagnostics.length)
|
|
123
|
+
if (parsed.parseDiagnostics.length) {
|
|
124
|
+
const diagnostic = parsed.parseDiagnostics[0], location = parsed.getLineAndCharacterOfPosition(diagnostic.start ?? 0);
|
|
125
|
+
const site = physicalImportSite({ line: location.line + 1, column: location.character + 1 }, context);
|
|
126
|
+
scopeFail('ENTROROVE_IMPORT_INVALID', `${context.sourceFilename ?? filename}:${site.line}:${site.column}: source syntax diagnostic ${diagnostic.code}`);
|
|
127
|
+
}
|
|
112
128
|
const found = new Set(), requires = new Map(), unresolvedImports = [], resolve = createScopeImportResolver(parsed); let dynamic = false;
|
|
113
129
|
const contexts = createScopeRequireContexts(parsed, context.repositoryRoot, context.sourceFilename ?? filename);
|
|
114
130
|
const unresolved = (expression, reason, message) => { dynamic = true;
|
|
@@ -160,11 +176,14 @@ function scriptImports(source, filename, context = {}) {
|
|
|
160
176
|
};
|
|
161
177
|
visit(parsed); return { specifiers: [...found].sort(), requires: [...requires.values()], dynamic, unresolvedImports };
|
|
162
178
|
}
|
|
163
|
-
function
|
|
179
|
+
function vueVariantScripts(source, filename, label) {
|
|
164
180
|
const { descriptor, errors } = parseVue(source, { filename, sourceMap: false,
|
|
165
181
|
templateParseOptions: { expressionPlugins: ['typescript'] } });
|
|
166
182
|
vueParseCache.clear();
|
|
167
|
-
if (errors.length)
|
|
183
|
+
if (errors.length) {
|
|
184
|
+
const error = errors[0], location = error.loc?.start;
|
|
185
|
+
scopeFail('ENTROROVE_IMPORT_INVALID', `${filename}:${location?.line ?? 1}:${location?.column ?? 1}: component syntax diagnostic ${error.code ?? 'invalid'} (${label})`);
|
|
186
|
+
}
|
|
168
187
|
if (descriptor.customBlocks.length || descriptor.template?.lang || descriptor.template?.src) {
|
|
169
188
|
scopeFail('ENTROROVE_IMPORT_UNSUPPORTED', 'component dependency inspection requires supported inline template syntax');
|
|
170
189
|
}
|
|
@@ -174,29 +193,40 @@ function vueScripts(source, filename) {
|
|
|
174
193
|
if (!['js', 'javascript', 'ts', 'typescript', 'jsx', 'tsx'].includes(language)) scopeFail('ENTROROVE_IMPORT_UNSUPPORTED', 'component script language is unsupported');
|
|
175
194
|
const extension = { javascript: 'js', typescript: 'ts' }[language] ?? language;
|
|
176
195
|
scripts.push({ source: block.src ? `import ${JSON.stringify(block.src)};` : block.content, filename: `${filename}.${extension}`,
|
|
177
|
-
lineOffset: block.loc.start.line - 1, vueComponent: true, vueSetup: block === descriptor.scriptSetup });
|
|
196
|
+
lineOffset: block.loc.start.line - 1, columnOffset: block.loc.start.column - 1, vueComponent: true, vueSetup: block === descriptor.scriptSetup });
|
|
178
197
|
}
|
|
179
198
|
const seen = new WeakSet();
|
|
180
199
|
const walk = node => {
|
|
181
200
|
if (!node || typeof node !== 'object' || seen.has(node)) return;
|
|
182
201
|
seen.add(node);
|
|
183
202
|
if (node.type === 7 && ['on', 'for'].includes(node.name) && node.exp?.content) {
|
|
184
|
-
scripts.push({ source: node.name === 'for' ? `for (${node.exp.content}) {}` : node.exp.content, filename: `${filename}.ts
|
|
203
|
+
scripts.push({ source: node.name === 'for' ? `for (${node.exp.content}) {}` : node.exp.content, filename: `${filename}.ts`,
|
|
204
|
+
lineOffset: node.exp.loc.start.line - 1, columnOffset: node.exp.loc.start.column - 1, syntheticPrefix: node.name === 'for' ? 5 : 0 });
|
|
185
205
|
seen.add(node.exp);
|
|
186
206
|
}
|
|
187
|
-
if (node.type === 4 && !node.isStatic && node.content) scripts.push({ source: `void (${node.content});`, filename: `${filename}.ts
|
|
207
|
+
if (node.type === 4 && !node.isStatic && node.content) scripts.push({ source: `void (${node.content});`, filename: `${filename}.ts`,
|
|
208
|
+
lineOffset: node.loc.start.line - 1, columnOffset: node.loc.start.column - 1, syntheticPrefix: 6 });
|
|
188
209
|
for (const [key, child] of Object.entries(node)) if (!['loc', 'ast', 'parent', 'codegenNode'].includes(key)) {
|
|
189
210
|
if (Array.isArray(child)) child.forEach(walk); else walk(child);
|
|
190
211
|
}
|
|
191
212
|
};
|
|
192
213
|
walk(descriptor.template?.ast); return scripts;
|
|
193
214
|
}
|
|
194
|
-
function
|
|
215
|
+
function vueScripts(source, filename, conditional) {
|
|
216
|
+
const { variants } = conditional ?? uniConditionalVariants(source, filename), scripts = [], inspected = new Set();
|
|
217
|
+
for (const variant of variants) {
|
|
218
|
+
if (!variant.source.trim() || inspected.has(variant.source)) continue;
|
|
219
|
+
inspected.add(variant.source); scripts.push(...vueVariantScripts(variant.source, filename, variant.label));
|
|
220
|
+
}
|
|
221
|
+
if (!scripts.length && !variants.some(variant => variant.source.trim())) scopeFail('ENTROROVE_IMPORT_INVALID', `${filename}:1:1: component has no inspectable conditional branch`);
|
|
222
|
+
return scripts;
|
|
223
|
+
}
|
|
224
|
+
function importSpecifiers(content, filename, repositoryRoot, conditional) {
|
|
195
225
|
try {
|
|
196
|
-
const source = content.toString('utf8'), parts = path.extname(filename).toLowerCase() === '.vue' ? vueScripts(source, filename) : [{ source, filename }];
|
|
226
|
+
const source = content.toString('utf8'), parts = path.extname(filename).toLowerCase() === '.vue' ? vueScripts(source, filename, conditional) : [{ source, filename }];
|
|
197
227
|
const specifiers = new Set(), requires = new Map(), unresolvedImports = []; let dynamic = false;
|
|
198
228
|
for (const part of parts) { const parsed = scriptImports(part.source, part.filename, { ...part, repositoryRoot, sourceFilename: filename }); dynamic ||= parsed.dynamic;
|
|
199
|
-
unresolvedImports.push(...parsed.unresolvedImports.map(site => (
|
|
229
|
+
unresolvedImports.push(...parsed.unresolvedImports.map(site => physicalImportSite(site, part)));
|
|
200
230
|
for (const require of parsed.requires) requires.set(`${require.anchor}\0${require.specifier}`, require);
|
|
201
231
|
for (const specifier of parsed.specifiers) specifiers.add(specifier); }
|
|
202
232
|
return { specifiers: [...specifiers].sort(), requires: [...requires.values()], dynamic, unresolvedImports };
|
|
@@ -261,6 +291,19 @@ async function configMetadata(root, relative, input) {
|
|
|
261
291
|
const referenceRoot = outside ? await externalConfigReference(root, relative, resolved) : undefined;
|
|
262
292
|
return { path: relative, sha256: read.sha256, contentBase64: read.content.toString('base64'), realPath, referenceRoot };
|
|
263
293
|
}
|
|
294
|
+
async function conditionalBuildDirectory(root, relative, maximumEntries) {
|
|
295
|
+
const target = relative === '.' ? root : (await scopeTarget(root, relative, false, dependencyReference(relative))).target;
|
|
296
|
+
const directory = await fs.opendir(target); const environmentFiles = []; let entries = 0;
|
|
297
|
+
for await (const entry of directory) {
|
|
298
|
+
if (++entries > maximumEntries) scopeFail('ENTROROVE_SCOPE_LIMIT', 'Conditional build directory exceeds its bounded entry inspection');
|
|
299
|
+
if (entry.name === '.env' || entry.name.startsWith('.env.')) environmentFiles.push(entry.name);
|
|
300
|
+
}
|
|
301
|
+
return { environmentFiles, entries };
|
|
302
|
+
}
|
|
303
|
+
function conditionalBuildInspection(root, input) {
|
|
304
|
+
return createUniBuildInspection(relative => configMetadata(root, relative, input),
|
|
305
|
+
(relative, maximumEntries) => conditionalBuildDirectory(root, relative, maximumEntries));
|
|
306
|
+
}
|
|
264
307
|
async function resolveLocalImport(root, relative, required = true) {
|
|
265
308
|
if (relative.startsWith('../') || relative === '..') scopeFail('ENTROROVE_PATH_UNSAFE', 'local import escapes the repository');
|
|
266
309
|
const replacements = /\.js$/.test(relative) ? [relative.replace(/\.js$/, '.ts'), relative.replace(/\.js$/, '.tsx')] : [];
|
|
@@ -280,6 +323,7 @@ export async function importGraph(root, originals, input = { repositoryRoot: roo
|
|
|
280
323
|
const pendingCreates = new Set(originals.filter(file => file.exists === false && input.scopeCreatePaths?.includes(file.path)).map(file => file.path));
|
|
281
324
|
const workspace = await workspaceIndex(root, input);
|
|
282
325
|
const metadata = new Map(workspace.metadata.map(item => [item.path, item]));
|
|
326
|
+
const inspectBuild = conditionalBuildInspection(root, input);
|
|
283
327
|
while (pending.length) {
|
|
284
328
|
const relative = pending.shift(); if (visited.has(relative)) continue;
|
|
285
329
|
if (visited.size >= 1000) scopeFail('ENTROROVE_SCOPE_LIMIT', 'import graph exceeds the 1000-file inspection bound');
|
|
@@ -298,8 +342,10 @@ export async function importGraph(root, originals, input = { repositoryRoot: roo
|
|
|
298
342
|
const configPath = contexts[0]?.configPath;
|
|
299
343
|
const aliases = await scopeTsconfigImports(target => configMetadata(root, target, input), relative, configPath);
|
|
300
344
|
for (const observed of aliases.observations) metadata.set(observed.path, observed);
|
|
345
|
+
const conditional = /\.vue$/i.test(relative) ? uniConditionalVariants(read.content.toString('utf8'), relative) : undefined;
|
|
346
|
+
if (conditional?.directives.length) for (const observed of await inspectBuild(relative, configPath)) metadata.set(observed.path, observed);
|
|
301
347
|
const declarations = declarationModules(read.content, relative);
|
|
302
|
-
const imports = importSpecifiers(read.content, relative, root); dynamic ||= imports.dynamic;
|
|
348
|
+
const imports = importSpecifiers(read.content, relative, root, conditional); dynamic ||= imports.dynamic;
|
|
303
349
|
unresolvedImports.push(...imports.unresolvedImports.map(site => ({ path: relative, ...site })));
|
|
304
350
|
if (imports.dynamic) dynamicSources.push({ path: relative, sha256: read.sha256 });
|
|
305
351
|
for (const specifier of imports.specifiers.sort()) {
|
|
@@ -327,18 +373,27 @@ export async function importGraph(root, originals, input = { repositoryRoot: roo
|
|
|
327
373
|
}
|
|
328
374
|
const nodes = [...visited.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
329
375
|
await stableScope(root, nodes, input);
|
|
376
|
+
await inspectBuild.verify();
|
|
330
377
|
const workspaceMetadata = [...metadata.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
331
378
|
await stableScope(root, workspaceMetadata, input);
|
|
332
|
-
|
|
379
|
+
const graph = { nodes, edges: edges.sort((a, b) => `${a.from}:${a.specifier}`.localeCompare(`${b.from}:${b.specifier}`)),
|
|
333
380
|
dynamic, complete: !dynamic, dynamicSources: dynamicSources.sort((a, b) => a.path.localeCompare(b.path)), workspaceMetadata,
|
|
334
381
|
...(unresolvedImports.length ? { unresolvedImports } : {}) };
|
|
382
|
+
const actualFiles = [...nodes, ...workspaceMetadata].map(file => ({ path: file.path, sha256: file.sha256 }));
|
|
383
|
+
inspectionFreshness.set(graph, async () => { await inspectBuild.verify(); await stableScope(root, actualFiles, input); });
|
|
384
|
+
return graph;
|
|
335
385
|
}
|
|
336
386
|
export async function scopeCandidateImports(root, file, input = { repositoryRoot: root }) {
|
|
337
|
-
const parsed = importSpecifiers(Buffer.from(file.content), file.path, root);
|
|
338
387
|
const workspace = await workspaceIndex(root, input);
|
|
339
388
|
const context = (input.importContexts ?? []).filter(item => file.path === item.sourcePrefix || file.path.startsWith(`${item.sourcePrefix}/`))
|
|
340
389
|
.sort((a, b) => b.sourcePrefix.length - a.sourcePrefix.length)[0];
|
|
341
390
|
const aliases = await scopeTsconfigImports(relative => configMetadata(root, relative, input), file.path, context?.configPath);
|
|
391
|
+
const conditional = /\.vue$/i.test(file.path) ? uniConditionalVariants(file.content, file.path) : undefined;
|
|
392
|
+
let inspectBuild;
|
|
393
|
+
if (conditional?.directives.length) {
|
|
394
|
+
inspectBuild = conditionalBuildInspection(root, input); await inspectBuild(file.path, context?.configPath);
|
|
395
|
+
}
|
|
396
|
+
const parsed = importSpecifiers(Buffer.from(file.content), file.path, root, conditional);
|
|
342
397
|
const declarations = declarationModules(Buffer.from(file.content), file.path), edges = [];
|
|
343
398
|
for (const specifier of parsed.specifiers) {
|
|
344
399
|
const target = await resolveImport(root, file.path, specifier, workspace, input, aliases, context?.configPath, declarations);
|
|
@@ -349,6 +404,7 @@ export async function scopeCandidateImports(root, file, input = { repositoryRoot
|
|
|
349
404
|
await stableScope(root, resolved.metadata, input);
|
|
350
405
|
edges.push({ from: file.path, to: resolved.external ? null : resolved.target, external: resolved.external, specifier: request.specifier });
|
|
351
406
|
}
|
|
407
|
+
await inspectBuild?.verify();
|
|
352
408
|
return { edges: edges.sort((a, b) => `${a.from}:${a.specifier}`.localeCompare(`${b.from}:${b.specifier}`)), dynamic: parsed.dynamic,
|
|
353
409
|
...(parsed.unresolvedImports.length ? { unresolvedImports: parsed.unresolvedImports.map(site => ({ path: file.path, ...site })) } : {}) };
|
|
354
410
|
}
|
|
@@ -366,6 +422,8 @@ export async function collectScopeFacts(root, requested, baselineRef, knownFiles
|
|
|
366
422
|
}
|
|
367
423
|
const graph = await importGraph(root, files, input);
|
|
368
424
|
await contextEvidence(root, graph, input);
|
|
425
|
+
const verifyGraph = inspectionFreshness.get(graph), observedMetadata = graph.workspaceMetadata.map(file => ({ path: file.path, sha256: file.sha256 }));
|
|
426
|
+
inspectionFreshness.set(graph, async () => { await verifyGraph(); await stableScope(root, observedMetadata, input); });
|
|
369
427
|
const observedGit = await observeGitFacts(root, files, baselineRef, input); const history = observedGit.facts;
|
|
370
428
|
const modules = [...new Map(graph.nodes.map(node => [node.boundary.path, node.boundary])).values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
371
429
|
await stableScope(root, files, input);
|
|
@@ -2,9 +2,11 @@ import { createPrivateKey, createPublicKey, randomUUID, sign } from 'node:crypto
|
|
|
2
2
|
import { promises as fs } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { canonical, sha256 } from './local-integrity.mjs';
|
|
5
|
-
import { scopeFail, scopeRead, scopeControlRead, scopeRoot } from './local-scope-files.mjs';
|
|
6
|
-
import { collectScopeFacts } from './local-scope-repository.mjs';
|
|
5
|
+
import { scopeFail, scopeRead, scopeControlRead, scopeRoot, stableScope } from './local-scope-files.mjs';
|
|
6
|
+
import { collectScopeFacts, verifyScopeInspectionFreshness } from './local-scope-repository.mjs';
|
|
7
7
|
import { readScopeSnapshot, verifyFreshScopeSnapshot } from './local-scope-snapshot.mjs';
|
|
8
|
+
import { scopeGitMetadata } from './local-scope-git-reads.mjs';
|
|
9
|
+
import { captureScopeReadSet } from './local-scope-read-set.mjs';
|
|
8
10
|
import { resolveScopePaths } from './local-scope-context.mjs';
|
|
9
11
|
import { rejectScopePrivileges, verifiedScopeCandidate } from './local-scope-candidate.mjs';
|
|
10
12
|
|
|
@@ -33,7 +35,31 @@ function binding(input, result) {
|
|
|
33
35
|
for (const field of ['taskDigest', 'chainDigest']) if (!digest.test(String(input[field] ?? ''))) scopeFail('ENTROROVE_SCOPE_ATTESTATION_BINDING', 'scope signature requires original task and chain digests');
|
|
34
36
|
if (!digest.test(String(result.snapshotDigest ?? ''))) scopeFail('ENTROROVE_SCOPE_ATTESTATION_BINDING', 'scope signature requires a verified preserved snapshot digest');
|
|
35
37
|
}
|
|
36
|
-
|
|
38
|
+
async function signingChallenge(input, beforeSign, maximumExpiresAt) {
|
|
39
|
+
if (beforeSign === undefined) return { input, maximumExpiresAt };
|
|
40
|
+
if (typeof beforeSign !== 'function' || !input.contextRevisionDigest || Object.hasOwn(input, 'handoffCandidate')) {
|
|
41
|
+
scopeFail('ENTROROVE_SCOPE_ATTESTATION_BINDING', 'Challenge renewal is restricted to an original context observation');
|
|
42
|
+
}
|
|
43
|
+
const fresh = await beforeSign();
|
|
44
|
+
if (!fresh || Object.keys(fresh).sort().join(',') !== 'challenge,maximumExpiresAt'
|
|
45
|
+
|| !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/.test(fresh.challenge ?? '')
|
|
46
|
+
|| !Number.isSafeInteger(fresh.maximumExpiresAt) || fresh.maximumExpiresAt <= Date.now()) {
|
|
47
|
+
scopeFail('ENTROROVE_SCOPE_ATTESTATION_BINDING', 'Actual scope signature requires a current exact challenge bound');
|
|
48
|
+
}
|
|
49
|
+
return { input: { ...input, challenge: fresh.challenge }, maximumExpiresAt: fresh.maximumExpiresAt };
|
|
50
|
+
}
|
|
51
|
+
async function verifySigningFreshness(input, root, actual) {
|
|
52
|
+
await verifyScopeInspectionFreshness(actual);
|
|
53
|
+
await verifyFreshScopeSnapshot(input, root, actual);
|
|
54
|
+
await stableScope(root, actual.importGraph.nodes, input);
|
|
55
|
+
await stableScope(root, actual.importGraph.workspaceMetadata, input);
|
|
56
|
+
await stableScope(root, actual.modules.filter(module => module.manifestPath)
|
|
57
|
+
.map(module => ({ path: module.manifestPath, sha256: module.manifestDigest })), input);
|
|
58
|
+
if (actual.git.available && (await scopeGitMetadata(root, ['rev-parse', '--verify', 'HEAD'], input, [128]))?.trim() !== actual.git.head) {
|
|
59
|
+
scopeFail('ENTROROVE_SCOPE_CHANGED', 'Git HEAD changed before the original context signature');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export async function attestScopeFacts(input, result, { maximumExpiresAt, beforeSign } = {}) {
|
|
37
63
|
if (maximumExpiresAt !== undefined && (!Number.isSafeInteger(maximumExpiresAt) || maximumExpiresAt < 1)) {
|
|
38
64
|
scopeFail('ENTROROVE_SCOPE_ATTESTATION_BINDING', 'scope signature deadline must be a finite exact bound');
|
|
39
65
|
}
|
|
@@ -42,8 +68,12 @@ export async function attestScopeFacts(input, result, { maximumExpiresAt } = {})
|
|
|
42
68
|
|| !Array.isArray(input.importContexts))) scopeFail('ENTROROVE_SCOPE_ATTESTATION_BINDING', 'Context proof requires its exact contexts and revision digest together');
|
|
43
69
|
if (input.signerConfigPath === undefined || input.signerConfigPath === null) return result;
|
|
44
70
|
binding(input, result);
|
|
71
|
+
const inspectedInput = canonical(input);
|
|
45
72
|
const root = await scopeRoot(input.repositoryRoot); const snapshot = await readScopeSnapshot(input, root);
|
|
46
|
-
const
|
|
73
|
+
const paths = await resolveScopePaths(input, root);
|
|
74
|
+
const inspect = () => collectScopeFacts(root, paths, input.baselineRef, snapshot.known, input);
|
|
75
|
+
const inspection = beforeSign ? await captureScopeReadSet(root, inspect) : { value: await inspect() };
|
|
76
|
+
const actual = inspection.value;
|
|
47
77
|
actual.snapshotEvidence = await verifyFreshScopeSnapshot(input, root, actual);
|
|
48
78
|
const candidate = await verifiedScopeCandidate(input, root, actual.snapshotEvidence.manifest);
|
|
49
79
|
if (root !== result.repositoryRoot || canonical(actual) !== canonical(result.facts)
|
|
@@ -62,13 +92,26 @@ export async function attestScopeFacts(input, result, { maximumExpiresAt } = {})
|
|
|
62
92
|
if (error.code === 'ENTROROVE_SCOPE_SIGNER_INVALID') throw error;
|
|
63
93
|
scopeFail('ENTROROVE_SCOPE_SIGNER_INVALID', 'scope signer configuration or signing key could not be read');
|
|
64
94
|
}
|
|
95
|
+
// The independent S scan is complete before requesting its short-lived signing challenge.
|
|
96
|
+
// The internal callback can change only the nonce and deadline, never observed facts or identity.
|
|
97
|
+
const originalInput = input;
|
|
98
|
+
({ input, maximumExpiresAt } = await signingChallenge(input, beforeSign, maximumExpiresAt));
|
|
99
|
+
if (canonical(originalInput) !== inspectedInput || root !== result.repositoryRoot
|
|
100
|
+
|| canonical(actual) !== canonical(result.facts) || actual.snapshotEvidence.snapshotDigest !== result.snapshotDigest) {
|
|
101
|
+
scopeFail('ENTROROVE_SCOPE_ATTESTATION_BINDING', 'scope signing preparation changed during challenge renewal');
|
|
102
|
+
}
|
|
103
|
+
if (beforeSign) { await inspection.verify(); await verifySigningFreshness(input, root, actual); }
|
|
104
|
+
if (canonical(originalInput) !== inspectedInput || root !== result.repositoryRoot
|
|
105
|
+
|| canonical(actual) !== canonical(result.facts) || actual.snapshotEvidence.snapshotDigest !== result.snapshotDigest) {
|
|
106
|
+
scopeFail('ENTROROVE_SCOPE_ATTESTATION_BINDING', 'scope signing preparation changed before its actual signature');
|
|
107
|
+
}
|
|
65
108
|
const now = Date.now();
|
|
66
109
|
const expiresAt = Math.min(now + config.receiptTtlMs, candidate ? Date.parse(candidate.expiresAt) : Infinity,
|
|
67
110
|
maximumExpiresAt ?? Infinity);
|
|
68
111
|
if (expiresAt <= now) scopeFail('ENTROROVE_SCOPE_ATTESTATION_BINDING', 'candidate challenge expired before its actual scope signature');
|
|
69
112
|
const core = { schemaVersion: 'entrorove.scope-facts/1.0', flowId: input.flowId, stepId: input.stepId,
|
|
70
113
|
taskDigest: input.taskDigest, ownerId: input.ownerId, chainDigest: input.chainDigest, repositoryId: input.repositoryId,
|
|
71
|
-
facts:
|
|
114
|
+
facts: actual, snapshotDigest: actual.snapshotEvidence.snapshotDigest, challenge: input.challenge,
|
|
72
115
|
issuedAt: new Date(now).toISOString(), expiresAt: new Date(expiresAt).toISOString(),
|
|
73
116
|
nonce: randomUUID(), keyId: config.keyId,
|
|
74
117
|
...(input.contextRevisionDigest === undefined ? {} : { importContexts: input.importContexts, contextRevisionDigest: input.contextRevisionDigest }) };
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
import { uniConditionalError } from './local-scope-uni-expressions.mjs';
|
|
4
|
+
|
|
5
|
+
const controls = /\bUNI_(?:CUSTOM_DEFINE|SCRIPT_(?:DEFINE|ENV)|COMPILER_VERSION|APP_X(?:_DOM2)?|UTS_PLATFORM)\b/;
|
|
6
|
+
const controlKeys = ['UNI_CUSTOM_DEFINE', 'UNI_SCRIPT_DEFINE', 'UNI_SCRIPT_ENV', 'UNI_COMPILER_VERSION', 'UNI_APP_X', 'UNI_APP_X_DOM2', 'UNI_UTS_PLATFORM'];
|
|
7
|
+
const configNames = ['vite.config.js', 'vite.config.mjs', 'vite.config.cjs', 'vite.config.ts',
|
|
8
|
+
'vite.config.mts', 'vite.config.cts', 'vue.config.js', 'vue.config.cjs'];
|
|
9
|
+
function staticConfiguration(source, filename, fail) {
|
|
10
|
+
if (controls.test(source)) throw fail('Custom conditional macro or compiler controls require explicit scanner support');
|
|
11
|
+
const tree = ts.createSourceFile(filename, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS), imports = new Map();
|
|
12
|
+
if (tree.parseDiagnostics.length) throw fail('Conditional build configuration has invalid syntax');
|
|
13
|
+
let exported = false, nodes = 0;
|
|
14
|
+
const value = (node, depth = 0) => {
|
|
15
|
+
if (++nodes > 2048 || depth > 24) throw fail('Conditional build configuration exceeds its static inspection bound');
|
|
16
|
+
if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isSatisfiesExpression(node)) return value(node.expression, depth + 1);
|
|
17
|
+
if (ts.isStringLiteral(node) || ts.isNumericLiteral(node)
|
|
18
|
+
|| [ts.SyntaxKind.TrueKeyword, ts.SyntaxKind.FalseKeyword, ts.SyntaxKind.NullKeyword].includes(node.kind)) return;
|
|
19
|
+
if (ts.isArrayLiteralExpression(node)) { for (const item of node.elements) value(item, depth + 1); return; }
|
|
20
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
21
|
+
for (const property of node.properties) {
|
|
22
|
+
if (!ts.isPropertyAssignment(property) || ts.isComputedPropertyName(property.name)) throw fail('Dynamic build properties cannot establish default conditional macros');
|
|
23
|
+
value(property.initializer, depth + 1);
|
|
24
|
+
}
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
|
|
28
|
+
const operation = imports.get(node.expression.text);
|
|
29
|
+
if (operation === 'uni' && node.arguments.length === 0) return;
|
|
30
|
+
if (operation === 'defineConfig' && node.arguments.length === 1) return value(node.arguments[0], depth + 1);
|
|
31
|
+
}
|
|
32
|
+
throw fail('Dynamic build configuration cannot establish default conditional macros without execution');
|
|
33
|
+
};
|
|
34
|
+
for (const statement of tree.statements) {
|
|
35
|
+
if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
36
|
+
const module = statement.moduleSpecifier.text, clause = statement.importClause;
|
|
37
|
+
if (module === '@dcloudio/vite-plugin-uni' && clause?.name && !clause.namedBindings) imports.set(clause.name.text, 'uni');
|
|
38
|
+
else if (module === 'vite' && !clause?.name && clause?.namedBindings && ts.isNamedImports(clause.namedBindings)
|
|
39
|
+
&& clause.namedBindings.elements.every(element => (element.propertyName ?? element.name).text === 'defineConfig')) {
|
|
40
|
+
for (const element of clause.namedBindings.elements) imports.set(element.name.text, 'defineConfig');
|
|
41
|
+
} else throw fail('Custom build imports cannot establish default conditional macros without execution');
|
|
42
|
+
} else if (ts.isExportAssignment(statement) && !exported && !statement.isExportEquals) { exported = true; value(statement.expression); }
|
|
43
|
+
else if (ts.isExpressionStatement(statement) && !exported && ts.isBinaryExpression(statement.expression)
|
|
44
|
+
&& statement.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken
|
|
45
|
+
&& statement.expression.left.getText(tree) === 'module.exports') { exported = true; value(statement.expression.right); }
|
|
46
|
+
else if (!ts.isEmptyStatement(statement)) throw fail('Custom build statements cannot establish default conditional macros without execution');
|
|
47
|
+
}
|
|
48
|
+
if (!exported) throw fail('Conditional build configuration requires a statically inspectable export');
|
|
49
|
+
}
|
|
50
|
+
function packageConfiguration(source, fail) {
|
|
51
|
+
let manifest;
|
|
52
|
+
try { manifest = JSON.parse(source); } catch { throw fail('Conditional build package manifest is invalid JSON'); }
|
|
53
|
+
const scripts = manifest?.['uni-app']?.scripts;
|
|
54
|
+
if (scripts !== undefined) {
|
|
55
|
+
if (!scripts || typeof scripts !== 'object' || Array.isArray(scripts)) throw fail('Custom uni-app build scripts are not statically inspectable');
|
|
56
|
+
for (const configuration of Object.values(scripts)) {
|
|
57
|
+
if (!configuration || typeof configuration !== 'object' || Array.isArray(configuration)) throw fail('Custom uni-app build entry is not statically inspectable');
|
|
58
|
+
if (configuration.define !== undefined && (!configuration.define || typeof configuration.define !== 'object'
|
|
59
|
+
|| Array.isArray(configuration.define) || Object.keys(configuration.define).length)) {
|
|
60
|
+
throw fail('Custom uni-app defines, including builtin macro overrides, require explicit scanner support');
|
|
61
|
+
}
|
|
62
|
+
if (controls.test(JSON.stringify(configuration))) throw fail('Custom conditional build environment requires explicit scanner support');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
for (const [name, command] of Object.entries(manifest?.scripts ?? {})) {
|
|
66
|
+
if (typeof command !== 'string' || controls.test(command)) throw fail('Custom conditional build command requires explicit scanner support');
|
|
67
|
+
if (/^(?:dev|build)(?::|$)/.test(name) && (!/^(?:cross-env\s+NODE_ENV=(?:development|production|test)\s+)?(?:uni|vite|vue-cli-service\s+uni-(?:build|serve))(?:\s+[A-Za-z0-9_./:@=-]+)*$/.test(command)
|
|
68
|
+
|| /(?:^|\s)(?:--config(?:[=\s]|$)|-c(?:\s|$)|--define(?:[=\s]|$))/.test(command))) {
|
|
69
|
+
throw fail('Custom conditional build command requires explicit scanner support');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function parentDirectories(relative, output, fail) {
|
|
74
|
+
let directory = path.posix.dirname(relative);
|
|
75
|
+
while (true) {
|
|
76
|
+
output.add(directory);
|
|
77
|
+
if (output.size > 128) throw fail('Conditional build roots exceed their 128-directory inspection bound');
|
|
78
|
+
if (directory === '.') return;
|
|
79
|
+
directory = path.posix.dirname(directory);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function assertProcessControls(filename) {
|
|
83
|
+
const present = controlKeys.filter(key => Object.hasOwn(process.env, key) || Object.hasOwn(process, key));
|
|
84
|
+
if (present.length) throw uniConditionalError(filename, 0, `Conditional compilation override controls are unsupported: ${present.join(', ')}`);
|
|
85
|
+
}
|
|
86
|
+
export function createUniBuildInspection(read, inspectDirectory) {
|
|
87
|
+
const inspected = new Map(), directoriesInspected = new Set(); let candidates = 0, bytes = 0, directoryEntries = 0;
|
|
88
|
+
const inspect = async relative => {
|
|
89
|
+
if (inspected.has(relative)) return inspected.get(relative);
|
|
90
|
+
if (++candidates > 2048) throw uniConditionalError(relative, 0, 'Conditional build lookup exceeds its 2048-path bound');
|
|
91
|
+
let item;
|
|
92
|
+
try { item = await read(relative); } catch (error) { if (error.code === 'ENOENT') { inspected.set(relative, null); return null; } throw error; }
|
|
93
|
+
const source = Buffer.from(item.contentBase64, 'base64').toString('utf8'), fail = reason => uniConditionalError(relative, 0, reason);
|
|
94
|
+
bytes += Buffer.byteLength(source);
|
|
95
|
+
if (Buffer.byteLength(source) > 1_048_576 || bytes > 8_388_608) throw fail('Conditional build metadata exceeds its bounded inspection size');
|
|
96
|
+
if (relative.endsWith('package.json')) packageConfiguration(source, fail); else staticConfiguration(source, relative, fail);
|
|
97
|
+
const observed = { path: item.path, sha256: item.sha256 }; inspected.set(relative, observed); return observed;
|
|
98
|
+
};
|
|
99
|
+
const inspectSource = async (sourcePath, configPath) => {
|
|
100
|
+
const directories = new Set(), fail = reason => uniConditionalError(sourcePath, 0, reason);
|
|
101
|
+
assertProcessControls(sourcePath);
|
|
102
|
+
parentDirectories(sourcePath, directories, fail); if (configPath) parentDirectories(configPath, directories, fail);
|
|
103
|
+
const observations = new Map();
|
|
104
|
+
for (const directory of directories) {
|
|
105
|
+
if (!directoriesInspected.has(directory)) {
|
|
106
|
+
if (typeof inspectDirectory !== 'function') throw fail('Conditional build environment absence requires actual bounded directory inspection');
|
|
107
|
+
const listing = await inspectDirectory(directory, Math.min(8192, 32768 - directoryEntries));
|
|
108
|
+
directoryEntries += listing.entries;
|
|
109
|
+
if (listing.environmentFiles.length) throw fail('Conditional sources with ancestor environment files require explicit scanner support; environment values were not read');
|
|
110
|
+
directoriesInspected.add(directory);
|
|
111
|
+
}
|
|
112
|
+
for (const name of ['package.json', ...configNames]) {
|
|
113
|
+
const relative = directory === '.' ? name : `${directory}/${name}`, observed = await inspect(relative);
|
|
114
|
+
if (observed) observations.set(relative, observed);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return [...observations.values()];
|
|
118
|
+
};
|
|
119
|
+
inspectSource.verify = async () => {
|
|
120
|
+
if (!directoriesInspected.size) return;
|
|
121
|
+
assertProcessControls('conditional-build'); let entries = 0;
|
|
122
|
+
for (const directory of directoriesInspected) {
|
|
123
|
+
const listing = await inspectDirectory(directory, Math.min(8192, 32768 - entries)); entries += listing.entries;
|
|
124
|
+
if (listing.environmentFiles.length) throw Object.assign(uniConditionalError(directory, 0,
|
|
125
|
+
'Conditional build environment appeared after its actual inspection; environment values were not read'), { code: 'ENTROROVE_SCOPE_CHANGED' });
|
|
126
|
+
}
|
|
127
|
+
for (const [relative, expected] of inspected) {
|
|
128
|
+
let actual;
|
|
129
|
+
try { actual = await read(relative); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
130
|
+
if ((actual?.sha256 ?? null) !== (expected?.sha256 ?? null)) throw Object.assign(
|
|
131
|
+
uniConditionalError(relative, 0, 'Conditional build metadata changed during inspection'), { code: 'ENTROROVE_SCOPE_CHANGED' });
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
return inspectSource;
|
|
135
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { uniDirectives } from './local-scope-uni-directives.mjs';
|
|
2
|
+
import { uniCondition, uniConditionContexts, uniConditionalError } from './local-scope-uni-expressions.mjs';
|
|
3
|
+
|
|
4
|
+
const blank = source => source.replace(/[^\r\n]/g, ' ');
|
|
5
|
+
function conditionalSegments(source, directives, filename) {
|
|
6
|
+
const stack = [], segments = [], conditions = []; let cursor = 0, conditionTokens = 0;
|
|
7
|
+
for (const item of directives) {
|
|
8
|
+
segments.push({ start: cursor, end: item.start, guards: stack.map(guard => ({ ...guard })) });
|
|
9
|
+
segments.push({ start: item.start, end: item.end, directive: true }); cursor = item.end;
|
|
10
|
+
const fail = reason => uniConditionalError(filename, item.start, reason, source);
|
|
11
|
+
if (['ifdef', 'ifndef', 'if'].includes(item.kind)) {
|
|
12
|
+
if (stack.length >= 32) throw fail('Conditional compilation exceeds its 32-level nesting bound');
|
|
13
|
+
const condition = uniCondition(item.expression, fail), index = conditions.length;
|
|
14
|
+
if ((conditionTokens += condition.tokens) > 4096) throw fail('Conditional expressions exceed their 4096-token aggregate inspection bound');
|
|
15
|
+
conditions.push(condition); stack.push({ index, negated: item.kind === 'ifndef', syntax: item.syntax, alternative: false });
|
|
16
|
+
} else {
|
|
17
|
+
const active = stack.at(-1);
|
|
18
|
+
if (!active || active.syntax !== item.syntax) throw fail('Conditional directive has no matching opening in the same source language');
|
|
19
|
+
if (item.kind === 'else') {
|
|
20
|
+
if (active.alternative) throw fail('Conditional block has more than one alternative');
|
|
21
|
+
active.negated = !active.negated; active.alternative = true;
|
|
22
|
+
} else stack.pop();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (stack.length) throw uniConditionalError(filename, directives.at(-1).start, 'Conditional block is missing its matching endif', source);
|
|
26
|
+
segments.push({ start: cursor, end: source.length, guards: [] });
|
|
27
|
+
return { conditions, segments };
|
|
28
|
+
}
|
|
29
|
+
const activeSegment = (segment, key) => !segment.directive && segment.guards.every(guard => (key[guard.index] === '1') !== guard.negated);
|
|
30
|
+
export function uniConditionalVariants(source, filename) {
|
|
31
|
+
const fail = reason => uniConditionalError(filename, 0, reason, source);
|
|
32
|
+
if (typeof source !== 'string') throw fail('Conditional inspection requires actual source text');
|
|
33
|
+
const unchanged = () => ({ directives: [], variants: [{ source, key: '', label: 'unconditional' }] });
|
|
34
|
+
if (!/(?:<!--|\/\/|\/\*)[ \t]*#/i.test(source)) return unchanged();
|
|
35
|
+
if (Buffer.byteLength(source) > 2_097_152) throw fail('Conditional source exceeds its 2 MiB inspection bound');
|
|
36
|
+
try {
|
|
37
|
+
const directives = uniDirectives(source, filename);
|
|
38
|
+
if (!directives.length) return unchanged();
|
|
39
|
+
const { conditions, segments } = conditionalSegments(source, directives, filename), contexts = uniConditionContexts(conditions, fail);
|
|
40
|
+
if (Buffer.byteLength(source) * contexts.length > 33_554_432) throw fail('Conditional source variants exceed their 32 MiB inspection bound');
|
|
41
|
+
for (const segment of segments) if (!segment.directive && source.slice(segment.start, segment.end).trim()
|
|
42
|
+
&& !contexts.some(context => activeSegment(segment, context.key))) {
|
|
43
|
+
throw uniConditionalError(filename, segment.start, 'Conditional branch has no proven platform context; custom or contradictory build definitions require explicit support', source);
|
|
44
|
+
}
|
|
45
|
+
const variants = contexts.map(context => ({ key: context.key, label: context.label,
|
|
46
|
+
source: segments.map(segment => {
|
|
47
|
+
const text = source.slice(segment.start, segment.end); return activeSegment(segment, context.key) ? text : blank(text);
|
|
48
|
+
}).join('') }));
|
|
49
|
+
return { directives: directives.map(({ kind, expression, syntax }) => ({ kind, expression, syntax })), variants };
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error instanceof RangeError) throw Object.assign(fail('Conditional syntax exceeds its parser or traversal bound'), { code: 'ENTROROVE_SCOPE_LIMIT' });
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { uniConditionalError } from './local-scope-uni-expressions.mjs';
|
|
3
|
+
|
|
4
|
+
function directive(source, start, end, text, syntax, filename, output) {
|
|
5
|
+
const match = /^\s*#([a-z]+)\b([\s\S]*)$/i.exec(text);
|
|
6
|
+
if (!match) return;
|
|
7
|
+
const kind = match[1].toLowerCase(), expression = match[2].trim();
|
|
8
|
+
if (!['ifdef', 'ifndef', 'if', 'else', 'endif', 'elif', 'elseif', 'exec', 'include', 'define', 'exclude', 'endexclude'].includes(kind)) return;
|
|
9
|
+
if (!['ifdef', 'ifndef', 'if', 'else', 'endif'].includes(kind) || /[\r\n]/.test(expression)
|
|
10
|
+
|| ['else', 'endif'].includes(kind) && expression) {
|
|
11
|
+
throw uniConditionalError(filename, start, 'Conditional directive syntax is unsupported', source);
|
|
12
|
+
}
|
|
13
|
+
if (output.length >= 256) throw uniConditionalError(filename, start, 'Conditional directives exceed their 256-entry bound', source);
|
|
14
|
+
output.push({ start, end, kind, expression, syntax });
|
|
15
|
+
}
|
|
16
|
+
function literalRanges(content, kind) {
|
|
17
|
+
const parsed = ts.createSourceFile('uni-script.ts', content, ts.ScriptTarget.Latest, true, kind);
|
|
18
|
+
const pending = [parsed], ranges = []; let nodes = 0;
|
|
19
|
+
while (pending.length) {
|
|
20
|
+
const node = pending.pop();
|
|
21
|
+
if (++nodes > 200_000) throw new RangeError('Conditional lexical inspection exceeds its node bound');
|
|
22
|
+
if (ts.isStringLiteralLike(node) || ts.isRegularExpressionLiteral(node)
|
|
23
|
+
|| [ts.SyntaxKind.TemplateHead, ts.SyntaxKind.TemplateMiddle, ts.SyntaxKind.TemplateTail].includes(node.kind)) {
|
|
24
|
+
ranges.push({ start: node.getStart(parsed), end: node.end });
|
|
25
|
+
} else ts.forEachChild(node, child => { pending.push(child); });
|
|
26
|
+
}
|
|
27
|
+
return ranges.sort((a, b) => a.start - b.start);
|
|
28
|
+
}
|
|
29
|
+
function scriptComments(source, start, end, filename, output, kind) {
|
|
30
|
+
const content = source.slice(start, end), literals = literalRanges(content, kind); let literal = 0;
|
|
31
|
+
for (let index = 0; index < content.length;) {
|
|
32
|
+
while (literals[literal]?.end <= index) literal++;
|
|
33
|
+
if (literals[literal]?.start <= index) { index = literals[literal].end; continue; }
|
|
34
|
+
if (content.startsWith('//', index)) {
|
|
35
|
+
let next = index + 2; while (next < content.length && !['\r', '\n'].includes(content[next])) next++;
|
|
36
|
+
directive(source, start + index, start + next, content.slice(index + 2, next), 'js', filename, output); index = next;
|
|
37
|
+
} else if (content.startsWith('/*', index)) {
|
|
38
|
+
const finish = content.indexOf('*/', index + 2), next = finish < 0 ? content.length : finish + 2;
|
|
39
|
+
directive(source, start + index, start + next, content.slice(index + 2, finish < 0 ? next : finish), 'js', filename, output); index = next;
|
|
40
|
+
} else index++;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function quoteEnd(source, index, quote, end = source.length) {
|
|
44
|
+
for (let cursor = index + 1; cursor < end; cursor++) {
|
|
45
|
+
if (source[cursor] === '\\') cursor++;
|
|
46
|
+
else if (source[cursor] === quote) return cursor + 1;
|
|
47
|
+
}
|
|
48
|
+
return end;
|
|
49
|
+
}
|
|
50
|
+
function styleComments(source, start, end, filename, output) {
|
|
51
|
+
for (let cursor = start; cursor < end;) {
|
|
52
|
+
if (['"', "'"].includes(source[cursor])) cursor = quoteEnd(source, cursor, source[cursor], end);
|
|
53
|
+
else if (source.startsWith('/*', cursor)) {
|
|
54
|
+
const finish = source.indexOf('*/', cursor + 2), next = finish < 0 || finish >= end ? end : finish + 2;
|
|
55
|
+
directive(source, cursor, next, source.slice(cursor + 2, next - 2), 'css', filename, output); cursor = next;
|
|
56
|
+
} else cursor++;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function tagEnd(source, start) {
|
|
60
|
+
for (let cursor = start; cursor < source.length; cursor++) {
|
|
61
|
+
if (['"', "'"].includes(source[cursor])) cursor = quoteEnd(source, cursor, source[cursor]) - 1;
|
|
62
|
+
else if (source[cursor] === '>') return cursor + 1;
|
|
63
|
+
}
|
|
64
|
+
return source.length;
|
|
65
|
+
}
|
|
66
|
+
function interpolationEnd(source, start) {
|
|
67
|
+
for (let cursor = start + 2; cursor < source.length; cursor++) {
|
|
68
|
+
if (['"', "'", '`'].includes(source[cursor])) cursor = quoteEnd(source, cursor, source[cursor]) - 1;
|
|
69
|
+
else if (source.startsWith('}}', cursor)) return cursor + 2;
|
|
70
|
+
}
|
|
71
|
+
return source.length;
|
|
72
|
+
}
|
|
73
|
+
export function uniDirectives(source, filename) {
|
|
74
|
+
const output = []; let inTag = false;
|
|
75
|
+
for (let cursor = 0; cursor < source.length;) {
|
|
76
|
+
if (source.startsWith('<!--', cursor)) {
|
|
77
|
+
const finish = source.indexOf('-->', cursor + 4), next = finish < 0 ? source.length : finish + 3;
|
|
78
|
+
directive(source, cursor, next, source.slice(cursor + 4, finish < 0 ? next : finish), 'html', filename, output); cursor = next;
|
|
79
|
+
} else if (inTag && ['"', "'"].includes(source[cursor])) cursor = quoteEnd(source, cursor, source[cursor]);
|
|
80
|
+
else if (source.startsWith('{{', cursor)) cursor = interpolationEnd(source, cursor);
|
|
81
|
+
else {
|
|
82
|
+
const block = /^<(script|style)\b/i.exec(source.slice(cursor, cursor + 16));
|
|
83
|
+
if (block) {
|
|
84
|
+
const start = tagEnd(source, cursor), closing = new RegExp(`</${block[1]}\\s*>`, 'ig'); closing.lastIndex = start;
|
|
85
|
+
const match = closing.exec(source), end = match?.index ?? source.length;
|
|
86
|
+
if (block[1].toLowerCase() === 'script') {
|
|
87
|
+
const header = source.slice(cursor, start), language = /\blang\s*=\s*['"]([^'"]+)['"]/i.exec(header)?.[1];
|
|
88
|
+
scriptComments(source, start, end, filename, output, ['ts', 'typescript'].includes(language) ? ts.ScriptKind.TS : ts.ScriptKind.TSX);
|
|
89
|
+
}
|
|
90
|
+
else styleComments(source, start, end, filename, output);
|
|
91
|
+
cursor = match ? closing.lastIndex : source.length; inTag = false;
|
|
92
|
+
} else { if (source[cursor] === '<') inTag = true; else if (source[cursor] === '>') inTag = false; cursor++; }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return output;
|
|
96
|
+
}
|