@entrorove/skill 0.1.69 → 0.1.71
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 +10 -0
- package/IDE-EXECUTION.md +5 -3
- package/SKILL.md +3 -3
- 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-budget-reads.mjs +7 -2
- package/local-budget.mjs +3 -2
- package/local-delegated-modify-analysis.mjs +16 -7
- package/local-scope-build-data.mjs +73 -0
- package/local-scope-build-facts.mjs +33 -0
- package/local-scope-build-io.mjs +57 -0
- package/local-scope-build-read.mjs +22 -0
- package/local-scope-build-readset.mjs +49 -0
- package/local-scope-files.mjs +33 -8
- package/local-scope-read-set.mjs +66 -0
- package/local-scope-repository.mjs +96 -28
- package/local-scope-signature.mjs +50 -5
- package/local-scope-state.mjs +7 -5
- package/local-scope-uni-build.mjs +85 -0
- package/local-scope-uni-conditional.mjs +56 -0
- package/local-scope-uni-directives.mjs +96 -0
- package/local-scope-uni-expressions.mjs +106 -0
- package/local-scope-workspaces.mjs +17 -5
- package/manifest.json +1 -1
- package/package.json +16 -2
- package/release-source-groups.mjs +39 -0
- package/release-state.json +36 -22
- package/release.mjs +6 -41
- package/releases.json +12 -0
- package/scope-context-client.mjs +11 -27
- package/scope-context-journal.mjs +44 -0
- package/scope-context-observer.mjs +37 -11
- package/scope-context-release.mjs +1 -1
|
@@ -5,7 +5,7 @@ import { promisify } from 'node:util';
|
|
|
5
5
|
import { builtinModules } from 'node:module';
|
|
6
6
|
import ts from 'typescript';
|
|
7
7
|
import { parse as parseVue, parseCache as vueParseCache } from '@vue/compiler-sfc';
|
|
8
|
-
import { lineCount, observedScopeRisk, packageBoundary, scopeFail, scopeFiles, scopeProbeTarget, scopeRead, scopeTarget, stableScope } from './local-scope-files.mjs';
|
|
8
|
+
import { lineCount, observedScopeRisk, packageBoundary, scopeFail, scopeFiles, scopeProbeTarget, scopeRead, scopeBuildSourceRead, scopeTarget, stableScope } from './local-scope-files.mjs';
|
|
9
9
|
import { sha256 } from './local-integrity.mjs';
|
|
10
10
|
import { workspaceImport, workspaceIndex } from './local-scope-workspaces.mjs';
|
|
11
11
|
import { assertBudgetRunnerAllowed } from './local-budget-reads.mjs';
|
|
@@ -17,8 +17,18 @@ 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';
|
|
22
|
+
import { createScopeBuildIO } from './local-scope-build-io.mjs';
|
|
23
|
+
import { observeBuildFacts } from './local-scope-build-facts.mjs';
|
|
20
24
|
|
|
21
25
|
const execute = promisify(execFile);
|
|
26
|
+
const inspectionFreshness = new WeakMap();
|
|
27
|
+
export async function verifyScopeInspectionFreshness(facts) {
|
|
28
|
+
const verify = facts && typeof facts === 'object' ? inspectionFreshness.get(facts.importGraph) : null;
|
|
29
|
+
if (!verify) scopeFail('ENTROROVE_SCOPE_ATTESTATION_BINDING', 'Fresh scope verification requires its original in-process graph inspection');
|
|
30
|
+
await verify();
|
|
31
|
+
}
|
|
22
32
|
async function git(root, args, allowed = [], input = { repositoryRoot: root }) {
|
|
23
33
|
await assertBudgetRunnerAllowed(input);
|
|
24
34
|
const executable = await scopeGitExecutable({ repositoryRoot: root });
|
|
@@ -93,6 +103,10 @@ export async function gitFacts(root, files, baselineRef, input = { repositoryRoo
|
|
|
93
103
|
const scriptKinds = new Map([['.ts', ts.ScriptKind.TS], ['.mts', ts.ScriptKind.TS], ['.cts', ts.ScriptKind.TS],
|
|
94
104
|
['.tsx', ts.ScriptKind.TSX], ['.js', ts.ScriptKind.JS], ['.mjs', ts.ScriptKind.JS], ['.cjs', ts.ScriptKind.JS], ['.jsx', ts.ScriptKind.JSX]]);
|
|
95
105
|
const dynamicNames = new Set(['eval', 'Function', 'createRequire']);
|
|
106
|
+
function physicalImportSite(site, context) {
|
|
107
|
+
return { ...site, line: site.line + (context.lineOffset ?? 0),
|
|
108
|
+
column: site.line === 1 ? Math.max(1, site.column + (context.columnOffset ?? 0) - (context.syntheticPrefix ?? 0)) : site.column };
|
|
109
|
+
}
|
|
96
110
|
function staticString(expression) {
|
|
97
111
|
while (expression && ts.isParenthesizedExpression(expression)) expression = expression.expression;
|
|
98
112
|
if (!expression || (!ts.isStringLiteral(expression) && !ts.isNoSubstitutionTemplateLiteral(expression))) return null;
|
|
@@ -108,7 +122,11 @@ function globalObject(expression) {
|
|
|
108
122
|
function scriptImports(source, filename, context = {}) {
|
|
109
123
|
const parsed = ts.createSourceFile(filename, source, ts.ScriptTarget.Latest, true,
|
|
110
124
|
scriptKinds.get(path.extname(filename).toLowerCase()) ?? ts.ScriptKind.TS);
|
|
111
|
-
if (parsed.parseDiagnostics.length)
|
|
125
|
+
if (parsed.parseDiagnostics.length) {
|
|
126
|
+
const diagnostic = parsed.parseDiagnostics[0], location = parsed.getLineAndCharacterOfPosition(diagnostic.start ?? 0);
|
|
127
|
+
const site = physicalImportSite({ line: location.line + 1, column: location.character + 1 }, context);
|
|
128
|
+
scopeFail('ENTROROVE_IMPORT_INVALID', `${context.sourceFilename ?? filename}:${site.line}:${site.column}: source syntax diagnostic ${diagnostic.code}`);
|
|
129
|
+
}
|
|
112
130
|
const found = new Set(), requires = new Map(), unresolvedImports = [], resolve = createScopeImportResolver(parsed); let dynamic = false;
|
|
113
131
|
const contexts = createScopeRequireContexts(parsed, context.repositoryRoot, context.sourceFilename ?? filename);
|
|
114
132
|
const unresolved = (expression, reason, message) => { dynamic = true;
|
|
@@ -160,11 +178,14 @@ function scriptImports(source, filename, context = {}) {
|
|
|
160
178
|
};
|
|
161
179
|
visit(parsed); return { specifiers: [...found].sort(), requires: [...requires.values()], dynamic, unresolvedImports };
|
|
162
180
|
}
|
|
163
|
-
function
|
|
181
|
+
function vueVariantScripts(source, filename, label) {
|
|
164
182
|
const { descriptor, errors } = parseVue(source, { filename, sourceMap: false,
|
|
165
183
|
templateParseOptions: { expressionPlugins: ['typescript'] } });
|
|
166
184
|
vueParseCache.clear();
|
|
167
|
-
if (errors.length)
|
|
185
|
+
if (errors.length) {
|
|
186
|
+
const error = errors[0], location = error.loc?.start;
|
|
187
|
+
scopeFail('ENTROROVE_IMPORT_INVALID', `${filename}:${location?.line ?? 1}:${location?.column ?? 1}: component syntax diagnostic ${error.code ?? 'invalid'} (${label})`);
|
|
188
|
+
}
|
|
168
189
|
if (descriptor.customBlocks.length || descriptor.template?.lang || descriptor.template?.src) {
|
|
169
190
|
scopeFail('ENTROROVE_IMPORT_UNSUPPORTED', 'component dependency inspection requires supported inline template syntax');
|
|
170
191
|
}
|
|
@@ -174,29 +195,40 @@ function vueScripts(source, filename) {
|
|
|
174
195
|
if (!['js', 'javascript', 'ts', 'typescript', 'jsx', 'tsx'].includes(language)) scopeFail('ENTROROVE_IMPORT_UNSUPPORTED', 'component script language is unsupported');
|
|
175
196
|
const extension = { javascript: 'js', typescript: 'ts' }[language] ?? language;
|
|
176
197
|
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 });
|
|
198
|
+
lineOffset: block.loc.start.line - 1, columnOffset: block.loc.start.column - 1, vueComponent: true, vueSetup: block === descriptor.scriptSetup });
|
|
178
199
|
}
|
|
179
200
|
const seen = new WeakSet();
|
|
180
201
|
const walk = node => {
|
|
181
202
|
if (!node || typeof node !== 'object' || seen.has(node)) return;
|
|
182
203
|
seen.add(node);
|
|
183
204
|
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
|
|
205
|
+
scripts.push({ source: node.name === 'for' ? `for (${node.exp.content}) {}` : node.exp.content, filename: `${filename}.ts`,
|
|
206
|
+
lineOffset: node.exp.loc.start.line - 1, columnOffset: node.exp.loc.start.column - 1, syntheticPrefix: node.name === 'for' ? 5 : 0 });
|
|
185
207
|
seen.add(node.exp);
|
|
186
208
|
}
|
|
187
|
-
if (node.type === 4 && !node.isStatic && node.content) scripts.push({ source: `void (${node.content});`, filename: `${filename}.ts
|
|
209
|
+
if (node.type === 4 && !node.isStatic && node.content) scripts.push({ source: `void (${node.content});`, filename: `${filename}.ts`,
|
|
210
|
+
lineOffset: node.loc.start.line - 1, columnOffset: node.loc.start.column - 1, syntheticPrefix: 6 });
|
|
188
211
|
for (const [key, child] of Object.entries(node)) if (!['loc', 'ast', 'parent', 'codegenNode'].includes(key)) {
|
|
189
212
|
if (Array.isArray(child)) child.forEach(walk); else walk(child);
|
|
190
213
|
}
|
|
191
214
|
};
|
|
192
215
|
walk(descriptor.template?.ast); return scripts;
|
|
193
216
|
}
|
|
194
|
-
function
|
|
217
|
+
function vueScripts(source, filename, conditional) {
|
|
218
|
+
const { variants } = conditional ?? uniConditionalVariants(source, filename), scripts = [], inspected = new Set();
|
|
219
|
+
for (const variant of variants) {
|
|
220
|
+
if (!variant.source.trim() || inspected.has(variant.source)) continue;
|
|
221
|
+
inspected.add(variant.source); scripts.push(...vueVariantScripts(variant.source, filename, variant.label));
|
|
222
|
+
}
|
|
223
|
+
if (!scripts.length && !variants.some(variant => variant.source.trim())) scopeFail('ENTROROVE_IMPORT_INVALID', `${filename}:1:1: component has no inspectable conditional branch`);
|
|
224
|
+
return scripts;
|
|
225
|
+
}
|
|
226
|
+
function importSpecifiers(content, filename, repositoryRoot, conditional) {
|
|
195
227
|
try {
|
|
196
|
-
const source = content.toString('utf8'), parts = path.extname(filename).toLowerCase() === '.vue' ? vueScripts(source, filename) : [{ source, filename }];
|
|
228
|
+
const source = content.toString('utf8'), parts = path.extname(filename).toLowerCase() === '.vue' ? vueScripts(source, filename, conditional) : [{ source, filename }];
|
|
197
229
|
const specifiers = new Set(), requires = new Map(), unresolvedImports = []; let dynamic = false;
|
|
198
230
|
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 => (
|
|
231
|
+
unresolvedImports.push(...parsed.unresolvedImports.map(site => physicalImportSite(site, part)));
|
|
200
232
|
for (const require of parsed.requires) requires.set(`${require.anchor}\0${require.specifier}`, require);
|
|
201
233
|
for (const specifier of parsed.specifiers) specifiers.add(specifier); }
|
|
202
234
|
return { specifiers: [...specifiers].sort(), requires: [...requires.values()], dynamic, unresolvedImports };
|
|
@@ -214,6 +246,9 @@ function declarationModules(content, from) {
|
|
|
214
246
|
&& node.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.DeclareKeyword)).map(node => node.name.text));
|
|
215
247
|
}
|
|
216
248
|
async function resolveImport(root, from, specifier, workspace, input, aliases, configPath, declarations) {
|
|
249
|
+
if (specifier.startsWith('/') || /^[A-Za-z]:/.test(specifier) || specifier.includes('\\') || /[\u0000-\u001f\u007f]/.test(specifier)) {
|
|
250
|
+
scopeFail('ENTROROVE_PATH_UNSAFE', 'Source module addresses require safe repository-relative or declared package selectors');
|
|
251
|
+
}
|
|
217
252
|
if (/^(?:https?:|file:|data:)/i.test(specifier)) scopeFail('ENTROROVE_IMPORT_UNSUPPORTED', 'remote source imports cannot be inspected as local facts');
|
|
218
253
|
if (declarations.has(specifier)) return null;
|
|
219
254
|
const label = /^[A-Za-z0-9@._/~+-]{1,256}$/.test(specifier) ? specifier : '[unsupported specifier]';
|
|
@@ -228,9 +263,9 @@ async function resolveImport(root, from, specifier, workspace, input, aliases, c
|
|
|
228
263
|
const localPackage = !specifier.startsWith('.') ? workspaceImport(workspace, specifier) : null;
|
|
229
264
|
if (!specifier.startsWith('.') && !localPackage) {
|
|
230
265
|
if (builtinModules.includes(specifier) || (specifier.startsWith('node:') && builtinModules.includes(specifier.slice(5)))) return null;
|
|
231
|
-
const boundary = await packageBoundary(root, configPath ?? from, input);
|
|
266
|
+
const boundary = await packageBoundary(root, configPath ?? from, input, scopeBuildSourceRead);
|
|
232
267
|
if (boundary.manifestPath) {
|
|
233
|
-
const manifest = JSON.parse((await
|
|
268
|
+
const manifest = JSON.parse((await scopeBuildSourceRead(root, boundary.manifestPath, input)).content.toString('utf8'));
|
|
234
269
|
const name = specifier.startsWith('@') ? specifier.split('/').slice(0, 2).join('/') : specifier.split('/')[0];
|
|
235
270
|
if (['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'].some(field => Object.hasOwn(manifest[field] ?? {}, name))) return null;
|
|
236
271
|
}
|
|
@@ -254,12 +289,29 @@ async function externalConfigReference(root, relative, resolved) {
|
|
|
254
289
|
async function configMetadata(root, relative, input) {
|
|
255
290
|
const target = await scopeProbeTarget(root, relative, dependencyReference(relative));
|
|
256
291
|
if (!target.exists || !target.stat.isFile()) throw Object.assign(new Error(`Missing configuration file: ${relative}`), { code: 'ENOENT' });
|
|
257
|
-
const read = await
|
|
292
|
+
const read = await scopeBuildSourceRead(root, relative, input), resolved = await fs.realpath(target.target);
|
|
258
293
|
const physical = path.relative(root, resolved).split(path.sep).join('/');
|
|
259
294
|
const outside = physical === '..' || physical.startsWith('../') || path.isAbsolute(physical);
|
|
260
295
|
const realPath = outside ? relative : physical;
|
|
261
296
|
const referenceRoot = outside ? await externalConfigReference(root, relative, resolved) : undefined;
|
|
262
|
-
return { path: relative, sha256: read.sha256, contentBase64: read.content.toString('base64'), realPath, referenceRoot };
|
|
297
|
+
return { path: relative, sha256: read.sha256, mode: read.mode, contentBase64: read.content.toString('base64'), realPath, referenceRoot };
|
|
298
|
+
}
|
|
299
|
+
function conditionalBuildInspection(root, input) {
|
|
300
|
+
const io = createScopeBuildIO(root, relative => configMetadata(root, relative, input));
|
|
301
|
+
return createUniBuildInspection(io.read, io.inspectDirectory, { probe: io.probe, repositoryRoot: root });
|
|
302
|
+
}
|
|
303
|
+
function sourceProjection(content, relative, root) {
|
|
304
|
+
// Only these pure parsers run here. Filesystem, privacy and budget checks occur
|
|
305
|
+
// outside this boundary and cannot become a semantic uncertainty.
|
|
306
|
+
try {
|
|
307
|
+
const conditional = /\.vue$/i.test(relative) ? uniConditionalVariants(content.toString('utf8'), relative) : undefined;
|
|
308
|
+
return { conditional, imports: importSpecifiers(content, relative, root, conditional) };
|
|
309
|
+
} catch (error) {
|
|
310
|
+
if (error.code !== 'ENTROROVE_IMPORT_UNSUPPORTED') throw error;
|
|
311
|
+
return { conditional: undefined, imports: { specifiers: [], requires: [], dynamic: true,
|
|
312
|
+
unresolvedImports: [{ line: error.line ?? 1, column: error.column ?? 1, reason: 'source-projection-unsupported',
|
|
313
|
+
message: 'Source syntax has no supported static projection; the original hashed source remains read-only' }] } };
|
|
314
|
+
}
|
|
263
315
|
}
|
|
264
316
|
async function resolveLocalImport(root, relative, required = true) {
|
|
265
317
|
if (relative.startsWith('../') || relative === '..') scopeFail('ENTROROVE_PATH_UNSAFE', 'local import escapes the repository');
|
|
@@ -275,11 +327,14 @@ async function resolveLocalImport(root, relative, required = true) {
|
|
|
275
327
|
}
|
|
276
328
|
export async function importGraph(root, originals, input = { repositoryRoot: root }, limits = {}) {
|
|
277
329
|
const pending = originals.filter(file => file.exists !== false).map(file => file.path); const visited = new Map(); const edges = []; let dynamic = false;
|
|
330
|
+
const explicitSources = new Set(pending);
|
|
331
|
+
const graphRead = (actualRoot, relative, binding) => (explicitSources.has(relative) ? scopeRead : scopeBuildSourceRead)(actualRoot, relative, binding);
|
|
278
332
|
let sourceBytes = 0;
|
|
279
333
|
const dynamicSources = [], unresolvedImports = [];
|
|
280
334
|
const pendingCreates = new Set(originals.filter(file => file.exists === false && input.scopeCreatePaths?.includes(file.path)).map(file => file.path));
|
|
281
335
|
const workspace = await workspaceIndex(root, input);
|
|
282
336
|
const metadata = new Map(workspace.metadata.map(item => [item.path, item]));
|
|
337
|
+
const inspectBuild = conditionalBuildInspection(root, input);
|
|
283
338
|
while (pending.length) {
|
|
284
339
|
const relative = pending.shift(); if (visited.has(relative)) continue;
|
|
285
340
|
if (visited.size >= 1000) scopeFail('ENTROROVE_SCOPE_LIMIT', 'import graph exceeds the 1000-file inspection bound');
|
|
@@ -290,7 +345,7 @@ export async function importGraph(root, originals, input = { repositoryRoot: roo
|
|
|
290
345
|
}
|
|
291
346
|
sourceBytes += target.stat.size;
|
|
292
347
|
}
|
|
293
|
-
const read = await
|
|
348
|
+
const read = await graphRead(root, relative, input); const boundary = await packageBoundary(root, relative, input, scopeBuildSourceRead);
|
|
294
349
|
visited.set(relative, { path: relative, sha256: read.sha256, module: boundary.path, boundary });
|
|
295
350
|
if (!sourceExtension.test(relative)) continue;
|
|
296
351
|
const contexts = (input.importContexts ?? []).filter(context => relative === context.sourcePrefix || relative.startsWith(`${context.sourcePrefix}/`))
|
|
@@ -298,18 +353,23 @@ export async function importGraph(root, originals, input = { repositoryRoot: roo
|
|
|
298
353
|
const configPath = contexts[0]?.configPath;
|
|
299
354
|
const aliases = await scopeTsconfigImports(target => configMetadata(root, target, input), relative, configPath);
|
|
300
355
|
for (const observed of aliases.observations) metadata.set(observed.path, observed);
|
|
356
|
+
const projection = sourceProjection(read.content, relative, root), conditional = projection.conditional;
|
|
357
|
+
const build = await inspectBuild(relative, configPath, { hasConditionals: Boolean(conditional?.directives.length) });
|
|
358
|
+
const opaque = await observeBuildFacts(root, build, input, { visited, metadata, dynamicSources, unresolvedImports });
|
|
359
|
+
dynamic ||= opaque;
|
|
301
360
|
const declarations = declarationModules(read.content, relative);
|
|
302
|
-
const imports =
|
|
361
|
+
const imports = projection.imports; dynamic ||= imports.dynamic;
|
|
303
362
|
unresolvedImports.push(...imports.unresolvedImports.map(site => ({ path: relative, ...site })));
|
|
304
|
-
if (imports.dynamic) dynamicSources.push({ path: relative, sha256: read.sha256 });
|
|
363
|
+
if (imports.dynamic && !dynamicSources.some(item => item.path === relative)) dynamicSources.push({ path: relative, sha256: read.sha256 });
|
|
305
364
|
for (const specifier of imports.specifiers.sort()) {
|
|
306
365
|
let target;
|
|
307
366
|
try { target = await resolveImport(root, relative, specifier, workspace, input, aliases, configPath, declarations); }
|
|
308
367
|
catch (error) {
|
|
309
368
|
const missing = error.code === 'ENTROROVE_IMPORT_UNRESOLVED' && specifier.startsWith('.')
|
|
310
369
|
? error.missingCandidates?.filter(candidate => pendingCreates.has(candidate)) : null;
|
|
311
|
-
if (
|
|
312
|
-
unresolvedImports.push({ path: relative, specifier, reason: 'approved-create-not-present'
|
|
370
|
+
if (error.code !== 'ENTROROVE_IMPORT_UNRESOLVED') throw error;
|
|
371
|
+
unresolvedImports.push({ path: relative, specifier, reason: missing?.length ? 'approved-create-not-present' : 'source-import-unresolved',
|
|
372
|
+
candidatePaths: missing?.length ? missing : error.missingCandidates ?? [] });
|
|
313
373
|
dynamic = true;
|
|
314
374
|
if (!dynamicSources.some(source => source.path === relative)) dynamicSources.push({ path: relative, sha256: read.sha256 });
|
|
315
375
|
continue;
|
|
@@ -326,19 +386,25 @@ export async function importGraph(root, originals, input = { repositoryRoot: roo
|
|
|
326
386
|
}
|
|
327
387
|
}
|
|
328
388
|
const nodes = [...visited.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
329
|
-
await stableScope(root, nodes, input);
|
|
389
|
+
await stableScope(root, nodes, input, graphRead);
|
|
390
|
+
await inspectBuild.verify();
|
|
330
391
|
const workspaceMetadata = [...metadata.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
331
|
-
await stableScope(root, workspaceMetadata, input);
|
|
332
|
-
|
|
392
|
+
await stableScope(root, workspaceMetadata, input, scopeBuildSourceRead);
|
|
393
|
+
const graph = { nodes, edges: edges.sort((a, b) => `${a.from}:${a.specifier}`.localeCompare(`${b.from}:${b.specifier}`)),
|
|
333
394
|
dynamic, complete: !dynamic, dynamicSources: dynamicSources.sort((a, b) => a.path.localeCompare(b.path)), workspaceMetadata,
|
|
334
395
|
...(unresolvedImports.length ? { unresolvedImports } : {}) };
|
|
396
|
+
const actualFiles = [...nodes, ...workspaceMetadata].map(file => ({ path: file.path, sha256: file.sha256 }));
|
|
397
|
+
inspectionFreshness.set(graph, async () => { await inspectBuild.verify(); await stableScope(root, actualFiles, input,
|
|
398
|
+
(actualRoot, relative, binding) => metadata.has(relative) ? scopeBuildSourceRead(actualRoot, relative, binding) : graphRead(actualRoot, relative, binding)); });
|
|
399
|
+
return graph;
|
|
335
400
|
}
|
|
336
401
|
export async function scopeCandidateImports(root, file, input = { repositoryRoot: root }) {
|
|
337
|
-
const parsed = importSpecifiers(Buffer.from(file.content), file.path, root);
|
|
338
402
|
const workspace = await workspaceIndex(root, input);
|
|
339
403
|
const context = (input.importContexts ?? []).filter(item => file.path === item.sourcePrefix || file.path.startsWith(`${item.sourcePrefix}/`))
|
|
340
404
|
.sort((a, b) => b.sourcePrefix.length - a.sourcePrefix.length)[0];
|
|
341
405
|
const aliases = await scopeTsconfigImports(relative => configMetadata(root, relative, input), file.path, context?.configPath);
|
|
406
|
+
const conditional = /\.vue$/i.test(file.path) ? uniConditionalVariants(file.content, file.path) : undefined;
|
|
407
|
+
const parsed = importSpecifiers(Buffer.from(file.content), file.path, root, conditional);
|
|
342
408
|
const declarations = declarationModules(Buffer.from(file.content), file.path), edges = [];
|
|
343
409
|
for (const specifier of parsed.specifiers) {
|
|
344
410
|
const target = await resolveImport(root, file.path, specifier, workspace, input, aliases, context?.configPath, declarations);
|
|
@@ -346,7 +412,7 @@ export async function scopeCandidateImports(root, file, input = { repositoryRoot
|
|
|
346
412
|
}
|
|
347
413
|
for (const request of parsed.requires) {
|
|
348
414
|
const resolved = await resolveCreatedRequire(file.path, request.anchor, request.specifier, target => configMetadata(root, target, input));
|
|
349
|
-
await stableScope(root, resolved.metadata, input);
|
|
415
|
+
await stableScope(root, resolved.metadata, input, scopeBuildSourceRead);
|
|
350
416
|
edges.push({ from: file.path, to: resolved.external ? null : resolved.target, external: resolved.external, specifier: request.specifier });
|
|
351
417
|
}
|
|
352
418
|
return { edges: edges.sort((a, b) => `${a.from}:${a.specifier}`.localeCompare(`${b.from}:${b.specifier}`)), dynamic: parsed.dynamic,
|
|
@@ -361,15 +427,17 @@ export async function collectScopeFacts(root, requested, baselineRef, knownFiles
|
|
|
361
427
|
files.push({ path: relative, exists: false, sha256: sha256('entrorove.missing-file/1.0'), bytes: 0, lines: 0, module: previous.module });
|
|
362
428
|
continue;
|
|
363
429
|
}
|
|
364
|
-
const read = await scopeRead(root, relative, input); const boundary = await packageBoundary(root, relative, input);
|
|
430
|
+
const read = await scopeRead(root, relative, input); const boundary = await packageBoundary(root, relative, input, scopeBuildSourceRead);
|
|
365
431
|
files.push({ path: relative, exists: true, sha256: read.sha256, bytes: read.bytes, lines: lineCount(read.content), module: boundary.path });
|
|
366
432
|
}
|
|
367
433
|
const graph = await importGraph(root, files, input);
|
|
368
434
|
await contextEvidence(root, graph, input);
|
|
435
|
+
const verifyGraph = inspectionFreshness.get(graph), observedMetadata = graph.workspaceMetadata.map(file => ({ path: file.path, sha256: file.sha256 }));
|
|
436
|
+
inspectionFreshness.set(graph, async () => { await verifyGraph(); await stableScope(root, observedMetadata, input, scopeBuildSourceRead); });
|
|
369
437
|
const observedGit = await observeGitFacts(root, files, baselineRef, input); const history = observedGit.facts;
|
|
370
438
|
const modules = [...new Map(graph.nodes.map(node => [node.boundary.path, node.boundary])).values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
371
439
|
await stableScope(root, files, input);
|
|
372
|
-
await stableScope(root, modules.filter(module => module.manifestPath).map(module => ({ path: module.manifestPath, sha256: module.manifestDigest })), input);
|
|
440
|
+
await stableScope(root, modules.filter(module => module.manifestPath).map(module => ({ path: module.manifestPath, sha256: module.manifestDigest })), input, scopeBuildSourceRead);
|
|
373
441
|
if (history.available && (await scopeGitMetadata(root, ['rev-parse', '--verify', 'HEAD'], input, [128]))?.trim() !== history.head) scopeFail('ENTROROVE_SCOPE_CHANGED', 'Git HEAD changed during scope inspection');
|
|
374
442
|
const sourceLines = files.reduce((sum, file) => sum + file.lines, 0); const bytes = files.reduce((sum, file) => sum + file.bytes, 0);
|
|
375
443
|
// An actual current diff (including empty untracked files) is authoritative; clean Git uses history, unknown Git uses source lines.
|
|
@@ -394,12 +462,12 @@ async function contextEvidence(root, graph, input) {
|
|
|
394
462
|
if (!target.stat.isFile() || target.stat.size > 1_048_576 || (bytes += target.stat.size) > 8_388_608) {
|
|
395
463
|
scopeFail('ENTROROVE_SCOPE_LIMIT', 'Context evidence exceeds its bounded actual file inspection');
|
|
396
464
|
}
|
|
397
|
-
const read = await
|
|
465
|
+
const read = await scopeBuildSourceRead(root, file.path, input);
|
|
398
466
|
if (read.sha256 !== file.sha256 || metadata.has(file.path) && metadata.get(file.path).sha256 !== read.sha256) {
|
|
399
467
|
scopeFail('ENTROROVE_SCOPE_CHANGED', 'Actual context build evidence changed since its exact review');
|
|
400
468
|
}
|
|
401
469
|
if (!metadata.has(file.path)) metadata.set(file.path, { path: file.path, sha256: read.sha256 });
|
|
402
470
|
}
|
|
403
471
|
graph.workspaceMetadata = [...metadata.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
404
|
-
await stableScope(root, graph.workspaceMetadata, input);
|
|
472
|
+
await stableScope(root, graph.workspaceMetadata, input, scopeBuildSourceRead);
|
|
405
473
|
}
|
|
@@ -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, scopeBuildSourceRead, 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,33 @@ 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
|
+
const explicit = new Set(actual.files.map(file => file.path));
|
|
55
|
+
await stableScope(root, actual.importGraph.nodes, input,
|
|
56
|
+
(actualRoot, relative, binding) => (explicit.has(relative) ? scopeRead : scopeBuildSourceRead)(actualRoot, relative, binding));
|
|
57
|
+
await stableScope(root, actual.importGraph.workspaceMetadata, input, scopeBuildSourceRead);
|
|
58
|
+
await stableScope(root, actual.modules.filter(module => module.manifestPath)
|
|
59
|
+
.map(module => ({ path: module.manifestPath, sha256: module.manifestDigest })), input, scopeBuildSourceRead);
|
|
60
|
+
if (actual.git.available && (await scopeGitMetadata(root, ['rev-parse', '--verify', 'HEAD'], input, [128]))?.trim() !== actual.git.head) {
|
|
61
|
+
scopeFail('ENTROROVE_SCOPE_CHANGED', 'Git HEAD changed before the original context signature');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export async function attestScopeFacts(input, result, { maximumExpiresAt, beforeSign } = {}) {
|
|
37
65
|
if (maximumExpiresAt !== undefined && (!Number.isSafeInteger(maximumExpiresAt) || maximumExpiresAt < 1)) {
|
|
38
66
|
scopeFail('ENTROROVE_SCOPE_ATTESTATION_BINDING', 'scope signature deadline must be a finite exact bound');
|
|
39
67
|
}
|
|
@@ -42,8 +70,12 @@ export async function attestScopeFacts(input, result, { maximumExpiresAt } = {})
|
|
|
42
70
|
|| !Array.isArray(input.importContexts))) scopeFail('ENTROROVE_SCOPE_ATTESTATION_BINDING', 'Context proof requires its exact contexts and revision digest together');
|
|
43
71
|
if (input.signerConfigPath === undefined || input.signerConfigPath === null) return result;
|
|
44
72
|
binding(input, result);
|
|
73
|
+
const inspectedInput = canonical(input);
|
|
45
74
|
const root = await scopeRoot(input.repositoryRoot); const snapshot = await readScopeSnapshot(input, root);
|
|
46
|
-
const
|
|
75
|
+
const paths = await resolveScopePaths(input, root);
|
|
76
|
+
const inspect = () => collectScopeFacts(root, paths, input.baselineRef, snapshot.known, input);
|
|
77
|
+
const inspection = beforeSign ? await captureScopeReadSet(root, inspect) : { value: await inspect() };
|
|
78
|
+
const actual = inspection.value;
|
|
47
79
|
actual.snapshotEvidence = await verifyFreshScopeSnapshot(input, root, actual);
|
|
48
80
|
const candidate = await verifiedScopeCandidate(input, root, actual.snapshotEvidence.manifest);
|
|
49
81
|
if (root !== result.repositoryRoot || canonical(actual) !== canonical(result.facts)
|
|
@@ -62,13 +94,26 @@ export async function attestScopeFacts(input, result, { maximumExpiresAt } = {})
|
|
|
62
94
|
if (error.code === 'ENTROROVE_SCOPE_SIGNER_INVALID') throw error;
|
|
63
95
|
scopeFail('ENTROROVE_SCOPE_SIGNER_INVALID', 'scope signer configuration or signing key could not be read');
|
|
64
96
|
}
|
|
97
|
+
// The independent S scan is complete before requesting its short-lived signing challenge.
|
|
98
|
+
// The internal callback can change only the nonce and deadline, never observed facts or identity.
|
|
99
|
+
const originalInput = input;
|
|
100
|
+
({ input, maximumExpiresAt } = await signingChallenge(input, beforeSign, maximumExpiresAt));
|
|
101
|
+
if (canonical(originalInput) !== inspectedInput || root !== result.repositoryRoot
|
|
102
|
+
|| canonical(actual) !== canonical(result.facts) || actual.snapshotEvidence.snapshotDigest !== result.snapshotDigest) {
|
|
103
|
+
scopeFail('ENTROROVE_SCOPE_ATTESTATION_BINDING', 'scope signing preparation changed during challenge renewal');
|
|
104
|
+
}
|
|
105
|
+
if (beforeSign) { await inspection.verify(); await verifySigningFreshness(input, root, actual); }
|
|
106
|
+
if (canonical(originalInput) !== inspectedInput || root !== result.repositoryRoot
|
|
107
|
+
|| canonical(actual) !== canonical(result.facts) || actual.snapshotEvidence.snapshotDigest !== result.snapshotDigest) {
|
|
108
|
+
scopeFail('ENTROROVE_SCOPE_ATTESTATION_BINDING', 'scope signing preparation changed before its actual signature');
|
|
109
|
+
}
|
|
65
110
|
const now = Date.now();
|
|
66
111
|
const expiresAt = Math.min(now + config.receiptTtlMs, candidate ? Date.parse(candidate.expiresAt) : Infinity,
|
|
67
112
|
maximumExpiresAt ?? Infinity);
|
|
68
113
|
if (expiresAt <= now) scopeFail('ENTROROVE_SCOPE_ATTESTATION_BINDING', 'candidate challenge expired before its actual scope signature');
|
|
69
114
|
const core = { schemaVersion: 'entrorove.scope-facts/1.0', flowId: input.flowId, stepId: input.stepId,
|
|
70
115
|
taskDigest: input.taskDigest, ownerId: input.ownerId, chainDigest: input.chainDigest, repositoryId: input.repositoryId,
|
|
71
|
-
facts:
|
|
116
|
+
facts: actual, snapshotDigest: actual.snapshotEvidence.snapshotDigest, challenge: input.challenge,
|
|
72
117
|
issuedAt: new Date(now).toISOString(), expiresAt: new Date(expiresAt).toISOString(),
|
|
73
118
|
nonce: randomUUID(), keyId: config.keyId,
|
|
74
119
|
...(input.contextRevisionDigest === undefined ? {} : { importContexts: input.importContexts, contextRevisionDigest: input.contextRevisionDigest }) };
|
package/local-scope-state.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
6
|
import { canonical, sha256, verifySnapshotManifest } from './local-integrity.mjs';
|
|
7
7
|
import { withStateLock } from './local-state-lock.mjs';
|
|
8
|
-
import { scopeFail, scopeRead, scopeControlRead, scopeTarget, stableScope } from './local-scope-files.mjs';
|
|
8
|
+
import { scopeFail, scopeRead, scopeBuildSourceRead, scopeControlRead, scopeTarget, stableScope } from './local-scope-files.mjs';
|
|
9
9
|
|
|
10
10
|
const ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
11
11
|
const order = ['lock', 'probe', 'swarm'];
|
|
@@ -63,13 +63,13 @@ async function originalSnapshot(input, root, base) {
|
|
|
63
63
|
}
|
|
64
64
|
return { manifest, directory, digest: manifest.snapshotDigest };
|
|
65
65
|
}
|
|
66
|
-
async function capture(root, files, directory, input) {
|
|
66
|
+
async function capture(root, files, directory, input, readSource = scopeRead) {
|
|
67
67
|
for (const file of files) {
|
|
68
68
|
if (file.exists === false) {
|
|
69
69
|
if ((await scopeTarget(root, file.path, true)).exists) scopeFail('ENTROROVE_SCOPE_CHANGED', 'a deleted source reappeared before preservation');
|
|
70
70
|
continue;
|
|
71
71
|
}
|
|
72
|
-
const read = await
|
|
72
|
+
const read = await readSource(root, file.path, input);
|
|
73
73
|
if (read.sha256 !== file.sha256) scopeFail('ENTROROVE_SCOPE_CHANGED', 'scope source changed before snapshot preservation');
|
|
74
74
|
const destination = path.join(directory, file.path);
|
|
75
75
|
await fs.mkdir(path.dirname(destination), { recursive: true, mode: 0o700 });
|
|
@@ -107,16 +107,18 @@ function preservedFiles(facts) {
|
|
|
107
107
|
}
|
|
108
108
|
async function saveRevision(location, root, state, facts, mode, initial, input) {
|
|
109
109
|
const staged = await fs.mkdtemp(path.join(location.directory, '.revision-'));
|
|
110
|
+
const explicit = new Set(facts.files.map(file => file.path));
|
|
111
|
+
const readSource = (actualRoot, relative, binding) => (explicit.has(relative) ? scopeRead : scopeBuildSourceRead)(actualRoot, relative, binding);
|
|
110
112
|
const final = path.join(location.directory, `revision-${state.revision + 1}-${randomUUID()}`); let committed = false;
|
|
111
113
|
try {
|
|
112
114
|
const currentDirectory = path.join(staged, 'current'); await fs.mkdir(currentDirectory, { mode: 0o700 });
|
|
113
|
-
await capture(root, preservedFiles(facts), currentDirectory, input);
|
|
115
|
+
await capture(root, preservedFiles(facts), currentDirectory, input, readSource);
|
|
114
116
|
if (initial) {
|
|
115
117
|
const original = path.join(staged, 'original'); await fs.mkdir(original, { mode: 0o700 });
|
|
116
118
|
await preserveSnapshotCopies14(input, initial.directory, initial.manifest, original);
|
|
117
119
|
await fs.writeFile(path.join(original, 'manifest.json'), `${canonical(initial.manifest)}\n`, { flag: 'wx', mode: 0o600 });
|
|
118
120
|
}
|
|
119
|
-
await stableScope(root, preservedFiles(facts), input); await fs.rename(staged, final);
|
|
121
|
+
await stableScope(root, preservedFiles(facts), input, readSource); await fs.rename(staged, final);
|
|
120
122
|
const record = historyRecord(state, facts, path.join(final, 'current'), mode);
|
|
121
123
|
const next = { ...state, revision: record.revision, mode, history: [...state.history, record] };
|
|
122
124
|
if (initial) next.originalSnapshot = { ...next.originalSnapshot, directory: path.join(final, 'original') };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { uniConditionalError } from './local-scope-uni-expressions.mjs';
|
|
3
|
+
import { createBuildDataReadset } from './local-scope-build-readset.mjs';
|
|
4
|
+
import { buildConfigurationNames, buildDataPath, buildModuleData, buildScriptReferences } from './local-scope-build-data.mjs';
|
|
5
|
+
|
|
6
|
+
const controlKeys = ['UNI_CUSTOM_DEFINE', 'UNI_CUSTOM_CONTEXT', 'UNI_SCRIPT_DEFINE', 'UNI_SCRIPT_ENV', 'UNI_COMPILER_VERSION',
|
|
7
|
+
'UNI_APP_X', 'UNI_APP_X_DOM2', 'UNI_UTS_PLATFORM', 'UNI_INPUT_DIR', 'VITE_ROOT_DIR', 'UNI_CLI_CONTEXT',
|
|
8
|
+
'UNI_HBUILDERX', 'UNI_HBUILDERX_PLUGINS', 'NODE_PATH', 'NODE_OPTIONS'];
|
|
9
|
+
function assertProcessControls(filename) {
|
|
10
|
+
const present = controlKeys.filter(key => Object.hasOwn(process.env, key) || Object.hasOwn(process, key));
|
|
11
|
+
if (present.length) throw uniConditionalError(filename, 0, `Build process override controls are unsupported: ${present.join(', ')}`);
|
|
12
|
+
}
|
|
13
|
+
function jsonData(item) {
|
|
14
|
+
let value; try { value = JSON.parse(Buffer.from(item.contentBase64, 'base64').toString('utf8')); }
|
|
15
|
+
catch { throw uniConditionalError(item.path, 0, 'Build package data is invalid JSON'); }
|
|
16
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || value.scripts !== undefined
|
|
17
|
+
&& (!value.scripts || typeof value.scripts !== 'object' || Array.isArray(value.scripts))) throw uniConditionalError(item.path, 0, 'Build package data requires an object and string script map');
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
async function hostData(sourcePath, configPath, state) {
|
|
21
|
+
let directory = path.posix.dirname(configPath ?? sourcePath);
|
|
22
|
+
for (let count = 0; count < 128; count++) {
|
|
23
|
+
const relative = path.posix.join(directory, 'package.json'), item = await state.observe(relative);
|
|
24
|
+
if (item) return { root: directory, item, manifest: jsonData(item) };
|
|
25
|
+
if (directory === '.') return { root: path.posix.dirname(configPath ?? sourcePath), item: null, manifest: {} };
|
|
26
|
+
directory = path.posix.dirname(directory);
|
|
27
|
+
}
|
|
28
|
+
throw uniConditionalError(sourcePath, 0, 'Build host data exceeds its bounded package ancestry');
|
|
29
|
+
}
|
|
30
|
+
function sourceText(item) { return Buffer.from(item.contentBase64, 'base64').toString('utf8'); }
|
|
31
|
+
export function createUniBuildInspection(read, inspectDirectory) {
|
|
32
|
+
const state = createBuildDataReadset(read, inspectDirectory), contexts = new Map(), modules = new Map(); let controlsRequired = false;
|
|
33
|
+
const inspectSource = async (sourcePath, configPath, context = { hasConditionals: true }) => {
|
|
34
|
+
const key = `${sourcePath}:${configPath ?? ''}:${Boolean(context.hasConditionals)}`;
|
|
35
|
+
if (contexts.has(key)) return contexts.get(key);
|
|
36
|
+
const host = await hostData(sourcePath, configPath, state), unresolved = new Map(), queue = [], visited = new Set();
|
|
37
|
+
const mark = (item, reason) => { if (item) unresolved.set(`${item.path}:${reason}`, { path: item.path, sha256: item.sha256, reason }); };
|
|
38
|
+
const enqueue = async (relative, reason) => {
|
|
39
|
+
const item = await state.observe(relative); if (item) { mark(item, reason); queue.push(item); } return item;
|
|
40
|
+
};
|
|
41
|
+
if (configPath) await state.observe(buildDataPath('.', configPath));
|
|
42
|
+
for (const name of buildConfigurationNames) await enqueue(buildDataPath(host.root, name), 'opaque-build-config');
|
|
43
|
+
const scripts = buildScriptReferences(host.manifest, host.item?.path);
|
|
44
|
+
if (scripts.opaque) mark(host.item, 'opaque-build-command');
|
|
45
|
+
for (const reference of scripts.references) await enqueue(buildDataPath(host.root, reference), 'opaque-build-loader');
|
|
46
|
+
if (host.manifest['uni-app']?.scripts !== undefined) mark(host.item, 'custom-build-definitions');
|
|
47
|
+
const roots = new Set([host.root]);
|
|
48
|
+
while (queue.length) {
|
|
49
|
+
const item = queue.shift(); if (visited.has(item.path)) continue; visited.add(item.path);
|
|
50
|
+
if (visited.size > 128) throw Object.assign(uniConditionalError(item.path, 0, 'Build source data exceeds its 128-module bound'), { code: 'ENTROROVE_SCOPE_LIMIT' });
|
|
51
|
+
if (/\.json$/.test(item.path) || /(?:^|\/)\.babelrc$/.test(item.path)) continue;
|
|
52
|
+
if (item.path.split('/').includes('node_modules')) continue;
|
|
53
|
+
if (!modules.has(item.path)) modules.set(item.path, buildModuleData(sourceText(item), item.path));
|
|
54
|
+
const parsed = modules.get(item.path);
|
|
55
|
+
for (const literal of parsed.literalRoots) {
|
|
56
|
+
const root = literal.root !== undefined ? buildDataPath(host.root, literal.root)
|
|
57
|
+
: literal.context !== undefined ? buildDataPath(host.root, literal.context) : host.root;
|
|
58
|
+
roots.add(literal.envDir === undefined ? root : buildDataPath(root, literal.envDir));
|
|
59
|
+
}
|
|
60
|
+
for (const reference of parsed.references) {
|
|
61
|
+
const target = buildDataPath(path.posix.dirname(item.path), reference);
|
|
62
|
+
const candidates = /\.[A-Za-z0-9]+$/.test(target) ? [target]
|
|
63
|
+
: ['.js', '.cjs', '.mjs', '.ts', '.mts', '.cts', '.json', '/index.js', '/index.cjs', '/index.ts'].map(extension => `${target}${extension}`);
|
|
64
|
+
for (const candidate of candidates) await enqueue(candidate, 'opaque-build-loader');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (unresolved.size || context.hasConditionals) {
|
|
68
|
+
controlsRequired = true; assertProcessControls(sourcePath);
|
|
69
|
+
for (const root of roots) {
|
|
70
|
+
const listing = await state.directory(root);
|
|
71
|
+
if (listing?.environmentFiles.length) {
|
|
72
|
+
const origin = [...visited].map(path => state.observations.find(item => item.path === path)).find(Boolean) ?? host.item;
|
|
73
|
+
if (origin) mark(origin, 'build-environment-present');
|
|
74
|
+
else mark(await state.observe(sourcePath), 'build-environment-present');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const proof = { sourceProjection: 'raw-source', buildObservation: unresolved.size ? 'opaque' : 'not-observed',
|
|
79
|
+
get observations() { return state.observations; },
|
|
80
|
+
unresolvedBuildSources: [...unresolved.values()].sort((a, b) => `${a.path}:${a.reason}`.localeCompare(`${b.path}:${b.reason}`)) };
|
|
81
|
+
contexts.set(key, proof); return proof;
|
|
82
|
+
};
|
|
83
|
+
inspectSource.verify = async () => { if (controlsRequired) assertProcessControls('build-data'); await state.verify(); };
|
|
84
|
+
return inspectSource;
|
|
85
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { uniDirectives } from './local-scope-uni-directives.mjs';
|
|
2
|
+
import { uniCondition, uniConditionContexts, uniConditionalError, uniConditionalLimitError } 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
|
+
const limit = reason => uniConditionalLimitError(filename, item.start, reason, source);
|
|
12
|
+
if (['ifdef', 'ifndef', 'if'].includes(item.kind)) {
|
|
13
|
+
if (stack.length >= 32) throw limit('Conditional compilation exceeds its 32-level nesting bound');
|
|
14
|
+
const condition = uniCondition(item.expression, fail), index = conditions.length;
|
|
15
|
+
if ((conditionTokens += condition.tokens) > 4096) throw limit('Conditional expressions exceed their 4096-token aggregate inspection bound');
|
|
16
|
+
conditions.push(condition); stack.push({ index, negated: item.kind === 'ifndef', syntax: item.syntax, alternative: false });
|
|
17
|
+
} else {
|
|
18
|
+
const active = stack.at(-1);
|
|
19
|
+
if (!active || active.syntax !== item.syntax) throw fail('Conditional directive has no matching opening in the same source language');
|
|
20
|
+
if (item.kind === 'else') {
|
|
21
|
+
if (active.alternative) throw fail('Conditional block has more than one alternative');
|
|
22
|
+
active.negated = !active.negated; active.alternative = true;
|
|
23
|
+
} else stack.pop();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (stack.length) throw uniConditionalError(filename, directives.at(-1).start, 'Conditional block is missing its matching endif', source);
|
|
27
|
+
segments.push({ start: cursor, end: source.length, guards: [] });
|
|
28
|
+
return { conditions, segments };
|
|
29
|
+
}
|
|
30
|
+
const activeSegment = (segment, key) => !segment.directive && segment.guards.every(guard => (key[guard.index] === '1') !== guard.negated);
|
|
31
|
+
export function uniConditionalVariants(source, filename) {
|
|
32
|
+
const fail = reason => uniConditionalError(filename, 0, reason, source);
|
|
33
|
+
const limit = reason => uniConditionalLimitError(filename, 0, reason, source);
|
|
34
|
+
if (typeof source !== 'string') throw fail('Conditional inspection requires actual source text');
|
|
35
|
+
const unchanged = () => ({ directives: [], variants: [{ source, key: '', label: 'unconditional' }] });
|
|
36
|
+
if (!/(?:<!--|\/\/|\/\*)[ \t]*#/i.test(source)) return unchanged();
|
|
37
|
+
if (Buffer.byteLength(source) > 2_097_152) throw limit('Conditional source exceeds its 2 MiB inspection bound');
|
|
38
|
+
try {
|
|
39
|
+
const directives = uniDirectives(source, filename);
|
|
40
|
+
if (!directives.length) return unchanged();
|
|
41
|
+
const { conditions, segments } = conditionalSegments(source, directives, filename), contexts = uniConditionContexts(conditions, fail);
|
|
42
|
+
if (Buffer.byteLength(source) * contexts.length > 33_554_432) throw limit('Conditional source variants exceed their 32 MiB inspection bound');
|
|
43
|
+
for (const segment of segments) if (!segment.directive && source.slice(segment.start, segment.end).trim()
|
|
44
|
+
&& !contexts.some(context => activeSegment(segment, context.key))) {
|
|
45
|
+
throw uniConditionalError(filename, segment.start, 'Conditional branch has no proven platform context; custom or contradictory build definitions require explicit support', source);
|
|
46
|
+
}
|
|
47
|
+
const variants = contexts.map(context => ({ key: context.key, label: context.label,
|
|
48
|
+
source: segments.map(segment => {
|
|
49
|
+
const text = source.slice(segment.start, segment.end); return activeSegment(segment, context.key) ? text : blank(text);
|
|
50
|
+
}).join('') }));
|
|
51
|
+
return { directives: directives.map(({ kind, expression, syntax }) => ({ kind, expression, syntax })), variants };
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (error instanceof RangeError) throw limit('Conditional syntax exceeds its parser or traversal bound');
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|