@entrorove/skill 0.1.68 → 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.
Files changed (48) hide show
  1. package/CAPABILITY-MAP.md +2 -0
  2. package/CHANGELOG.md +10 -0
  3. package/IDE-EXECUTION.md +16 -2
  4. package/SKILL.md +3 -2
  5. package/cli.mjs +1 -1
  6. package/cloud-locale.d.mts +7 -0
  7. package/cloud-locale.mjs +59 -0
  8. package/cloud-task-client.mjs +21 -13
  9. package/cloud-watch-client.mjs +11 -3
  10. package/connected-cli.mjs +2 -0
  11. package/design-lint.mjs +20 -2
  12. package/ide-command.mjs +1 -1
  13. package/ide-hook.mjs +4 -4
  14. package/ide-presentation.mjs +15 -4
  15. package/local-archguard-components.mjs +31 -4
  16. package/local-contracts.mjs +5 -2
  17. package/local-delegated-modify-analysis.mjs +16 -7
  18. package/local-discipline-gate.mjs +2 -0
  19. package/local-discipline-snapshot.mjs +8 -2
  20. package/local-mutation-snapshot.mjs +2 -2
  21. package/local-preserved-mutation-protection.mjs +2 -0
  22. package/local-scope-files.mjs +14 -2
  23. package/local-scope-read-set.mjs +66 -0
  24. package/local-scope-repository.mjs +97 -13
  25. package/local-scope-signature.mjs +52 -6
  26. package/local-scope-uni-build.mjs +135 -0
  27. package/local-scope-uni-conditional.mjs +54 -0
  28. package/local-scope-uni-directives.mjs +96 -0
  29. package/local-scope-uni-expressions.mjs +101 -0
  30. package/local-scope-workspaces.mjs +14 -2
  31. package/manifest.json +1 -1
  32. package/mutation-client.mjs +8 -5
  33. package/package.json +17 -2
  34. package/release-state.json +46 -31
  35. package/release.mjs +4 -3
  36. package/releases.json +12 -0
  37. package/remote-client.mjs +1 -0
  38. package/scope-amendment-observer.mjs +3 -1
  39. package/scope-amendment-state.mjs +3 -1
  40. package/scope-context-client.mjs +118 -0
  41. package/scope-context-journal.mjs +44 -0
  42. package/scope-context-observation.mjs +42 -0
  43. package/scope-context-observer.mjs +86 -0
  44. package/scope-context-release.mjs +8 -0
  45. package/scope-context-remote.mjs +60 -0
  46. package/scope-context-state.mjs +88 -0
  47. package/scope-context-wire.mjs +84 -0
  48. package/scope-runner-client.mjs +9 -5
@@ -103,7 +103,8 @@ const scopeTask = object({ goal: { ...text, pattern: '\\S', maxLength: 12000 },
103
103
  unclear: { type: 'boolean' }, architectureContract: { type: 'boolean' }, explicitAimlockRequested: { type: 'boolean' }, explicitEntroroveRequested: { type: 'boolean' }, userSpecifiedSkills: array(text),
104
104
  requirements: { ...array(scopeRequirement, 1), maxItems: 500 }, phase: enumeration(['plan', 'coordinate', 'architecture', 'verify', 'merge', 'decision']), baselineRef: text, interaction: scopeInteraction }, ['goal', 'workType', 'difficulty', 'risk']);
105
105
  const probe = { ...step, paths: probePaths, targetPaths: probePaths, task: scopeTask, baselineRef: text, snapshotId: identifier,
106
- targetSymbols: array(text, 1), contextMapPath: relativePath, importContexts, signerConfigPath: nullable(text),
106
+ targetSymbols: array(text, 1), contextMapPath: relativePath, importContexts, contextRevisionDigest: sha256,
107
+ contextEvidence: { ...array(object({ path: relativePath, sha256 }, ['path', 'sha256'])), maxItems: 200 }, signerConfigPath: nullable(text),
107
108
  taskDigest: sha256, ownerId: ledgerIdentifier, chainDigest: sha256, repositoryId: identifier, challenge: ledgerIdentifier };
108
109
  add('probe', { ...probe, handoffCandidate: HANDOFF_SNAPSHOT_CANDIDATE_SCHEMA }, ['repositoryRoot']);
109
110
  schemas.probe.allOf = [{ if: { type: 'object', anyOf: ['flowId', 'stepId', 'snapshotId'].map(field => ({ required: [field] })) },
@@ -112,7 +113,9 @@ schemas.probe.allOf = [{ if: { type: 'object', anyOf: ['flowId', 'stepId', 'snap
112
113
  then: { type: 'object', required: ['flowId', 'stepId', 'snapshotId', 'taskDigest', 'ownerId', 'chainDigest', 'repositoryId', 'challenge'] } },
113
114
  { if: { type: 'object', required: ['handoffCandidate'] }, then: { type: 'object', required: ['flowId', 'stepId', 'snapshotId', 'paths', 'taskDigest', 'ownerId', 'chainDigest', 'repositoryId', 'challenge', 'signerConfigPath'],
114
115
  properties: { signerConfigPath: text } } },
115
- { if: { type: 'object', required: ['targetSymbols'] }, then: { type: 'object', required: ['contextMapPath'] } }];
116
+ { if: { type: 'object', required: ['targetSymbols'] }, then: { type: 'object', required: ['contextMapPath'] } },
117
+ { if: { type: 'object', required: ['contextRevisionDigest'] }, then: { type: 'object', required: ['importContexts'] } },
118
+ { if: { type: 'object', required: ['contextEvidence'] }, then: { type: 'object', required: ['contextRevisionDigest', 'importContexts'] } }];
116
119
  add('reassess', { ...probe, snapshotId: nullable(identifier), currentMode: modes, expectedRevision: integer(1) }, ['repositoryRoot']);
117
120
  schemas.reassess.allOf = [{ if: { type: 'object', anyOf: [
118
121
  ...['flowId', 'stepId', 'expectedRevision', 'taskDigest', 'ownerId', 'chainDigest', 'repositoryId', 'challenge'].map(field => ({ required: [field] })),
@@ -3,6 +3,7 @@ import { parse as parseVue, parseCache } from '@vue/compiler-sfc';
3
3
  import { canonical } from './local-integrity.mjs';
4
4
  import { createScopeImportResolver } from './local-scope-import-expressions.mjs';
5
5
  import { isDynamicFunctionReference, isTypeOnlyReference } from './local-typescript-function-reference.mjs';
6
+ import { uniConditionalVariants } from './local-scope-uni-conditional.mjs';
6
7
 
7
8
  const fail = () => { throw Object.assign(new Error('Preserved modification changed an existing definition, public contract, dependency, or unsupported source boundary'),
8
9
  { code: 'ENTROROVE_MUTATION_DELEGATION_CHANGE' }); };
@@ -216,17 +217,25 @@ function parts(path, content) {
216
217
  function retained(before, after) {
217
218
  for (const [key, count] of before) if ((after.get(key) ?? 0) < count) fail();
218
219
  }
220
+ function compareSource(path, before, after) {
221
+ if (!before.trim() && !after.trim()) return;
222
+ const original = parts(path, before), candidate = parts(path, after);
223
+ if (canonical(original.metadata) !== canonical(candidate.metadata) || original.scripts.length !== candidate.scripts.length) fail();
224
+ for (let index = 0; index < original.scripts.length; index++) {
225
+ const a = scriptProjection(original.scripts[index]), b = scriptProjection(candidate.scripts[index]);
226
+ if (canonical(a.dependencies) !== canonical(b.dependencies) || canonical(a.public) !== canonical(b.public)) fail();
227
+ retained(a.definitions, b.definitions); retained(a.signatures, b.signatures); retained(b.unknown, a.unknown);
228
+ }
229
+ }
219
230
  export function assertDelegatedSourceModification(input) {
220
231
  const { path, before, after } = input ?? {};
221
232
  if (typeof path !== 'string' || typeof before !== 'string' || typeof after !== 'string'
222
233
  || Buffer.byteLength(before) > 1_048_576 || Buffer.byteLength(after) > 1_048_576) fail();
223
234
  try {
224
- const original = parts(path, before), candidate = parts(path, after);
225
- if (canonical(original.metadata) !== canonical(candidate.metadata) || original.scripts.length !== candidate.scripts.length) fail();
226
- for (let index = 0; index < original.scripts.length; index++) {
227
- const a = scriptProjection(original.scripts[index]), b = scriptProjection(candidate.scripts[index]);
228
- if (canonical(a.dependencies) !== canonical(b.dependencies) || canonical(a.public) !== canonical(b.public)) fail();
229
- retained(a.definitions, b.definitions); retained(a.signatures, b.signatures); retained(b.unknown, a.unknown);
230
- }
235
+ if (!/\.vue$/i.test(path)) return compareSource(path, before, after);
236
+ const original = uniConditionalVariants(before, path), candidate = uniConditionalVariants(after, path);
237
+ if (canonical(original.directives) !== canonical(candidate.directives)
238
+ || canonical(original.variants.map(value => value.key)) !== canonical(candidate.variants.map(value => value.key))) fail();
239
+ for (let index = 0; index < original.variants.length; index++) compareSource(path, original.variants[index].source, candidate.variants[index].source);
231
240
  } catch { fail(); }
232
241
  }
@@ -9,6 +9,7 @@ import { assertDelegatedSourceModification } from './local-delegated-modify-anal
9
9
  import { assertDesignWrite } from './design-gate.mjs';
10
10
  import { assertPackageReplacement } from './package-replacement.mjs';
11
11
  import { assertRollbackCandidate } from './rollback-history.mjs';
12
+ import { scopeContextSnapshotInput } from './scope-context-observation.mjs';
12
13
 
13
14
  const needs = message => mutationFail14('ENTROROVE_DISCIPLINE_NEEDS_HUMAN', message);
14
15
  function declaredWrite(input, pass, baseline) {
@@ -84,6 +85,7 @@ export async function disciplineWritePreflight(input, value, content) {
84
85
  }
85
86
  const { pass, target, binding, snapshot, ledger } = value;
86
87
  const review = await readDisciplineSnapshot(snapshot), baseline = snapshot.manifest.files.find(file => file.path === target);
88
+ input = (await scopeContextSnapshotInput(input, binding.root, review.scopeContext)).input;
87
89
  await assertDisciplineRepository(input, binding.root, review, ledger);
88
90
  const intent = declaredWrite(input, pass, baseline);
89
91
  const originalBytes = baseline.exists ? (await readSnapshotCopy14(input, snapshot.snapshotRoot, snapshot.manifest, target)).content : Buffer.alloc(0);
@@ -2,16 +2,22 @@ import path from 'node:path';
2
2
  import { canonical, sha256 } from './local-integrity.mjs';
3
3
  import { atomicMutationJson14, mutationFail14, readMutationJson14 } from './local-mutation-files.mjs';
4
4
  import { disciplineRepositoryObservation } from './local-discipline-repository.mjs';
5
+ import { scopeContextSnapshotInput } from './scope-context-observation.mjs';
5
6
 
6
7
  export function snapshotDisciplineBinding(value) {
7
8
  if (!Object.hasOwn(value, 'disciplineReviewDigest')) return {};
8
9
  if (!/^[0-9a-f]{64}$/.test(value.disciplineReviewDigest ?? '')) mutationFail14('ENTROROVE_DISCIPLINE_REVIEW_INVALID', 'Snapshot discipline review digest is invalid');
9
10
  return { disciplineReviewDigest: value.disciplineReviewDigest };
10
11
  }
11
- export async function captureDisciplineSnapshot(input, root, location, binding) {
12
- const observed = await disciplineRepositoryObservation(input, root, { analyze: true });
12
+ export async function captureDisciplineSnapshot(input, root, location, binding, contextAuthority) {
13
+ // A fresh S context candidate carries its own signed challenge mapping before
14
+ // admission. It must not be mistaken for an already accepted M snapshot head.
15
+ const selected = !contextAuthority && input.contextRevisionDigest !== undefined ? { input, context: null }
16
+ : await scopeContextSnapshotInput(input, root, contextAuthority, { initial: true });
17
+ const observed = await disciplineRepositoryObservation(selected.input, root, { analyze: true });
13
18
  const review = { schemaVersion: 'entrorove.discipline-review/1.0', ...binding,
14
19
  repositoryDigest: sha256(root).slice(0, 32), createdAt: new Date().toISOString(), ...observed,
20
+ ...(selected.context ? { scopeContext: selected.context } : {}),
15
21
  thresholds: { deletionAdditionRatio: null, semanticOverlap: null, directLines: null, usageScopeLimit: null } };
16
22
  const digest = sha256(canonical(review));
17
23
  await atomicMutationJson14(path.join(location.snapshotRoot, 'discipline-review.json'), review, { privateFrom: location.stateRoot });
@@ -148,7 +148,7 @@ async function copySource(input, root, snapshotRoot, relative, stateRoot) {
148
148
  await writeMutationFile14(path.join(snapshotRoot, 'files', relative), read.content, { privateFrom: stateRoot });
149
149
  return { path: relative, exists: true, sha256: read.sha256, mode: read.mode };
150
150
  }
151
- export async function snapshotPlan14(input, { handoffBinding: binding = {} } = {}) {
151
+ export async function snapshotPlan14(input, { handoffBinding: binding = {}, contextAuthority } = {}) {
152
152
  const root = await mutationRoot14(input.repositoryRoot);
153
153
  const flowId = mutationId14(input.flowId, 'flowId'); const stepId = mutationId14(input.stepId, 'stepId');
154
154
  const paths = mutationPaths14(input.paths); const handoff = handoffBinding(binding);
@@ -162,7 +162,7 @@ export async function snapshotPlan14(input, { handoffBinding: binding = {} } = {
162
162
  await privateMutationDirectory14(path.join(loc.snapshotRoot, 'files'), { privateFrom: loc.stateRoot });
163
163
  const files = [];
164
164
  for (const relative of paths) files.push(await copySource(input, root, loc.snapshotRoot, relative, loc.stateRoot));
165
- const discipline = await captureDisciplineSnapshot(input, root, loc, { snapshotId, flowId, stepId });
165
+ const discipline = await captureDisciplineSnapshot(input, root, loc, { snapshotId, flowId, stepId }, contextAuthority);
166
166
  const core = { snapshotId, flowId, stepId, paths, files, contractDigest, ...handoff, ...discipline };
167
167
  const manifest = { schemaVersion: SNAPSHOT_SCHEMA, ...core, repositoryDigest: repoKey(root), copyLayout: COPY_LAYOUT,
168
168
  createdAt: new Date().toISOString(), snapshotDigest: sha256(canonical(core)) };
@@ -5,6 +5,7 @@ import { readDisciplineSnapshot } from './local-discipline-snapshot.mjs';
5
5
  import { preservedMutationDelegation, preservedMutationFail } from './local-preserved-mutation-wire.mjs';
6
6
  import { preservedMutationProtectedFiles } from './local-preserved-mutation-state.mjs';
7
7
  import { scopeCandidateImports } from './local-scope-repository.mjs';
8
+ import { scopeContextSnapshotInput } from './scope-context-observation.mjs';
8
9
 
9
10
  function unknownTarget(path, site) {
10
11
  const reason = typeof site?.reason === 'string' && /^[a-z-]+$/.test(site.reason) ? site.reason : 'unknown-import-source';
@@ -23,6 +24,7 @@ async function observe(input, root, expected) {
23
24
  }
24
25
  export async function capturePreservedMutationProtection(input, root, authority, snapshot) {
25
26
  const delegation = preservedMutationDelegation(authority), review = await readDisciplineSnapshot(snapshot);
27
+ input = (await scopeContextSnapshotInput(input, root, review.scopeContext)).input;
26
28
  for (const expected of delegation.targetFiles) {
27
29
  const file = snapshot.manifest.files.find(item => item.path === expected.path);
28
30
  if (delegation.purpose === 'planned-file-create') {
@@ -4,6 +4,7 @@ import { canonical, sha256 } from './local-integrity.mjs';
4
4
  import { readBudgetedProjectFile } from './local-budget-reads.mjs';
5
5
  import { verifiedSfnt } from './local-scope-font.mjs';
6
6
  import { verifiedZip } from './local-scope-zip.mjs';
7
+ import { observeScopeAbsence, observeScopeRead, observeScopeTarget } from './local-scope-read-set.mjs';
7
8
 
8
9
  export function scopeFail(code, message) { throw Object.assign(new Error(message), { code }); }
9
10
  export function observedScopeRisk(files) {
@@ -66,6 +67,7 @@ export async function scopeTarget(root, relative, allowMissing = false, allowDep
66
67
  current = path.join(current, part);
67
68
  let stat;
68
69
  try { stat = await fs.lstat(current); } catch (error) {
70
+ if (error.code === 'ENOENT') observeScopeAbsence(root, relative, () => scopeTarget(root, relative, true, allowDependency));
69
71
  if (error.code === 'ENOENT' && allowMissing) return { target: path.join(root, relative), exists: false, stat: null };
70
72
  throw error;
71
73
  }
@@ -82,7 +84,11 @@ export async function scopeTarget(root, relative, allowMissing = false, allowDep
82
84
  }
83
85
  }
84
86
  if (index < parts.length - 1 && !stat.isDirectory()) await invalidScopeTarget(root, relative, current, 'non-directory-ancestor');
85
- if (index === parts.length - 1) return { target: current, exists: true, stat, dependency: followDependency };
87
+ if (index === parts.length - 1) {
88
+ const target = { target: current, exists: true, stat, dependency: followDependency };
89
+ observeScopeTarget(root, relative, target, () => scopeTarget(root, relative, true, allowDependency));
90
+ return target;
91
+ }
86
92
  }
87
93
  }
88
94
  // Only speculative, read-only resolver candidates may treat a non-directory ancestor as absent.
@@ -90,6 +96,7 @@ export async function scopeProbeTarget(root, relative, allowDependency = false)
90
96
  try { return await scopeTarget(root, relative, true, allowDependency); }
91
97
  catch (error) {
92
98
  if (error.code !== 'ENTROROVE_FILE_INVALID' || error.reason !== 'non-directory-ancestor') throw error;
99
+ observeScopeAbsence(root, relative, () => scopeProbeTarget(root, relative, allowDependency));
93
100
  return { target: path.join(root, relative), exists: false, stat: null };
94
101
  }
95
102
  }
@@ -109,7 +116,12 @@ async function safeScopeRead(root, relative, input, allowDependency = true) {
109
116
  || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs || latest.ino !== after.ino
110
117
  || latest.dev !== after.dev || !dependency && latest.isSymbolicLink() || beforeReal !== afterReal) scopeFail('ENTROROVE_SCOPE_CHANGED', 'scope source changed during inspection');
111
118
  await scopeTarget(root, relative, false, dependency);
112
- return { content, mode: after.mode & 0o777, sha256: sha256(content), bytes: content.length };
119
+ const digest = sha256(content);
120
+ observeScopeRead(root, relative, { sha256: digest, mode: after.mode & 0o777, realPath: afterReal ?? target.target }, async () => {
121
+ const actual = await safeScopeRead(root, relative, input, allowDependency);
122
+ return { sha256: actual.sha256, mode: actual.mode, realPath: await fs.realpath(target.target) };
123
+ });
124
+ return { content, mode: after.mode & 0o777, sha256: digest, bytes: content.length };
113
125
  } finally { await handle.close(); }
114
126
  }
115
127
  export const scopeRead = (root, relative, input = { repositoryRoot: root }) => safeScopeRead(root, relative, input, true);
@@ -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
  }
@@ -364,7 +420,11 @@ export async function collectScopeFacts(root, requested, baselineRef, knownFiles
364
420
  const read = await scopeRead(root, relative, input); const boundary = await packageBoundary(root, relative, input);
365
421
  files.push({ path: relative, exists: true, sha256: read.sha256, bytes: read.bytes, lines: lineCount(read.content), module: boundary.path });
366
422
  }
367
- const graph = await importGraph(root, files, input); const observedGit = await observeGitFacts(root, files, baselineRef, input); const history = observedGit.facts;
423
+ const graph = await importGraph(root, files, input);
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); });
427
+ const observedGit = await observeGitFacts(root, files, baselineRef, input); const history = observedGit.facts;
368
428
  const modules = [...new Map(graph.nodes.map(node => [node.boundary.path, node.boundary])).values()].sort((a, b) => a.path.localeCompare(b.path));
369
429
  await stableScope(root, files, input);
370
430
  await stableScope(root, modules.filter(module => module.manifestPath).map(module => ({ path: module.manifestPath, sha256: module.manifestDigest })), input);
@@ -377,3 +437,27 @@ export async function collectScopeFacts(root, requested, baselineRef, knownFiles
377
437
  crossModule: modules.length > 1, risk: observedScopeRisk(files), importGraph: graph, git: history,
378
438
  estimatedChangedLines };
379
439
  }
440
+ async function contextEvidence(root, graph, input) {
441
+ if (input.contextEvidence === undefined) return;
442
+ if (!/^[a-f0-9]{64}$/.test(input.contextRevisionDigest ?? '') || !Array.isArray(input.contextEvidence)
443
+ || input.contextEvidence.length > 200 || new Set(input.contextEvidence.map(file => file?.path)).size !== input.contextEvidence.length) {
444
+ scopeFail('ENTROROVE_SCOPE_CONTEXT_BINDING', 'Context evidence requires its bounded unique actual file hashes and revision');
445
+ }
446
+ const metadata = new Map(graph.workspaceMetadata.map(file => [file.path, file])); let bytes = 0;
447
+ for (const file of input.contextEvidence) {
448
+ if (!file || Object.keys(file).sort().join(',') !== 'path,sha256' || !/^[a-f0-9]{64}$/.test(file.sha256 ?? '')) {
449
+ scopeFail('ENTROROVE_SCOPE_CONTEXT_BINDING', 'Context evidence requires exact path and hash fields');
450
+ }
451
+ const target = await scopeTarget(root, file.path);
452
+ if (!target.stat.isFile() || target.stat.size > 1_048_576 || (bytes += target.stat.size) > 8_388_608) {
453
+ scopeFail('ENTROROVE_SCOPE_LIMIT', 'Context evidence exceeds its bounded actual file inspection');
454
+ }
455
+ const read = await scopeRead(root, file.path, input);
456
+ if (read.sha256 !== file.sha256 || metadata.has(file.path) && metadata.get(file.path).sha256 !== read.sha256) {
457
+ scopeFail('ENTROROVE_SCOPE_CHANGED', 'Actual context build evidence changed since its exact review');
458
+ }
459
+ if (!metadata.has(file.path)) metadata.set(file.path, { path: file.path, sha256: read.sha256 });
460
+ }
461
+ graph.workspaceMetadata = [...metadata.values()].sort((left, right) => left.path.localeCompare(right.path));
462
+ await stableScope(root, graph.workspaceMetadata, input);
463
+ }
@@ -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,15 +35,45 @@ 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
  }
40
66
  rejectScopePrivileges(input);
67
+ if (Object.hasOwn(input, 'contextRevisionDigest') && (!/^[a-f0-9]{64}$/.test(input.contextRevisionDigest ?? '')
68
+ || !Array.isArray(input.importContexts))) scopeFail('ENTROROVE_SCOPE_ATTESTATION_BINDING', 'Context proof requires its exact contexts and revision digest together');
41
69
  if (input.signerConfigPath === undefined || input.signerConfigPath === null) return result;
42
70
  binding(input, result);
71
+ const inspectedInput = canonical(input);
43
72
  const root = await scopeRoot(input.repositoryRoot); const snapshot = await readScopeSnapshot(input, root);
44
- 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;
45
77
  actual.snapshotEvidence = await verifyFreshScopeSnapshot(input, root, actual);
46
78
  const candidate = await verifiedScopeCandidate(input, root, actual.snapshotEvidence.manifest);
47
79
  if (root !== result.repositoryRoot || canonical(actual) !== canonical(result.facts)
@@ -60,15 +92,29 @@ export async function attestScopeFacts(input, result, { maximumExpiresAt } = {})
60
92
  if (error.code === 'ENTROROVE_SCOPE_SIGNER_INVALID') throw error;
61
93
  scopeFail('ENTROROVE_SCOPE_SIGNER_INVALID', 'scope signer configuration or signing key could not be read');
62
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
+ }
63
108
  const now = Date.now();
64
109
  const expiresAt = Math.min(now + config.receiptTtlMs, candidate ? Date.parse(candidate.expiresAt) : Infinity,
65
110
  maximumExpiresAt ?? Infinity);
66
111
  if (expiresAt <= now) scopeFail('ENTROROVE_SCOPE_ATTESTATION_BINDING', 'candidate challenge expired before its actual scope signature');
67
112
  const core = { schemaVersion: 'entrorove.scope-facts/1.0', flowId: input.flowId, stepId: input.stepId,
68
113
  taskDigest: input.taskDigest, ownerId: input.ownerId, chainDigest: input.chainDigest, repositoryId: input.repositoryId,
69
- facts: result.facts, snapshotDigest: result.snapshotDigest, challenge: input.challenge,
114
+ facts: actual, snapshotDigest: actual.snapshotEvidence.snapshotDigest, challenge: input.challenge,
70
115
  issuedAt: new Date(now).toISOString(), expiresAt: new Date(expiresAt).toISOString(),
71
- nonce: randomUUID(), keyId: config.keyId };
116
+ nonce: randomUUID(), keyId: config.keyId,
117
+ ...(input.contextRevisionDigest === undefined ? {} : { importContexts: input.importContexts, contextRevisionDigest: input.contextRevisionDigest }) };
72
118
  const signature = sign(null, Buffer.from(canonical(core)), privateKey).toString('base64url');
73
119
  return { ...result, attestation: { ...core, signature }, trusted: false, status: 'reported' };
74
120
  }