@entrorove/skill 0.1.69 → 0.1.70

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