gate-evm-tools 1.2.1 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/bin/cli.js +3 -0
- package/bin/delivery.cjs +11 -6
- package/bin/migration.js +38 -0
- package/dist/commands/prepare.d.ts.map +1 -1
- package/dist/commands/prepare.js +7 -1
- package/dist/commands/prepare.js.map +1 -1
- package/dist/core/BytecodeChecker.d.ts.map +1 -1
- package/dist/core/BytecodeChecker.js +27 -4
- package/dist/core/BytecodeChecker.js.map +1 -1
- package/dist/core/preparation/artifacts.d.ts.map +1 -1
- package/dist/core/preparation/artifacts.js +12 -5
- package/dist/core/preparation/artifacts.js.map +1 -1
- package/dist/core/preparation/index.d.ts +3 -0
- package/dist/core/preparation/index.d.ts.map +1 -1
- package/dist/core/preparation/index.js +9 -2
- package/dist/core/preparation/index.js.map +1 -1
- package/dist/core/preparation/zksync.d.ts.map +1 -1
- package/dist/core/preparation/zksync.js +5 -0
- package/dist/core/preparation/zksync.js.map +1 -1
- package/dist/core/validateAndGenerateCalldata.d.ts.map +1 -1
- package/dist/core/validateAndGenerateCalldata.js +3 -0
- package/dist/core/validateAndGenerateCalldata.js.map +1 -1
- package/dist/deployment-zksync/index.d.ts +40 -0
- package/dist/deployment-zksync/index.d.ts.map +1 -0
- package/dist/deployment-zksync/index.js +59 -0
- package/dist/deployment-zksync/index.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -12
- package/dist/index.js.map +1 -1
- package/dist/migration/index.d.ts +76 -0
- package/dist/migration/index.d.ts.map +1 -0
- package/dist/migration/index.js +284 -0
- package/dist/migration/index.js.map +1 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist-cjs/core/BytecodeChecker.js +27 -4
- package/dist-cjs/core/BytecodeChecker.js.map +1 -1
- package/dist-cjs/core/preparation/artifacts.js +11 -4
- package/dist-cjs/core/preparation/artifacts.js.map +1 -1
- package/dist-cjs/core/preparation/index.js +47 -6
- package/dist-cjs/core/preparation/index.js.map +1 -1
- package/dist-cjs/core/preparation/zksync.js +5 -0
- package/dist-cjs/core/preparation/zksync.js.map +1 -1
- package/dist-cjs/deployment-zksync/index.js +67 -0
- package/dist-cjs/deployment-zksync/index.js.map +1 -0
- package/dist-cjs/migration/index.js +328 -0
- package/dist-cjs/migration/index.js.map +1 -0
- package/dist-runtime/delivery/aggregate-diagnostics.js +3 -0
- package/dist-runtime/delivery/aggregate-diagnostics.js.map +1 -1
- package/dist-runtime/delivery/aggregate-inputs.d.ts +32 -6
- package/dist-runtime/delivery/aggregate-inputs.js +7 -3
- package/dist-runtime/delivery/aggregate-inputs.js.map +1 -1
- package/dist-runtime/delivery/aggregate-worker.js +3 -3
- package/dist-runtime/delivery/aggregate-worker.js.map +1 -1
- package/dist-runtime/delivery/aggregate.js +2 -2
- package/dist-runtime/delivery/aggregate.js.map +1 -1
- package/dist-runtime/delivery/cli-host.d.ts +17 -0
- package/dist-runtime/delivery/cli-host.js +118 -0
- package/dist-runtime/delivery/cli-host.js.map +1 -0
- package/dist-runtime/delivery/cli.d.ts +114 -0
- package/dist-runtime/delivery/cli.js +228 -0
- package/dist-runtime/delivery/cli.js.map +1 -0
- package/dist-runtime/delivery/compiler-dependency-pins.js +30 -1
- package/dist-runtime/delivery/compiler-dependency-pins.js.map +1 -1
- package/dist-runtime/delivery/configuration-definition.js +152 -11
- package/dist-runtime/delivery/configuration-definition.js.map +1 -1
- package/dist-runtime/delivery/configuration-hre.js +1 -1
- package/dist-runtime/delivery/configuration-hre.js.map +1 -1
- package/dist-runtime/delivery/configuration.js +7 -4
- package/dist-runtime/delivery/configuration.js.map +1 -1
- package/dist-runtime/delivery/custom-format.d.ts +2 -1
- package/dist-runtime/delivery/custom-format.js +15 -2
- package/dist-runtime/delivery/custom-format.js.map +1 -1
- package/dist-runtime/delivery/custom.d.ts +4 -0
- package/dist-runtime/delivery/custom.js +21 -0
- package/dist-runtime/delivery/custom.js.map +1 -1
- package/dist-runtime/delivery/development-inputs.d.ts +9 -0
- package/dist-runtime/delivery/development-inputs.js +41 -0
- package/dist-runtime/delivery/development-inputs.js.map +1 -1
- package/dist-runtime/delivery/development.d.ts +1 -1
- package/dist-runtime/delivery/development.js +5 -2
- package/dist-runtime/delivery/development.js.map +1 -1
- package/dist-runtime/delivery/execution-records.d.ts +16 -0
- package/dist-runtime/delivery/execution-records.js +48 -0
- package/dist-runtime/delivery/execution-records.js.map +1 -0
- package/dist-runtime/delivery/existing-code.d.ts +3 -0
- package/dist-runtime/delivery/existing-code.js +20 -0
- package/dist-runtime/delivery/existing-code.js.map +1 -1
- package/dist-runtime/delivery/input-files.d.ts +2 -0
- package/dist-runtime/delivery/input-files.js +5 -0
- package/dist-runtime/delivery/input-files.js.map +1 -1
- package/dist-runtime/delivery/inputs.js +11 -6
- package/dist-runtime/delivery/inputs.js.map +1 -1
- package/dist-runtime/delivery/materialization.js +20 -1
- package/dist-runtime/delivery/materialization.js.map +1 -1
- package/dist-runtime/delivery/planning.d.ts +1 -0
- package/dist-runtime/delivery/planning.js +10 -2
- package/dist-runtime/delivery/planning.js.map +1 -1
- package/dist-runtime/delivery/production-finalization.js +3 -2
- package/dist-runtime/delivery/production-finalization.js.map +1 -1
- package/dist-runtime/delivery/production.d.ts +2 -0
- package/dist-runtime/delivery/production.js +5 -4
- package/dist-runtime/delivery/production.js.map +1 -1
- package/dist-runtime/delivery/project-files.js +6 -1
- package/dist-runtime/delivery/project-files.js.map +1 -1
- package/dist-runtime/delivery/project-rpc.d.ts +1 -1
- package/dist-runtime/delivery/project-rpc.js +10 -4
- package/dist-runtime/delivery/project-rpc.js.map +1 -1
- package/dist-runtime/delivery/protocol.d.ts +6 -4
- package/dist-runtime/delivery/protocol.js +16 -5
- package/dist-runtime/delivery/protocol.js.map +1 -1
- package/dist-runtime/delivery/query.d.ts +21 -0
- package/dist-runtime/delivery/query.js +58 -0
- package/dist-runtime/delivery/query.js.map +1 -0
- package/dist-runtime/delivery/release-graph.d.ts +17 -0
- package/dist-runtime/delivery/release-graph.js +23 -0
- package/dist-runtime/delivery/release-graph.js.map +1 -1
- package/dist-runtime/delivery/release-history-capture.js +5 -2
- package/dist-runtime/delivery/release-history-capture.js.map +1 -1
- package/dist-runtime/delivery/release-history.js +19 -12
- package/dist-runtime/delivery/release-history.js.map +1 -1
- package/dist-runtime/delivery/release.d.ts +22 -0
- package/dist-runtime/delivery/release.js +13 -3
- package/dist-runtime/delivery/release.js.map +1 -1
- package/dist-runtime/delivery/scripts.d.ts +116 -0
- package/dist-runtime/delivery/scripts.js +289 -0
- package/dist-runtime/delivery/scripts.js.map +1 -0
- package/dist-runtime/delivery/setup.d.ts +1 -1
- package/dist-runtime/delivery/setup.js +26 -36
- package/dist-runtime/delivery/setup.js.map +1 -1
- package/dist-runtime/delivery/source.d.ts +5 -2
- package/dist-runtime/delivery/source.js +4 -3
- package/dist-runtime/delivery/source.js.map +1 -1
- package/dist-runtime/delivery/standalone-source.d.ts +40 -0
- package/dist-runtime/delivery/standalone-source.js +54 -0
- package/dist-runtime/delivery/standalone-source.js.map +1 -0
- package/dist-runtime/runtime-identity.json +2 -2
- package/dist-runtime/workflow/lib/calls.js +21 -1
- package/dist-runtime/workflow/lib/calls.js.map +1 -1
- package/docs/configuration-runtime-observation.md +4 -2
- package/docs/delivery-custom-input-closure.md +2 -0
- package/docs/delivery-development.md +2 -0
- package/docs/delivery-inputs.md +2 -0
- package/docs/delivery-release.md +3 -1
- package/docs/delivery-setup.md +25 -20
- package/docs/delivery-source.md +4 -2
- package/docs/migration.md +108 -0
- package/docs/script-execution.md +85 -0
- package/package.json +42 -19
- package/runtime/delivery/aggregate-diagnostics.ts +2 -0
- package/runtime/delivery/aggregate-inputs.ts +6 -5
- package/runtime/delivery/aggregate-worker.ts +3 -3
- package/runtime/delivery/aggregate.ts +2 -2
- package/runtime/delivery/cli-host.ts +55 -0
- package/runtime/delivery/cli.ts +124 -0
- package/runtime/delivery/compiler-dependency-pins.ts +30 -1
- package/runtime/delivery/configuration-definition.ts +80 -9
- package/runtime/delivery/configuration-hre.ts +1 -1
- package/runtime/delivery/configuration.ts +7 -5
- package/runtime/delivery/custom-format.ts +11 -3
- package/runtime/delivery/custom.ts +13 -0
- package/runtime/delivery/development-inputs.ts +31 -0
- package/runtime/delivery/development.ts +4 -3
- package/runtime/delivery/execution-records.ts +31 -0
- package/runtime/delivery/existing-code.ts +12 -1
- package/runtime/delivery/input-files.ts +5 -1
- package/runtime/delivery/inputs.ts +10 -6
- package/runtime/delivery/materialization.ts +8 -1
- package/runtime/delivery/planning.ts +10 -2
- package/runtime/delivery/production-finalization.ts +3 -2
- package/runtime/delivery/production.ts +5 -4
- package/runtime/delivery/project-files.ts +5 -1
- package/runtime/delivery/project-rpc.ts +10 -4
- package/runtime/delivery/protocol.ts +13 -5
- package/runtime/delivery/query.ts +34 -0
- package/runtime/delivery/release-graph.ts +17 -0
- package/runtime/delivery/release-history-capture.ts +2 -1
- package/runtime/delivery/release-history.ts +8 -3
- package/runtime/delivery/release.ts +7 -4
- package/runtime/delivery/scripts.ts +162 -0
- package/runtime/delivery/setup.ts +21 -31
- package/runtime/delivery/source.ts +7 -3
- package/runtime/delivery/standalone-source.ts +40 -0
- package/runtime/workflow/lib/calls.ts +19 -1
- package/dist-runtime/delivery/toolchain.d.ts +0 -3
- package/dist-runtime/delivery/toolchain.js +0 -52
- package/dist-runtime/delivery/toolchain.js.map +0 -1
- package/runtime/delivery/toolchain.ts +0 -48
|
@@ -39,13 +39,14 @@ function inspect(raw: ProductionOptions, command: ProductionCommand) {
|
|
|
39
39
|
const input = deepFreeze(strictData(raw));
|
|
40
40
|
exactObject(input, keys, 'production options');
|
|
41
41
|
safeId(input.executionId, 'executionId');
|
|
42
|
-
const mode = command === 'prepare' ? 'simulate' : command;
|
|
43
|
-
const request = parseDeliveryRequest({ version: 1, environment: 'prod', purpose: 'prod-execution', mode, network: input.target, operator: input.operator, releaseRef: input.releaseRef, sourceRef: { path: input.handoff, taskId: input.taskId, planKey: input.planKey, rev: input.rev } });
|
|
44
42
|
const release = inspectDeliveryRelease(input);
|
|
45
43
|
if (release.status === 'pending')
|
|
46
44
|
return { status: 'pending' as const, input };
|
|
45
|
+
const mode = command === 'prepare' ? 'simulate' : command;
|
|
46
|
+
const native= (require('./release-graph') as typeof import('./release-graph')).standaloneReleaseDefinition(input);
|
|
47
|
+
const request = parseDeliveryRequest({ version: native?2:1, environment: 'prod', purpose: 'prod-execution', mode, network: input.target, operator: input.operator, releaseRef: input.releaseRef, sourceRef: native?{kind:'standalone',path:input.handoff,deploymentId:input.deploymentId}:{ path: input.handoff, taskId: input.taskId, planKey: input.planKey, rev: input.rev } });
|
|
47
48
|
const source = readOwnedDeliverySource({ workspaceRoot: input.workspaceRoot, projectRoot: input.projectRoot, request });
|
|
48
|
-
if ((source.
|
|
49
|
+
if ((source.scope as any).deploymentId !== input.deploymentId)
|
|
49
50
|
throw Error('Production deployment mismatch');
|
|
50
51
|
const target = release.release.targets.find((t: any) => t.id === input.target);
|
|
51
52
|
if (!target)
|
|
@@ -69,7 +70,7 @@ export function inspectDeliveryProduction(raw: ProductionOptions, command: Produ
|
|
|
69
70
|
if(canonicalJson(deliveryCalls)!==canonicalJson(record.deliveryCalls))throw Error('Frozen production public projection mismatch');
|
|
70
71
|
if (record.releaseHash !== current.releaseHash || plan.inputsHash !== current.release.inputsHash || plan.sourceDefinitionHash !== current.release.sourceDefinitionHash || plan.executionId !== current.input.executionId || plan.target !== current.input.target || plan.operator !== current.input.operator.toLowerCase() || plan.environment !== 'prod' || plan.chainId !== String(current.target.chainId))
|
|
71
72
|
throw Error('Production artifact context mismatch');
|
|
72
|
-
return deepFreeze({ status: 'ready' as const, plan, collection: record.collection as DeliveryCollection, deliveryCalls, operations: current.release.operations as any[], finalization: record.finalization, releaseHash: current.releaseHash, artifactHash, sha256: byteHash(bytes), path: artifactPath(current.input.executionId), ...(current.target.safe ? { safeTarget: current.target.safe } : {}) });
|
|
73
|
+
return deepFreeze({ status: 'ready' as const, plan, collection: record.collection as DeliveryCollection, deliveryCalls, parameters:current.release.parameters.find((p:any)=>p.target===current.input.target)?.resolved.values, operations: current.release.operations as any[], finalization: record.finalization, releaseHash: current.releaseHash, artifactHash, sha256: byteHash(bytes), path: artifactPath(current.input.executionId), ...(current.target.safe ? { safeTarget: current.target.safe } : {}) });
|
|
73
74
|
}
|
|
74
75
|
/** Instantiate the delivered business operations, never accept replacement operations or parameters. */
|
|
75
76
|
export async function prepareDeliveryProduction(raw: ProductionOptions) {
|
|
@@ -35,7 +35,10 @@ export function readFile(root: string, relative: string): string | undefined {
|
|
|
35
35
|
return stat(file) ? fs.readFileSync(file, 'utf8') : undefined;
|
|
36
36
|
}
|
|
37
37
|
export function deliveryProjectMarker(root: string): void {
|
|
38
|
-
|
|
38
|
+
const marker = readFile(root, '.gate-template.json');
|
|
39
|
+
// Existing Hardhat projects have no generator provenance. Setup validates their
|
|
40
|
+
// real configuration and local capabilities without inventing a template identity.
|
|
41
|
+
if (marker !== undefined) parseDeliveryProjectMarker(marker);
|
|
39
42
|
}
|
|
40
43
|
/** Validate the same captured marker bytes whose digest is bound to current inputs. */
|
|
41
44
|
export function parseDeliveryProjectMarker(bytes: string | Buffer): void {
|
|
@@ -52,6 +55,7 @@ export function parseDeliveryProjectMarker(bytes: string | Buffer): void {
|
|
|
52
55
|
const optional = ['README.md', 'AGENTS.md', '.gitignore'];
|
|
53
56
|
const assets = ['package.json', 'package-lock.json', 'hardhat.config.ts', 'rpc.env', '.env.example', 'tsconfig.json', ...optional, 'contracts/.gitkeep', 'test/.gitkeep',
|
|
54
57
|
'foundry.toml', 'config.yaml',
|
|
58
|
+
...(Object.hasOwn(files, 'hardhat.zk.config.ts') ? ['hardhat.zk.config.ts'] : []),
|
|
55
59
|
...(Object.hasOwn(files, 'vendor/gate-evm-tools-1.2.0.tgz') ? ['vendor/gate-evm-tools-1.2.0.tgz'] : []),
|
|
56
60
|
...(inputs.examples === true ? ['contracts/Counter.sol', 'test/Counter.ts', 'test/Counter.t.sol'] : [])];
|
|
57
61
|
if (typeof inputs.projectName !== 'string' || !inputs.projectName || !Array.isArray(inputs.networks)
|
|
@@ -10,7 +10,7 @@ export const PROJECT_ENV_BOOTSTRAP = `for (const file of ["rpc.env", ".env"]) {
|
|
|
10
10
|
process.loadEnvFile(envFile);
|
|
11
11
|
} catch (error) {
|
|
12
12
|
if (file !== ".env" || !["ENOENT", "EPERM", "EACCES"].includes((error as NodeJS.ErrnoException).code ?? "")) throw error;
|
|
13
|
-
console.warn("Project credentials are unavailable; compilation and public checks can continue. Use the
|
|
13
|
+
console.warn("Project credentials are unavailable; compilation and public checks can continue. Use the terminal or App delivery host for credential-bearing operations.");
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
}`;
|
|
@@ -30,7 +30,11 @@ function statement(source: string): string {
|
|
|
30
30
|
const file = ts.createSourceFile('rpc.ts', source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
31
31
|
return syntax(file.statements[0], file);
|
|
32
32
|
}
|
|
33
|
-
|
|
33
|
+
// Retain the previous public diagnostic spelling used by migrated projects;
|
|
34
|
+
// the loader's file access, precedence and error handling remain exact.
|
|
35
|
+
const bootstrapSyntax = new Set([PROJECT_ENV_BOOTSTRAP,
|
|
36
|
+
PROJECT_ENV_BOOTSTRAP.replace('the terminal or App delivery host for credential-bearing operations', 'the application host for credential-bearing delivery operations'),
|
|
37
|
+
].map(statement)), helperSyntax = statement(PROJECT_RPC_HELPER);
|
|
34
38
|
|
|
35
39
|
export function projectRpcSyntax(file: ts.SourceFile): { statements: Set<ts.Statement>; bootstrap: boolean; helper: boolean } {
|
|
36
40
|
const statements = new Set<ts.Statement>();
|
|
@@ -46,7 +50,7 @@ export function projectRpcSyntax(file: ts.SourceFile): { statements: Set<ts.Stat
|
|
|
46
50
|
|| clause.namedBindings.elements.length !== 1 || clause.namedBindings.elements[0].name.text !== wanted
|
|
47
51
|
|| clause.namedBindings.elements[0].propertyName) throw Error('Unsupported project RPC import');
|
|
48
52
|
imports.add(name); statements.add(node);
|
|
49
|
-
} else if (ts.isForOfStatement(node) && syntax(node, file)
|
|
53
|
+
} else if (ts.isForOfStatement(node) && bootstrapSyntax.has(syntax(node, file))) {
|
|
50
54
|
if (bootstrap) throw Error('Duplicate project RPC loader');
|
|
51
55
|
bootstrap = true; statements.add(node);
|
|
52
56
|
} else if (ts.isFunctionDeclaration(node) && node.name?.text === 'rpcUrl') {
|
|
@@ -97,7 +101,9 @@ function projectEnvironment(root: string, source: string, env: NodeJS.ProcessEnv
|
|
|
97
101
|
for (const [key, value] of Object.entries(parseEnv(contents))) if (effective[key] === undefined) effective[key] = value;
|
|
98
102
|
} catch (error) {
|
|
99
103
|
if (name === '.env' && ['EPERM', 'EACCES'].includes((error as NodeJS.ErrnoException).code ?? '')) {
|
|
100
|
-
|
|
104
|
+
// Match the canonical Hardhat bootstrap: public RPC configuration can proceed
|
|
105
|
+
// without a protected credential file. Signing hosts load credentials separately.
|
|
106
|
+
continue;
|
|
101
107
|
}
|
|
102
108
|
throw Error(`Cannot read local project RPC environment (${name})`);
|
|
103
109
|
}
|
|
@@ -3,12 +3,12 @@ import { validateNetworkAlias } from './policy';
|
|
|
3
3
|
export type DeliveryEnvironment = 'dev' | 'prod';
|
|
4
4
|
export type DeliveryMode = 'simulate' | 'execute' | 'export-calldata' | 'reconcile' | 'verify';
|
|
5
5
|
export interface DeliveryRequest {
|
|
6
|
-
version: 1;
|
|
6
|
+
version: 1 | 2;
|
|
7
7
|
environment: DeliveryEnvironment;
|
|
8
8
|
purpose: 'dev-execution' | 'prod-qualification' | 'prod-execution';
|
|
9
9
|
mode: DeliveryMode;
|
|
10
10
|
network: string;
|
|
11
|
-
sourceRef: { path: string; taskId
|
|
11
|
+
sourceRef: { kind?: 'gateflow' | 'standalone'; path: string; taskId?: string; planKey?: string; rev?: number; deploymentId?: string };
|
|
12
12
|
releaseRef?: { path: string; releaseId: string; inputsHash: string };
|
|
13
13
|
fork?: { blockNumber: number; blockHash: string; readOnly: true };
|
|
14
14
|
devSigner?: string;
|
|
@@ -43,7 +43,7 @@ export function positiveInteger(value: unknown, label: string): number {
|
|
|
43
43
|
}
|
|
44
44
|
export function parseDeliveryRequest(input: unknown): DeliveryRequest {
|
|
45
45
|
const value = object(input, 'delivery request', ['version', 'environment', 'purpose', 'mode', 'network', 'sourceRef', 'releaseRef', 'fork', 'devSigner', 'operator']);
|
|
46
|
-
if (value.version !== 1) throw new Error('Unsupported delivery request version');
|
|
46
|
+
if (value.version !== 1 && value.version !== 2) throw new Error('Unsupported delivery request version');
|
|
47
47
|
if (typeof value.environment !== 'string' || !['dev', 'prod'].includes(value.environment)) throw new Error('environment must be dev or prod');
|
|
48
48
|
if (typeof value.purpose !== 'string' || !['dev-execution', 'prod-qualification', 'prod-execution'].includes(value.purpose)) throw new Error('Invalid purpose');
|
|
49
49
|
const dev = value.purpose === 'dev-execution';
|
|
@@ -51,8 +51,16 @@ export function parseDeliveryRequest(input: unknown): DeliveryRequest {
|
|
|
51
51
|
const modes = dev ? ['simulate', 'execute', 'reconcile', 'verify'] : value.purpose === 'prod-qualification' ? ['simulate', 'verify'] : ['simulate', 'execute', 'export-calldata', 'reconcile', 'verify'];
|
|
52
52
|
if (typeof value.mode !== 'string' || !modes.includes(value.mode)) throw new Error('Explicit valid mode is required for purpose');
|
|
53
53
|
validateNetworkAlias(value.network, 'network', false);
|
|
54
|
-
const source = object(value.sourceRef, 'sourceRef', ['path', 'taskId', 'planKey', 'rev']);
|
|
55
|
-
portablePath(source.path, 'sourceRef.path');
|
|
54
|
+
const source = object(value.sourceRef, 'sourceRef', ['kind', 'path', 'taskId', 'planKey', 'rev', 'deploymentId']);
|
|
55
|
+
portablePath(source.path, 'sourceRef.path');
|
|
56
|
+
if (value.version === 2 && !['standalone','gateflow'].includes(String(source.kind))) throw Error('v2 requires explicit source kind');
|
|
57
|
+
if (source.kind === 'standalone') {
|
|
58
|
+
if (value.version !== 2 || Object.keys(source).some(k=>!['kind','path','deploymentId'].includes(k))) throw Error('Invalid standalone source reference');
|
|
59
|
+
safeId(source.deploymentId, 'source deploymentId');
|
|
60
|
+
} else {
|
|
61
|
+
if (source.kind !== undefined && source.kind !== 'gateflow' || source.deploymentId !== undefined) throw Error('Invalid GateFlow source reference');
|
|
62
|
+
safeId(source.taskId, 'taskId'); safeId(source.planKey, 'planKey'); positiveInteger(source.rev, 'rev');
|
|
63
|
+
}
|
|
56
64
|
if (dev) {
|
|
57
65
|
address(value.devSigner, 'devSigner');
|
|
58
66
|
if ('operator' in value) throw new Error('devSigner and prod operator are separate roles');
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Bounded read-only ABI query; no provider or signer is exposed to a business module. */
|
|
2
|
+
import { Interface } from 'ethers';
|
|
3
|
+
import { address, object } from './protocol';
|
|
4
|
+
import { validateAbiArguments, validateAbiValue, normalizeRuntimeValue, compareAbiResults } from './abi';
|
|
5
|
+
import { strictData } from './data';
|
|
6
|
+
import { deepFreeze } from '../workflow/lib/canonicalData';
|
|
7
|
+
import type { ContractQuery } from './scripts';
|
|
8
|
+
export function parseContractQueryEvidence(raw:unknown){
|
|
9
|
+
const e=object(strictData(raw),'contract query evidence',['chainId','block','source','address','abi','method','args','caller','result','returnTypes']);
|
|
10
|
+
if(typeof e.chainId!=='string'||!(/^[1-9][0-9]*$/).test(e.chainId))throw Error('Invalid query chain');
|
|
11
|
+
const block=object(e.block,'query block',['number','hash']),source=object(e.source,'query source',['blockNumber','blockHash']);
|
|
12
|
+
if(typeof block.number!=='string'||!/^0x[\da-f]+$/i.test(block.number)||typeof block.hash!=='string'||!/^0x[\da-f]{64}$/i.test(block.hash)
|
|
13
|
+
||!Number.isSafeInteger(source.blockNumber)||Number(source.blockNumber)<0||typeof source.blockHash!=='string'||!/^0x[\da-f]{64}$/i.test(source.blockHash))throw Error('Invalid query block identity');
|
|
14
|
+
address(e.address,'query');address(e.caller,'query caller');
|
|
15
|
+
if(!Array.isArray(e.abi)||e.abi.length!==1||typeof e.abi[0]!=='string')throw Error('Invalid query ABI');
|
|
16
|
+
const fn=new Interface(e.abi).getFunction(e.method as string);
|
|
17
|
+
if(!fn||!['view','pure'].includes(fn.stateMutability)||fn.format('sighash')!==e.method)throw Error('Invalid read-only query method');
|
|
18
|
+
validateAbiArguments(e.abi,fn.format('sighash'),e.args);
|
|
19
|
+
if(JSON.stringify(fn.outputs.map(p=>p.format('full')))!==JSON.stringify(e.returnTypes))throw Error('Query return types mismatch');
|
|
20
|
+
if(fn.outputs.length===1)validateAbiValue(fn.outputs[0],e.result,'query result');
|
|
21
|
+
else {if(!Array.isArray(e.result)||e.result.length!==fn.outputs.length)throw Error('Invalid query result count');fn.outputs.forEach((p,i)=>validateAbiValue(p,(e.result as unknown[])[i],'query result'));}
|
|
22
|
+
return deepFreeze(e);
|
|
23
|
+
}
|
|
24
|
+
export async function readScriptContract(query: ContractQuery, input:{request(method:string,params:unknown[]):Promise<any>;chainId:string;block:{number:string;hash:string};caller:string;source:{blockNumber:number;blockHash:string}}){
|
|
25
|
+
const q=object(strictData(query),'contract query',['address','abi','method','args','caller']);
|
|
26
|
+
address(q.address,'query');if(!Array.isArray(q.abi)||q.abi.some(x=>typeof x!=='string'))throw Error('Query ABI must use function strings');
|
|
27
|
+
const iface=new Interface(q.abi),fn=iface.getFunction(q.method as string);if(!fn||!['view','pure'].includes(fn.stateMutability))throw Error('readContract requires a view/pure function ABI');
|
|
28
|
+
const caller=address(q.caller??input.caller,'query caller'),args=q.args??[];validateAbiArguments(q.abi,fn.format('sighash'),args);
|
|
29
|
+
const data=iface.encodeFunctionData(fn,args as any[]),raw=await input.request('eth_call',[{to:q.address,from:caller,data},input.block.number]);
|
|
30
|
+
const decoded=iface.decodeFunctionResult(fn,raw),value=normalizeRuntimeValue(fn.outputs.length===1?decoded[0]:decoded);
|
|
31
|
+
const block=await input.request('eth_getBlockByNumber',[input.block.number,false]);if(block?.hash!==input.block.hash)throw Error('Query block changed');
|
|
32
|
+
return deepFreeze({value,evidence:parseContractQueryEvidence({chainId:input.chainId,block:input.block,source:input.source,address:(q.address as string).toLowerCase(),abi:[fn.format('full')],method:fn.format('sighash'),args,caller:caller.toLowerCase(),result:value,returnTypes:fn.outputs.map(p=>p.format('full'))})});
|
|
33
|
+
}
|
|
34
|
+
export function requireQueryResult(query:ContractQuery & {expect:unknown},actual:unknown){const c=compareAbiResults(query.abi,query.method,actual,query.expect);if(c.actual!==c.expected)throw Error(`Unmet live dependency ${query.method}: expected ${c.expected}, got ${c.actual}`);}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {readStandaloneTask} from './standalone-source';
|
|
1
2
|
/** Actual-file release graph classification. No caller-selected roots or target subsets. */
|
|
2
3
|
import {parseCanonicalPlan,projectCanonicalDefinitions,deploymentPurpose} from './source';
|
|
3
4
|
import {projectRoot} from './project-files';
|
|
@@ -31,7 +32,23 @@ export function readReleaseUniverse(input:Omit<ReleaseGraphOptions,'releaseRef'>
|
|
|
31
32
|
const upstream=projectCanonicalDefinitions(text,{roots:qualifications.map(t=>t.planKey),mode:'upstream'});
|
|
32
33
|
return {workspace,root,text,plan,entries,relevant,targets,qualifications,upstream,developmentNetwork:task.developmentNetwork as string};
|
|
33
34
|
}
|
|
35
|
+
export function standaloneReleaseDefinition(input:Omit<ReleaseGraphOptions,'releaseRef'>){
|
|
36
|
+
const root=projectRoot(input.workspaceRoot,input.projectRoot),relative=`scripts/tasks/${safeId(input.deploymentId,'deploymentId')}/task.yaml`;
|
|
37
|
+
if(input.handoff!==relative)return undefined;
|
|
38
|
+
const task=object(parseInputDocument(readInputBytes(root,relative),'yaml'),'task');
|
|
39
|
+
if(task.source!=='standalone')return undefined;
|
|
40
|
+
if(input.handoff!==relative||input.taskId!==input.deploymentId)throw Error('Native release source mismatch');
|
|
41
|
+
return readStandaloneTask(root,relative);
|
|
42
|
+
}
|
|
43
|
+
export function selectedReleaseTargets(input:Omit<ReleaseGraphOptions,'releaseRef'>){return standaloneReleaseDefinition(input)?.releaseTargetNetworks??readReleaseUniverse(input).targets;}
|
|
34
44
|
export function readReleaseGraph(input:ReleaseGraphOptions){
|
|
45
|
+
const native=standaloneReleaseDefinition(input);
|
|
46
|
+
if(native){
|
|
47
|
+
safeId(input.releaseRef.releaseId,'releaseId');if(input.releaseRef.path!==`scripts/tasks/${input.deploymentId}/releases/${input.releaseRef.releaseId}/release.json`||!/^[a-f0-9]{64}$/.test(input.releaseRef.inputsHash))throw Error('Invalid native release reference');
|
|
48
|
+
const graph={schema:'gate-delivery-standalone-release',version:1,definition:native,releaseRef:input.releaseRef,targets:native.releaseTargetNetworks,stages:['qualify','prepare','execute-or-export','reconcile','verify']};
|
|
49
|
+
return deepFreeze({targets:native.releaseTargetNetworks,graph,prodTaskGraphHash:semanticHash(graph)});
|
|
50
|
+
}
|
|
51
|
+
|
|
35
52
|
const u=readReleaseUniverse(input);safeId(input.releaseRef.releaseId,'releaseId');portablePath(input.releaseRef.path,'releaseRef.path');if(!/^[a-f0-9]{64}$/.test(input.releaseRef.inputsHash))throw Error('Invalid release input hash');
|
|
36
53
|
// Classify ALL relevant production tasks first. Filtering a good ref first conceals split/wrong bindings.
|
|
37
54
|
const groups=new Map<string,typeof u.relevant>();
|
|
@@ -36,7 +36,8 @@ export function captureDevelopmentReleaseHistory(input:{root:string;source:Deliv
|
|
|
36
36
|
const {sourcePath:sourceFile,aggregatePath:aggregateFile}=persistDevelopmentInputs(input);
|
|
37
37
|
const descriptor:HistoricalDevelopmentFiles={version:2,aggregateFile,sourceFile,acceptanceFile,readyFile,inputsRoot:`${base}/inputs`,archiveFile};
|
|
38
38
|
for(const relative of [sourceFile,aggregateFile,acceptanceFile])saved.push({path:relative,sha256:byteHash(readInputBytes(root,relative))});
|
|
39
|
-
|
|
39
|
+
const snapshot=JSON.parse(readInputBytes(root,`.gate-delivery/development/${plan.executionId}.inputs.snapshot.json`).toString());
|
|
40
|
+
for(const name of [`${a.inputs.business.businessRoot}/task.yaml`,`${a.inputs.business.businessRoot}/parameters.yaml`,`${a.inputs.business.businessRoot}/parameters.provenance.json`,'config.yaml','hardhat.config.ts','package-lock.json','contractInfo.json','deploymentInfo.json']){const original=snapshot.files.find((f:any)=>f.path===name);write(`${base}/inputs/${name}`,original?Buffer.from(original.base64,'base64'):readInputBytes(root,name));}
|
|
40
41
|
write(archiveFile,archive);
|
|
41
42
|
writeImmutableDeliveryFile(root,readyFile,Buffer.from(canonicalJson({schema:'gate-delivery-history-ready',version:2,generation:'complete',planHash:plan.planHash,aggregateHash:semanticHash(a),sourceHash:semanticHash(input.source),descriptor,files:saved})+'\n'));return descriptor;
|
|
42
43
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Read-only consumption of R1 evidence. Historical runtime is verified, never executed or relabelled. */
|
|
2
|
-
import {existingCodeCoverage,requiredExistingAddresses} from './existing-code';
|
|
2
|
+
import {existingCodeCoverage,requiredExistingAddresses,interactionCoverageRegistry} from './existing-code';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import {aggregateInputHash} from './execution-identity';
|
|
5
5
|
export {aggregateInputHash} from './execution-identity';
|
|
@@ -110,10 +110,15 @@ export function validateHistoricalDevelopmentFiles(input:{root:string;workspaceR
|
|
|
110
110
|
// A custom script reading a whole changed container makes the relaxed projection unsafe.
|
|
111
111
|
const dependencyFiles=[...aggregate.inputs.targets.flatMap(t=>t.custom?.files??[]),...aggregate.inputs.business.parameters.flatMap(p=>p.closure?.files??[])];
|
|
112
112
|
for(const file of dependencyFiles)if(inputDigest(now(file.path))!==file.digest)throw Error('Historical custom/producer dependency changed');
|
|
113
|
+
if(source.targets[0].request.sourceRef.kind==='standalone'){
|
|
114
|
+
const native=require('./standalone-source').readStandaloneTask(root,source.targets[0].request.sourceRef.path);
|
|
115
|
+
const historical=(aggregate.inputs.sourceDefinitions as any[]);same({...native,releaseTargetNetworks:oldTask.releaseTargetNetworks},historical[0]?.definition,'Historical native scope with bounded prod expansion');
|
|
116
|
+
} else {
|
|
113
117
|
const handoff=source.targets[0].request.sourceRef.path,text=readInputBytes(workspace,handoff).toString(),actual=parseCanonicalPlan(text);if(actual.taskId!==source.targets[0].request.sourceRef.taskId)throw Error('Historical task source identity mismatch');
|
|
114
|
-
const projection=projectCanonicalDefinitions(text,{roots:[source.targets[0].request.sourceRef.planKey],mode:'upstream'}).snapshot;
|
|
118
|
+
const projection=projectCanonicalDefinitions(text,{roots:[source.targets[0].request.sourceRef.planKey!],mode:'upstream'}).snapshot;
|
|
115
119
|
const normalize=(v:any)=>{const copy=JSON.parse(canonicalJson(v));for(const t of copy.tasks)if(t.scope?.deployment&&Object.hasOwn(t.scope.deployment,'releaseTargetNetworks'))t.scope.deployment.releaseTargetNetworks=oldTask.releaseTargetNetworks;return copy;};
|
|
116
120
|
const historical=(aggregate.inputs.sourceDefinitions as any[]);if(historical.length!==1||historical[0].path!==handoff)throw Error('Historical source document mismatch');same(normalize(projection),historical[0].definition,'Historical upstream definition with bounded prod expansion');
|
|
121
|
+
}
|
|
117
122
|
const expected=expectedDevelopmentAssertions(collection,aggregate.inputs.business.steps);
|
|
118
123
|
if(!Array.isArray(acceptance.assertions)||acceptance.assertions.length!==expected.length)throw Error('Missing development business assertions');
|
|
119
124
|
for(const [index,raw] of acceptance.assertions.entries()){const assertion=parseVerificationObservation(raw);same({...assertion,actual:expected[index].actual},expected[index],'Historical complete typed assertion');const comparable=compareAbiResults(assertion.abi,assertion.method,decode(assertion.actual),decode(assertion.expected));if(comparable.actual!==comparable.expected)throw Error('Historical business assertion failed');}
|
|
@@ -136,7 +141,7 @@ export function validateHistoricalDevelopmentFiles(input:{root:string;workspaceR
|
|
|
136
141
|
if(!Array.isArray(record.changes))throw Error('Missing development registry changes');
|
|
137
142
|
const expectedCode:ExpectedDevelopmentCode[]=[],effective:any={[plan.target]:{}};
|
|
138
143
|
for(const change of record.changes){effective[plan.target][change.contractInfoKey]=change.address;const key=change.contractInfoKey.split(':')[0];expectedCode.push({key,contract:change.actualContract,address:change.implementationAddress??change.address});if(change.proxyKind)expectedCode.push({key:`${key}_Proxy`,contract:change.proxyKind==='transparent'?'TransparentUpgradeableProxy':'ERC1967Proxy',address:change.address});if(change.proxyKind==='transparent')expectedCode.push({key:`${key}_ProxyAdmin`,contract:'ProxyAdmin',address:change.proxyAdminAddress});}
|
|
139
|
-
const pure=!preparations.length?existingCodeCoverage({target:plan.target,addresses:requiredExistingAddresses(collection),registry:JSON.parse(historicalRegistration('contractInfo.json').toString()),metadata:JSON.parse(historicalRegistration('deploymentInfo.json').toString()),compiler:aggregate.inputs.targets.find(t=>t.target===plan.target)!.compiler,manifests:(acceptance.materialization.manifests??[]).map((m:any)=>({path:m.file,contents:now(m.file).toString()}))}):null;
|
|
144
|
+
const pure=!preparations.length?existingCodeCoverage({target:plan.target,addresses:requiredExistingAddresses(collection),registry:interactionCoverageRegistry(collection,JSON.parse(historicalRegistration('contractInfo.json').toString()),plan.target),metadata:JSON.parse(historicalRegistration('deploymentInfo.json').toString()),compiler:aggregate.inputs.targets.find(t=>t.target===plan.target)!.compiler,manifests:(acceptance.materialization.manifests??[]).map((m:any)=>({path:m.file,contents:now(m.file).toString()}))}):null;
|
|
140
145
|
if(pure){if(record.changes.length)throw Error('Pure-call history cannot invent deployment changes');Object.assign(effective,pure.effective);expectedCode.push(...pure.expected);same(acceptance.materialization.manifests,pure.manifests,'Historical existing manifests');}
|
|
141
146
|
const code=acceptance.checkCode;if(code?.version!==1||code.executionId!==plan.executionId||code.planHash!==plan.planHash||code.target!==plan.target||typeof code.rawFile!=='string'||!code.rawFile.startsWith(`.gate-delivery/check-code/${plan.executionId}/`))throw Error('Missing real CheckCode identity');
|
|
142
147
|
same(byteHash(historicalRegistration('contractInfo.json')),code.registryHash,'Historical registry');same(byteHash(historicalRegistration('deploymentInfo.json')),code.metadataHash,'Historical deployment metadata');same(byteHash(canonicalJson(effective)+'\n'),code.effectiveHash,'Historical CheckCode effective registry');
|
|
@@ -13,7 +13,7 @@ import {parsePreparationJournal} from './journal';
|
|
|
13
13
|
import {expectedDevelopmentAssertions} from './development';
|
|
14
14
|
import {verifyCapturedForkAudit} from './forkAudit';
|
|
15
15
|
import {parseCanonicalPlan,projectCanonicalDefinitions} from './source';
|
|
16
|
-
import {readReleaseUniverse,currentReleasePrerequisites,readReleaseGraph,isDevelopmentAcceptancePrerequisite,sameReleaseData as same,type ReleaseGraphOptions} from './release-graph';
|
|
16
|
+
import {readReleaseUniverse,standaloneReleaseDefinition,selectedReleaseTargets,currentReleasePrerequisites,readReleaseGraph,isDevelopmentAcceptancePrerequisite,sameReleaseData as same,type ReleaseGraphOptions} from './release-graph';
|
|
17
17
|
import {aggregateInputHash,validateHistoricalDevelopmentFiles,verifyHistoricalDevelopmentChain,type HistoricalDevelopmentFiles} from './release-history';
|
|
18
18
|
import {atomicDevelopmentWrite,fileBytes,withDevelopmentFileLock,byteHash} from './materialization';
|
|
19
19
|
import {canonicalJson,deepFreeze,semanticHash,exactObject} from '../workflow/lib/canonicalData';
|
|
@@ -49,6 +49,8 @@ function taskEvidence(input:Scope&{planKey:string}){
|
|
|
49
49
|
export function recordDeliveryPrerequisite(input:Scope&{planKey:string}){const {root,envelope}=taskEvidence(input);atomicDevelopmentWrite(root,prerequisitePath(input.taskId,input.planKey),encode(envelope));return deepFreeze(envelope);}
|
|
50
50
|
export function validateDeliveryPrerequisite(input:Scope&{planKey:string}){const {root,envelope}=taskEvidence(input),actual=JSON.parse(readInputBytes(root,prerequisitePath(input.taskId,input.planKey)).toString());same(actual,envelope,'Current prerequisite report/artifact binding');return deepFreeze(actual);}
|
|
51
51
|
function prerequisites(scope:Scope,history:ReturnType<typeof validateHistoricalDevelopmentFiles>){
|
|
52
|
+
const native=standaloneReleaseDefinition(scope);
|
|
53
|
+
if(native){if(history.source.targets[0].request.sourceRef.kind!=='standalone'||history.plan.deploymentId!==native.deploymentId||history.plan.target!==native.developmentNetwork)throw Error('Native release requires its own real accepted development');return [{source:'standalone',executionId:history.plan.executionId,planHash:history.plan.planHash,acceptanceHash:semanticHash(history.acceptance)}];}
|
|
52
54
|
const current=currentReleasePrerequisites(scope),receipts=[];
|
|
53
55
|
for(const {task} of current.required){if(isDevelopmentAcceptancePrerequisite(task)){const d=task.scope?.deployment as any;if(d?.environment!=='dev'||d.targetNetwork!==history.plan.target||d.deploymentId!==history.plan.deploymentId)throw Error('Development prerequisite scope mismatch');if(!verificationOnly(task)&&(task.planKey!==history.source.targets[0].request.sourceRef.planKey||task.rev!==history.source.targets[0].request.sourceRef.rev))throw Error('Stale development prerequisite revision');const evidence=task.result?.evidence;if(!Array.isArray(evidence)||!evidence.some((e:any)=>e.ref===scopePath(history,verificationOnly(task))))throw Error('Development prerequisite must reference actual acceptance/CheckCode');receipts.push({planKey:task.planKey,rev:task.rev,definitionHash:semanticHash(projectCanonicalDefinitions(current.text,{roots:[task.planKey],mode:'full'}).snapshot),evidenceHash:semanticHash(verificationOnly(task)?history.acceptance.checkCode:history.acceptance)});}
|
|
54
56
|
else receipts.push(validateDeliveryPrerequisite({...scope,planKey:task.planKey}));}
|
|
@@ -83,16 +85,17 @@ function currentCandidate(scope:Scope,c:any,requireCommittedSources=false){
|
|
|
83
85
|
same(openAggregateSources({workspaceRoot:scope.workspaceRoot,projectRoot:scope.projectRoot,requests:c.targets.map((t:any)=>t.request)}).definitions,c.aggregate.inputs.sourceDefinitions,'Current full upstream source');
|
|
84
86
|
const h=validateHistoricalDevelopmentFiles({root,workspaceRoot:scope.workspaceRoot,projectRoot:scope.projectRoot,history:c.development});same(h.association,c.historical.association,'Historical development association');same(prerequisites(scope,h),c.prerequisites,'Current prerequisite evidence');
|
|
85
87
|
same(h.files.filter(f=>historicalRegistrationFile(f.path)),c.historical.files.filter((f:any)=>historicalRegistrationFile(f.path)),'Original historical registry baseline');
|
|
86
|
-
same(
|
|
88
|
+
same(selectedReleaseTargets(scope),c.aggregate.inputs.business.targets.map((t:any)=>t.id),'Current full selected target set');
|
|
87
89
|
return {root,source,history:h};
|
|
88
90
|
}
|
|
89
91
|
/** Qualification is generated by the actual owned aggregate, never accepted from caller JSON. */
|
|
90
92
|
export async function prepareDeliveryRelease(raw:ReleasePreparationOptions){
|
|
91
93
|
const input=strictData(raw);exactObject(input,['workspaceRoot','projectRoot','handoff','taskId','deploymentId','releaseId','targets','development'],'release preparation');safeId(input.releaseId,'releaseId');
|
|
92
|
-
const {workspaceRoot,projectRoot:relative,handoff,taskId,deploymentId}=input,scope={workspaceRoot,projectRoot:relative,handoff,taskId,deploymentId},u=readReleaseUniverse(scope),root=
|
|
94
|
+
const {workspaceRoot,projectRoot:relative,handoff,taskId,deploymentId}=input,scope={workspaceRoot,projectRoot:relative,handoff,taskId,deploymentId},native=standaloneReleaseDefinition(scope),u=native?undefined:readReleaseUniverse(scope),root=projectRoot(workspaceRoot,relative);
|
|
93
95
|
if(path.resolve(process.cwd())!==root)throw Error('Release preparation must run from actual project root');
|
|
94
96
|
if(fileBytes(root,`scripts/tasks/${deploymentId}/releases/${input.releaseId}/release.json`)!==null)throw Error('Release already has a ready marker');
|
|
95
|
-
same(input.targets.map(t=>({network:t.request.network,
|
|
97
|
+
if(native){same(input.targets.map(t=>({network:t.request.network,kind:t.request.sourceRef.kind,path:t.request.sourceRef.path,deploymentId:t.request.sourceRef.deploymentId,purpose:t.request.purpose})),native.releaseTargetNetworks.map(network=>({network,kind:'standalone',path:handoff,deploymentId,purpose:'prod-qualification'})),'Native exact qualification targets');}
|
|
98
|
+
else same(input.targets.map(t=>({network:t.request.network,sourceRef:t.request.sourceRef,purpose:t.request.purpose,environment:t.request.environment,mode:t.request.mode})),u!.qualifications.map(t=>({network:(t.scope!.deployment as any).targetNetwork,sourceRef:{path:handoff,taskId,planKey:t.planKey,rev:t.rev},purpose:'prod-qualification',environment:'prod',mode:'simulate'})),'Exact current qualification tasks');
|
|
96
99
|
const history=validateHistoricalDevelopmentFiles({root,workspaceRoot,projectRoot:relative,history:input.development}),prerequisiteEvidence=prerequisites(scope,history);
|
|
97
100
|
const committedBefore=captureCommittedContractSource(root,history.aggregate);
|
|
98
101
|
await verifyHistoricalDevelopmentChain(history,root);
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/** Public script protocol and preparation API shared by CLI and application hosts. */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import YAML from 'yaml';
|
|
6
|
+
import Ajv from 'ajv';
|
|
7
|
+
import { CustomModuleClosure } from './custom-closure';
|
|
8
|
+
import { strictData, pointerPart } from './data';
|
|
9
|
+
import { readInputBytes, parseInputDocument } from './input-files';
|
|
10
|
+
import { object, safeId, portablePath } from './protocol';
|
|
11
|
+
import { deepFreeze, semanticHash, canonicalJson } from '../workflow/lib/canonicalData';
|
|
12
|
+
import { writeImmutableDeliveryFile } from './development-inputs';
|
|
13
|
+
import { loadDeliveryPublicTarget } from './config';
|
|
14
|
+
export interface ContractQuery { address: string; abi: string[]; method: string; args?: unknown[]; caller?: string }
|
|
15
|
+
export interface ScriptStep {
|
|
16
|
+
id: string; dependsOn?: string[];
|
|
17
|
+
/** Explicit live evidence for omitted prerequisites, evaluated before the selected step. */
|
|
18
|
+
satisfiedBy?: Record<string, ContractQuery & { expect: unknown }>;
|
|
19
|
+
reads?: { files?: string[]; params?: string[]; state?: string[]; target?: string[]; chain?: boolean };
|
|
20
|
+
}
|
|
21
|
+
export interface DeliveryScript {
|
|
22
|
+
version: 1; description: string; inputSchema: Record<string, any>;
|
|
23
|
+
defaults?: Record<string, unknown>; steps: ScriptStep[];
|
|
24
|
+
}
|
|
25
|
+
export interface ScriptPorts {
|
|
26
|
+
resolve(key: string): unknown;
|
|
27
|
+
readJson(file: string): Promise<unknown>;
|
|
28
|
+
readBytes(file: string): Promise<unknown>;
|
|
29
|
+
readContract(query: ContractQuery): Promise<unknown>;
|
|
30
|
+
}
|
|
31
|
+
export interface ScriptInvocation { stepId: string; prerequisites: Array<ContractQuery & { expect: unknown }> }
|
|
32
|
+
export function inspectDeliveryScript(root: string, script: string) {
|
|
33
|
+
portablePath(script,'business script');
|
|
34
|
+
if(!script.startsWith('scripts/operations/')&&!/^scripts\/tasks\/[^/]+\//.test(script))throw Error('Script must be under scripts/operations or the task directory');
|
|
35
|
+
const closure=new CustomModuleClosure(root),loaded=closure.load(script),d=closure.normalize(loaded.module.delivery) as DeliveryScript;
|
|
36
|
+
object(d,'delivery script',['version','description','inputSchema','defaults','steps']);
|
|
37
|
+
if(d.version!==1||typeof d.description!=='string'||!d.description.trim()||!Array.isArray(d.steps)||!d.steps.length||typeof loaded.module.planOperations!=='function')throw Error('Script requires delivery v1 and planOperations(ports,input)');
|
|
38
|
+
object(d.inputSchema,'inputSchema');
|
|
39
|
+
if(d.inputSchema.type!=='object'||d.inputSchema.additionalProperties!==false)throw Error('Input schema requires object with additionalProperties:false');
|
|
40
|
+
const names=d.steps.map(step=>{object(step,'script step',['id','dependsOn','satisfiedBy','reads']);return safeId(step.id,'business step');});
|
|
41
|
+
if(new Set(names).size!==names.length)throw Error('Duplicate script step');
|
|
42
|
+
d.steps.forEach((s,i)=>{if(s.dependsOn!==undefined&&(!Array.isArray(s.dependsOn)||new Set(s.dependsOn).size!==s.dependsOn.length))throw Error('Invalid dependencies');for(const dep of s.dependsOn??[])if(!names.slice(0,i).includes(dep))throw Error(`Dependency ${dep} must precede ${s.id}`);});
|
|
43
|
+
return {definition:deepFreeze(d),closure:closure.seal()};
|
|
44
|
+
}
|
|
45
|
+
const isObject=(v:unknown):v is Record<string,any>=>!!v&&typeof v==='object'&&!Array.isArray(v);
|
|
46
|
+
export function mergeScriptInputs(base:Record<string,any>,over:Record<string,any>):Record<string,any>{
|
|
47
|
+
return Object.fromEntries([...new Set([...Object.keys(base),...Object.keys(over)])].map(k=>[k,Object.hasOwn(over,k)?isObject(base[k])&&isObject(over[k])?mergeScriptInputs(base[k],over[k]):over[k]:base[k]]));
|
|
48
|
+
}
|
|
49
|
+
function schemaDefaults(s:any):any{
|
|
50
|
+
if(Object.hasOwn(s,'default'))return strictData(s.default);
|
|
51
|
+
if(s.type==='object'){const entries=Object.entries(s.properties??{}).filter(([k])=>!(s.required??[]).includes(k)).map(([k,v])=>[k,schemaDefaults(v)]).filter(([,v])=>v!==undefined);return Object.fromEntries(entries);}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
function parameterFile(root:string,file:string){portablePath(file,'parameter file');if(!/\.(json|ya?ml)$/.test(file)||file.split('/').some(p=>/^(?:\.env|rpc\.env|node_modules|\.git|\.gate-delivery|keys?|secrets?|credentials?)(?:\.|$)/i.test(p)))throw Error('Parameter file must be a public JSON/YAML business input');return readInputBytes(root,file);}
|
|
55
|
+
function inputSchema(raw:any):any {const s=JSON.parse(canonicalJson(raw));const visit=(v:any)=>{delete v.default;delete v['x-derived'];if(v.properties)Object.values(v.properties).forEach(visit);if(v.items)visit(v.items);};visit(s);return s;}
|
|
56
|
+
export function resolveScriptInputs(definition:DeliveryScript,file:unknown={},sets:string[]=[]){
|
|
57
|
+
object(file,'input parameters');
|
|
58
|
+
let values=mergeScriptInputs(mergeScriptInputs(schemaDefaults(definition.inputSchema)??{},definition.defaults??{}),strictData(file) as Record<string,any>);
|
|
59
|
+
const seen=new Set<string>();
|
|
60
|
+
for(const set of sets){const eq=set.indexOf('=');if(eq<1)throw Error('--set expects path=value');const key=set.slice(0,eq),parts=key.split('.');if(seen.has(key)||[...seen].some(p=>p.startsWith(key+'.')||key.startsWith(p+'.')))throw Error(`Conflicting --set ${key}`);seen.add(key);
|
|
61
|
+
let s=definition.inputSchema,v=values;
|
|
62
|
+
for(const [i,p]of parts.entries()){if(['__proto__','constructor','prototype'].includes(p)||s.type!=='object'||!s.properties?.[p])throw Error(`Unknown parameter ${key}`);s=s.properties[p];if(s['x-derived'])throw Error(`Derived parameter cannot be overridden: ${key}`);if(i<parts.length-1){v[p]??={};if(!isObject(v[p]))throw Error(`Non-object parameter ${p}`);v=v[p];}else{const text=set.slice(eq+1);let value:any;if(s.type==='string')value=text;else{try{value=JSON.parse(text);}catch{throw Error(`Invalid ${s.type} at ${key}`);}}v[p]=value;}}
|
|
63
|
+
}
|
|
64
|
+
const checkDerived=(s:any,v:any,p='')=>{if(s['x-derived']&&v!==undefined)throw Error(`Derived value ${p} belongs in planOperations, not input parameters`);if(isObject(v))for(const [k,x]of Object.entries(v))checkDerived(s.properties?.[k]??{},x,p+'/'+k);};checkDerived(definition.inputSchema,values);
|
|
65
|
+
const ajv=new Ajv({allErrors:true,strict:true});ajv.addKeyword({keyword:'x-policy',valid:true});const validate=ajv.compile(inputSchema(definition.inputSchema));
|
|
66
|
+
if(!validate(values))throw Error(`Script inputs: ${ajv.errorsText(validate.errors)}`);
|
|
67
|
+
strictData(values);return deepFreeze(values);
|
|
68
|
+
}
|
|
69
|
+
export function scriptManifest(root:string,script:string,selected?:string[]) {
|
|
70
|
+
const {definition,closure}=inspectDeliveryScript(root,script),ids=selected?.length?selected:definition.steps.map(s=>s.id);
|
|
71
|
+
if(new Set(ids).size!==ids.length||ids.some(id=>!definition.steps.some(s=>s.id===id)))throw Error('Unknown or duplicate selected step');
|
|
72
|
+
const steps=definition.steps.filter(s=>ids.includes(s.id)).map(s=>{
|
|
73
|
+
const missing=(s.dependsOn??[]).filter(dep=>!ids.includes(dep));
|
|
74
|
+
for(const dep of missing)if(!s.satisfiedBy?.[dep])throw Error(`Step ${s.id} requires ${dep}; explicitly select it or declare live satisfiedBy evidence`);
|
|
75
|
+
const reads=s.reads??{};
|
|
76
|
+
return {id:s.id,kind:'custom',script,input:{stepId:s.id,prerequisites:missing.map(dep=>s.satisfiedBy![dep])},reads:{files:reads.files??[],params:reads.params??Object.keys(definition.inputSchema.properties??{}),state:reads.state??[],target:reads.target??[],...(reads.chain||missing.length?{chain:true}:{})}};
|
|
77
|
+
});
|
|
78
|
+
return {definition,closure,steps,selectedSteps:steps.map(s=>s.id)};
|
|
79
|
+
}
|
|
80
|
+
/** Creates a fresh business round; no signature/approval and no GateFlow state involved. */
|
|
81
|
+
export function prepareStandaloneTask(input:{root:string;script:string;network:string;paramsFile?:string;sets?:string[];steps?:string[];description?:string;releaseNetworks?:string[];referenceDeploymentId?:string}){
|
|
82
|
+
return writeStandaloneTask(input);
|
|
83
|
+
}
|
|
84
|
+
function writeStandaloneTask(input:{root:string;script:string;network:string;paramsFile?:string;sets?:string[];steps?:string[];description?:string;releaseNetworks?:string[];referenceDeploymentId?:string;values?:Record<string,unknown>;networkOverrides?:Record<string,Record<string,unknown>>;reuseId?:string}){
|
|
85
|
+
const root=fs.realpathSync(input.root),script=portablePath(input.script,'script'),selected=scriptManifest(root,script,input.steps);
|
|
86
|
+
loadDeliveryPublicTarget(root,input.network,'dev');
|
|
87
|
+
for(const n of input.releaseNetworks??[])loadDeliveryPublicTarget(root,n,'prod');
|
|
88
|
+
const bytes=input.paramsFile?parameterFile(root,input.paramsFile):undefined;
|
|
89
|
+
const values=resolveScriptInputs(selected.definition,mergeScriptInputs(input.values??{},bytes?parseInputDocument(bytes,/\.ya?ml$/.test(input.paramsFile!)?'yaml':'json') as Record<string,any>:{}),input.sets);
|
|
90
|
+
const overrides=input.networkOverrides??{};
|
|
91
|
+
for(const [network,v]of Object.entries(overrides)){if(!(input.releaseNetworks??[]).includes(network))throw Error('Parameter override outside release scope');resolveScriptInputs(selected.definition,mergeScriptInputs(values,v));}
|
|
92
|
+
const id=input.reuseId??`${path.basename(script).replace(/\.[^.]+$/,'').replace(/[^a-zA-Z0-9_-]/g,'-').slice(0,60)}-${randomUUID()}`,base=`scripts/tasks/${id}`;
|
|
93
|
+
const task={version:2,id,source:'standalone',description:input.description??selected.definition.description,developmentNetwork:input.network,releaseTargetNetworks:input.releaseNetworks??[],script,selectedSteps:selected.selectedSteps,...(input.referenceDeploymentId?{referenceDeploymentId:safeId(input.referenceDeploymentId,'referenceDeploymentId')}:{})};
|
|
94
|
+
const schema=inputSchema(selected.definition.inputSchema);schema.$schema='http://json-schema.org/draft-07/schema#';schema['x-policy']={kind:'manual'};
|
|
95
|
+
const manual:Record<string,unknown>={};const leaves=(v:any,p='')=>{const entries=v&&typeof v==='object'?Object.entries(v):[];if(!entries.length)manual[p]={source:'standalone-input',value:v};else for(const[k,x]of entries)leaves(x,p+'/'+pointerPart(k));};leaves(values);
|
|
96
|
+
const write=(file:string,data:unknown,yaml=false)=>{const bytes=yaml?YAML.stringify(data):canonicalJson(data)+'\n';if(input.reuseId)require('./materialization').atomicDevelopmentWrite(root,`${base}/${file}`,bytes);else writeImmutableDeliveryFile(root,`${base}/${file}`,Buffer.from(bytes));};
|
|
97
|
+
write('task.yaml',task,true);write('manifest.yaml',{id,description:task.description,steps:selected.steps},true);write('parameters.schema.json',schema);
|
|
98
|
+
const targets=Object.fromEntries(Object.entries(overrides).map(([network,v])=>{const entries:Record<string,unknown>={};const visit=(value:any,p='')=>{const fields=value&&typeof value==='object'?Object.entries(value):[];if(!fields.length)entries[p]={source:'standalone-input',value};else for(const[k,x]of fields)visit(x,p+'/'+pointerPart(k));};visit(v);return [network,{manual:entries,computed:{}}];}));
|
|
99
|
+
write('parameters.yaml',{defaults:values,params:{[input.network]:{},...overrides}},true);write('parameters.provenance.json',{version:1,producers:{},defaults:{manual,computed:{}},targets});
|
|
100
|
+
const captured=selected.closure.files.map(f=>({path:f.path,digest:f.digest,base64:readInputBytes(root,f.path).toString('base64')}));
|
|
101
|
+
if(bytes)captured.push({path:input.paramsFile!,digest:require('./input-files').inputDigest(bytes),base64:bytes.toString('base64')});
|
|
102
|
+
write('inputs/preparation.snapshot.json',{schema:'gate-delivery-script-inputs',version:1,selectedSteps:selected.selectedSteps,closure:selected.closure,files:captured});
|
|
103
|
+
return deepFreeze({deploymentId:id,taskPath:`${base}/task.yaml`,values,steps:selected.selectedSteps});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Re-prepare the same native round before signing; historical preparations stay immutable and revoked. */
|
|
107
|
+
export async function reprepareStandaloneTask(input:{root:string;executionId:string;store:import('./execution-store').ExecutionStore;paramsFile?:string;sets?:string[];steps?:string[]}){
|
|
108
|
+
const source=JSON.parse(readInputBytes(input.root,`.gate-delivery/development/${safeId(input.executionId,'executionId')}.source.json`).toString());
|
|
109
|
+
const ref=source.targets?.[0]?.request?.sourceRef;if(ref?.kind!=='standalone')throw Error('Native reprepare requires a standalone source; managed rounds must use their owning host');
|
|
110
|
+
const task=require('./standalone-source').readStandaloneTask(input.root,ref.path) as import('./standalone-source').StandaloneDefinition;
|
|
111
|
+
const aggregate=JSON.parse(readInputBytes(input.root,`.gate-delivery/development/${input.executionId}.aggregate.json`).toString());
|
|
112
|
+
const previous=aggregate.inputs.sourceDefinitions[0].definition;
|
|
113
|
+
const withoutSelection=({selectedSteps,...rest}:any)=>rest;
|
|
114
|
+
if(canonicalJson(withoutSelection(task))!==canonicalJson(withoutSelection(previous)))throw Error('Reprepare cannot change the original task scope; create a new round');
|
|
115
|
+
const parameters=YAML.parse(readInputBytes(input.root,`scripts/tasks/${task.deploymentId}/parameters.yaml`).toString());
|
|
116
|
+
const networkOverrides=Object.fromEntries(Object.entries(parameters.params??{}).filter(([network])=>network!==task.developmentNetwork)) as Record<string,Record<string,unknown>>;
|
|
117
|
+
// Validate prospective inputs before revoking a usable preparation.
|
|
118
|
+
const selected=scriptManifest(input.root,task.script,input.steps??task.selectedSteps);
|
|
119
|
+
const file=input.paramsFile?parseInputDocument(parameterFile(input.root,input.paramsFile),/\.ya?ml$/.test(input.paramsFile)?'yaml':'json'):{};
|
|
120
|
+
const values=resolveScriptInputs(selected.definition,mergeScriptInputs(aggregate.inputs.business.parameters[0].resolved.values,file as Record<string,any>),input.sets);
|
|
121
|
+
for(const v of Object.values(networkOverrides))resolveScriptInputs(selected.definition,mergeScriptInputs(values,v));
|
|
122
|
+
const records=path.join(input.root,'.gate-delivery/executions');
|
|
123
|
+
if(fs.existsSync(records))for(const file of fs.readdirSync(records).filter(f=>f.endsWith('.json'))){const r=JSON.parse(readInputBytes(input.root,`.gate-delivery/executions/${file}`).toString());if(r.kind==='development'&&r.deploymentId===task.deploymentId&&r.executionId!==input.executionId&&!fs.existsSync(path.join(input.root,`.gate-delivery/development/${r.executionId}.superseded.json`)))throw Error(`Round already has replacement execution ${r.executionId}; resume it`);}
|
|
124
|
+
const original=await require('./development-inputs').invalidateDevelopmentPreparation(input);
|
|
125
|
+
return require('./materialization').withDevelopmentFileLock(input.root,async()=>{
|
|
126
|
+
const marker=`.gate-delivery/development/${input.executionId}.replacement.json`;
|
|
127
|
+
const intentHash=semanticHash({values,networkOverrides,steps:selected.selectedSteps,closure:selected.closure});
|
|
128
|
+
let executionId:string;
|
|
129
|
+
if(fs.existsSync(path.join(input.root,marker))){const prior=JSON.parse(readInputBytes(input.root,marker).toString());if(prior.intentHash!==intentHash)throw Error('Replacement preparation already reserved with different inputs; finish or resume that replacement');executionId=safeId(prior.executionId,'replacement executionId');}
|
|
130
|
+
else{executionId=randomUUID();writeImmutableDeliveryFile(input.root,marker,Buffer.from(canonicalJson({version:1,executionId,intentHash})+'\n'));}
|
|
131
|
+
return {...writeStandaloneTask({root:input.root,script:task.script,network:task.developmentNetwork,releaseNetworks:task.releaseTargetNetworks,referenceDeploymentId:task.referenceDeploymentId,description:task.description,steps:selected.selectedSteps,values,networkOverrides,reuseId:original.deploymentId}),executionId};
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Prepare a reusable script inside an existing, genuinely confirmed GateFlow round. */
|
|
136
|
+
export async function prepareGateFlowScriptTask(input:{root:string;request:import('./protocol').DeliveryRequest;script:string;steps?:string[];values:Record<string,unknown>;sets?:string[];paramsFile?:string;networkInputs?:Record<string,{values:Record<string,unknown>;sets?:string[]}>}){
|
|
137
|
+
const source=require('./source').readOwnedDeliverySource({workspaceRoot:input.root,projectRoot:'.',request:input.request}) as import('./source').DeliverySourceBinding;
|
|
138
|
+
if(source.kind!=='gateflow')throw Error('GateFlow preparation requires its real source adapter');
|
|
139
|
+
const deploymentId=safeId(source.scope.deploymentId,'deploymentId'),base=`scripts/tasks/${deploymentId}`,definition=scriptManifest(input.root,input.script,input.steps);
|
|
140
|
+
const file=input.paramsFile?parseInputDocument(parameterFile(input.root,input.paramsFile),/\.ya?ml$/.test(input.paramsFile)?'yaml':'json'):{};
|
|
141
|
+
const values=resolveScriptInputs(definition.definition,mergeScriptInputs(file as Record<string,any>,input.values),input.sets);
|
|
142
|
+
for(const network of Object.keys(input.networkInputs??{}))if(!(source.scope.releaseTargetNetworks as string[]).includes(network))throw Error('Network inputs outside confirmed release scope');
|
|
143
|
+
const networkValues=Object.fromEntries(Object.entries(input.networkInputs??{}).map(([network,v])=>[network,resolveScriptInputs(definition.definition,mergeScriptInputs(file as Record<string,any>,v.values),v.sets)]));
|
|
144
|
+
const schema=inputSchema(definition.definition.inputSchema);schema.$schema='http://json-schema.org/draft-07/schema#';schema['x-policy']={kind:'manual'};
|
|
145
|
+
const manual:Record<string,unknown>={};const leaves=(v:any,p='')=>{const entries=v&&typeof v==='object'?Object.entries(v):[];if(!entries.length)manual[p]={source:'confirmed-requirement',value:v};else for(const[k,x]of entries)leaves(x,p+'/'+pointerPart(k));};leaves(values);
|
|
146
|
+
// Refuse replacing the inputs of any already prepared execution, even before the first signature.
|
|
147
|
+
const dir=path.join(input.root,'.gate-delivery/development');if(fs.existsSync(dir))for(const f of fs.readdirSync(dir).filter(f=>f.endsWith('.aggregate.json'))){const a=JSON.parse(fs.readFileSync(path.join(dir,f),'utf8'));if(a.inputs?.business?.deploymentId===deploymentId&&!fs.existsSync(path.join(dir,f.replace('.aggregate.json','.superseded.json'))))throw Error('Round already prepared; invalidate through the owning host or create a new round');}
|
|
148
|
+
const targets:Record<string,unknown>={};for(const [network,value]of Object.entries(networkValues)){const observations:Record<string,unknown>={};const visit=(v:any,p='')=>{const entries=v&&typeof v==='object'?Object.entries(v):[];if(!entries.length)observations[p]={source:'confirmed-requirement',value:v};else for(const[k,x]of entries)visit(x,p+'/'+pointerPart(k));};visit(value);targets[network]={manual:observations,computed:{}};}
|
|
149
|
+
const writes:{[file:string]:string}={
|
|
150
|
+
'manifest.yaml':YAML.stringify({id:deploymentId,description:definition.definition.description,steps:definition.steps}),
|
|
151
|
+
'parameters.schema.json':canonicalJson(schema)+'\n',
|
|
152
|
+
'parameters.yaml':YAML.stringify({defaults:values,params:{[input.request.network]:{},...networkValues}}),
|
|
153
|
+
'parameters.provenance.json':canonicalJson({version:1,producers:{},defaults:{manual,computed:{}},targets})+'\n',
|
|
154
|
+
};
|
|
155
|
+
await require('./materialization').withDevelopmentFileLock(input.root,async()=>{
|
|
156
|
+
const current=require('./source').readOwnedDeliverySource({workspaceRoot:input.root,projectRoot:'.',request:input.request});
|
|
157
|
+
if(current.sourceDefinitionHash!==source.sourceDefinitionHash)throw Error('Confirmed source changed while preparing');
|
|
158
|
+
if(fs.existsSync(dir))for(const f of fs.readdirSync(dir).filter(f=>f.endsWith('.aggregate.json'))){const a=JSON.parse(readInputBytes(input.root,`.gate-delivery/development/${f}`).toString());if(a.inputs?.business?.deploymentId===deploymentId&&!fs.existsSync(path.join(dir,f.replace('.aggregate.json','.superseded.json'))))throw Error('Round already prepared; original execution is immutable');}
|
|
159
|
+
for(const[file,bytes]of Object.entries(writes))require('./materialization').atomicDevelopmentWrite(input.root,`${base}/${file}`,bytes);
|
|
160
|
+
});
|
|
161
|
+
return {deploymentId,script:input.script,selectedSteps:definition.selectedSteps,values,files:Object.keys(writes).map(f=>`${base}/${f}`)};
|
|
162
|
+
}
|