@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.
@@ -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) scopeFail('ENTROROVE_IMPORT_INVALID', 'source has invalid or unterminated syntax');
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 vueScripts(source, filename) {
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) scopeFail('ENTROROVE_IMPORT_INVALID', 'component has invalid or unterminated syntax');
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 importSpecifiers(content, filename, repositoryRoot) {
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 => ({ ...site, line: site.line + (part.lineOffset ?? 0) })));
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 scopeRead(root, boundary.manifestPath, input)).content.toString('utf8'));
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 scopeRead(root, relative, input), resolved = await fs.realpath(target.target);
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 scopeRead(root, relative, input); const boundary = await packageBoundary(root, relative, input);
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 = importSpecifiers(read.content, relative, root); dynamic ||= imports.dynamic;
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 (!missing?.length) throw error;
312
- unresolvedImports.push({ path: relative, specifier, reason: 'approved-create-not-present', candidatePaths: missing });
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
- return { nodes, edges: edges.sort((a, b) => `${a.from}:${a.specifier}`.localeCompare(`${b.from}:${b.specifier}`)),
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 scopeRead(root, file.path, input);
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
- export async function attestScopeFacts(input, result, { maximumExpiresAt } = {}) {
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 actual = await collectScopeFacts(root, await resolveScopePaths(input, root), input.baselineRef, snapshot.known, input);
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: result.facts, snapshotDigest: result.snapshotDigest, challenge: input.challenge,
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 }) };
@@ -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 scopeRead(root, file.path, input);
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
+ }