pi-apexlang 0.1.0 → 0.2.0
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.
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { cp, lstat, mkdir, readFile, readdir, realpath, rm, writeFile } from "node:fs/promises";
|
|
3
4
|
import { dirname, extname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { stripVTControlCharacters } from "node:util";
|
|
5
7
|
|
|
6
8
|
const moduleDirectory = dirname(fileURLToPath(import.meta.url));
|
|
7
9
|
|
|
@@ -19,11 +21,46 @@ export const APEXLANG_ACTIONS = Object.freeze([
|
|
|
19
21
|
|
|
20
22
|
const apexctlPath = resolve(APEXLANG_SKILL_ROOT, "tools/apexctl.mjs");
|
|
21
23
|
const queryValidPropsPath = resolve(APEXLANG_SKILL_ROOT, "tools/query-valid-props.mjs");
|
|
24
|
+
const runtimeRoundtripPath = resolve(moduleDirectory, "apexlang-runtime-roundtrip.mjs");
|
|
25
|
+
const sqlclPtyProxyPath = resolve(moduleDirectory, "apexlang-sqlcl-pty.py");
|
|
22
26
|
const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f]/;
|
|
23
27
|
const CONNECTION_NAME_PATTERN = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/;
|
|
24
28
|
const WORKSPACE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_$#.-]{0,127}$/;
|
|
25
29
|
const COMPONENT_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,255}$/;
|
|
26
30
|
const DEFAULT_MAX_BUFFER = 8 * 1024 * 1024;
|
|
31
|
+
const SQLCL_WARNING_HEADER_PATTERN = /^APEXlang Compile Warnings:\s*$/gim;
|
|
32
|
+
const SQLCL_IMPORT_TARGET_PATTERN = /^Importing application ID:[^\S\r\n]*(\d+)[^\S\r\n]+into workspace:[^\S\r\n]*([^\r\n]*\S)[^\S\r\n]*$/gm;
|
|
33
|
+
const SQLCL_IMPORT_SUCCESS_PATTERN = /^Import successful\.[^\S\r\n]*$/gm;
|
|
34
|
+
const SQLCL_HARD_FAILURE_PATTERNS = Object.freeze([
|
|
35
|
+
/APEXlang (?:Compile|Import) Errors:/i,
|
|
36
|
+
/(?:^|\s)(?:ORA|SP2|PLS)-\d+/im,
|
|
37
|
+
/(?:^|\s)(?:TNS|DPI)-\d+/im,
|
|
38
|
+
/\b(?:Validation|Import) failed\b/i,
|
|
39
|
+
/\bFATAL(?:!|:|\s|$)/i,
|
|
40
|
+
/\bERROR!(?:\s|$)/i,
|
|
41
|
+
/\[ERROR\]/i,
|
|
42
|
+
/(?:^|[\r\n])\s*(?:SQL>\s*)?ERROR(?:\s*:|\s+)/i,
|
|
43
|
+
/Exception in thread/i,
|
|
44
|
+
/\b(?:SQLRecoverableException|SQLException)\b/i,
|
|
45
|
+
/\bI\/?O Error\b/i,
|
|
46
|
+
/\bConnection failed\b/i,
|
|
47
|
+
/\bconnection (?:refused|reset|closed|timed out)\b/i,
|
|
48
|
+
/\b(?:could not|unable to|failed to)\s+(?:connect|open|read|write|find|import|validate)\b/i,
|
|
49
|
+
/\b(?:permission|access) denied\b/i,
|
|
50
|
+
/\bnot connected\b/i,
|
|
51
|
+
/invalid username\/password/i,
|
|
52
|
+
/(?:enter|provide).*password/i,
|
|
53
|
+
/\b(?:EPERM|ENOENT)\b/,
|
|
54
|
+
/(?:unknown|unrecognized) command/i,
|
|
55
|
+
/No such file or directory/i,
|
|
56
|
+
/Could not find file or directory/i,
|
|
57
|
+
/APEXlang process (?:timed out|output limit exceeded)/i
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
const COMPATIBLE_VALIDATION_BLOCKING_REASONS = Object.freeze(new Set([
|
|
61
|
+
"Live APEX validation did not pass or did not produce pass evidence.",
|
|
62
|
+
"problems.json contains unresolved validation problems."
|
|
63
|
+
]));
|
|
27
64
|
|
|
28
65
|
function assertPlainText(value, key) {
|
|
29
66
|
if (CONTROL_CHARACTER_PATTERN.test(value)) {
|
|
@@ -105,6 +142,14 @@ function optionalComponentName(input, key) {
|
|
|
105
142
|
return value;
|
|
106
143
|
}
|
|
107
144
|
|
|
145
|
+
function normalizeApexlangInput(input) {
|
|
146
|
+
const normalized = { ...input };
|
|
147
|
+
for (const key of ["app_path", "db_connection_name", "workspace_name"]) {
|
|
148
|
+
if (typeof normalized[key] === "string") normalized[key] = normalized[key].trim();
|
|
149
|
+
}
|
|
150
|
+
return normalized;
|
|
151
|
+
}
|
|
152
|
+
|
|
108
153
|
function addOption(args, name, value) {
|
|
109
154
|
if (value !== undefined) {
|
|
110
155
|
args.push(name, value);
|
|
@@ -263,6 +308,7 @@ export function buildApexlangCommand(input) {
|
|
|
263
308
|
addOption(args, "--execution-mode", optionalText(input, "execution_mode"));
|
|
264
309
|
addOption(args, "--apex-root", optionalPath(input, "apex_root"));
|
|
265
310
|
addOption(args, "--compiler-oracle-home", optionalPath(input, "compiler_oracle_home"));
|
|
311
|
+
addFlag(args, "--supporting-objects", input.supporting_objects);
|
|
266
312
|
return apexctl(args, [{ scriptPath: apexctlPath, args: probeArgs }]);
|
|
267
313
|
}
|
|
268
314
|
|
|
@@ -303,7 +349,11 @@ export function buildApexlangImportCommand(input, importOptions = {}) {
|
|
|
303
349
|
addOption(args, "--execution-mode", optionalText(input, "execution_mode"));
|
|
304
350
|
addOption(args, "--apex-root", optionalPath(input, "apex_root"));
|
|
305
351
|
addFlag(args, "--supporting-objects", input.supporting_objects);
|
|
306
|
-
return
|
|
352
|
+
return {
|
|
353
|
+
scriptPath: runtimeRoundtripPath,
|
|
354
|
+
args,
|
|
355
|
+
prelude: [{ scriptPath: apexctlPath, args: probeArgs }]
|
|
356
|
+
};
|
|
307
357
|
}
|
|
308
358
|
|
|
309
359
|
function abortError() {
|
|
@@ -441,11 +491,141 @@ async function assertWorkspaceProbeCannotFollowSymlinks(cwd) {
|
|
|
441
491
|
await walk(resolve(cwd), 0);
|
|
442
492
|
}
|
|
443
493
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
494
|
+
function runtimeActionUsesDeployment(input) {
|
|
495
|
+
return Boolean(input.app_path) &&
|
|
496
|
+
["runtime_validate", "runtime_preflight", "runtime_doctor"].includes(input.action);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function workspaceName(value) {
|
|
500
|
+
return String(value ?? "").trim();
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function workspaceNamesMatch(left, right) {
|
|
504
|
+
return left.toUpperCase() === right.toUpperCase();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function stableJsonValue(value) {
|
|
508
|
+
if (Array.isArray(value)) return value.map(stableJsonValue);
|
|
509
|
+
if (!value || typeof value !== "object") return value;
|
|
510
|
+
return Object.fromEntries(
|
|
511
|
+
Object.keys(value)
|
|
512
|
+
.sort()
|
|
513
|
+
.map((key) => [key, stableJsonValue(value[key])])
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function canonicalDeploymentBytes(contents) {
|
|
518
|
+
let deployment;
|
|
519
|
+
try {
|
|
520
|
+
deployment = JSON.parse(contents.toString("utf8"));
|
|
521
|
+
} catch (error) {
|
|
522
|
+
throw new Error(
|
|
523
|
+
`Cannot digest invalid deployments/default.json: ${error instanceof Error ? error.message : String(error)}`
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
if (!deployment || typeof deployment !== "object" || Array.isArray(deployment)) {
|
|
527
|
+
throw new Error("Cannot digest deployments/default.json unless it contains a JSON object");
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// prepareRuntimeApp may inject this run-scoped field and reformat the JSON. The
|
|
531
|
+
// explicit workspace input is checked separately, so omit only that injected
|
|
532
|
+
// name while retaining every other deployment property in the content digest.
|
|
533
|
+
if (deployment.workspace && typeof deployment.workspace === "object" &&
|
|
534
|
+
!Array.isArray(deployment.workspace)) {
|
|
535
|
+
const workspace = { ...deployment.workspace };
|
|
536
|
+
delete workspace.name;
|
|
537
|
+
if (Object.keys(workspace).length === 0) {
|
|
538
|
+
delete deployment.workspace;
|
|
539
|
+
} else {
|
|
540
|
+
deployment.workspace = workspace;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return Buffer.from(JSON.stringify(stableJsonValue(deployment)), "utf8");
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export async function computeApexlangAppDigest(appPath) {
|
|
547
|
+
const root = resolve(appPath);
|
|
548
|
+
const rootStats = await lstat(root);
|
|
549
|
+
if (rootStats.isSymbolicLink() || !rootStats.isDirectory()) {
|
|
550
|
+
throw new Error("APEXlang app digest requires a real application directory");
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const hash = createHash("sha256");
|
|
554
|
+
hash.update("pi-apexlang-app-tree-v1\0");
|
|
555
|
+
|
|
556
|
+
async function walk(directory) {
|
|
557
|
+
const entries = (await readdir(directory, { withFileTypes: true }))
|
|
558
|
+
.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
|
|
559
|
+
for (const entry of entries) {
|
|
560
|
+
const entryPath = resolve(directory, entry.name);
|
|
561
|
+
const relativePath = relative(root, entryPath).split(sep).join("/");
|
|
562
|
+
if (entry.isSymbolicLink()) {
|
|
563
|
+
throw new Error(`APEXlang app digest refuses symbolic links: ${entryPath}`);
|
|
564
|
+
}
|
|
565
|
+
if (entry.isDirectory()) {
|
|
566
|
+
hash.update(`directory\0${relativePath}\0`);
|
|
567
|
+
await walk(entryPath);
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
if (!entry.isFile()) {
|
|
571
|
+
throw new Error(`APEXlang app digest refuses non-regular files: ${entryPath}`);
|
|
572
|
+
}
|
|
573
|
+
const rawContents = await readFile(entryPath);
|
|
574
|
+
const contents = relativePath === "deployments/default.json"
|
|
575
|
+
? canonicalDeploymentBytes(rawContents)
|
|
576
|
+
: rawContents;
|
|
577
|
+
hash.update(`file\0${relativePath}\0${contents.length}\0`);
|
|
578
|
+
hash.update(contents);
|
|
579
|
+
hash.update("\0");
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
await walk(root);
|
|
584
|
+
return hash.digest("hex");
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function requireAppDigest(value, label = "expectedAppDigest") {
|
|
588
|
+
const digest = String(value ?? "").trim().toLowerCase();
|
|
589
|
+
if (!/^[a-f0-9]{64}$/.test(digest)) {
|
|
590
|
+
throw new Error(`${label} must be a 64-character SHA-256 app digest`);
|
|
591
|
+
}
|
|
592
|
+
return digest;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function commandWithAppPath(command, appPath) {
|
|
596
|
+
const args = [...command.args];
|
|
597
|
+
const appPathIndex = args.indexOf("--app-path");
|
|
598
|
+
if (appPathIndex >= 0) {
|
|
599
|
+
args[appPathIndex + 1] = appPath;
|
|
600
|
+
}
|
|
601
|
+
return { ...command, args };
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function clearStaleRuntimeEvidence(command, outputRoot) {
|
|
605
|
+
if (command.args[0] !== "runtime") return;
|
|
606
|
+
if (command.args[1] === "validate") {
|
|
607
|
+
await Promise.all([
|
|
608
|
+
rm(resolve(outputRoot, "logs/validation"), { recursive: true, force: true }),
|
|
609
|
+
rm(resolve(outputRoot, "logs/compat/sqlcl-warning-policy-validation.json"), { force: true })
|
|
610
|
+
]);
|
|
447
611
|
return;
|
|
448
612
|
}
|
|
613
|
+
if (command.args[1] === "roundtrip") {
|
|
614
|
+
await Promise.all([
|
|
615
|
+
rm(resolve(outputRoot, "logs/runtime-run.json"), { force: true }),
|
|
616
|
+
rm(resolve(outputRoot, "logs/runtime-run.log"), { force: true }),
|
|
617
|
+
rm(resolve(outputRoot, "logs/compat/sqlcl-warning-policy-import.json"), { force: true }),
|
|
618
|
+
rm(resolve(outputRoot, "logs/compat/sqlcl-warning-policy-import-failure.json"), { force: true }),
|
|
619
|
+
rm(resolve(outputRoot, "logs/compat/sqlcl-warning-policy-import.log"), { force: true })
|
|
620
|
+
]);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
export async function prepareRuntimeApp(input, cwd, outputRoot, prepareOptions = {}) {
|
|
625
|
+
if (!runtimeActionUsesDeployment(input)) {
|
|
626
|
+
return { appPath: input.app_path ? resolve(cwd, input.app_path) : undefined, staged: false };
|
|
627
|
+
}
|
|
628
|
+
|
|
449
629
|
const appPath = resolve(cwd, input.app_path);
|
|
450
630
|
const deploymentPath = resolve(appPath, "deployments/default.json");
|
|
451
631
|
let deployment;
|
|
@@ -456,16 +636,80 @@ async function assertDeploymentWorkspace(input, cwd) {
|
|
|
456
636
|
`deployments/default.json is required before ${input.action}: ${error instanceof Error ? error.message : String(error)}`
|
|
457
637
|
);
|
|
458
638
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
639
|
+
if (!deployment || typeof deployment !== "object" || Array.isArray(deployment)) {
|
|
640
|
+
throw new Error("deployments/default.json must contain a JSON object");
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const topLevelWorkspace = workspaceName(deployment?.workspace?.name);
|
|
644
|
+
const exportedWorkspace = workspaceName(deployment?.app?.workspace?.name);
|
|
645
|
+
const requestedWorkspace = workspaceName(input.workspace_name);
|
|
646
|
+
if (!requestedWorkspace) {
|
|
647
|
+
throw new Error(`workspace_name is required for ${input.action}`);
|
|
648
|
+
}
|
|
649
|
+
if (topLevelWorkspace && exportedWorkspace &&
|
|
650
|
+
!workspaceNamesMatch(topLevelWorkspace, exportedWorkspace)) {
|
|
651
|
+
throw new Error(
|
|
652
|
+
`deployments/default.json contains conflicting workspace names: ${topLevelWorkspace} and ${exportedWorkspace}`
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const declaredWorkspace = topLevelWorkspace || exportedWorkspace;
|
|
657
|
+
if (declaredWorkspace === "__REQUIRED_WORKSPACE_NAME__") {
|
|
462
658
|
throw new Error("deployments/default.json must declare the exact destination workspace.name");
|
|
463
659
|
}
|
|
464
|
-
if (declaredWorkspace
|
|
660
|
+
if (declaredWorkspace && !workspaceNamesMatch(declaredWorkspace, requestedWorkspace)) {
|
|
465
661
|
throw new Error(
|
|
466
662
|
`workspace_name ${requestedWorkspace} does not match deployments/default.json workspace.name ${declaredWorkspace}`
|
|
467
663
|
);
|
|
468
664
|
}
|
|
665
|
+
|
|
666
|
+
if (topLevelWorkspace && prepareOptions.forceStage !== true) {
|
|
667
|
+
return { appPath, staged: false, workspaceSource: "workspace.name" };
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const stageKey = createHash("sha256")
|
|
671
|
+
.update(`${appPath}\0${requestedWorkspace.toUpperCase()}`)
|
|
672
|
+
.digest("hex")
|
|
673
|
+
.slice(0, 16);
|
|
674
|
+
const stagedAppPath = resolve(outputRoot, "runtime-apps", stageKey);
|
|
675
|
+
if (isWithin(appPath, stagedAppPath)) {
|
|
676
|
+
throw new Error("APEXLANG_OUTPUT_ROOT must not be inside the application tree");
|
|
677
|
+
}
|
|
678
|
+
const [appRealPath, outputRealAncestor] = await Promise.all([
|
|
679
|
+
realpath(appPath),
|
|
680
|
+
nearestExistingRealPath(resolve(outputRoot))
|
|
681
|
+
]);
|
|
682
|
+
if (isWithin(appRealPath, outputRealAncestor)) {
|
|
683
|
+
throw new Error("APEXLANG_OUTPUT_ROOT must not resolve inside the application tree");
|
|
684
|
+
}
|
|
685
|
+
await rm(stagedAppPath, { recursive: true, force: true });
|
|
686
|
+
await mkdir(dirname(stagedAppPath), { recursive: true });
|
|
687
|
+
await cp(appPath, stagedAppPath, {
|
|
688
|
+
recursive: true,
|
|
689
|
+
force: false,
|
|
690
|
+
errorOnExist: true,
|
|
691
|
+
preserveTimestamps: true
|
|
692
|
+
});
|
|
693
|
+
deployment.workspace = {
|
|
694
|
+
...(deployment.workspace && typeof deployment.workspace === "object"
|
|
695
|
+
? deployment.workspace
|
|
696
|
+
: {}),
|
|
697
|
+
name: requestedWorkspace
|
|
698
|
+
};
|
|
699
|
+
await writeFile(
|
|
700
|
+
resolve(stagedAppPath, "deployments/default.json"),
|
|
701
|
+
`${JSON.stringify(deployment, null, 2)}\n`,
|
|
702
|
+
"utf8"
|
|
703
|
+
);
|
|
704
|
+
return {
|
|
705
|
+
appPath: stagedAppPath,
|
|
706
|
+
staged: true,
|
|
707
|
+
workspaceSource: topLevelWorkspace
|
|
708
|
+
? "workspace.name"
|
|
709
|
+
: exportedWorkspace
|
|
710
|
+
? "app.workspace.name"
|
|
711
|
+
: "explicit_workspace_name"
|
|
712
|
+
};
|
|
469
713
|
}
|
|
470
714
|
|
|
471
715
|
function isDirectChild(root, candidate) {
|
|
@@ -594,6 +838,48 @@ async function killProcessTree(child) {
|
|
|
594
838
|
});
|
|
595
839
|
}
|
|
596
840
|
|
|
841
|
+
async function killPosixProcessGroup(groupId) {
|
|
842
|
+
try {
|
|
843
|
+
process.kill(-groupId, "SIGKILL");
|
|
844
|
+
} catch (error) {
|
|
845
|
+
if (error?.code === "ESRCH") return;
|
|
846
|
+
throw error;
|
|
847
|
+
}
|
|
848
|
+
const deadline = Date.now() + 2_000;
|
|
849
|
+
while (Date.now() < deadline) {
|
|
850
|
+
try {
|
|
851
|
+
process.kill(-groupId, 0);
|
|
852
|
+
} catch (error) {
|
|
853
|
+
if (error?.code === "ESRCH") return;
|
|
854
|
+
throw error;
|
|
855
|
+
}
|
|
856
|
+
await new Promise((resolveDelay) => setTimeout(resolveDelay, 20));
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
async function killPtyProcessTree(child, ptyProcessGroupPromise) {
|
|
861
|
+
if (!child.pid || process.platform === "win32") {
|
|
862
|
+
await killProcessTree(child);
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// The helper publishes its forkpty child PGID on a private pipe before the
|
|
867
|
+
// adapter sends any SQLcl command. Kill that group directly; never rely on
|
|
868
|
+
// the proxy being scheduled in time to forward a signal.
|
|
869
|
+
try {
|
|
870
|
+
child.kill("SIGCONT");
|
|
871
|
+
} catch {
|
|
872
|
+
// The proxy may already have exited.
|
|
873
|
+
}
|
|
874
|
+
const ptyProcessGroupId = await ptyProcessGroupPromise;
|
|
875
|
+
if (Number.isInteger(ptyProcessGroupId) && ptyProcessGroupId > 1) {
|
|
876
|
+
await killPosixProcessGroup(ptyProcessGroupId);
|
|
877
|
+
}
|
|
878
|
+
// When no valid PGID was published, the helper contract guarantees that it
|
|
879
|
+
// killed/reaped any forkpty child before closing the control pipe.
|
|
880
|
+
await killProcessTree(child);
|
|
881
|
+
}
|
|
882
|
+
|
|
597
883
|
export function executeProcessTree(executable, args, options) {
|
|
598
884
|
if (options.signal?.aborted) {
|
|
599
885
|
return Promise.reject(abortError());
|
|
@@ -605,7 +891,7 @@ export function executeProcessTree(executable, args, options) {
|
|
|
605
891
|
detached: process.platform !== "win32",
|
|
606
892
|
env: options.env,
|
|
607
893
|
shell: false,
|
|
608
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
894
|
+
stdio: [options.input === undefined ? "ignore" : "pipe", "pipe", "pipe"],
|
|
609
895
|
windowsHide: true
|
|
610
896
|
});
|
|
611
897
|
const stdoutChunks = [];
|
|
@@ -617,6 +903,13 @@ export function executeProcessTree(executable, args, options) {
|
|
|
617
903
|
let killPromise = Promise.resolve();
|
|
618
904
|
let settled = false;
|
|
619
905
|
|
|
906
|
+
if (options.input !== undefined) {
|
|
907
|
+
child.stdin.on("error", (error) => {
|
|
908
|
+
if (error?.code !== "EPIPE") processError = error;
|
|
909
|
+
});
|
|
910
|
+
child.stdin.end(String(options.input));
|
|
911
|
+
}
|
|
912
|
+
|
|
620
913
|
const terminate = (reason) => {
|
|
621
914
|
if (terminationReason) return;
|
|
622
915
|
terminationReason = reason;
|
|
@@ -674,6 +967,312 @@ export function executeProcessTree(executable, args, options) {
|
|
|
674
967
|
});
|
|
675
968
|
}
|
|
676
969
|
|
|
970
|
+
export function executeSqlclValidationThenImport(executable, args, options) {
|
|
971
|
+
if (options.signal?.aborted) {
|
|
972
|
+
return Promise.reject(abortError());
|
|
973
|
+
}
|
|
974
|
+
if (process.platform === "win32") {
|
|
975
|
+
return Promise.reject(new Error(
|
|
976
|
+
"Warning-compatible import requires an ordered merged SQLcl output stream and is unavailable on Windows"
|
|
977
|
+
));
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
const markerToken = randomUUID().replaceAll("-", "").toUpperCase();
|
|
981
|
+
const sessionReadyMarker = `__PI_APEXLANG_SESSION_READY_${markerToken}__`;
|
|
982
|
+
const validateDoneMarker = `__PI_APEXLANG_VALIDATE_DONE_${markerToken}__`;
|
|
983
|
+
const importDoneMarker = `__PI_APEXLANG_IMPORT_DONE_${markerToken}__`;
|
|
984
|
+
|
|
985
|
+
return new Promise((resolveProcess, rejectProcess) => {
|
|
986
|
+
const child = spawn("python3", [
|
|
987
|
+
sqlclPtyProxyPath,
|
|
988
|
+
executable,
|
|
989
|
+
...args
|
|
990
|
+
], {
|
|
991
|
+
cwd: options.cwd,
|
|
992
|
+
detached: process.platform !== "win32",
|
|
993
|
+
env: {
|
|
994
|
+
...options.env,
|
|
995
|
+
TERM: "dumb",
|
|
996
|
+
COLUMNS: "120",
|
|
997
|
+
LINES: "40",
|
|
998
|
+
PYTHONDONTWRITEBYTECODE: "1"
|
|
999
|
+
},
|
|
1000
|
+
shell: false,
|
|
1001
|
+
stdio: ["pipe", "pipe", "pipe", "pipe"],
|
|
1002
|
+
windowsHide: true
|
|
1003
|
+
});
|
|
1004
|
+
const stdoutChunks = [];
|
|
1005
|
+
const stderrChunks = [];
|
|
1006
|
+
const maxBuffer = options.maxBuffer ?? DEFAULT_MAX_BUFFER;
|
|
1007
|
+
let bufferedBytes = 0;
|
|
1008
|
+
let terminationReason;
|
|
1009
|
+
let processError;
|
|
1010
|
+
let killPromise = Promise.resolve();
|
|
1011
|
+
let settled = false;
|
|
1012
|
+
let sessionSetupTimer;
|
|
1013
|
+
let sessionSetupSent = false;
|
|
1014
|
+
let sessionReady = false;
|
|
1015
|
+
let validationSent = false;
|
|
1016
|
+
let validationStartIndex = -1;
|
|
1017
|
+
let validationDecided = false;
|
|
1018
|
+
let validationAccepted = false;
|
|
1019
|
+
let validationEvidence = { accepted: false, reason: "validation_marker_missing" };
|
|
1020
|
+
let importSent = false;
|
|
1021
|
+
let ptyProcessGroupReady = false;
|
|
1022
|
+
let ptyControlBuffer = "";
|
|
1023
|
+
let ptyControlSettled = false;
|
|
1024
|
+
let resolvePtyProcessGroup;
|
|
1025
|
+
const ptyProcessGroupPromise = new Promise((resolveGroup) => {
|
|
1026
|
+
resolvePtyProcessGroup = resolveGroup;
|
|
1027
|
+
});
|
|
1028
|
+
const settlePtyProcessGroup = (groupId) => {
|
|
1029
|
+
if (ptyControlSettled) return;
|
|
1030
|
+
ptyControlSettled = true;
|
|
1031
|
+
if (Number.isInteger(groupId) && groupId > 1 && groupId !== child.pid) {
|
|
1032
|
+
ptyProcessGroupReady = true;
|
|
1033
|
+
resolvePtyProcessGroup(groupId);
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
resolvePtyProcessGroup(undefined);
|
|
1037
|
+
};
|
|
1038
|
+
|
|
1039
|
+
const terminate = (reason) => {
|
|
1040
|
+
if (terminationReason) return;
|
|
1041
|
+
terminationReason = reason;
|
|
1042
|
+
killPromise = killPtyProcessTree(child, ptyProcessGroupPromise);
|
|
1043
|
+
};
|
|
1044
|
+
const onAbort = () => terminate("aborted");
|
|
1045
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1046
|
+
if (options.signal?.aborted) onAbort();
|
|
1047
|
+
const timeout = options.timeoutMs > 0
|
|
1048
|
+
? setTimeout(() => terminate("timed out"), options.timeoutMs)
|
|
1049
|
+
: undefined;
|
|
1050
|
+
timeout?.unref();
|
|
1051
|
+
|
|
1052
|
+
child.stdin.on("error", (error) => {
|
|
1053
|
+
if (error?.code !== "EPIPE") processError = error;
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
const endSqlclInput = (commands) => {
|
|
1057
|
+
if (terminationReason || child.stdin.destroyed) return;
|
|
1058
|
+
child.stdin.end([...commands, ""].join("\n"));
|
|
1059
|
+
};
|
|
1060
|
+
|
|
1061
|
+
const scheduleSessionSetup = () => {
|
|
1062
|
+
if (!ptyProcessGroupReady || sessionSetupSent || validationDecided ||
|
|
1063
|
+
terminationReason || child.stdin.destroyed) {
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
if (sessionSetupTimer) clearTimeout(sessionSetupTimer);
|
|
1067
|
+
sessionSetupTimer = setTimeout(() => {
|
|
1068
|
+
sessionSetupTimer = undefined;
|
|
1069
|
+
if (!ptyProcessGroupReady || sessionSetupSent || validationDecided ||
|
|
1070
|
+
terminationReason || child.stdin.destroyed) {
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
const startupOutput = Buffer.concat(stdoutChunks).toString("utf8");
|
|
1074
|
+
if (containsSqlclHardFailure(startupOutput)) {
|
|
1075
|
+
validationDecided = true;
|
|
1076
|
+
validationEvidence = { accepted: false, reason: "sqlcl_session_start_failed" };
|
|
1077
|
+
endSqlclInput(["exit failure rollback"]);
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
sessionSetupSent = true;
|
|
1081
|
+
child.stdin.write([
|
|
1082
|
+
'set sqlprompt ""',
|
|
1083
|
+
`prompt ${sessionReadyMarker}`,
|
|
1084
|
+
""
|
|
1085
|
+
].join("\n"));
|
|
1086
|
+
}, 250);
|
|
1087
|
+
};
|
|
1088
|
+
|
|
1089
|
+
const inspectSqlclOutput = () => {
|
|
1090
|
+
if (!ptyProcessGroupReady || terminationReason || child.stdin.destroyed) return;
|
|
1091
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf8");
|
|
1092
|
+
const normalizedStdout = normalizeInteractiveSqlclOutput(stdout);
|
|
1093
|
+
|
|
1094
|
+
if (!sessionSetupSent) {
|
|
1095
|
+
if (containsSqlclHardFailure(normalizedStdout)) {
|
|
1096
|
+
validationDecided = true;
|
|
1097
|
+
validationEvidence = { accepted: false, reason: "sqlcl_session_start_failed" };
|
|
1098
|
+
endSqlclInput(["exit failure rollback"]);
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
scheduleSessionSetup();
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
const readyMarkerSpan = exactSqlclMarkerSpan(
|
|
1106
|
+
normalizedStdout,
|
|
1107
|
+
sessionReadyMarker,
|
|
1108
|
+
{ allowPromptPrefix: true }
|
|
1109
|
+
);
|
|
1110
|
+
if (!sessionReady) {
|
|
1111
|
+
if (!readyMarkerSpan) return;
|
|
1112
|
+
const startupOutput = normalizedStdout.slice(0, readyMarkerSpan.start);
|
|
1113
|
+
if (containsSqlclHardFailure(startupOutput)) {
|
|
1114
|
+
validationDecided = true;
|
|
1115
|
+
validationEvidence = { accepted: false, reason: "sqlcl_session_start_failed" };
|
|
1116
|
+
endSqlclInput(["exit failure rollback"]);
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
sessionReady = true;
|
|
1120
|
+
validationSent = true;
|
|
1121
|
+
validationStartIndex = readyMarkerSpan.end;
|
|
1122
|
+
child.stdin.write([
|
|
1123
|
+
"whenever sqlerror exit failure rollback",
|
|
1124
|
+
"whenever oserror exit failure rollback",
|
|
1125
|
+
options.validateCommand,
|
|
1126
|
+
`prompt ${validateDoneMarker}`,
|
|
1127
|
+
""
|
|
1128
|
+
].join("\n"));
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
if (validationDecided) return;
|
|
1133
|
+
const validateMarkerSpan = exactSqlclMarkerSpan(normalizedStdout, validateDoneMarker);
|
|
1134
|
+
if (!validateMarkerSpan) return;
|
|
1135
|
+
validationDecided = true;
|
|
1136
|
+
const validationOutput = normalizedStdout.slice(
|
|
1137
|
+
validationStartIndex,
|
|
1138
|
+
validateMarkerSpan.start
|
|
1139
|
+
);
|
|
1140
|
+
const currentStderr = Buffer.concat(stderrChunks).toString("utf8");
|
|
1141
|
+
validationEvidence = classifyWarningCompatibleSqlclValidation(
|
|
1142
|
+
[validationOutput, currentStderr].filter(Boolean).join("\n")
|
|
1143
|
+
);
|
|
1144
|
+
validationAccepted = validationEvidence.accepted;
|
|
1145
|
+
if (!validationAccepted) {
|
|
1146
|
+
endSqlclInput(["exit failure rollback"]);
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
importSent = true;
|
|
1150
|
+
endSqlclInput([
|
|
1151
|
+
options.importCommand,
|
|
1152
|
+
`prompt ${importDoneMarker}`,
|
|
1153
|
+
"exit success commit"
|
|
1154
|
+
]);
|
|
1155
|
+
};
|
|
1156
|
+
|
|
1157
|
+
const capture = (chunks, inspectOutput = false) => (chunk) => {
|
|
1158
|
+
if (terminationReason === "output limit exceeded") return;
|
|
1159
|
+
bufferedBytes += chunk.length;
|
|
1160
|
+
if (bufferedBytes > maxBuffer) {
|
|
1161
|
+
terminate("output limit exceeded");
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
chunks.push(chunk);
|
|
1165
|
+
if (inspectOutput) inspectSqlclOutput();
|
|
1166
|
+
};
|
|
1167
|
+
const ptyControlStream = child.stdio[3];
|
|
1168
|
+
ptyControlStream.setEncoding("utf8");
|
|
1169
|
+
ptyControlStream.on("data", (chunk) => {
|
|
1170
|
+
if (ptyControlSettled) return;
|
|
1171
|
+
ptyControlBuffer += chunk;
|
|
1172
|
+
if (ptyControlBuffer.length > 32) {
|
|
1173
|
+
processError = new Error("PTY proxy returned invalid process-group evidence");
|
|
1174
|
+
settlePtyProcessGroup(undefined);
|
|
1175
|
+
terminate("PTY control channel failed");
|
|
1176
|
+
}
|
|
1177
|
+
});
|
|
1178
|
+
ptyControlStream.once("error", (error) => {
|
|
1179
|
+
processError = error;
|
|
1180
|
+
settlePtyProcessGroup(undefined);
|
|
1181
|
+
terminate("PTY control channel failed");
|
|
1182
|
+
});
|
|
1183
|
+
ptyControlStream.once("end", () => {
|
|
1184
|
+
if (ptyControlSettled) return;
|
|
1185
|
+
const match = /^([1-9]\d{0,9})\n$/.exec(ptyControlBuffer);
|
|
1186
|
+
const groupId = match ? Number(match[1]) : undefined;
|
|
1187
|
+
if (!Number.isSafeInteger(groupId) || groupId <= 1 || groupId === child.pid) {
|
|
1188
|
+
processError = new Error("PTY proxy returned invalid process-group evidence");
|
|
1189
|
+
settlePtyProcessGroup(undefined);
|
|
1190
|
+
terminate("PTY control channel failed");
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
settlePtyProcessGroup(groupId);
|
|
1194
|
+
// Process-group proof authorizes safe cleanup, but it is not a SQLcl
|
|
1195
|
+
// readiness signal. Sending input before SQLcl emits terminal output
|
|
1196
|
+
// can be discarded by its startup terminal initialization.
|
|
1197
|
+
if (stdoutChunks.length > 0) inspectSqlclOutput();
|
|
1198
|
+
});
|
|
1199
|
+
child.stdout.on("data", capture(stdoutChunks, true));
|
|
1200
|
+
child.stderr.on("data", capture(stderrChunks));
|
|
1201
|
+
child.once("error", (error) => {
|
|
1202
|
+
processError = error;
|
|
1203
|
+
});
|
|
1204
|
+
child.once("close", async (code) => {
|
|
1205
|
+
if (settled) return;
|
|
1206
|
+
settled = true;
|
|
1207
|
+
if (timeout) clearTimeout(timeout);
|
|
1208
|
+
if (sessionSetupTimer) clearTimeout(sessionSetupTimer);
|
|
1209
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
1210
|
+
await killPromise;
|
|
1211
|
+
|
|
1212
|
+
if (terminationReason === "aborted") {
|
|
1213
|
+
rejectProcess(abortError());
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf8");
|
|
1218
|
+
const normalizedStdout = normalizeInteractiveSqlclOutput(stdout);
|
|
1219
|
+
const capturedStderr = Buffer.concat(stderrChunks).toString("utf8");
|
|
1220
|
+
const readyMarkerSpan = exactSqlclMarkerSpan(
|
|
1221
|
+
normalizedStdout,
|
|
1222
|
+
sessionReadyMarker,
|
|
1223
|
+
{ allowPromptPrefix: true }
|
|
1224
|
+
);
|
|
1225
|
+
const validateMarkerSpan = exactSqlclMarkerSpan(normalizedStdout, validateDoneMarker);
|
|
1226
|
+
const importMarkerSpan = exactSqlclMarkerSpan(normalizedStdout, importDoneMarker);
|
|
1227
|
+
const validationOutput = validateMarkerSpan && validationStartIndex >= 0
|
|
1228
|
+
? normalizedStdout.slice(validationStartIndex, validateMarkerSpan.start)
|
|
1229
|
+
: normalizedStdout;
|
|
1230
|
+
const importOutput = validateMarkerSpan
|
|
1231
|
+
? normalizedStdout.slice(
|
|
1232
|
+
validateMarkerSpan.end,
|
|
1233
|
+
importMarkerSpan?.start
|
|
1234
|
+
)
|
|
1235
|
+
: "";
|
|
1236
|
+
const diagnostic = terminationReason
|
|
1237
|
+
? `APEXlang process ${terminationReason}.`
|
|
1238
|
+
: processError instanceof Error
|
|
1239
|
+
? processError.message
|
|
1240
|
+
: "";
|
|
1241
|
+
const stderr = [capturedStderr.trimEnd(), diagnostic].filter(Boolean).join("\n");
|
|
1242
|
+
const markersAreOrdered = Boolean(
|
|
1243
|
+
readyMarkerSpan &&
|
|
1244
|
+
validateMarkerSpan &&
|
|
1245
|
+
importMarkerSpan &&
|
|
1246
|
+
readyMarkerSpan.start < validateMarkerSpan.start &&
|
|
1247
|
+
validateMarkerSpan.start < importMarkerSpan.start
|
|
1248
|
+
);
|
|
1249
|
+
const importCompleted = importSent && markersAreOrdered;
|
|
1250
|
+
const hardFailureDetected = containsSqlclHardFailure(
|
|
1251
|
+
[stdout, stderr].filter(Boolean).join("\n")
|
|
1252
|
+
);
|
|
1253
|
+
resolveProcess({
|
|
1254
|
+
ok: code === 0 && !terminationReason && !processError && ptyProcessGroupReady &&
|
|
1255
|
+
validationAccepted && importCompleted && !hardFailureDetected,
|
|
1256
|
+
code: Number.isInteger(code) && !terminationReason ? code : 1,
|
|
1257
|
+
stdout,
|
|
1258
|
+
stderr,
|
|
1259
|
+
validationAccepted,
|
|
1260
|
+
validationEvidence,
|
|
1261
|
+
sessionReady,
|
|
1262
|
+
validationSent,
|
|
1263
|
+
importSent,
|
|
1264
|
+
importCompleted,
|
|
1265
|
+
hardFailureDetected,
|
|
1266
|
+
validationOutput,
|
|
1267
|
+
importOutput,
|
|
1268
|
+
orderedMergedOutput: true,
|
|
1269
|
+
ptyBacked: true,
|
|
1270
|
+
ptyProcessGroupReady
|
|
1271
|
+
});
|
|
1272
|
+
});
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
|
|
677
1276
|
function invoke(command, options) {
|
|
678
1277
|
return executeProcessTree(process.execPath, [command.scriptPath, ...command.args], {
|
|
679
1278
|
cwd: options.cwd,
|
|
@@ -688,6 +1287,765 @@ function invoke(command, options) {
|
|
|
688
1287
|
});
|
|
689
1288
|
}
|
|
690
1289
|
|
|
1290
|
+
function parseJsonRecord(value) {
|
|
1291
|
+
try {
|
|
1292
|
+
const parsed = JSON.parse(String(value ?? ""));
|
|
1293
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : undefined;
|
|
1294
|
+
} catch {
|
|
1295
|
+
return undefined;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
function countMatches(value, pattern) {
|
|
1300
|
+
return [...String(value).matchAll(pattern)].length;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
function normalizeInteractiveSqlclOutput(output) {
|
|
1304
|
+
return stripVTControlCharacters(String(output))
|
|
1305
|
+
.replace(/\r+\n/g, "\n")
|
|
1306
|
+
.replace(/\r/g, "\n");
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
function escapeRegularExpression(value) {
|
|
1310
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
function exactSqlclMarkerSpan(output, marker, options = {}) {
|
|
1314
|
+
const normalizedOutput = normalizeInteractiveSqlclOutput(output);
|
|
1315
|
+
const escapedMarker = escapeRegularExpression(marker);
|
|
1316
|
+
const promptPrefix = options.allowPromptPrefix === true
|
|
1317
|
+
? "[^\\r\\n]{0,256}"
|
|
1318
|
+
: "(?:SQL>[^\\S\\r\\n]*)?";
|
|
1319
|
+
const match = new RegExp(
|
|
1320
|
+
`(^|\\n)${promptPrefix}${escapedMarker}(?=\\n|$)`
|
|
1321
|
+
).exec(normalizedOutput);
|
|
1322
|
+
if (!match) return undefined;
|
|
1323
|
+
return {
|
|
1324
|
+
start: match.index + match[1].length,
|
|
1325
|
+
end: match.index + match[0].length
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
function transcriptSection(transcript, label) {
|
|
1330
|
+
const header = `## ${label}`;
|
|
1331
|
+
const lines = String(transcript).split(/\r?\n/);
|
|
1332
|
+
const headerIndex = lines.findIndex((line) => line === header);
|
|
1333
|
+
if (headerIndex < 0) return "";
|
|
1334
|
+
const nextHeaderIndex = lines.findIndex(
|
|
1335
|
+
(line, index) => index > headerIndex && line.startsWith("## ")
|
|
1336
|
+
);
|
|
1337
|
+
return lines.slice(headerIndex + 1, nextHeaderIndex < 0 ? undefined : nextHeaderIndex).join("\n");
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
function containsSqlclHardFailure(output) {
|
|
1341
|
+
const normalizedOutput = stripVTControlCharacters(String(output));
|
|
1342
|
+
return SQLCL_HARD_FAILURE_PATTERNS.some((pattern) => pattern.test(normalizedOutput));
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
export function classifyWarningCompatibleSqlclValidation(output) {
|
|
1346
|
+
const warningCount = countMatches(output, SQLCL_WARNING_HEADER_PATTERN);
|
|
1347
|
+
const validationSuccessCount = countMatches(output, /^Validation successful\.\s*$/gim);
|
|
1348
|
+
if (warningCount !== 1 || validationSuccessCount !== 1) {
|
|
1349
|
+
return {
|
|
1350
|
+
accepted: false,
|
|
1351
|
+
reason: "warning_success_markers_not_exact",
|
|
1352
|
+
warningCount,
|
|
1353
|
+
validationSuccessCount
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
if (containsSqlclHardFailure(output)) {
|
|
1357
|
+
return {
|
|
1358
|
+
accepted: false,
|
|
1359
|
+
reason: "hard_failure_marker_present",
|
|
1360
|
+
warningCount,
|
|
1361
|
+
validationSuccessCount
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
return {
|
|
1365
|
+
accepted: true,
|
|
1366
|
+
reason: "sqlcl_explicit_validation_success_with_compile_warnings",
|
|
1367
|
+
warningCount,
|
|
1368
|
+
validationSuccessCount,
|
|
1369
|
+
outputSha256: createHash("sha256").update(String(output)).digest("hex")
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
export function classifyWarningOnlyRuntimePayload(
|
|
1374
|
+
payload,
|
|
1375
|
+
transcript,
|
|
1376
|
+
expectedImportIntent = "validate-only"
|
|
1377
|
+
) {
|
|
1378
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
1379
|
+
return { accepted: false, reason: "missing_runtime_payload" };
|
|
1380
|
+
}
|
|
1381
|
+
if (payload.failure_class !== "live_validate_failed" || payload.blocking_reason !== "live_validate_failed") {
|
|
1382
|
+
return { accepted: false, reason: "unexpected_failure_class" };
|
|
1383
|
+
}
|
|
1384
|
+
if (payload.import_intent_choice !== expectedImportIntent) {
|
|
1385
|
+
return { accepted: false, reason: "unexpected_import_intent" };
|
|
1386
|
+
}
|
|
1387
|
+
const phases = Array.isArray(payload.phase_reports) ? payload.phase_reports : [];
|
|
1388
|
+
const livePhaseIndex = phases.findIndex((phase) => phase?.phase === "live_validate");
|
|
1389
|
+
if (livePhaseIndex < 0) {
|
|
1390
|
+
return { accepted: false, reason: "missing_live_validate_phase" };
|
|
1391
|
+
}
|
|
1392
|
+
const livePhase = phases[livePhaseIndex];
|
|
1393
|
+
if (livePhase?.status !== "fail" || livePhase?.failure_class !== "live_validate_failed") {
|
|
1394
|
+
return { accepted: false, reason: "unexpected_live_validate_phase" };
|
|
1395
|
+
}
|
|
1396
|
+
if (phases.slice(0, livePhaseIndex).some((phase) => phase?.status !== "pass")) {
|
|
1397
|
+
return { accepted: false, reason: "earlier_phase_did_not_pass" };
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
const attemptLabel = "roundtrip_sql_name_alias";
|
|
1401
|
+
const attempt = transcriptSection(transcript, attemptLabel);
|
|
1402
|
+
if (!attempt) {
|
|
1403
|
+
return { accepted: false, reason: "preferred_sqlcl_attempt_missing" };
|
|
1404
|
+
}
|
|
1405
|
+
const sqlclEvidence = classifyWarningCompatibleSqlclValidation(attempt);
|
|
1406
|
+
if (!sqlclEvidence.accepted) return sqlclEvidence;
|
|
1407
|
+
return {
|
|
1408
|
+
accepted: true,
|
|
1409
|
+
reason: "sqlcl_explicit_validation_success_with_compile_warnings",
|
|
1410
|
+
attemptLabel,
|
|
1411
|
+
warningCount: sqlclEvidence.warningCount,
|
|
1412
|
+
validationSuccessCount: sqlclEvidence.validationSuccessCount,
|
|
1413
|
+
attemptSha256: createHash("sha256").update(attempt).digest("hex")
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
async function resolveOutputArtifact(outputRoot, candidate, label) {
|
|
1418
|
+
const outputPath = resolve(outputRoot);
|
|
1419
|
+
const artifactPath = resolve(String(candidate ?? ""));
|
|
1420
|
+
if (!candidate || !isWithin(outputPath, artifactPath)) {
|
|
1421
|
+
throw new Error(`${label} must stay inside APEXLANG_OUTPUT_ROOT`);
|
|
1422
|
+
}
|
|
1423
|
+
const [outputRealPath, artifactRealPath] = await Promise.all([
|
|
1424
|
+
realpath(outputPath),
|
|
1425
|
+
realpath(artifactPath)
|
|
1426
|
+
]);
|
|
1427
|
+
if (!isWithin(outputRealPath, artifactRealPath)) {
|
|
1428
|
+
throw new Error(`${label} must not escape APEXLANG_OUTPUT_ROOT through a symbolic link`);
|
|
1429
|
+
}
|
|
1430
|
+
return artifactRealPath;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
async function writeCompatibilityRecord(outputRoot, filename, record) {
|
|
1434
|
+
const compatibilityDirectory = resolve(outputRoot, "logs/compat");
|
|
1435
|
+
await mkdir(compatibilityDirectory, { recursive: true });
|
|
1436
|
+
const recordPath = resolve(compatibilityDirectory, filename);
|
|
1437
|
+
await writeFile(recordPath, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
1438
|
+
return recordPath;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
export async function proveWarningOnlyProblemsAreDiagnostics(payload, result) {
|
|
1442
|
+
const blockingReasons = Array.isArray(payload.blocking_reasons)
|
|
1443
|
+
? payload.blocking_reasons.map(String)
|
|
1444
|
+
: [];
|
|
1445
|
+
const hasProblemsBlocker = blockingReasons.includes(
|
|
1446
|
+
"problems.json contains unresolved validation problems."
|
|
1447
|
+
);
|
|
1448
|
+
const exactCount = (value) => {
|
|
1449
|
+
if (typeof value === "number") {
|
|
1450
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
|
1451
|
+
}
|
|
1452
|
+
if (typeof value === "string" && /^(?:0|[1-9]\d*)$/.test(value.trim())) {
|
|
1453
|
+
const parsed = Number(value.trim());
|
|
1454
|
+
return Number.isSafeInteger(parsed) ? parsed : undefined;
|
|
1455
|
+
}
|
|
1456
|
+
return undefined;
|
|
1457
|
+
};
|
|
1458
|
+
const outerProblemCount = exactCount(payload.problem_count);
|
|
1459
|
+
const outerUnresolvedCount = exactCount(payload.unresolved_count);
|
|
1460
|
+
if (outerProblemCount === undefined || outerUnresolvedCount === undefined) {
|
|
1461
|
+
return { accepted: false, reason: "runtime_problem_counts_are_invalid" };
|
|
1462
|
+
}
|
|
1463
|
+
if (outerProblemCount === 0 && outerUnresolvedCount === 0) {
|
|
1464
|
+
if (hasProblemsBlocker) {
|
|
1465
|
+
return { accepted: false, reason: "problem_blocker_conflicts_with_zero_counts" };
|
|
1466
|
+
}
|
|
1467
|
+
const zeroProblemsCandidate = payload.artifacts?.problems_path;
|
|
1468
|
+
if (!zeroProblemsCandidate) {
|
|
1469
|
+
return { accepted: false, reason: "problems_artifact_missing_for_zero_counts" };
|
|
1470
|
+
}
|
|
1471
|
+
const zeroProblemsPath = await resolveOutputArtifact(
|
|
1472
|
+
result.outputRoot,
|
|
1473
|
+
zeroProblemsCandidate,
|
|
1474
|
+
"problems_path"
|
|
1475
|
+
);
|
|
1476
|
+
const rawZeroProblems = await readFile(zeroProblemsPath, "utf8");
|
|
1477
|
+
const zeroProblemsPayload = parseJsonRecord(rawZeroProblems);
|
|
1478
|
+
const zeroProblems = zeroProblemsPayload?.problems;
|
|
1479
|
+
if (!Array.isArray(zeroProblems) || zeroProblems.length !== 0 ||
|
|
1480
|
+
exactCount(zeroProblemsPayload?.problem_count) !== 0 ||
|
|
1481
|
+
exactCount(zeroProblemsPayload?.unresolved_count) !== 0) {
|
|
1482
|
+
return { accepted: false, reason: "zero_problem_counts_conflict_with_artifact" };
|
|
1483
|
+
}
|
|
1484
|
+
return {
|
|
1485
|
+
accepted: true,
|
|
1486
|
+
problemCount: 0,
|
|
1487
|
+
problemsPath: zeroProblemsPath,
|
|
1488
|
+
problemsSha256: createHash("sha256").update(rawZeroProblems).digest("hex")
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
if (outerProblemCount === 0 || outerUnresolvedCount === 0) {
|
|
1492
|
+
return { accepted: false, reason: "runtime_problem_counts_are_inconsistent" };
|
|
1493
|
+
}
|
|
1494
|
+
const problemsPath = await resolveOutputArtifact(
|
|
1495
|
+
result.outputRoot,
|
|
1496
|
+
payload.artifacts?.problems_path,
|
|
1497
|
+
"problems_path"
|
|
1498
|
+
);
|
|
1499
|
+
const rawProblems = await readFile(problemsPath, "utf8");
|
|
1500
|
+
const problemsPayload = parseJsonRecord(rawProblems);
|
|
1501
|
+
const problems = Array.isArray(problemsPayload?.problems) ? problemsPayload.problems : [];
|
|
1502
|
+
const declaredProblemCount = exactCount(problemsPayload?.problem_count);
|
|
1503
|
+
const declaredUnresolvedCount = exactCount(problemsPayload?.unresolved_count);
|
|
1504
|
+
const warningsOnly = problems.length > 0 && problems.every((problem) =>
|
|
1505
|
+
problem?.source === "apex_validate" &&
|
|
1506
|
+
String(problem?.severity ?? "").toLowerCase() === "warning" &&
|
|
1507
|
+
/^Warning:\s/.test(String(problem?.message ?? "")) &&
|
|
1508
|
+
!containsSqlclHardFailure(problem?.message)
|
|
1509
|
+
);
|
|
1510
|
+
if (!warningsOnly ||
|
|
1511
|
+
declaredProblemCount !== problems.length ||
|
|
1512
|
+
declaredUnresolvedCount !== problems.length ||
|
|
1513
|
+
outerProblemCount !== problems.length ||
|
|
1514
|
+
outerUnresolvedCount !== problems.length) {
|
|
1515
|
+
return { accepted: false, reason: "problems_artifact_is_not_warning_only" };
|
|
1516
|
+
}
|
|
1517
|
+
return {
|
|
1518
|
+
accepted: true,
|
|
1519
|
+
problemCount: problems.length,
|
|
1520
|
+
problemsPath,
|
|
1521
|
+
problemsSha256: createHash("sha256").update(rawProblems).digest("hex")
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
async function proveWarningOnlyValidationIdentity(
|
|
1526
|
+
input,
|
|
1527
|
+
result,
|
|
1528
|
+
payload,
|
|
1529
|
+
roundtripPayload,
|
|
1530
|
+
roundtripReportPath,
|
|
1531
|
+
transcriptPath
|
|
1532
|
+
) {
|
|
1533
|
+
const runtimeAppPathValue = result.runtimeApp?.appPath;
|
|
1534
|
+
if (!runtimeAppPathValue) throw new Error("runtime app path is missing");
|
|
1535
|
+
const runtimeAppPath = await realpath(resolve(runtimeAppPathValue));
|
|
1536
|
+
for (const candidate of [
|
|
1537
|
+
roundtripPayload.final_app_path,
|
|
1538
|
+
roundtripPayload.temp_app_path,
|
|
1539
|
+
roundtripPayload.frozen_preflight_facts?.app_path
|
|
1540
|
+
]) {
|
|
1541
|
+
if (!candidate || await realpath(resolve(candidate)) !== runtimeAppPath) {
|
|
1542
|
+
throw new Error("runtime evidence app path does not match the current runtime app");
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
if (roundtripPayload.report_path &&
|
|
1546
|
+
await realpath(resolve(roundtripPayload.report_path)) !== roundtripReportPath) {
|
|
1547
|
+
throw new Error("runtime report path does not match the current artifact");
|
|
1548
|
+
}
|
|
1549
|
+
if (roundtripPayload.transcript_path &&
|
|
1550
|
+
await realpath(resolve(roundtripPayload.transcript_path)) !== transcriptPath) {
|
|
1551
|
+
throw new Error("runtime transcript path does not match the current artifact");
|
|
1552
|
+
}
|
|
1553
|
+
const outerTimestamp = Date.parse(String(payload.timestamp ?? ""));
|
|
1554
|
+
const roundtripTimestamp = Date.parse(String(roundtripPayload.timestamp ?? ""));
|
|
1555
|
+
if (!Number.isFinite(outerTimestamp) || !Number.isFinite(roundtripTimestamp) ||
|
|
1556
|
+
Math.abs(outerTimestamp - roundtripTimestamp) > 5_000) {
|
|
1557
|
+
throw new Error("runtime artifacts do not share the same validation timestamp");
|
|
1558
|
+
}
|
|
1559
|
+
const sourceId = Number(roundtripPayload.source_application_id);
|
|
1560
|
+
const frozen = roundtripPayload.frozen_preflight_facts;
|
|
1561
|
+
if (!Number.isInteger(sourceId) || sourceId <= 0 ||
|
|
1562
|
+
Number(frozen?.requested_application_id) !== sourceId ||
|
|
1563
|
+
frozen?.execution_mode_selected !== "path" ||
|
|
1564
|
+
frozen?.db_connection_name !== input.db_connection_name ||
|
|
1565
|
+
roundtripPayload.db_connection_name !== input.db_connection_name ||
|
|
1566
|
+
!sameName(frozen?.requested_application_alias, roundtripPayload.source_application_alias)) {
|
|
1567
|
+
throw new Error("runtime evidence source identity does not match frozen preflight facts");
|
|
1568
|
+
}
|
|
1569
|
+
const deployment = parseJsonRecord(
|
|
1570
|
+
await readFile(resolve(runtimeAppPath, "deployments/default.json"), "utf8")
|
|
1571
|
+
);
|
|
1572
|
+
const deploymentWorkspace = workspaceName(
|
|
1573
|
+
deployment?.workspace?.name ?? deployment?.app?.workspace?.name
|
|
1574
|
+
);
|
|
1575
|
+
if (Number(deployment?.app?.id) !== sourceId ||
|
|
1576
|
+
!workspaceNamesMatch(deploymentWorkspace, input.workspace_name)) {
|
|
1577
|
+
throw new Error("runtime evidence does not match staged deployment identity");
|
|
1578
|
+
}
|
|
1579
|
+
return {
|
|
1580
|
+
runtimeAppPath,
|
|
1581
|
+
sourceId,
|
|
1582
|
+
sourceAlias: roundtripPayload.source_application_alias,
|
|
1583
|
+
timestampDeltaMs: Math.abs(outerTimestamp - roundtripTimestamp)
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
async function normalizeWarningOnlyValidation(input, result) {
|
|
1588
|
+
input = normalizeApexlangInput(input);
|
|
1589
|
+
if (result.ok || input.action !== "runtime_validate") return result;
|
|
1590
|
+
const payload = parseJsonRecord(result.stdout);
|
|
1591
|
+
const artifacts = payload?.artifacts;
|
|
1592
|
+
if (!artifacts || typeof artifacts !== "object") return result;
|
|
1593
|
+
|
|
1594
|
+
let roundtripReportPath;
|
|
1595
|
+
let transcriptPath;
|
|
1596
|
+
try {
|
|
1597
|
+
[roundtripReportPath, transcriptPath] = await Promise.all([
|
|
1598
|
+
resolveOutputArtifact(
|
|
1599
|
+
result.outputRoot,
|
|
1600
|
+
artifacts.roundtrip_report_path,
|
|
1601
|
+
"roundtrip_report_path"
|
|
1602
|
+
),
|
|
1603
|
+
resolveOutputArtifact(
|
|
1604
|
+
result.outputRoot,
|
|
1605
|
+
artifacts.validation_transcript_path,
|
|
1606
|
+
"validation_transcript_path"
|
|
1607
|
+
)
|
|
1608
|
+
]);
|
|
1609
|
+
} catch {
|
|
1610
|
+
return result;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
const [roundtripPayload, transcript] = await Promise.all([
|
|
1614
|
+
readFile(roundtripReportPath, "utf8").then(parseJsonRecord),
|
|
1615
|
+
readFile(transcriptPath, "utf8")
|
|
1616
|
+
]);
|
|
1617
|
+
const evidence = classifyWarningOnlyRuntimePayload(roundtripPayload, transcript, "validate-only");
|
|
1618
|
+
if (!evidence.accepted) return result;
|
|
1619
|
+
if (roundtripPayload.db_connection_name !== input.db_connection_name) return result;
|
|
1620
|
+
const originalBlockingReasons = Array.isArray(payload.blocking_reasons)
|
|
1621
|
+
? payload.blocking_reasons.map(String)
|
|
1622
|
+
: [];
|
|
1623
|
+
if (originalBlockingReasons.some(
|
|
1624
|
+
(reason) => !COMPATIBLE_VALIDATION_BLOCKING_REASONS.has(reason)
|
|
1625
|
+
)) {
|
|
1626
|
+
return result;
|
|
1627
|
+
}
|
|
1628
|
+
let identityEvidence;
|
|
1629
|
+
let problemEvidence;
|
|
1630
|
+
try {
|
|
1631
|
+
[identityEvidence, problemEvidence] = await Promise.all([
|
|
1632
|
+
proveWarningOnlyValidationIdentity(
|
|
1633
|
+
input,
|
|
1634
|
+
result,
|
|
1635
|
+
payload,
|
|
1636
|
+
roundtripPayload,
|
|
1637
|
+
roundtripReportPath,
|
|
1638
|
+
transcriptPath
|
|
1639
|
+
),
|
|
1640
|
+
proveWarningOnlyProblemsAreDiagnostics(payload, result)
|
|
1641
|
+
]);
|
|
1642
|
+
} catch {
|
|
1643
|
+
return result;
|
|
1644
|
+
}
|
|
1645
|
+
if (!problemEvidence.accepted) return result;
|
|
1646
|
+
const diagnosticReasons = originalBlockingReasons.filter(
|
|
1647
|
+
(reason) => reason !== "Live APEX validation did not pass or did not produce pass evidence."
|
|
1648
|
+
);
|
|
1649
|
+
|
|
1650
|
+
const compatibilityRecord = {
|
|
1651
|
+
status: "accepted",
|
|
1652
|
+
policy: "sqlcl_explicit_validation_success_with_compile_warnings",
|
|
1653
|
+
timestamp: new Date().toISOString(),
|
|
1654
|
+
db_connection_name: input.db_connection_name,
|
|
1655
|
+
workspace_name: input.workspace_name,
|
|
1656
|
+
original_live_check_status: payload.live_check_status,
|
|
1657
|
+
original_validation_status: payload.validation_status,
|
|
1658
|
+
original_unresolved_count: payload.unresolved_count,
|
|
1659
|
+
original_blocking_reasons: originalBlockingReasons,
|
|
1660
|
+
diagnostic_reasons: diagnosticReasons,
|
|
1661
|
+
validation_identity: identityEvidence,
|
|
1662
|
+
warning_only_problems: problemEvidence,
|
|
1663
|
+
roundtrip_report_path: roundtripReportPath,
|
|
1664
|
+
transcript_path: transcriptPath,
|
|
1665
|
+
transcript_sha256: createHash("sha256").update(transcript).digest("hex"),
|
|
1666
|
+
...evidence
|
|
1667
|
+
};
|
|
1668
|
+
delete compatibilityRecord.accepted;
|
|
1669
|
+
const compatibilityReportPath = await writeCompatibilityRecord(
|
|
1670
|
+
result.outputRoot,
|
|
1671
|
+
"sqlcl-warning-policy-validation.json",
|
|
1672
|
+
compatibilityRecord
|
|
1673
|
+
);
|
|
1674
|
+
const normalizedPayload = {
|
|
1675
|
+
...payload,
|
|
1676
|
+
live_check_status: "pass",
|
|
1677
|
+
validation_status: "pass",
|
|
1678
|
+
warnings_as_errors: false,
|
|
1679
|
+
validation_sources: {
|
|
1680
|
+
...(payload.validation_sources ?? {}),
|
|
1681
|
+
live_validator: {
|
|
1682
|
+
...(payload.validation_sources?.live_validator ?? {}),
|
|
1683
|
+
status: "pass",
|
|
1684
|
+
policy: compatibilityRecord.policy,
|
|
1685
|
+
compatibility_report_path: compatibilityReportPath
|
|
1686
|
+
},
|
|
1687
|
+
...(payload.validation_sources?.compiler_truth
|
|
1688
|
+
? {
|
|
1689
|
+
compiler_truth: {
|
|
1690
|
+
...payload.validation_sources.compiler_truth,
|
|
1691
|
+
gate_role: "diagnostic_after_live_pass"
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
: {}),
|
|
1695
|
+
...(payload.validation_sources?.vscode_problems
|
|
1696
|
+
? {
|
|
1697
|
+
vscode_problems: {
|
|
1698
|
+
...payload.validation_sources.vscode_problems,
|
|
1699
|
+
gate_role: "diagnostic_after_live_pass"
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
: {})
|
|
1703
|
+
},
|
|
1704
|
+
blocking_reasons: [],
|
|
1705
|
+
diagnostic_reasons: diagnosticReasons,
|
|
1706
|
+
import_eligibility: "validate-only-passed-with-compile-warnings",
|
|
1707
|
+
compatibility_fallback: compatibilityRecord,
|
|
1708
|
+
notes: [
|
|
1709
|
+
...(Array.isArray(payload.notes) ? payload.notes : []),
|
|
1710
|
+
"SQLcl returned an explicit Validation successful marker; compile warnings remain recorded as diagnostics."
|
|
1711
|
+
]
|
|
1712
|
+
};
|
|
1713
|
+
return {
|
|
1714
|
+
...result,
|
|
1715
|
+
ok: true,
|
|
1716
|
+
code: 0,
|
|
1717
|
+
stdout: `${JSON.stringify(normalizedPayload, null, 2)}\n`
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
function sameName(left, right) {
|
|
1722
|
+
return String(left ?? "").trim().toUpperCase() === String(right ?? "").trim().toUpperCase();
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
export function validateUpdateExistingImportProof(payload, input, stagedAppPath) {
|
|
1726
|
+
if (payload.target_resolution_mode !== "update-existing" ||
|
|
1727
|
+
payload.target_resolution_status !== "resolved_existing_app" ||
|
|
1728
|
+
payload.direct_import_fallback_allowed !== true ||
|
|
1729
|
+
payload.canonical_mapping_status !== "resolved") {
|
|
1730
|
+
throw new Error("APEXlang compatibility import requires a resolved existing-app target");
|
|
1731
|
+
}
|
|
1732
|
+
const canonicalId = Number(payload.canonical_application_id);
|
|
1733
|
+
const candidateIds = Array.isArray(payload.candidate_ids)
|
|
1734
|
+
? payload.candidate_ids.map(Number)
|
|
1735
|
+
: [];
|
|
1736
|
+
if (!Number.isInteger(canonicalId) || canonicalId <= 0 ||
|
|
1737
|
+
payload.candidate_count !== 1 ||
|
|
1738
|
+
candidateIds.length !== 1 ||
|
|
1739
|
+
candidateIds[0] !== canonicalId) {
|
|
1740
|
+
throw new Error("APEXlang compatibility import requires exactly one canonical application id");
|
|
1741
|
+
}
|
|
1742
|
+
if (!sameName(payload.canonical_application_alias, payload.source_application_alias) ||
|
|
1743
|
+
!payload.canonical_application_alias) {
|
|
1744
|
+
throw new Error("APEXlang compatibility import requires matching source and canonical aliases");
|
|
1745
|
+
}
|
|
1746
|
+
const sourceId = Number(payload.source_application_id);
|
|
1747
|
+
if (!Number.isInteger(sourceId) || sourceId <= 0 ||
|
|
1748
|
+
Number(payload.frozen_preflight_facts?.requested_application_id) !== sourceId ||
|
|
1749
|
+
!sameName(
|
|
1750
|
+
payload.frozen_preflight_facts?.requested_application_alias,
|
|
1751
|
+
payload.source_application_alias
|
|
1752
|
+
) ||
|
|
1753
|
+
!sameName(
|
|
1754
|
+
payload.frozen_preflight_facts?.requested_application_alias,
|
|
1755
|
+
payload.canonical_application_alias
|
|
1756
|
+
)) {
|
|
1757
|
+
throw new Error("APEXlang compatibility import source identity does not match frozen preflight facts");
|
|
1758
|
+
}
|
|
1759
|
+
const workspaceId = String(payload.lookup_scope_workspaceid ?? "").trim();
|
|
1760
|
+
if (!/^\d+$/.test(workspaceId) || workspaceId !== String(payload.workspaceid ?? "").trim()) {
|
|
1761
|
+
throw new Error("APEXlang compatibility import requires one proven numeric workspace id");
|
|
1762
|
+
}
|
|
1763
|
+
if (!sameName(payload.lookup_scope_workspace_name, input.workspace_name) ||
|
|
1764
|
+
!sameName(payload.frozen_preflight_facts?.workspace_scope?.workspace_name, input.workspace_name) ||
|
|
1765
|
+
String(payload.frozen_preflight_facts?.workspace_scope?.workspace_id ?? "").trim() !== workspaceId) {
|
|
1766
|
+
throw new Error("APEXlang compatibility import workspace proof does not match workspace_name");
|
|
1767
|
+
}
|
|
1768
|
+
if (payload.db_connection_name !== input.db_connection_name ||
|
|
1769
|
+
payload.frozen_preflight_facts?.db_connection_name !== input.db_connection_name) {
|
|
1770
|
+
throw new Error("APEXlang compatibility import connection proof does not match db_connection_name");
|
|
1771
|
+
}
|
|
1772
|
+
if (payload.execution_mode_used !== "path" ||
|
|
1773
|
+
payload.frozen_preflight_facts?.execution_mode_selected !== "path" ||
|
|
1774
|
+
resolve(payload.final_app_path) !== resolve(stagedAppPath) ||
|
|
1775
|
+
resolve(payload.temp_app_path) !== resolve(stagedAppPath) ||
|
|
1776
|
+
resolve(payload.frozen_preflight_facts?.app_path ?? "") !== resolve(stagedAppPath)) {
|
|
1777
|
+
throw new Error("APEXlang compatibility import requires the proven staged PATH runtime app");
|
|
1778
|
+
}
|
|
1779
|
+
return {
|
|
1780
|
+
canonicalId,
|
|
1781
|
+
canonicalAlias: payload.canonical_application_alias,
|
|
1782
|
+
sourceId,
|
|
1783
|
+
sourceAlias: payload.source_application_alias,
|
|
1784
|
+
workspaceId,
|
|
1785
|
+
workspaceName: payload.lookup_scope_workspace_name
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
export function buildWarningCompatibleImportCommands({ appPath, workspaceId, canonicalId }) {
|
|
1790
|
+
const safeAppPath = validatePath(assertPlainText(String(appPath), "app_path"), "app_path");
|
|
1791
|
+
if (!/^\d+$/.test(String(workspaceId)) || !Number.isInteger(canonicalId) || canonicalId <= 0) {
|
|
1792
|
+
throw new Error("A proven workspace id and canonical application id are required for import");
|
|
1793
|
+
}
|
|
1794
|
+
return {
|
|
1795
|
+
validateCommand: `apex validate -input "${safeAppPath}" -workspaceid ${workspaceId}`,
|
|
1796
|
+
importCommand: `apex import -input "${safeAppPath}" -workspaceid ${workspaceId} -id ${canonicalId}`
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
export async function runWarningCompatibleImport(input, result, options, dependencies = {}) {
|
|
1801
|
+
input = normalizeApexlangInput(input);
|
|
1802
|
+
const payload = parseJsonRecord(result.stdout);
|
|
1803
|
+
if (!payload || !result.runtimeApp?.staged || !result.runtimeApp.appPath) return result;
|
|
1804
|
+
if (payload.transcript_path === undefined) return result;
|
|
1805
|
+
|
|
1806
|
+
const transcriptPath = await resolveOutputArtifact(
|
|
1807
|
+
result.outputRoot,
|
|
1808
|
+
payload.transcript_path,
|
|
1809
|
+
"runtime transcript_path"
|
|
1810
|
+
);
|
|
1811
|
+
const transcript = await readFile(transcriptPath, "utf8");
|
|
1812
|
+
const evidence = classifyWarningOnlyRuntimePayload(
|
|
1813
|
+
payload,
|
|
1814
|
+
transcript,
|
|
1815
|
+
"validate-and-import"
|
|
1816
|
+
);
|
|
1817
|
+
if (!evidence.accepted) return result;
|
|
1818
|
+
|
|
1819
|
+
const stagedProofPath = resolve(result.runtimeApp.appPath);
|
|
1820
|
+
const stagedAppPath = await realpath(stagedProofPath);
|
|
1821
|
+
const runtimeAppsPath = await realpath(resolve(result.outputRoot, "runtime-apps"));
|
|
1822
|
+
if (!isWithin(runtimeAppsPath, stagedAppPath)) {
|
|
1823
|
+
throw new Error("APEXlang compatibility import app must stay under APEXLANG_OUTPUT_ROOT/runtime-apps");
|
|
1824
|
+
}
|
|
1825
|
+
const proof = validateUpdateExistingImportProof(payload, input, stagedProofPath);
|
|
1826
|
+
const deploymentPath = resolve(stagedAppPath, "deployments/default.json");
|
|
1827
|
+
const deployment = parseJsonRecord(await readFile(deploymentPath, "utf8"));
|
|
1828
|
+
if (!deployment || !deployment.app || typeof deployment.app !== "object" || Array.isArray(deployment.app)) {
|
|
1829
|
+
throw new Error("Staged deployments/default.json must contain an app object before import");
|
|
1830
|
+
}
|
|
1831
|
+
const previousApplicationId = Number(deployment.app.id);
|
|
1832
|
+
if (!Number.isInteger(previousApplicationId) || previousApplicationId !== proof.sourceId) {
|
|
1833
|
+
throw new Error("Staged deployment application id does not match the frozen source identity");
|
|
1834
|
+
}
|
|
1835
|
+
const validatedAppDigest = requireAppDigest(result.appDigest, "validated appDigest");
|
|
1836
|
+
const currentAppDigest = await computeApexlangAppDigest(stagedAppPath);
|
|
1837
|
+
if (currentAppDigest !== validatedAppDigest) {
|
|
1838
|
+
throw new Error(
|
|
1839
|
+
"Staged APEXlang application changed after validation; revalidate before importing"
|
|
1840
|
+
);
|
|
1841
|
+
}
|
|
1842
|
+
deployment.app.id = proof.canonicalId;
|
|
1843
|
+
await writeFile(deploymentPath, `${JSON.stringify(deployment, null, 2)}\n`, "utf8");
|
|
1844
|
+
|
|
1845
|
+
const commands = buildWarningCompatibleImportCommands({
|
|
1846
|
+
appPath: stagedAppPath,
|
|
1847
|
+
workspaceId: proof.workspaceId,
|
|
1848
|
+
canonicalId: proof.canonicalId
|
|
1849
|
+
});
|
|
1850
|
+
const sessionRunner = dependencies.sessionRunner ?? executeSqlclValidationThenImport;
|
|
1851
|
+
const importResult = await sessionRunner("sql", ["-S", "-name", input.db_connection_name], {
|
|
1852
|
+
cwd: options.cwd,
|
|
1853
|
+
env: process.env,
|
|
1854
|
+
...commands,
|
|
1855
|
+
maxBuffer: DEFAULT_MAX_BUFFER,
|
|
1856
|
+
signal: options.signal,
|
|
1857
|
+
timeoutMs: options.timeoutMs
|
|
1858
|
+
});
|
|
1859
|
+
const compatibilityDirectory = resolve(result.outputRoot, "logs/compat");
|
|
1860
|
+
await mkdir(compatibilityDirectory, { recursive: true });
|
|
1861
|
+
const importLogPath = resolve(compatibilityDirectory, "sqlcl-warning-policy-import.log");
|
|
1862
|
+
const fullSessionOutput = [importResult.stdout, importResult.stderr].filter(Boolean).join("\n");
|
|
1863
|
+
await writeFile(importLogPath, fullSessionOutput, "utf8");
|
|
1864
|
+
const validationOutput = [importResult.validationOutput, importResult.stderr]
|
|
1865
|
+
.filter(Boolean)
|
|
1866
|
+
.join("\n");
|
|
1867
|
+
const validationEvidence = classifyWarningCompatibleSqlclValidation(validationOutput);
|
|
1868
|
+
const hardFailureDetected = containsSqlclHardFailure(fullSessionOutput);
|
|
1869
|
+
const normalizedImportOutput = normalizeInteractiveSqlclOutput(
|
|
1870
|
+
importResult.importOutput ?? ""
|
|
1871
|
+
);
|
|
1872
|
+
const importedTargetMatches = [
|
|
1873
|
+
...normalizedImportOutput.matchAll(SQLCL_IMPORT_TARGET_PATTERN)
|
|
1874
|
+
];
|
|
1875
|
+
const importedTargets = importedTargetMatches.map((match) => ({
|
|
1876
|
+
applicationId: Number(match[1]),
|
|
1877
|
+
workspaceName: String(match[2]).trim()
|
|
1878
|
+
}));
|
|
1879
|
+
const importedIds = importedTargets.map((target) => target.applicationId);
|
|
1880
|
+
const importedWorkspaceNames = importedTargets.map((target) => target.workspaceName);
|
|
1881
|
+
const importedTarget = importedTargets.length === 1 ? importedTargets[0] : null;
|
|
1882
|
+
const importedId = importedTarget?.applicationId ?? null;
|
|
1883
|
+
const importSuccessMatches = [
|
|
1884
|
+
...normalizedImportOutput.matchAll(SQLCL_IMPORT_SUCCESS_PATTERN)
|
|
1885
|
+
];
|
|
1886
|
+
const importSuccessCount = importSuccessMatches.length;
|
|
1887
|
+
const targetReportedBeforeSuccess = Boolean(
|
|
1888
|
+
importedTargetMatches.length === 1 &&
|
|
1889
|
+
importSuccessMatches.length === 1 &&
|
|
1890
|
+
importedTargetMatches[0].index < importSuccessMatches[0].index
|
|
1891
|
+
);
|
|
1892
|
+
const importSuccessIsTerminal = Boolean(
|
|
1893
|
+
importSuccessMatches.length === 1 &&
|
|
1894
|
+
normalizedImportOutput.slice(
|
|
1895
|
+
importSuccessMatches[0].index + importSuccessMatches[0][0].length
|
|
1896
|
+
).trim() === ""
|
|
1897
|
+
);
|
|
1898
|
+
if (!importResult.ok || importResult.code !== 0 ||
|
|
1899
|
+
importResult.ptyBacked !== true ||
|
|
1900
|
+
importResult.ptyProcessGroupReady !== true ||
|
|
1901
|
+
importResult.sessionReady !== true ||
|
|
1902
|
+
importResult.validationSent !== true ||
|
|
1903
|
+
importResult.validationAccepted !== true ||
|
|
1904
|
+
importResult.importSent !== true ||
|
|
1905
|
+
importResult.importCompleted !== true ||
|
|
1906
|
+
importResult.orderedMergedOutput !== true ||
|
|
1907
|
+
!validationEvidence.accepted ||
|
|
1908
|
+
hardFailureDetected ||
|
|
1909
|
+
importSuccessCount !== 1 ||
|
|
1910
|
+
!targetReportedBeforeSuccess ||
|
|
1911
|
+
!importSuccessIsTerminal ||
|
|
1912
|
+
importedTargets.length !== 1 ||
|
|
1913
|
+
importedId !== proof.canonicalId ||
|
|
1914
|
+
!sameName(importedTarget?.workspaceName, proof.workspaceName)) {
|
|
1915
|
+
await writeCompatibilityRecord(
|
|
1916
|
+
result.outputRoot,
|
|
1917
|
+
"sqlcl-warning-policy-import-failure.json",
|
|
1918
|
+
{
|
|
1919
|
+
status: "fail",
|
|
1920
|
+
timestamp: new Date().toISOString(),
|
|
1921
|
+
canonical_application_id: proof.canonicalId,
|
|
1922
|
+
workspace_id: proof.workspaceId,
|
|
1923
|
+
pty_backed: importResult.ptyBacked === true,
|
|
1924
|
+
pty_process_group_ready: importResult.ptyProcessGroupReady === true,
|
|
1925
|
+
session_ready: importResult.sessionReady === true,
|
|
1926
|
+
validation_sent: importResult.validationSent === true,
|
|
1927
|
+
validation_accepted: importResult.validationAccepted === true,
|
|
1928
|
+
import_sent: importResult.importSent === true,
|
|
1929
|
+
import_completed: importResult.importCompleted === true,
|
|
1930
|
+
ordered_merged_output: importResult.orderedMergedOutput === true,
|
|
1931
|
+
imported_application_ids: importedIds,
|
|
1932
|
+
imported_workspace_names: importedWorkspaceNames,
|
|
1933
|
+
import_success_count: importSuccessCount,
|
|
1934
|
+
target_reported_before_success: targetReportedBeforeSuccess,
|
|
1935
|
+
import_success_is_terminal: importSuccessIsTerminal,
|
|
1936
|
+
hard_failure_detected: hardFailureDetected,
|
|
1937
|
+
validation_evidence: validationEvidence,
|
|
1938
|
+
import_log_path: importLogPath,
|
|
1939
|
+
import_log_sha256: createHash("sha256").update(fullSessionOutput).digest("hex")
|
|
1940
|
+
}
|
|
1941
|
+
);
|
|
1942
|
+
throw new Error("APEXlang compatibility validate-and-import did not return proven SQLcl success");
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
const compatibilityRecord = {
|
|
1946
|
+
status: "pass",
|
|
1947
|
+
policy: "sqlcl_explicit_validation_success_with_compile_warnings",
|
|
1948
|
+
timestamp: new Date().toISOString(),
|
|
1949
|
+
target_resolution_status: payload.target_resolution_status,
|
|
1950
|
+
canonical_application_id: proof.canonicalId,
|
|
1951
|
+
canonical_application_alias: proof.canonicalAlias,
|
|
1952
|
+
workspace_id: proof.workspaceId,
|
|
1953
|
+
workspace_name: proof.workspaceName,
|
|
1954
|
+
db_connection_name: input.db_connection_name,
|
|
1955
|
+
staged_app_path: stagedAppPath,
|
|
1956
|
+
validated_app_digest: validatedAppDigest,
|
|
1957
|
+
staged_deployment_previous_application_id: Number.isInteger(previousApplicationId)
|
|
1958
|
+
? previousApplicationId
|
|
1959
|
+
: null,
|
|
1960
|
+
staged_deployment_reconciled: previousApplicationId !== proof.canonicalId,
|
|
1961
|
+
imported_application_id: importedId,
|
|
1962
|
+
imported_workspace_name: importedTarget.workspaceName,
|
|
1963
|
+
import_success_count: importSuccessCount,
|
|
1964
|
+
target_reported_before_success: true,
|
|
1965
|
+
import_success_is_terminal: true,
|
|
1966
|
+
sqlcl_entrypoint: "sql -S -name <saved-alias>",
|
|
1967
|
+
pty_backed_sqlcl_session: true,
|
|
1968
|
+
pty_process_group_ready: true,
|
|
1969
|
+
session_ready_marker: true,
|
|
1970
|
+
one_session_validate_and_import: true,
|
|
1971
|
+
validation_gated_before_import_send: true,
|
|
1972
|
+
ordered_merged_output: true,
|
|
1973
|
+
source_runtime_report_path: payload.report_path,
|
|
1974
|
+
source_runtime_transcript_path: transcriptPath,
|
|
1975
|
+
source_runtime_transcript_sha256: createHash("sha256").update(transcript).digest("hex"),
|
|
1976
|
+
import_log_path: importLogPath,
|
|
1977
|
+
import_log_sha256: createHash("sha256").update(fullSessionOutput).digest("hex"),
|
|
1978
|
+
validation_evidence: validationEvidence,
|
|
1979
|
+
...evidence
|
|
1980
|
+
};
|
|
1981
|
+
delete compatibilityRecord.accepted;
|
|
1982
|
+
const compatibilityReportPath = await writeCompatibilityRecord(
|
|
1983
|
+
result.outputRoot,
|
|
1984
|
+
"sqlcl-warning-policy-import.json",
|
|
1985
|
+
compatibilityRecord
|
|
1986
|
+
);
|
|
1987
|
+
const normalizedPayload = {
|
|
1988
|
+
...payload,
|
|
1989
|
+
warnings_as_errors: false,
|
|
1990
|
+
validate_status: "pass",
|
|
1991
|
+
live_check_status: "pass",
|
|
1992
|
+
live_check_token: "APEXLANG_LIVE_CHECK_OK",
|
|
1993
|
+
final_check_status: "pass",
|
|
1994
|
+
import_status: "pass",
|
|
1995
|
+
runtime_gate_status: "pass",
|
|
1996
|
+
failure_class: "",
|
|
1997
|
+
blocking_reason: "",
|
|
1998
|
+
blocking_findings_count: 0,
|
|
1999
|
+
repair_loop_required: false,
|
|
2000
|
+
validation_feedback_status: "diagnostics-recorded",
|
|
2001
|
+
review_status: "diagnostics_recorded",
|
|
2002
|
+
recommended_next_action: "Import completed; review the recorded compile-warning diagnostics.",
|
|
2003
|
+
direct_import_bypass_forbidden: true,
|
|
2004
|
+
import_mode_used: "direct",
|
|
2005
|
+
import_lane_fallback_used: true,
|
|
2006
|
+
import_lane_fallback_reason: compatibilityRecord.policy,
|
|
2007
|
+
session_entrypoint_used: "compat_sql_name_alias",
|
|
2008
|
+
canonical_mapping_reconciled_source: previousApplicationId !== proof.canonicalId,
|
|
2009
|
+
phase_reports: [
|
|
2010
|
+
...(Array.isArray(payload.phase_reports)
|
|
2011
|
+
? payload.phase_reports.map((phase) => phase?.phase === "live_validate"
|
|
2012
|
+
? {
|
|
2013
|
+
...phase,
|
|
2014
|
+
original_status: phase.status,
|
|
2015
|
+
original_failure_class: phase.failure_class,
|
|
2016
|
+
status: "pass",
|
|
2017
|
+
failure_class: "",
|
|
2018
|
+
next_safe_action: "Continue to import.",
|
|
2019
|
+
compatibility_policy: compatibilityRecord.policy
|
|
2020
|
+
}
|
|
2021
|
+
: phase)
|
|
2022
|
+
: []),
|
|
2023
|
+
{
|
|
2024
|
+
phase: "import",
|
|
2025
|
+
status: "pass",
|
|
2026
|
+
failure_class: "",
|
|
2027
|
+
next_safe_action: "Import completed.",
|
|
2028
|
+
compatibility_policy: compatibilityRecord.policy
|
|
2029
|
+
}
|
|
2030
|
+
],
|
|
2031
|
+
compatibility_fallback: {
|
|
2032
|
+
...compatibilityRecord,
|
|
2033
|
+
report_path: compatibilityReportPath
|
|
2034
|
+
},
|
|
2035
|
+
notes: [
|
|
2036
|
+
...(Array.isArray(payload.notes) ? payload.notes : []),
|
|
2037
|
+
"Imported after SQLcl explicitly validated the same staged app in the same saved-alias session; compile warnings remain recorded as diagnostics."
|
|
2038
|
+
]
|
|
2039
|
+
};
|
|
2040
|
+
return {
|
|
2041
|
+
...result,
|
|
2042
|
+
ok: true,
|
|
2043
|
+
code: 0,
|
|
2044
|
+
stdout: `${JSON.stringify(normalizedPayload, null, 2)}\n`,
|
|
2045
|
+
stderr: ""
|
|
2046
|
+
};
|
|
2047
|
+
}
|
|
2048
|
+
|
|
691
2049
|
async function enforceMaterializationGate(input, result, options) {
|
|
692
2050
|
if (!result.ok || input.action !== "new_app_materialize") {
|
|
693
2051
|
return result;
|
|
@@ -749,11 +2107,8 @@ async function enforceMaterializationGate(input, result, options) {
|
|
|
749
2107
|
return result;
|
|
750
2108
|
}
|
|
751
2109
|
|
|
752
|
-
async function runCommand(input, command, options) {
|
|
753
|
-
input =
|
|
754
|
-
for (const key of ["app_path", "db_connection_name", "workspace_name"]) {
|
|
755
|
-
if (typeof input[key] === "string") input[key] = input[key].trim();
|
|
756
|
-
}
|
|
2110
|
+
async function runCommand(input, command, options, runOptions = {}) {
|
|
2111
|
+
input = normalizeApexlangInput(input);
|
|
757
2112
|
if (typeof input.app_path === "string" && input.app_path.trim()) {
|
|
758
2113
|
const appPath = await assertProjectAppPath(input.app_path.trim(), options.cwd);
|
|
759
2114
|
await assertSymlinkFreeTree(appPath, {
|
|
@@ -763,7 +2118,21 @@ async function runCommand(input, command, options) {
|
|
|
763
2118
|
if (input.action === "workspace_probe" || command.prelude.length > 0) {
|
|
764
2119
|
await assertWorkspaceProbeCannotFollowSymlinks(options.cwd);
|
|
765
2120
|
}
|
|
766
|
-
await
|
|
2121
|
+
const runtimeApp = await prepareRuntimeApp(input, options.cwd, options.outputRoot, {
|
|
2122
|
+
forceStage: runOptions.forceRuntimeStage === true
|
|
2123
|
+
});
|
|
2124
|
+
const preRunAppDigest = runtimeApp.appPath && runtimeActionUsesDeployment(input)
|
|
2125
|
+
? await computeApexlangAppDigest(runtimeApp.appPath)
|
|
2126
|
+
: undefined;
|
|
2127
|
+
if (runOptions.expectedAppDigest !== undefined) {
|
|
2128
|
+
const expectedAppDigest = requireAppDigest(runOptions.expectedAppDigest);
|
|
2129
|
+
if (!preRunAppDigest || preRunAppDigest !== expectedAppDigest) {
|
|
2130
|
+
throw new Error(
|
|
2131
|
+
"APEXlang application changed after the approved live check; revalidate before importing"
|
|
2132
|
+
);
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
command = runtimeApp.staged ? commandWithAppPath(command, runtimeApp.appPath) : command;
|
|
767
2136
|
const preludeResults = [];
|
|
768
2137
|
for (const preludeCommand of command.prelude) {
|
|
769
2138
|
const processResult = await invoke(preludeCommand, options);
|
|
@@ -775,36 +2144,67 @@ async function runCommand(input, command, options) {
|
|
|
775
2144
|
action: input.action,
|
|
776
2145
|
command,
|
|
777
2146
|
outputRoot: options.outputRoot,
|
|
778
|
-
preludeResults
|
|
2147
|
+
preludeResults,
|
|
2148
|
+
runtimeApp,
|
|
2149
|
+
appDigest: preRunAppDigest
|
|
779
2150
|
};
|
|
780
2151
|
}
|
|
781
2152
|
}
|
|
782
2153
|
|
|
2154
|
+
await clearStaleRuntimeEvidence(command, options.outputRoot);
|
|
783
2155
|
const result = await invoke(command, options);
|
|
2156
|
+
const postRunAppDigest = runtimeApp.appPath && runtimeActionUsesDeployment(input)
|
|
2157
|
+
? await computeApexlangAppDigest(runtimeApp.appPath)
|
|
2158
|
+
: undefined;
|
|
2159
|
+
if (preRunAppDigest && postRunAppDigest !== preRunAppDigest) {
|
|
2160
|
+
throw new Error(
|
|
2161
|
+
"APEXlang runtime changed the staged application during validation; review and revalidate before importing"
|
|
2162
|
+
);
|
|
2163
|
+
}
|
|
784
2164
|
return {
|
|
785
2165
|
...result,
|
|
786
2166
|
action: input.action,
|
|
787
2167
|
command,
|
|
788
2168
|
outputRoot: options.outputRoot,
|
|
789
|
-
preludeResults
|
|
2169
|
+
preludeResults,
|
|
2170
|
+
runtimeApp,
|
|
2171
|
+
appDigest: postRunAppDigest ?? preRunAppDigest
|
|
790
2172
|
};
|
|
791
2173
|
}
|
|
792
2174
|
|
|
793
|
-
export function runApexlang(input, options) {
|
|
794
|
-
|
|
2175
|
+
export async function runApexlang(input, options) {
|
|
2176
|
+
const normalizedInput = normalizeApexlangInput(input);
|
|
2177
|
+
const result = await runCommand(
|
|
2178
|
+
normalizedInput,
|
|
2179
|
+
buildApexlangCommand(normalizedInput),
|
|
2180
|
+
options
|
|
2181
|
+
);
|
|
2182
|
+
return normalizeWarningOnlyValidation(normalizedInput, result);
|
|
795
2183
|
}
|
|
796
2184
|
|
|
797
|
-
export function runApexlangImport(input, options, importOptions = {}) {
|
|
798
|
-
|
|
2185
|
+
export async function runApexlangImport(input, options, importOptions = {}) {
|
|
2186
|
+
const normalizedInput = normalizeApexlangInput(input);
|
|
2187
|
+
const expectedAppDigest = requireAppDigest(importOptions.expectedAppDigest);
|
|
2188
|
+
const result = await runCommand(
|
|
2189
|
+
normalizedInput,
|
|
2190
|
+
buildApexlangImportCommand(normalizedInput, importOptions),
|
|
2191
|
+
options,
|
|
2192
|
+
{ forceRuntimeStage: true, expectedAppDigest }
|
|
2193
|
+
);
|
|
2194
|
+
if (result.ok || importOptions.targetResolutionMode === "create-new") return result;
|
|
2195
|
+
return runWarningCompatibleImport(normalizedInput, result, options);
|
|
799
2196
|
}
|
|
800
2197
|
|
|
801
|
-
export function runApexlangCreateNewProof(input, options) {
|
|
2198
|
+
export function runApexlangCreateNewProof(input, options, proofOptions = {}) {
|
|
2199
|
+
const normalizedInput = normalizeApexlangInput(input);
|
|
2200
|
+
const expectedAppDigest = requireAppDigest(proofOptions.expectedAppDigest);
|
|
802
2201
|
return runCommand(
|
|
803
|
-
|
|
804
|
-
buildApexlangImportCommand(
|
|
2202
|
+
normalizedInput,
|
|
2203
|
+
buildApexlangImportCommand(normalizedInput, {
|
|
805
2204
|
targetResolutionMode: "create-new",
|
|
806
2205
|
createNewConfirmed: false
|
|
807
2206
|
}),
|
|
808
|
-
options
|
|
2207
|
+
options,
|
|
2208
|
+
{ forceRuntimeStage: true, expectedAppDigest }
|
|
809
2209
|
);
|
|
810
2210
|
}
|