pi-apexlang 0.2.1 → 0.3.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.
- package/README.md +47 -2
- package/extensions/apexlang/compatibility.ts +227 -0
- package/extensions/apexlang/index.ts +60 -12
- package/extensions/apexlang/ords-sqlcl-compatibility.json +67 -0
- package/extensions/lib/apexlang-cli.mjs +3 -2
- package/extensions/lib/apexlang-local-validate.mjs +83 -0
- package/extensions/lib/apexlang-local-validator.py +116 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -76,6 +76,11 @@ The registered `apexlang` tool supports:
|
|
|
76
76
|
| `runtime_doctor` | Diagnoses runtime configuration. |
|
|
77
77
|
| `runtime_validate` | Runs Oracle's live check gate, then offers an interactive check-only or same-session validate-and-import choice. |
|
|
78
78
|
|
|
79
|
+
`local_validate` preserves Oracle's complete validator and report semantics.
|
|
80
|
+
The extension accelerates its repeated block and nesting queries in a
|
|
81
|
+
process-local cache; the vendored Oracle skill remains unchanged and no checks
|
|
82
|
+
are skipped.
|
|
83
|
+
|
|
79
84
|
The model cannot request import as a standalone action. Oracle's skill defaults
|
|
80
85
|
to checking code; only after authoritative live validation passes does the
|
|
81
86
|
extension offer a separate GUI choice. Selecting **Check and import APEXlang
|
|
@@ -103,6 +108,45 @@ repeat export produces one current snapshot instead of collision copies such
|
|
|
103
108
|
as `application_1.apx` and `p00005_1.apx`. Because this replaces the complete
|
|
104
109
|
local export, preserve or import any local-only work before refreshing it.
|
|
105
110
|
|
|
111
|
+
## SQLcl and ORDS compatibility advisory
|
|
112
|
+
|
|
113
|
+
Oracle documents minimum APEXlang prerequisites, but does not publish an exact
|
|
114
|
+
ORDS-to-SQLcl patch certification matrix. The extension therefore keeps this
|
|
115
|
+
mapping advisory and records whether a row is Oracle-confirmed or an extension
|
|
116
|
+
diagnostic recommendation:
|
|
117
|
+
|
|
118
|
+
| Server environment | SQLcl guidance | Basis |
|
|
119
|
+
| --- | --- | --- |
|
|
120
|
+
| ORDS before 26.1.1 or APEX before 26.1 | APEXlang is unsupported; upgrade the server components first. | Oracle-confirmed |
|
|
121
|
+
| APEX 26.1.x with ORDS 26.1.1 through 26.1.x | Oracle minimum: SQLcl 26.1. Diagnostic baseline: `26.1.2.132.1334`. | Oracle minimum plus extension advisory |
|
|
122
|
+
| APEX 26.1.x with ORDS 26.2.x | Oracle does not mandate SQLcl 26.2. On mass findings, compare with `26.1.2.132.1334`. | Extension advisory |
|
|
123
|
+
| Other or newer APEX, MMD, or ORDS lines | Verify current Oracle release documentation and use compiler metadata supporting the app `mmdVersion`. | Verify current docs |
|
|
124
|
+
|
|
125
|
+
The complete machine-readable table, source URLs, thresholds, and the direct
|
|
126
|
+
diagnostic-build download are in
|
|
127
|
+
[`extensions/apexlang/ords-sqlcl-compatibility.json`](extensions/apexlang/ords-sqlcl-compatibility.json).
|
|
128
|
+
The exact build is a known diagnostic baseline, not a universal pin, and SQLcl
|
|
129
|
+
26.2 is not categorically blocked.
|
|
130
|
+
|
|
131
|
+
When a validation result contains at least 50 structured findings, the
|
|
132
|
+
extension appends the table and recommends checking:
|
|
133
|
+
|
|
134
|
+
- local `sql -version`;
|
|
135
|
+
- the app's `.apex/apexlang.json` `mmdVersion`;
|
|
136
|
+
- the server APEX release and ORDS version with its administrator.
|
|
137
|
+
|
|
138
|
+
For local and compiler-truth text reports, the mass-error signal additionally
|
|
139
|
+
requires at least five distinct `.apx` files, which avoids treating one noisy
|
|
140
|
+
file as an environment-wide mismatch. An explicitly unsupported MMD version
|
|
141
|
+
also produces the compatibility advice. The advice never changes validation
|
|
142
|
+
status, relaxes an import gate, or claims that the application is valid.
|
|
143
|
+
|
|
144
|
+
[ORDS 26.1.1 introduced APEXlang support](https://www.oracle.com/tools/ords/ords-relnotes-26.1.1.html),
|
|
145
|
+
[APEX 26.1 requires ORDS 26.1.1 or later](https://docs.oracle.com/en/database/oracle/apex/26.1/htmrn/changed-behavior.html),
|
|
146
|
+
and Oracle's [SQLcl prerequisites](https://docs.oracle.com/en/database/oracle/sql-developer-command-line/26.1/sqcug/prerequisites-apexlang.html)
|
|
147
|
+
set the SQLcl minimum at 26.1. SQLcl
|
|
148
|
+
[26.1.2 added full APEXlang support](https://www.oracle.com/tools/sqlcl/sqlcl-relnotes-26.1.2.html).
|
|
149
|
+
|
|
106
150
|
All app paths must remain inside the active Pi workspace. The adapter rejects
|
|
107
151
|
traversal, app-tree symlinks, and multiply linked files on mutating vocabulary
|
|
108
152
|
fixes; verifies live workspace input against `deployments/default.json`; and
|
|
@@ -116,8 +160,9 @@ Offline routing, templates, generation, and most local checks need Node.js
|
|
|
116
160
|
|
|
117
161
|
Oracle documents these requirements for live APEXlang work:
|
|
118
162
|
|
|
119
|
-
- Oracle APEX with APEXlang support
|
|
120
|
-
-
|
|
163
|
+
- Oracle APEX 26.1 with APEXlang support.
|
|
164
|
+
- ORDS 26.1.1 or newer.
|
|
165
|
+
- SQLcl 26.1 or newer, selected with the compatibility advisory above.
|
|
121
166
|
- Java 17 or Java 21 for SQLcl.
|
|
122
167
|
- A saved SQLcl connection name and its corresponding APEX workspace name.
|
|
123
168
|
- A local APEX app or authoritative schema, model, API, or table metadata.
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import compatibility from "./ords-sqlcl-compatibility.json" with { type: "json" };
|
|
4
|
+
import type { ApexlangRunResult } from "../lib/apexlang-cli.mjs";
|
|
5
|
+
|
|
6
|
+
type JsonRecord = Record<string, unknown>;
|
|
7
|
+
|
|
8
|
+
export type ValidationCompatibilitySignal = {
|
|
9
|
+
findingCount?: number;
|
|
10
|
+
distinctFileCount?: number;
|
|
11
|
+
source: "structured-result" | "local-reports" | "compiler-truth-output" | "unsupported-mmd";
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const ORDS_SQLCL_COMPATIBILITY = compatibility;
|
|
15
|
+
export const ORDS_SQLCL_COMPATIBILITY_TABLE = compatibility.rows;
|
|
16
|
+
export const MASS_VALIDATION_FINDING_THRESHOLD = compatibility.massValidation.minimumFindings;
|
|
17
|
+
export const MASS_VALIDATION_DISTINCT_FILE_THRESHOLD =
|
|
18
|
+
compatibility.massValidation.minimumDistinctFilesForAppWideReports;
|
|
19
|
+
export const ORDS_SQLCL_COMPATIBILITY_GUIDELINE =
|
|
20
|
+
`When a validation action reports ${MASS_VALIDATION_FINDING_THRESHOLD}+ findings, recommend checking the server APEX/ORDS release, the app mmdVersion, and local \`sql -version\` against the extension's advisory compatibility table. For APEX 26.1, SQLcl 26.1 is Oracle's minimum and 26.1.2.132.1334 is the diagnostic baseline; do not hard-block other SQLcl versions or weaken validation/import gates.`;
|
|
21
|
+
|
|
22
|
+
const VALIDATION_ACTIONS = new Set([
|
|
23
|
+
"local_validate",
|
|
24
|
+
"compiler_truth_audit",
|
|
25
|
+
"runtime_validate"
|
|
26
|
+
]);
|
|
27
|
+
const LOCAL_VALIDATION_REPORT_NAMES = [
|
|
28
|
+
"apexlang-dsl-report.json",
|
|
29
|
+
"apexlang-validations-report.json",
|
|
30
|
+
"apexlang-vocab-report.json"
|
|
31
|
+
] as const;
|
|
32
|
+
|
|
33
|
+
function asRecord(value: unknown): JsonRecord | undefined {
|
|
34
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
35
|
+
? value as JsonRecord
|
|
36
|
+
: undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseJsonRecord(value: string): JsonRecord | undefined {
|
|
40
|
+
try {
|
|
41
|
+
return asRecord(JSON.parse(value));
|
|
42
|
+
} catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function safeCount(value: unknown): number | undefined {
|
|
48
|
+
return Number.isSafeInteger(value) && Number(value) >= 0 ? Number(value) : undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function maximumStructuredFindingCount(payload: JsonRecord): number | undefined {
|
|
52
|
+
const compatibilityFallback = asRecord(payload.compatibility_fallback);
|
|
53
|
+
const candidates = [
|
|
54
|
+
safeCount(payload.unresolved_count),
|
|
55
|
+
safeCount(payload.problem_count),
|
|
56
|
+
Array.isArray(payload.issues) ? payload.issues.length : undefined,
|
|
57
|
+
Array.isArray(payload.problems) ? payload.problems.length : undefined,
|
|
58
|
+
safeCount(compatibilityFallback?.original_unresolved_count)
|
|
59
|
+
].filter((value): value is number => value !== undefined);
|
|
60
|
+
return candidates.length > 0 ? Math.max(...candidates) : undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function diagnosticIdentity(issue: unknown, reportName: string, index: number): {
|
|
64
|
+
key: string;
|
|
65
|
+
file?: string;
|
|
66
|
+
} {
|
|
67
|
+
if (typeof issue === "string") {
|
|
68
|
+
const raw = issue.trim();
|
|
69
|
+
const match = raw.match(/^\s*-?\s*(.+?\.apx):(\d+)(?::\d+)?:\s+[A-Z][A-Z0-9_]+\b/);
|
|
70
|
+
return { key: raw || `${reportName}:${index}`, ...(match?.[1] ? { file: match[1] } : {}) };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const record = asRecord(issue);
|
|
74
|
+
if (!record) return { key: `${reportName}:${index}:${String(issue)}` };
|
|
75
|
+
const file = typeof record.file === "string" ? record.file.trim() : "";
|
|
76
|
+
const raw = typeof record.raw === "string" ? record.raw.trim() : "";
|
|
77
|
+
const key = raw || [
|
|
78
|
+
file,
|
|
79
|
+
String(record.line ?? ""),
|
|
80
|
+
String(record.column ?? ""),
|
|
81
|
+
String(record.rule ?? record.code ?? ""),
|
|
82
|
+
String(record.message ?? "")
|
|
83
|
+
].join(":");
|
|
84
|
+
return {
|
|
85
|
+
key: key || `${reportName}:${index}:${JSON.stringify(record)}`,
|
|
86
|
+
...(file ? { file } : {})
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function readJsonReport(path: string): Promise<JsonRecord | undefined> {
|
|
91
|
+
try {
|
|
92
|
+
return parseJsonRecord(await readFile(path, "utf8"));
|
|
93
|
+
} catch {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function clearLocalValidationCompatibilityReports(outputRoot: string): Promise<void> {
|
|
99
|
+
await Promise.all(
|
|
100
|
+
LOCAL_VALIDATION_REPORT_NAMES.map((name) => rm(join(outputRoot, "logs", name), { force: true }))
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function detectLocalReportSignal(outputRoot: string): Promise<ValidationCompatibilitySignal | undefined> {
|
|
105
|
+
const reports = await Promise.all(
|
|
106
|
+
LOCAL_VALIDATION_REPORT_NAMES.map(async (name) => ({
|
|
107
|
+
name,
|
|
108
|
+
payload: await readJsonReport(join(outputRoot, "logs", name))
|
|
109
|
+
}))
|
|
110
|
+
);
|
|
111
|
+
const vocabularyReport = reports.find(({ name }) => name === "apexlang-vocab-report.json")?.payload;
|
|
112
|
+
if (vocabularyReport?.blocking_reason === "UNSUPPORTED_MMD_VERSION") {
|
|
113
|
+
return { source: "unsupported-mmd" };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const findingKeys = new Set<string>();
|
|
117
|
+
const files = new Set<string>();
|
|
118
|
+
for (const { name, payload } of reports) {
|
|
119
|
+
for (const collection of [payload?.issues, payload?.unresolved]) {
|
|
120
|
+
if (!Array.isArray(collection)) continue;
|
|
121
|
+
collection.forEach((issue, index) => {
|
|
122
|
+
const identity = diagnosticIdentity(issue, name, index);
|
|
123
|
+
findingKeys.add(identity.key);
|
|
124
|
+
if (identity.file) files.add(identity.file);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (
|
|
129
|
+
findingKeys.size >= MASS_VALIDATION_FINDING_THRESHOLD &&
|
|
130
|
+
files.size >= MASS_VALIDATION_DISTINCT_FILE_THRESHOLD
|
|
131
|
+
) {
|
|
132
|
+
return {
|
|
133
|
+
source: "local-reports",
|
|
134
|
+
findingCount: findingKeys.size,
|
|
135
|
+
distinctFileCount: files.size
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function detectCompilerTruthOutputSignal(output: string): ValidationCompatibilitySignal | undefined {
|
|
142
|
+
const findings = new Set<string>();
|
|
143
|
+
const files = new Set<string>();
|
|
144
|
+
for (const line of output.split(/\r?\n/)) {
|
|
145
|
+
const match = line.match(/^\s*-\s+(.+?\.apx):(\d+)(?::\d+)?:\s+([A-Z][A-Z0-9_]+)\b\s*(.*)$/);
|
|
146
|
+
if (!match?.[1]) continue;
|
|
147
|
+
findings.add(`${match[1]}:${match[2]}:${match[3]}:${match[4] ?? ""}`);
|
|
148
|
+
files.add(match[1]);
|
|
149
|
+
}
|
|
150
|
+
if (
|
|
151
|
+
findings.size >= MASS_VALIDATION_FINDING_THRESHOLD &&
|
|
152
|
+
files.size >= MASS_VALIDATION_DISTINCT_FILE_THRESHOLD
|
|
153
|
+
) {
|
|
154
|
+
return {
|
|
155
|
+
source: "compiler-truth-output",
|
|
156
|
+
findingCount: findings.size,
|
|
157
|
+
distinctFileCount: files.size
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function detectValidationCompatibilitySignal(
|
|
164
|
+
result: ApexlangRunResult
|
|
165
|
+
): Promise<ValidationCompatibilitySignal | undefined> {
|
|
166
|
+
if (!VALIDATION_ACTIONS.has(result.action)) return undefined;
|
|
167
|
+
|
|
168
|
+
const payload = parseJsonRecord(result.stdout);
|
|
169
|
+
if (payload) {
|
|
170
|
+
const findingCount = maximumStructuredFindingCount(payload);
|
|
171
|
+
if (findingCount !== undefined && findingCount >= MASS_VALIDATION_FINDING_THRESHOLD) {
|
|
172
|
+
return { source: "structured-result", findingCount };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (result.action === "local_validate") {
|
|
176
|
+
return detectLocalReportSignal(result.outputRoot);
|
|
177
|
+
}
|
|
178
|
+
if (result.action === "compiler_truth_audit") {
|
|
179
|
+
return detectCompilerTruthOutputSignal([result.stdout, result.stderr].filter(Boolean).join("\n"));
|
|
180
|
+
}
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function tableCell(value: string): string {
|
|
185
|
+
return value.replaceAll("|", "\\|").replaceAll("\n", " ");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function renderOrdsSqlclCompatibilityTable(): string {
|
|
189
|
+
const rows = ORDS_SQLCL_COMPATIBILITY_TABLE.map((row) => {
|
|
190
|
+
const guidance = row.diagnosticSqlclBuild && row.diagnosticSqlclDownloadUrl
|
|
191
|
+
? `${row.sqlclGuidance} [Download ${row.diagnosticSqlclBuild}](${row.diagnosticSqlclDownloadUrl})`
|
|
192
|
+
: row.sqlclGuidance;
|
|
193
|
+
return `| ${tableCell(row.environment)} | ${tableCell(guidance)} | ${tableCell(row.basis)} |`;
|
|
194
|
+
});
|
|
195
|
+
return [
|
|
196
|
+
"| Server environment | SQLcl guidance | Basis |",
|
|
197
|
+
"| --- | --- | --- |",
|
|
198
|
+
...rows
|
|
199
|
+
].join("\n");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function formatValidationCompatibilityAdvisory(
|
|
203
|
+
signal: ValidationCompatibilitySignal
|
|
204
|
+
): string {
|
|
205
|
+
const findingSummary = signal.findingCount === undefined
|
|
206
|
+
? "The application declares an unsupported APEXlang compiler metadata version."
|
|
207
|
+
: `Large-scale validation output detected (${signal.findingCount} findings${
|
|
208
|
+
signal.distinctFileCount === undefined ? "" : ` across ${signal.distinctFileCount} files`
|
|
209
|
+
}).`;
|
|
210
|
+
return [
|
|
211
|
+
"### SQLcl/ORDS compatibility advisory",
|
|
212
|
+
"",
|
|
213
|
+
`${findingSummary} Before changing many application files, check local \`sql -version\`, the app's \`.apex/apexlang.json\` \`mmdVersion\`, and the server APEX and ORDS versions with its administrator. A version/metadata mismatch can create broad diagnostic noise.`,
|
|
214
|
+
"",
|
|
215
|
+
renderOrdsSqlclCompatibilityTable(),
|
|
216
|
+
"",
|
|
217
|
+
compatibility.caveat,
|
|
218
|
+
"This advice does not turn a failed validation into a pass and does not authorize import."
|
|
219
|
+
].join("\n");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export async function buildValidationCompatibilityAdvisory(
|
|
223
|
+
result: ApexlangRunResult
|
|
224
|
+
): Promise<string> {
|
|
225
|
+
const signal = await detectValidationCompatibilitySignal(result);
|
|
226
|
+
return signal ? formatValidationCompatibilityAdvisory(signal) : "";
|
|
227
|
+
}
|
|
@@ -13,6 +13,18 @@ import {
|
|
|
13
13
|
type ApexlangInput,
|
|
14
14
|
type ApexlangRunResult
|
|
15
15
|
} from "../lib/apexlang-cli.mjs";
|
|
16
|
+
import {
|
|
17
|
+
MASS_VALIDATION_DISTINCT_FILE_THRESHOLD,
|
|
18
|
+
MASS_VALIDATION_FINDING_THRESHOLD,
|
|
19
|
+
ORDS_SQLCL_COMPATIBILITY,
|
|
20
|
+
ORDS_SQLCL_COMPATIBILITY_GUIDELINE,
|
|
21
|
+
ORDS_SQLCL_COMPATIBILITY_TABLE,
|
|
22
|
+
buildValidationCompatibilityAdvisory,
|
|
23
|
+
clearLocalValidationCompatibilityReports,
|
|
24
|
+
detectValidationCompatibilitySignal,
|
|
25
|
+
formatValidationCompatibilityAdvisory,
|
|
26
|
+
renderOrdsSqlclCompatibilityTable
|
|
27
|
+
} from "./compatibility.ts";
|
|
16
28
|
|
|
17
29
|
const MAX_TOOL_OUTPUT = 80_000;
|
|
18
30
|
const COMMAND_TIMEOUT_MS = 10 * 60 * 1000;
|
|
@@ -77,14 +89,20 @@ function createNewTargetProved(result: ApexlangRunResult): boolean {
|
|
|
77
89
|
payload?.failure_class === "create_new_confirmation_required";
|
|
78
90
|
}
|
|
79
91
|
|
|
80
|
-
function processOutput(result: ApexlangRunResult): string {
|
|
92
|
+
function processOutput(result: ApexlangRunResult, compatibilityAdvisory = ""): string {
|
|
81
93
|
const streams = result.ok ? [result.stdout, result.stderr] : [result.stderr, result.stdout];
|
|
82
|
-
|
|
94
|
+
const output = trimOutput(streams.filter(Boolean).join("\n").trim(), result.outputRoot);
|
|
95
|
+
return [output, compatibilityAdvisory].filter(Boolean).join("\n\n");
|
|
83
96
|
}
|
|
84
97
|
|
|
85
|
-
function failureOutput(
|
|
86
|
-
|
|
87
|
-
|
|
98
|
+
function failureOutput(
|
|
99
|
+
result: ApexlangRunResult,
|
|
100
|
+
fallback: string,
|
|
101
|
+
compatibilityAdvisory = ""
|
|
102
|
+
): string {
|
|
103
|
+
const streams = result.ok ? [result.stdout, result.stderr] : [result.stderr, result.stdout];
|
|
104
|
+
const output = trimOutput(streams.filter(Boolean).join("\n").trim(), result.outputRoot) || fallback;
|
|
105
|
+
return `${[output, compatibilityAdvisory].filter(Boolean).join("\n\n")}\n\nAPEXlang reports: ${result.outputRoot}`;
|
|
88
106
|
}
|
|
89
107
|
|
|
90
108
|
type ApexlangDependencies = {
|
|
@@ -113,6 +131,7 @@ export function createApexlangTool(overrides: Partial<ApexlangDependencies> = {}
|
|
|
113
131
|
"Load the apexlang skill before using the apexlang tool and follow its routing and Missing Inputs rules.",
|
|
114
132
|
"Use apexlang workspace_probe before app-scoped APEXlang work.",
|
|
115
133
|
EXPORT_OVERWRITE_GUIDELINE,
|
|
134
|
+
ORDS_SQLCL_COMPATIBILITY_GUIDELINE,
|
|
116
135
|
"Use apexlang runtime_validate only after the user provides both db_connection_name and the matching APEX workspace_name; import requires the tool's separate post-check GUI choice."
|
|
117
136
|
],
|
|
118
137
|
executionMode: "sequential",
|
|
@@ -194,11 +213,25 @@ export function createApexlangTool(overrides: Partial<ApexlangDependencies> = {}
|
|
|
194
213
|
...(signal ? { signal } : {}),
|
|
195
214
|
timeoutMs: COMMAND_TIMEOUT_MS
|
|
196
215
|
};
|
|
216
|
+
if (input.action === "local_validate") {
|
|
217
|
+
await clearLocalValidationCompatibilityReports(outputRoot);
|
|
218
|
+
}
|
|
197
219
|
const result = await dependencies.run(input, runOptions);
|
|
198
|
-
const
|
|
220
|
+
const compatibilityAdvisory = await buildValidationCompatibilityAdvisory(result);
|
|
221
|
+
if (compatibilityAdvisory) {
|
|
222
|
+
onUpdate?.({
|
|
223
|
+
content: [{ type: "text", text: compatibilityAdvisory }],
|
|
224
|
+
details: { action: input.action, compatibilityAdvisory: true }
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
const output = processOutput(result, compatibilityAdvisory);
|
|
199
228
|
if (!result.ok) {
|
|
200
229
|
throw new Error(
|
|
201
|
-
failureOutput(
|
|
230
|
+
failureOutput(
|
|
231
|
+
result,
|
|
232
|
+
`APEXlang ${input.action} failed with exit code ${result.code}.`,
|
|
233
|
+
compatibilityAdvisory
|
|
234
|
+
)
|
|
202
235
|
);
|
|
203
236
|
}
|
|
204
237
|
|
|
@@ -207,7 +240,8 @@ export function createApexlangTool(overrides: Partial<ApexlangDependencies> = {}
|
|
|
207
240
|
throw new Error(
|
|
208
241
|
failureOutput(
|
|
209
242
|
result,
|
|
210
|
-
"APEXlang runtime validation did not produce authoritative live pass evidence."
|
|
243
|
+
"APEXlang runtime validation did not produce authoritative live pass evidence.",
|
|
244
|
+
compatibilityAdvisory
|
|
211
245
|
)
|
|
212
246
|
);
|
|
213
247
|
}
|
|
@@ -299,10 +333,12 @@ export function createApexlangTool(overrides: Partial<ApexlangDependencies> = {}
|
|
|
299
333
|
expectedAppDigest
|
|
300
334
|
});
|
|
301
335
|
if (!createNewTargetProved(proofResult)) {
|
|
336
|
+
const proofCompatibilityAdvisory = await buildValidationCompatibilityAdvisory(proofResult);
|
|
302
337
|
throw new Error(
|
|
303
338
|
failureOutput(
|
|
304
339
|
proofResult,
|
|
305
|
-
"Oracle did not prove that the create-new target is absent from the selected workspace."
|
|
340
|
+
"Oracle did not prove that the create-new target is absent from the selected workspace.",
|
|
341
|
+
proofCompatibilityAdvisory
|
|
306
342
|
)
|
|
307
343
|
);
|
|
308
344
|
}
|
|
@@ -343,12 +379,14 @@ export function createApexlangTool(overrides: Partial<ApexlangDependencies> = {}
|
|
|
343
379
|
createNewConfirmed,
|
|
344
380
|
expectedAppDigest
|
|
345
381
|
});
|
|
346
|
-
const
|
|
382
|
+
const importCompatibilityAdvisory = await buildValidationCompatibilityAdvisory(importResult);
|
|
383
|
+
const importOutput = processOutput(importResult, importCompatibilityAdvisory);
|
|
347
384
|
if (!liveImportPassed(importResult)) {
|
|
348
385
|
throw new Error(
|
|
349
386
|
failureOutput(
|
|
350
387
|
importResult,
|
|
351
|
-
"APEXlang validate-and-import did not produce authoritative import pass evidence."
|
|
388
|
+
"APEXlang validate-and-import did not produce authoritative import pass evidence.",
|
|
389
|
+
importCompatibilityAdvisory
|
|
352
390
|
)
|
|
353
391
|
);
|
|
354
392
|
}
|
|
@@ -429,11 +467,21 @@ export {
|
|
|
429
467
|
IMPORT_CHOICE,
|
|
430
468
|
CREATE_NEW_CHOICE,
|
|
431
469
|
EXPORT_OVERWRITE_GUIDELINE,
|
|
470
|
+
MASS_VALIDATION_DISTINCT_FILE_THRESHOLD,
|
|
471
|
+
MASS_VALIDATION_FINDING_THRESHOLD,
|
|
472
|
+
ORDS_SQLCL_COMPATIBILITY,
|
|
473
|
+
ORDS_SQLCL_COMPATIBILITY_GUIDELINE,
|
|
474
|
+
ORDS_SQLCL_COMPATIBILITY_TABLE,
|
|
432
475
|
UPDATE_EXISTING_CHOICE,
|
|
433
476
|
actionWritesProject,
|
|
477
|
+
buildValidationCompatibilityAdvisory,
|
|
478
|
+
clearLocalValidationCompatibilityReports,
|
|
434
479
|
confirmationMessage,
|
|
435
480
|
createNewTargetProved,
|
|
481
|
+
detectValidationCompatibilitySignal,
|
|
482
|
+
formatValidationCompatibilityAdvisory,
|
|
436
483
|
liveImportPassed,
|
|
437
|
-
liveValidationPassed
|
|
484
|
+
liveValidationPassed,
|
|
485
|
+
renderOrdsSqlclCompatibilityTable
|
|
438
486
|
};
|
|
439
487
|
export type { ApexlangAction };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"policy": "advisory-only",
|
|
4
|
+
"lastReviewed": "2026-07-19",
|
|
5
|
+
"massValidation": {
|
|
6
|
+
"minimumFindings": 50,
|
|
7
|
+
"minimumDistinctFilesForAppWideReports": 5
|
|
8
|
+
},
|
|
9
|
+
"caveat": "Oracle publishes minimum APEXlang prerequisites, not an ORDS-to-SQLcl patch certification matrix. Exact SQLcl builds in this table are diagnostic baselines and never validation gates.",
|
|
10
|
+
"rows": [
|
|
11
|
+
{
|
|
12
|
+
"environment": "ORDS before 26.1.1 or APEX before 26.1",
|
|
13
|
+
"matchMode": "any",
|
|
14
|
+
"ordsRange": "<26.1.1",
|
|
15
|
+
"apexRange": "<26.1",
|
|
16
|
+
"sqlclGuidance": "APEXlang is unsupported; upgrade the server components before changing SQLcl.",
|
|
17
|
+
"diagnosticSqlclBuild": "",
|
|
18
|
+
"diagnosticSqlclDownloadUrl": "",
|
|
19
|
+
"basis": "oracle-confirmed",
|
|
20
|
+
"sourceUrls": [
|
|
21
|
+
"https://www.oracle.com/tools/ords/ords-relnotes-26.1.1.html",
|
|
22
|
+
"https://docs.oracle.com/en/database/oracle/apex/26.1/htmrn/changed-behavior.html"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"environment": "APEX 26.1.x with ORDS 26.1.1 through 26.1.x",
|
|
27
|
+
"matchMode": "all",
|
|
28
|
+
"ordsRange": ">=26.1.1 <26.2.0",
|
|
29
|
+
"apexRange": "26.1.x",
|
|
30
|
+
"sqlclGuidance": "Oracle minimum: SQLcl 26.1. Diagnostic baseline: 26.1.2.132.1334.",
|
|
31
|
+
"diagnosticSqlclBuild": "26.1.2.132.1334",
|
|
32
|
+
"diagnosticSqlclDownloadUrl": "https://download.oracle.com/otn_software/java/sqldeveloper/sqlcl-26.1.2.132.1334.zip",
|
|
33
|
+
"basis": "oracle-minimum-plus-extension-advisory",
|
|
34
|
+
"sourceUrls": [
|
|
35
|
+
"https://docs.oracle.com/en/database/oracle/sql-developer-command-line/26.1/sqcug/prerequisites-apexlang.html",
|
|
36
|
+
"https://www.oracle.com/tools/sqlcl/sqlcl-relnotes-26.1.2.html"
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"environment": "APEX 26.1.x with ORDS 26.2.x",
|
|
41
|
+
"matchMode": "all",
|
|
42
|
+
"ordsRange": ">=26.2.0 <26.3.0",
|
|
43
|
+
"apexRange": "26.1.x",
|
|
44
|
+
"sqlclGuidance": "Oracle does not mandate SQLcl 26.2. On mass findings, compare with diagnostic baseline 26.1.2.132.1334.",
|
|
45
|
+
"diagnosticSqlclBuild": "26.1.2.132.1334",
|
|
46
|
+
"diagnosticSqlclDownloadUrl": "https://download.oracle.com/otn_software/java/sqldeveloper/sqlcl-26.1.2.132.1334.zip",
|
|
47
|
+
"basis": "extension-advisory",
|
|
48
|
+
"sourceUrls": [
|
|
49
|
+
"https://docs.oracle.com/en/database/oracle/sql-developer-command-line/26.1/sqcug/prerequisites-apexlang.html",
|
|
50
|
+
"https://www.oracle.com/tools/sqlcl/sqlcl-downloads.html"
|
|
51
|
+
]
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"environment": "Other or newer APEX, MMD, or ORDS release lines",
|
|
55
|
+
"matchMode": "fallback",
|
|
56
|
+
"ordsRange": "other",
|
|
57
|
+
"apexRange": "other",
|
|
58
|
+
"sqlclGuidance": "Verify current Oracle release documentation and use SQLcl compiler metadata that supports the app mmdVersion.",
|
|
59
|
+
"diagnosticSqlclBuild": "",
|
|
60
|
+
"diagnosticSqlclDownloadUrl": "",
|
|
61
|
+
"basis": "verify-current-docs",
|
|
62
|
+
"sourceUrls": [
|
|
63
|
+
"https://docs.oracle.com/en/database/oracle/apex/26.1/apxdc/using-sqlcl-apexlang.html"
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
}
|
|
@@ -21,6 +21,7 @@ export const APEXLANG_ACTIONS = Object.freeze([
|
|
|
21
21
|
|
|
22
22
|
const apexctlPath = resolve(APEXLANG_SKILL_ROOT, "tools/apexctl.mjs");
|
|
23
23
|
const queryValidPropsPath = resolve(APEXLANG_SKILL_ROOT, "tools/query-valid-props.mjs");
|
|
24
|
+
const localValidatePath = resolve(moduleDirectory, "apexlang-local-validate.mjs");
|
|
24
25
|
const runtimeRoundtripPath = resolve(moduleDirectory, "apexlang-runtime-roundtrip.mjs");
|
|
25
26
|
const sqlclPtyProxyPath = resolve(moduleDirectory, "apexlang-sqlcl-pty.py");
|
|
26
27
|
const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f]/;
|
|
@@ -214,9 +215,9 @@ export function buildApexlangCommand(input) {
|
|
|
214
215
|
}
|
|
215
216
|
|
|
216
217
|
if (action === "local_validate") {
|
|
217
|
-
const args = ["
|
|
218
|
+
const args = ["--app-path", requirePath(input, "app_path", action)];
|
|
218
219
|
addFlag(args, "--fix-vocab", input.fix_vocab);
|
|
219
|
-
return
|
|
220
|
+
return { scriptPath: localValidatePath, args, prelude: [] };
|
|
220
221
|
}
|
|
221
222
|
|
|
222
223
|
if (action === "compiler_truth_audit") {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { mkdirSync } from "node:fs";
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const moduleDirectory = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const skillRoot = resolve(moduleDirectory, "../../skills/apexlang");
|
|
10
|
+
const runtimeRoot = resolve(skillRoot, "runtime");
|
|
11
|
+
const pythonRoot = resolve(runtimeRoot, "internal/python");
|
|
12
|
+
const optimizedDslValidator = resolve(moduleDirectory, "apexlang-local-validator.py");
|
|
13
|
+
|
|
14
|
+
function readOption(args, name) {
|
|
15
|
+
const index = args.indexOf(name);
|
|
16
|
+
return index >= 0 && index + 1 < args.length ? String(args[index + 1]).trim() : "";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function main() {
|
|
20
|
+
const args = process.argv.slice(2);
|
|
21
|
+
const appPath = readOption(args, "--app-path");
|
|
22
|
+
if (!appPath) {
|
|
23
|
+
console.error("Missing required --app-path");
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const outputRoot = String(process.env.APEXLANG_OUTPUT_ROOT ?? "").trim();
|
|
28
|
+
if (!outputRoot) {
|
|
29
|
+
console.error("APEXLANG_OUTPUT_ROOT is required in packaged apexlang runtime");
|
|
30
|
+
return 1;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const reportDir = resolve(outputRoot, "logs");
|
|
34
|
+
mkdirSync(reportDir, { recursive: true });
|
|
35
|
+
const fixVocab = args.includes("--fix-vocab");
|
|
36
|
+
const commands = [
|
|
37
|
+
[
|
|
38
|
+
resolve(pythonRoot, "validate_apexlang_vocab.py"),
|
|
39
|
+
[
|
|
40
|
+
"--app-path",
|
|
41
|
+
appPath,
|
|
42
|
+
"--report-path",
|
|
43
|
+
join(reportDir, "apexlang-vocab-report.json"),
|
|
44
|
+
fixVocab ? "--rewrite" : "--check-only"
|
|
45
|
+
]
|
|
46
|
+
],
|
|
47
|
+
[
|
|
48
|
+
optimizedDslValidator,
|
|
49
|
+
["--report-path", join(reportDir, "apexlang-dsl-report.json"), appPath]
|
|
50
|
+
],
|
|
51
|
+
[
|
|
52
|
+
resolve(pythonRoot, "validate_validations.py"),
|
|
53
|
+
["--report-path", join(reportDir, "apexlang-validations-report.json"), appPath]
|
|
54
|
+
]
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
let failures = 0;
|
|
58
|
+
for (const [scriptPath, scriptArgs] of commands) {
|
|
59
|
+
const result = spawnSync("python3", [scriptPath, ...scriptArgs], {
|
|
60
|
+
cwd: process.cwd(),
|
|
61
|
+
encoding: "utf8",
|
|
62
|
+
env: {
|
|
63
|
+
...process.env,
|
|
64
|
+
APEXLANG_EMBEDDED_TOOLS_ROOT: resolve(skillRoot, "tools"),
|
|
65
|
+
APEXLANG_PACKAGE_ROOT: skillRoot,
|
|
66
|
+
APEXLANG_RUNTIME_ROOT: runtimeRoot,
|
|
67
|
+
PYTHONDONTWRITEBYTECODE: "1"
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
71
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
72
|
+
if (typeof result.status !== "number" || result.status !== 0) failures += 1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (failures > 0) {
|
|
76
|
+
console.log("APEXLANG_LOCAL_CHECK_FAILED");
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
console.log("APEXLANG_LOCAL_CHECK_OK");
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
process.exitCode = main();
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Run Oracle's DSL validator with per-process parser acceleration.
|
|
3
|
+
|
|
4
|
+
The vendored Oracle skill stays byte-for-byte unchanged. This launcher imports
|
|
5
|
+
its validator, replaces only pure parsing helpers with equivalent cached
|
|
6
|
+
implementations, and then calls the original command-line entry point.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from bisect import bisect_right
|
|
12
|
+
from functools import lru_cache, wraps
|
|
13
|
+
import importlib.util
|
|
14
|
+
import os
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
import sys
|
|
17
|
+
from types import ModuleType
|
|
18
|
+
from typing import Any, Callable
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _SparseDepthIndex:
|
|
22
|
+
"""Store lexical depth checkpoints only at offsets requested by validators."""
|
|
23
|
+
|
|
24
|
+
__slots__ = ("positions", "states")
|
|
25
|
+
|
|
26
|
+
def __init__(self) -> None:
|
|
27
|
+
self.positions: list[int] = [0]
|
|
28
|
+
self.states: list[tuple[int, int, bool]] = [(0, 0, False)]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_DEPTH_INDEXES: dict[str, _SparseDepthIndex] = {}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _sparse_nesting_depth(text: str, idx: int) -> tuple[int, int]:
|
|
35
|
+
"""Return Oracle-compatible nesting depth using the closest prior checkpoint."""
|
|
36
|
+
target_idx = min(len(text), idx) if idx >= 0 else max(0, len(text) + idx)
|
|
37
|
+
index = _DEPTH_INDEXES.setdefault(text, _SparseDepthIndex())
|
|
38
|
+
checkpoint_slot = bisect_right(index.positions, target_idx) - 1
|
|
39
|
+
start = index.positions[checkpoint_slot]
|
|
40
|
+
paren_depth, brace_depth, in_string = index.states[checkpoint_slot]
|
|
41
|
+
|
|
42
|
+
for pos in range(start, target_idx):
|
|
43
|
+
ch = text[pos]
|
|
44
|
+
if ch == '"' and (pos == 0 or text[pos - 1] != "\\"):
|
|
45
|
+
in_string = not in_string
|
|
46
|
+
continue
|
|
47
|
+
if in_string:
|
|
48
|
+
continue
|
|
49
|
+
if ch == "(":
|
|
50
|
+
paren_depth += 1
|
|
51
|
+
elif ch == ")":
|
|
52
|
+
paren_depth = max(0, paren_depth - 1)
|
|
53
|
+
elif ch == "{":
|
|
54
|
+
brace_depth += 1
|
|
55
|
+
elif ch == "}":
|
|
56
|
+
brace_depth = max(0, brace_depth - 1)
|
|
57
|
+
|
|
58
|
+
if start != target_idx:
|
|
59
|
+
insert_at = checkpoint_slot + 1
|
|
60
|
+
index.positions.insert(insert_at, target_idx)
|
|
61
|
+
index.states.insert(insert_at, (paren_depth, brace_depth, in_string))
|
|
62
|
+
return paren_depth, brace_depth
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _validator_path() -> Path:
|
|
66
|
+
runtime_override = os.environ.get("APEXLANG_RUNTIME_ROOT", "").strip()
|
|
67
|
+
runtime_root = (
|
|
68
|
+
Path(runtime_override).resolve()
|
|
69
|
+
if runtime_override
|
|
70
|
+
else Path(__file__).resolve().parents[2] / "skills" / "apexlang" / "runtime"
|
|
71
|
+
)
|
|
72
|
+
return runtime_root / "internal" / "python" / "validate_apexlang.py"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _load_oracle_validator(path: Path) -> ModuleType:
|
|
76
|
+
sys.path.insert(0, str(path.parent))
|
|
77
|
+
spec = importlib.util.spec_from_file_location("_pi_apexlang_oracle_validator", path)
|
|
78
|
+
if spec is None or spec.loader is None:
|
|
79
|
+
raise RuntimeError(f"Cannot load Oracle APEXlang validator: {path}")
|
|
80
|
+
module = importlib.util.module_from_spec(spec)
|
|
81
|
+
sys.modules[spec.name] = module
|
|
82
|
+
spec.loader.exec_module(module)
|
|
83
|
+
return module
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _cache_list_function(function: Callable[..., list[Any]]) -> Callable[..., list[Any]]:
|
|
87
|
+
"""Cache immutable results while preserving Oracle's fresh-list return type."""
|
|
88
|
+
cached = lru_cache(maxsize=None)(lambda *args: tuple(function(*args)))
|
|
89
|
+
|
|
90
|
+
@wraps(function)
|
|
91
|
+
def wrapper(*args: Any) -> list[Any]:
|
|
92
|
+
return list(cached(*args))
|
|
93
|
+
|
|
94
|
+
return wrapper
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _accelerate(module: ModuleType) -> None:
|
|
98
|
+
module.nesting_depth = _sparse_nesting_depth
|
|
99
|
+
for name in (
|
|
100
|
+
"find_component_blocks",
|
|
101
|
+
"find_named_brace_blocks",
|
|
102
|
+
"find_immediate_component_blocks",
|
|
103
|
+
"find_immediate_named_brace_blocks",
|
|
104
|
+
):
|
|
105
|
+
setattr(module, name, _cache_list_function(getattr(module, name)))
|
|
106
|
+
module.block_body = lru_cache(maxsize=None)(module.block_body)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def main(argv: list[str]) -> int:
|
|
110
|
+
validator = _load_oracle_validator(_validator_path())
|
|
111
|
+
_accelerate(validator)
|
|
112
|
+
return int(validator.main(argv))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
if __name__ == "__main__":
|
|
116
|
+
sys.exit(main(sys.argv[1:]))
|