pi-oracle 0.7.11 → 0.7.13
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/CHANGELOG.md +26 -0
- package/README.md +22 -11
- package/docs/ORACLE_DESIGN.md +7 -6
- package/docs/ORACLE_ISOLATED_PI_VALIDATION.md +21 -2
- package/docs/platform-smoke.md +3 -2
- package/extensions/oracle/lib/archive.ts +725 -0
- package/extensions/oracle/lib/config.ts +14 -5
- package/extensions/oracle/lib/jobs.ts +49 -3
- package/extensions/oracle/lib/provider-capabilities.ts +41 -0
- package/extensions/oracle/lib/runtime.ts +9 -5
- package/extensions/oracle/lib/tools.ts +10 -576
- package/extensions/oracle/worker/chatgpt-ui-helpers.d.mts +2 -0
- package/extensions/oracle/worker/chatgpt-ui-helpers.mjs +23 -3
- package/extensions/oracle/worker/run-job.mjs +65 -16
- package/package.json +8 -5
- package/platform-smoke.config.mjs +1 -1
- package/scripts/oracle-chatgpt-preset-proof.mjs +352 -0
- package/scripts/oracle-real-smoke.mjs +3 -1
- package/scripts/platform-smoke/invariants.mjs +1 -1
|
@@ -248,9 +248,29 @@ function hasLegacyEffortCombobox(entries) {
|
|
|
248
248
|
});
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
-
function compactSelectionFromEntry(entry, _entries,
|
|
252
|
-
if (entry.disabled
|
|
253
|
-
|
|
251
|
+
function compactSelectionFromEntry(entry, _entries, options = {}) {
|
|
252
|
+
if (entry.disabled) return undefined;
|
|
253
|
+
const kind = entry.kind || "";
|
|
254
|
+
if (COMPACT_INTELLIGENCE_CONTROL_KINDS.has(kind)) return parseCompactIntelligenceSelection(entry.label);
|
|
255
|
+
if (options.allowClosedButtons && kind === "button" && !/\bexpanded=true\b/.test(String(entry.line || ""))) {
|
|
256
|
+
return parseCompactIntelligenceSelection(entry.label);
|
|
257
|
+
}
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function matchesCompactIntelligenceControlLabel(label) {
|
|
262
|
+
return Boolean(parseCompactIntelligenceSelection(label));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function snapshotHasClosedCompactSelection(snapshot, selection) {
|
|
266
|
+
/** @type {SnapshotEntry[]} */
|
|
267
|
+
const entries = parseSnapshotEntries(snapshot);
|
|
268
|
+
if (hasRemovableComposerModelChip(entries) || hasLegacyEffortCombobox(entries) || hasCompactIntelligenceMenuContext(entries)) return false;
|
|
269
|
+
return entries.some((entry) => {
|
|
270
|
+
if (entry.kind !== "button" || entry.disabled) return false;
|
|
271
|
+
const compactSelection = compactSelectionFromEntry(entry, entries, { allowClosedButtons: true });
|
|
272
|
+
return compactSelectionMatchesRequestedInSnapshot(snapshot, selection, compactSelection);
|
|
273
|
+
});
|
|
254
274
|
}
|
|
255
275
|
|
|
256
276
|
function compactSelectionMatchesRequested(selection, compactSelection) {
|
|
@@ -23,12 +23,14 @@ import { extractArtifactLabels, FILE_LABEL_PATTERN_SOURCE, GENERIC_ARTIFACT_LABE
|
|
|
23
23
|
import {
|
|
24
24
|
buildAllowedChatGptOrigins,
|
|
25
25
|
deriveAssistantCompletionSignature,
|
|
26
|
+
matchesCompactIntelligenceControlLabel,
|
|
26
27
|
matchesCompactIntelligenceOpenerLabel,
|
|
27
28
|
matchesModelFamilyLabel,
|
|
28
29
|
matchesRequestedModelControlLabel,
|
|
29
30
|
requestedEffortLabel,
|
|
30
31
|
effortSelectionVisible,
|
|
31
32
|
snapshotCanSafelySkipModelConfiguration,
|
|
33
|
+
snapshotHasClosedCompactSelection,
|
|
32
34
|
snapshotHasModelConfigurationUi,
|
|
33
35
|
snapshotHasModelOpener,
|
|
34
36
|
snapshotHasUsableComposerControls,
|
|
@@ -78,6 +80,7 @@ const ARTIFACT_DOWNLOAD_TIMEOUT_MS = 90_000;
|
|
|
78
80
|
const ARTIFACT_DOWNLOAD_MAX_ATTEMPTS = 2;
|
|
79
81
|
const AGENT_BROWSER_CLOSE_TIMEOUT_MS = 10_000;
|
|
80
82
|
const PROFILE_CLONE_TIMEOUT_MS = 120_000;
|
|
83
|
+
const MODEL_CONFIGURATION_OPEN_TIMEOUT_MS = 45_000;
|
|
81
84
|
const MODEL_CONFIGURATION_SETTLE_TIMEOUT_MS = 20_000;
|
|
82
85
|
const MODEL_CONFIGURATION_SETTLE_POLL_MS = 250;
|
|
83
86
|
const MODEL_CONFIGURATION_CLOSE_RETRY_MS = 1_000;
|
|
@@ -1091,15 +1094,9 @@ function classifyChatPage({ job, url, snapshot, body, probe }) {
|
|
|
1091
1094
|
return { state: "challenge_blocking", message: "ChatGPT is showing a challenge/verification page" };
|
|
1092
1095
|
}
|
|
1093
1096
|
|
|
1094
|
-
const
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
/an error occurred while connecting to the websocket/i,
|
|
1098
|
-
/try again later/i,
|
|
1099
|
-
/rate limit/i,
|
|
1100
|
-
];
|
|
1101
|
-
if (outagePatterns.some((pattern) => pattern.test(text))) {
|
|
1102
|
-
return { state: "transient_outage_error", message: "ChatGPT is showing a transient outage/error page" };
|
|
1097
|
+
const outageText = detectProviderTransientErrorText(text);
|
|
1098
|
+
if (outageText) {
|
|
1099
|
+
return { state: "transient_outage_error", message: `ChatGPT is showing a transient outage/rate-limit page: ${outageText}` };
|
|
1103
1100
|
}
|
|
1104
1101
|
|
|
1105
1102
|
const allowedOrigins = buildAllowedChatGptOrigins(job.config.browser.chatUrl, job.config.browser.authUrl);
|
|
@@ -1162,8 +1159,9 @@ function classifyGrokPage({ url, snapshot, body }) {
|
|
|
1162
1159
|
if (/captcha|cloudflare|verify you are human|unusual activity|suspicious activity/i.test(text)) {
|
|
1163
1160
|
return { state: "challenge_blocking", message: "Grok is showing a challenge/verification page" };
|
|
1164
1161
|
}
|
|
1165
|
-
|
|
1166
|
-
|
|
1162
|
+
const outageText = detectProviderTransientErrorText(text);
|
|
1163
|
+
if (outageText) {
|
|
1164
|
+
return { state: "transient_outage_error", message: `Grok is showing a transient outage/rate-limit page: ${outageText}` };
|
|
1167
1165
|
}
|
|
1168
1166
|
const onGrokOrigin = typeof url === "string" && url.startsWith("https://grok.com");
|
|
1169
1167
|
if (onGrokOrigin && hasGrokLoginCta(text)) {
|
|
@@ -1250,6 +1248,42 @@ function detectUploadErrorText(text) {
|
|
|
1250
1248
|
return patterns.find((pattern) => text.toLowerCase().includes(pattern.toLowerCase()));
|
|
1251
1249
|
}
|
|
1252
1250
|
|
|
1251
|
+
function detectProviderTransientErrorText(text) {
|
|
1252
|
+
const patterns = [
|
|
1253
|
+
"Too many requests",
|
|
1254
|
+
"rate limit",
|
|
1255
|
+
"try again later",
|
|
1256
|
+
"Something went wrong",
|
|
1257
|
+
"A network error occurred",
|
|
1258
|
+
"An error occurred while connecting to the websocket",
|
|
1259
|
+
];
|
|
1260
|
+
return patterns.find((pattern) => text.toLowerCase().includes(pattern.toLowerCase()));
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
function detectProviderVisibleBlockerText(text) {
|
|
1264
|
+
const patterns = [
|
|
1265
|
+
"Too many requests",
|
|
1266
|
+
"rate limit",
|
|
1267
|
+
];
|
|
1268
|
+
return patterns.find((pattern) => text.toLowerCase().includes(pattern.toLowerCase()));
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function formatProviderTransientErrorMessage(job, errorText, context) {
|
|
1272
|
+
const providerLabel = isGrokJob(job) ? "Grok" : "ChatGPT";
|
|
1273
|
+
return `${providerLabel} is showing a transient outage/rate-limit page${context ? ` while ${context}` : ""}: ${errorText}`;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
function providerTransientErrorMessage(job, text, context) {
|
|
1277
|
+
const errorText = detectProviderVisibleBlockerText(text);
|
|
1278
|
+
if (!errorText) return "";
|
|
1279
|
+
return formatProviderTransientErrorMessage(job, errorText, context);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
function throwIfProviderTransientError(job, text, context) {
|
|
1283
|
+
const message = providerTransientErrorMessage(job, text, context);
|
|
1284
|
+
if (message) throw new Error(message);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1253
1287
|
function detectResponseFailureText(text) {
|
|
1254
1288
|
const patterns = [
|
|
1255
1289
|
"Message delivery timed out",
|
|
@@ -1289,6 +1323,7 @@ async function waitForUploadConfirmed(job, fileLabel, baselineCount) {
|
|
|
1289
1323
|
while (Date.now() < timeoutAt) {
|
|
1290
1324
|
await heartbeat();
|
|
1291
1325
|
const [snapshot, body] = await Promise.all([snapshotText(job), pageText(job).catch(() => "")]);
|
|
1326
|
+
throwIfProviderTransientError(job, snapshot, "uploading the archive");
|
|
1292
1327
|
|
|
1293
1328
|
const errorText = detectUploadErrorText(`${snapshot}\n${body}`);
|
|
1294
1329
|
if (errorText) {
|
|
@@ -1323,6 +1358,7 @@ async function waitForSendReady(job) {
|
|
|
1323
1358
|
await heartbeat();
|
|
1324
1359
|
const snapshot = await snapshotText(job);
|
|
1325
1360
|
const body = await pageText(job).catch(() => "");
|
|
1361
|
+
throwIfProviderTransientError(job, snapshot, "waiting for send readiness");
|
|
1326
1362
|
const errorText = detectUploadErrorText(`${snapshot}\n${body}`);
|
|
1327
1363
|
if (errorText) {
|
|
1328
1364
|
throw new Error(`Upload error detected: ${errorText}`);
|
|
@@ -1366,6 +1402,7 @@ async function sendAcceptanceState(job, baselineAssistantCount) {
|
|
|
1366
1402
|
urlKnown: urlResult.ok,
|
|
1367
1403
|
assistantCount: Math.max(baselineAssistantCount, messages.length),
|
|
1368
1404
|
stopStreaming: isGrokJob(job) ? snapshot.includes(GROK_LABELS.stop) : snapshot.includes("Stop streaming"),
|
|
1405
|
+
transientErrorText: detectProviderVisibleBlockerText(snapshot) || "",
|
|
1369
1406
|
};
|
|
1370
1407
|
}
|
|
1371
1408
|
|
|
@@ -1386,6 +1423,7 @@ async function waitForSendAccepted(job, beforeSend, options = {}) {
|
|
|
1386
1423
|
while (Date.now() < timeoutAt) {
|
|
1387
1424
|
await heartbeat();
|
|
1388
1425
|
const afterSend = await sendAcceptanceState(job, beforeSend.assistantCount || 0);
|
|
1426
|
+
if (afterSend.transientErrorText) throw new Error(formatProviderTransientErrorMessage(job, afterSend.transientErrorText, "waiting for send acceptance"));
|
|
1389
1427
|
if (providerSendAccepted(beforeSend, afterSend)) return true;
|
|
1390
1428
|
await sleep(500);
|
|
1391
1429
|
}
|
|
@@ -1420,12 +1458,13 @@ async function dismissProFeedbackModal(job, snapshot) {
|
|
|
1420
1458
|
}
|
|
1421
1459
|
|
|
1422
1460
|
async function openModelConfiguration(job) {
|
|
1423
|
-
const timeoutAt = Date.now() +
|
|
1461
|
+
const timeoutAt = Date.now() + MODEL_CONFIGURATION_OPEN_TIMEOUT_MS;
|
|
1424
1462
|
let lastSnapshot = "";
|
|
1425
1463
|
|
|
1426
1464
|
while (Date.now() < timeoutAt) {
|
|
1427
1465
|
const initialSnapshot = await snapshotText(job);
|
|
1428
1466
|
lastSnapshot = initialSnapshot;
|
|
1467
|
+
throwIfProviderTransientError(job, initialSnapshot, "opening model configuration");
|
|
1429
1468
|
if (snapshotHasModelConfigurationUi(initialSnapshot)) return initialSnapshot;
|
|
1430
1469
|
if (await dismissProFeedbackModal(job, initialSnapshot)) continue;
|
|
1431
1470
|
|
|
@@ -1438,6 +1477,7 @@ async function openModelConfiguration(job) {
|
|
|
1438
1477
|
await agentBrowser(job, "wait", "800");
|
|
1439
1478
|
const after = await snapshotText(job);
|
|
1440
1479
|
lastSnapshot = after;
|
|
1480
|
+
throwIfProviderTransientError(job, after, "opening model configuration");
|
|
1441
1481
|
if (snapshotHasModelConfigurationUi(after)) return after;
|
|
1442
1482
|
if (canUseOpenModelMenuForSelection(after, job.selection)) return after;
|
|
1443
1483
|
|
|
@@ -1451,6 +1491,7 @@ async function openModelConfiguration(job) {
|
|
|
1451
1491
|
await agentBrowser(job, "wait", "1200");
|
|
1452
1492
|
const postConfigure = await snapshotText(job);
|
|
1453
1493
|
lastSnapshot = postConfigure;
|
|
1494
|
+
throwIfProviderTransientError(job, postConfigure, "opening model configuration");
|
|
1454
1495
|
if (snapshotHasModelConfigurationUi(postConfigure)) return postConfigure;
|
|
1455
1496
|
if (canUseOpenModelMenuForSelection(postConfigure, job.selection)) return postConfigure;
|
|
1456
1497
|
}
|
|
@@ -1544,22 +1585,28 @@ async function configureModel(job) {
|
|
|
1544
1585
|
throw new Error(`Could not find model family control for ${job.selection.modelFamily}`);
|
|
1545
1586
|
}
|
|
1546
1587
|
|
|
1588
|
+
let compactSelectionVerifiedAfterClick = false;
|
|
1547
1589
|
if (!alreadyConfiguredInUi && !familyAlreadySelectedInUi && familyEntry) {
|
|
1590
|
+
const clickedCompactControl = matchesCompactIntelligenceControlLabel(familyEntry.label);
|
|
1548
1591
|
await clickRef(job, familyEntry.ref);
|
|
1549
1592
|
await agentBrowser(job, "wait", "800");
|
|
1550
1593
|
familySnapshot = await snapshotText(job);
|
|
1551
1594
|
verificationSnapshot = familySnapshot;
|
|
1595
|
+
compactSelectionVerifiedAfterClick = clickedCompactControl && snapshotHasClosedCompactSelection(familySnapshot, job.selection);
|
|
1596
|
+
if (compactSelectionVerifiedAfterClick) {
|
|
1597
|
+
await log(`Verified compact ChatGPT selection after menu close for family=${job.selection.modelFamily} effort=${job.selection?.effort || "(none)"}`);
|
|
1598
|
+
}
|
|
1552
1599
|
const postClickControlOptions = {
|
|
1553
1600
|
ignoreCompactTierButtons: snapshotHasCompactIntelligenceMenuControls(familySnapshot),
|
|
1554
1601
|
ignoreCompactOnlyButtons: snapshotHasLegacyEffortCombobox(familySnapshot),
|
|
1555
1602
|
};
|
|
1556
1603
|
familyEntry = findEntry(familySnapshot, (candidate) => matchesRequestedModelControl(candidate, job.selection, postClickControlOptions));
|
|
1557
|
-
if (!familyEntry && !snapshotStronglyMatchesRequestedModel(familySnapshot, job.selection)) {
|
|
1604
|
+
if (!compactSelectionVerifiedAfterClick && !familyEntry && !snapshotStronglyMatchesRequestedModel(familySnapshot, job.selection)) {
|
|
1558
1605
|
throw new Error(`Requested model family did not remain selected: ${job.selection.modelFamily}`);
|
|
1559
1606
|
}
|
|
1560
1607
|
}
|
|
1561
1608
|
|
|
1562
|
-
if (job.selection.modelFamily === "thinking" || job.selection.modelFamily === "pro") {
|
|
1609
|
+
if ((job.selection.modelFamily === "thinking" || job.selection.modelFamily === "pro") && !compactSelectionVerifiedAfterClick) {
|
|
1563
1610
|
const effortLabel = requestedEffortLabel(job.selection);
|
|
1564
1611
|
if (effortLabel && !effortSelectionVisible(familySnapshot, effortLabel)) {
|
|
1565
1612
|
const opened = await openEffortDropdown(job);
|
|
@@ -1589,7 +1636,8 @@ async function configureModel(job) {
|
|
|
1589
1636
|
if (job.selection.modelFamily === "instant") {
|
|
1590
1637
|
const desiredAutoSwitchState = job.selection.autoSwitchToThinking === true;
|
|
1591
1638
|
const currentAutoSwitchState = autoSwitchToThinkingSelectionVisible(familySnapshot);
|
|
1592
|
-
const compactInstantAlreadyVerified =
|
|
1639
|
+
const compactInstantAlreadyVerified = compactSelectionVerifiedAfterClick
|
|
1640
|
+
|| (desiredAutoSwitchState && currentAutoSwitchState === undefined && snapshotStronglyMatchesRequestedModel(familySnapshot, job.selection));
|
|
1593
1641
|
if (!compactInstantAlreadyVerified && currentAutoSwitchState !== desiredAutoSwitchState && (desiredAutoSwitchState || currentAutoSwitchState === true)) {
|
|
1594
1642
|
await clickAutoSwitchToThinkingControl(job);
|
|
1595
1643
|
await agentBrowser(job, "wait", "400");
|
|
@@ -1598,7 +1646,7 @@ async function configureModel(job) {
|
|
|
1598
1646
|
}
|
|
1599
1647
|
}
|
|
1600
1648
|
|
|
1601
|
-
const stronglyVerified = snapshotStronglyMatchesRequestedModel(verificationSnapshot, job.selection);
|
|
1649
|
+
const stronglyVerified = compactSelectionVerifiedAfterClick || snapshotStronglyMatchesRequestedModel(verificationSnapshot, job.selection);
|
|
1602
1650
|
if (!stronglyVerified) {
|
|
1603
1651
|
throw new Error(`Could not verify requested model settings in configuration UI for ${job.selection.modelFamily}`);
|
|
1604
1652
|
}
|
|
@@ -1793,6 +1841,7 @@ async function waitForChatCompletion(job, baselineAssistantCount) {
|
|
|
1793
1841
|
const hasStopStreaming = isGrokJob(job) ? snapshot.includes(GROK_LABELS.stop) : snapshot.includes("Stop streaming");
|
|
1794
1842
|
const hasRetryButton = snapshot.includes('button "Retry"');
|
|
1795
1843
|
const copyResponseCount = isGrokJob(job) ? (snapshot.match(/button "Copy"/g) || []).length : (snapshot.match(/Copy response/g) || []).length;
|
|
1844
|
+
throwIfProviderTransientError(job, snapshot, "waiting for response completion");
|
|
1796
1845
|
const responseFailureText = detectResponseFailureText(`${snapshot}\n${body}`);
|
|
1797
1846
|
const messages = await assistantMessages(job);
|
|
1798
1847
|
const targetMessage = messages[baselineAssistantCount];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-oracle",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.13",
|
|
4
4
|
"description": "ChatGPT and Grok web-oracle extension for pi with isolated browser auth, async jobs, and project-context archives.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
"platform-smoke.config.mjs",
|
|
37
37
|
"scripts/platform-smoke.mjs",
|
|
38
38
|
"scripts/platform-smoke",
|
|
39
|
-
"scripts/oracle-real-smoke.mjs"
|
|
39
|
+
"scripts/oracle-real-smoke.mjs",
|
|
40
|
+
"scripts/oracle-chatgpt-preset-proof.mjs"
|
|
40
41
|
],
|
|
41
42
|
"pi": {
|
|
42
43
|
"extensions": [
|
|
@@ -49,7 +50,7 @@
|
|
|
49
50
|
"typecheck:worker-helpers": "tsc --noEmit -p tsconfig.worker-helpers.json",
|
|
50
51
|
"sanity:oracle": "node scripts/oracle-sanity-runner.mjs",
|
|
51
52
|
"pack:check": "npm pack --dry-run",
|
|
52
|
-
"verify:oracle": "npm run check:oracle-extension && npm run check:platform-smoke && npm run check:oracle-real-smoke && npm run typecheck && npm run typecheck:worker-helpers && npm run sanity:oracle && npm run pack:check",
|
|
53
|
+
"verify:oracle": "npm run check:oracle-extension && npm run check:platform-smoke && npm run check:oracle-real-smoke && npm run check:oracle-release-proof && npm run typecheck && npm run typecheck:worker-helpers && npm run sanity:oracle && npm run pack:check",
|
|
53
54
|
"test": "npm run verify:oracle",
|
|
54
55
|
"prepublishOnly": "npm run release:check",
|
|
55
56
|
"check:platform-smoke": "node --check scripts/platform-smoke.mjs && node --check scripts/platform-smoke/assertions.mjs && node --check scripts/platform-smoke/artifacts.mjs && node --check scripts/platform-smoke/crabbox-runner.mjs && node --check scripts/platform-smoke/doctor.mjs && node --check scripts/platform-smoke/targets.mjs && node scripts/platform-smoke/invariants.mjs",
|
|
@@ -61,12 +62,14 @@
|
|
|
61
62
|
"smoke:platform:windows-native": "node scripts/platform-smoke.mjs run --target windows-native",
|
|
62
63
|
"smoke:real": "npm run smoke:real:packed",
|
|
63
64
|
"smoke:real:doctor": "node scripts/oracle-real-smoke.mjs doctor",
|
|
64
|
-
"release:check": "npm run verify:oracle && npm run smoke:platform:all",
|
|
65
|
+
"release:check": "npm run verify:oracle && npm run release:proof:chatgpt-presets && npm run smoke:platform:all",
|
|
65
66
|
"check:oracle-real-smoke": "node --check scripts/oracle-real-smoke.mjs",
|
|
67
|
+
"check:oracle-release-proof": "node --check scripts/oracle-chatgpt-preset-proof.mjs",
|
|
68
|
+
"release:proof:chatgpt-presets": "node scripts/oracle-chatgpt-preset-proof.mjs check",
|
|
66
69
|
"smoke:real:packed": "node scripts/oracle-real-smoke.mjs run --mode packed",
|
|
67
70
|
"smoke:real:source": "node scripts/oracle-real-smoke.mjs run --mode source",
|
|
68
71
|
"sanity:oracle:platform": "node scripts/oracle-sanity-runner.mjs --mode platform",
|
|
69
|
-
"verify:oracle:platform": "npm run check:oracle-extension && npm run check:platform-smoke && npm run check:oracle-real-smoke && npm run sanity:oracle:platform && npm run pack:check"
|
|
72
|
+
"verify:oracle:platform": "npm run check:oracle-extension && npm run check:platform-smoke && npm run check:oracle-real-smoke && npm run check:oracle-release-proof && npm run sanity:oracle:platform && npm run pack:check"
|
|
70
73
|
},
|
|
71
74
|
"dependencies": {
|
|
72
75
|
"@steipete/sweet-cookie": "^0.3.0"
|
|
@@ -23,7 +23,7 @@ export default {
|
|
|
23
23
|
commands: ["npm run smoke:platform:all"],
|
|
24
24
|
},
|
|
25
25
|
release: {
|
|
26
|
-
description: "Full release gate: local verification plus the doctor-first platform matrix.",
|
|
26
|
+
description: "Full release gate: local verification, fresh ChatGPT preset proof, plus the doctor-first platform matrix.",
|
|
27
27
|
commands: ["npm run release:check"],
|
|
28
28
|
},
|
|
29
29
|
},
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Purpose: Release-blocking proof gate for live ChatGPT preset selection.
|
|
3
|
+
// Responsibilities: Validate that a fresh manual/live oracle job matrix covered every canonical ChatGPT preset before publish.
|
|
4
|
+
// Scope: Maintainer release safety only; the script does not submit jobs or touch provider accounts.
|
|
5
|
+
// Usage: npm run release:proof:chatgpt-presets, or `node scripts/oracle-chatgpt-preset-proof.mjs template`.
|
|
6
|
+
|
|
7
|
+
import { execFileSync } from "node:child_process";
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { dirname, resolve } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const REPO_ROOT = resolve(SCRIPT_DIR, "..");
|
|
14
|
+
const DEFAULT_PROOF_PATH = ".artifacts/chatgpt-preset-proof/latest.json";
|
|
15
|
+
const PROOF_PATH_ENV = "PI_ORACLE_CHATGPT_PRESET_PROOF";
|
|
16
|
+
const JOBS_DIR_ENV = "PI_ORACLE_JOBS_DIR";
|
|
17
|
+
const MAX_AGE_HOURS_ENV = "PI_ORACLE_CHATGPT_PRESET_PROOF_MAX_AGE_HOURS";
|
|
18
|
+
const DEFAULT_MAX_AGE_HOURS = 72;
|
|
19
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
20
|
+
const ZERO_UUID = "00000000-0000-0000-0000-000000000000";
|
|
21
|
+
|
|
22
|
+
function usage() {
|
|
23
|
+
console.log(`Usage: node scripts/oracle-chatgpt-preset-proof.mjs <check|template>
|
|
24
|
+
|
|
25
|
+
Commands:
|
|
26
|
+
check Validate release-blocking live ChatGPT preset proof. Default.
|
|
27
|
+
template Print a non-valid proof-file template for the current package version/git head.
|
|
28
|
+
|
|
29
|
+
Environment:
|
|
30
|
+
${PROOF_PATH_ENV} Proof JSON path (default: ${DEFAULT_PROOF_PATH})
|
|
31
|
+
${JOBS_DIR_ENV} Oracle jobs root for job lookup (default also checks /tmp)
|
|
32
|
+
${MAX_AGE_HOURS_ENV} Freshness window in hours (default: ${DEFAULT_MAX_AGE_HOURS})
|
|
33
|
+
|
|
34
|
+
Proof file contract:
|
|
35
|
+
The proof must reference live oracle job state produced by the loaded extension
|
|
36
|
+
after the current git HEAD. It must include one completed ChatGPT job per
|
|
37
|
+
canonical ORACLE_SUBMIT_PRESETS id. Shape-only proof is rejected.
|
|
38
|
+
`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function fail(message) {
|
|
42
|
+
console.error(message);
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readJson(path) {
|
|
47
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function git(args) {
|
|
51
|
+
return execFileSync("git", args, { cwd: REPO_ROOT, encoding: "utf8" }).trim();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function packageMetadata() {
|
|
55
|
+
const pkg = readJson(resolve(REPO_ROOT, "package.json"));
|
|
56
|
+
return { name: pkg.name, version: pkg.version };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function currentGitHead() {
|
|
60
|
+
return git(["rev-parse", "HEAD"]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function currentGitHeadCommittedAt() {
|
|
64
|
+
return git(["show", "-s", "--format=%cI", "HEAD"]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function currentGitStatus() {
|
|
68
|
+
return git(["status", "--short"]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function canonicalPresets() {
|
|
72
|
+
const configSource = readFileSync(resolve(REPO_ROOT, "extensions/oracle/lib/config.ts"), "utf8");
|
|
73
|
+
const registryMatch = configSource.match(/export const ORACLE_SUBMIT_PRESETS = \{([\s\S]*?)\n\} as const;/);
|
|
74
|
+
if (!registryMatch) throw new Error("Could not locate ORACLE_SUBMIT_PRESETS registry in extensions/oracle/lib/config.ts");
|
|
75
|
+
const entries = [...registryMatch[1].matchAll(
|
|
76
|
+
/^\s{2}([a-z0-9_]+):\s*\{\s*label:\s*"[^"]+",\s*modelFamily:\s*"([a-z]+)"\s+as const(?:,\s*effort:\s*"([a-z]+)"\s+as const)?,\s*autoSwitchToThinking:\s*(true|false)\s*\}/gm,
|
|
77
|
+
)];
|
|
78
|
+
if (entries.length === 0) throw new Error("Could not parse ORACLE_SUBMIT_PRESETS registry entries");
|
|
79
|
+
return Object.fromEntries(entries.map((match) => [match[1], {
|
|
80
|
+
modelFamily: match[2],
|
|
81
|
+
effort: match[3],
|
|
82
|
+
autoSwitchToThinking: match[4] === "true",
|
|
83
|
+
}]));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function canonicalPresetIds() {
|
|
87
|
+
return Object.keys(canonicalPresets());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function proofPath() {
|
|
91
|
+
return resolve(REPO_ROOT, process.env[PROOF_PATH_ENV] || DEFAULT_PROOF_PATH);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function maxAgeHours() {
|
|
95
|
+
const raw = process.env[MAX_AGE_HOURS_ENV];
|
|
96
|
+
if (!raw) return DEFAULT_MAX_AGE_HOURS;
|
|
97
|
+
const parsed = Number(raw);
|
|
98
|
+
if (!Number.isFinite(parsed) || parsed <= 0) throw new Error(`${MAX_AGE_HOURS_ENV} must be a positive number of hours`);
|
|
99
|
+
return parsed;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isIsoDate(value) {
|
|
103
|
+
if (typeof value !== "string" || !value.trim()) return false;
|
|
104
|
+
const millis = Date.parse(value);
|
|
105
|
+
return Number.isFinite(millis) && new Date(millis).toISOString() === value;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function parseIsoMillis(value) {
|
|
109
|
+
return isIsoDate(value) ? Date.parse(value) : undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function unique(values) {
|
|
113
|
+
return [...new Set(values.filter(Boolean))];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function candidateJobJsonPaths(jobId, proofJob) {
|
|
117
|
+
const paths = [];
|
|
118
|
+
if (typeof proofJob.jobJsonPath === "string" && proofJob.jobJsonPath.trim()) {
|
|
119
|
+
paths.push(resolve(REPO_ROOT, proofJob.jobJsonPath));
|
|
120
|
+
}
|
|
121
|
+
if (typeof proofJob.jobDir === "string" && proofJob.jobDir.trim()) {
|
|
122
|
+
paths.push(resolve(REPO_ROOT, proofJob.jobDir, "job.json"));
|
|
123
|
+
}
|
|
124
|
+
if (process.env[JOBS_DIR_ENV]) {
|
|
125
|
+
paths.push(resolve(process.env[JOBS_DIR_ENV], `oracle-${jobId}`, "job.json"));
|
|
126
|
+
}
|
|
127
|
+
paths.push(resolve("/tmp", `oracle-${jobId}`, "job.json"));
|
|
128
|
+
return unique(paths);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function loadOracleJobState(jobId, proofJob) {
|
|
132
|
+
const candidates = candidateJobJsonPaths(jobId, proofJob);
|
|
133
|
+
for (const candidate of candidates) {
|
|
134
|
+
if (!existsSync(candidate)) continue;
|
|
135
|
+
return { path: candidate, state: readJson(candidate) };
|
|
136
|
+
}
|
|
137
|
+
return { path: undefined, state: undefined, candidates };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function requireActualJobEvidence({ preset, canonicalPreset, proofJob, packageName, packageVersion, gitHead, gitHeadCommittedAtMs, proofValidatedAtMs, errors }) {
|
|
141
|
+
if (!proofJob || typeof proofJob !== "object" || Array.isArray(proofJob)) {
|
|
142
|
+
errors.push(`missing jobs.${preset}`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (proofJob.preset !== preset) errors.push(`jobs.${preset}.preset must be ${preset}`);
|
|
146
|
+
if (proofJob.provider !== "chatgpt") errors.push(`jobs.${preset}.provider must be chatgpt`);
|
|
147
|
+
|
|
148
|
+
const jobId = proofJob.jobId;
|
|
149
|
+
if (typeof jobId !== "string" || !UUID_PATTERN.test(jobId) || jobId === ZERO_UUID) {
|
|
150
|
+
errors.push(`jobs.${preset}.jobId must be a real oracle UUID job id, not a placeholder`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const loaded = loadOracleJobState(jobId, proofJob);
|
|
155
|
+
if (!loaded.state) {
|
|
156
|
+
errors.push(`jobs.${preset} could not find actual oracle job.json for ${jobId}; checked ${loaded.candidates.join(", ")}`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const state = loaded.state;
|
|
161
|
+
const responsePath = typeof state.responsePath === "string" ? state.responsePath : undefined;
|
|
162
|
+
const workerLogPath = typeof state.workerLogPath === "string" ? state.workerLogPath : undefined;
|
|
163
|
+
const response = responsePath && existsSync(responsePath) ? readFileSync(responsePath, "utf8") : "";
|
|
164
|
+
const workerLog = workerLogPath && existsSync(workerLogPath) ? readFileSync(workerLogPath, "utf8") : "";
|
|
165
|
+
const completedAtMs = parseIsoMillis(state.completedAt || state.phaseAt);
|
|
166
|
+
|
|
167
|
+
if (state.id !== jobId) errors.push(`jobs.${preset} job.json id mismatch: expected ${jobId}, got ${state.id || "<missing>"}`);
|
|
168
|
+
if (state.status !== "complete") errors.push(`jobs.${preset} actual job status must be complete, got ${state.status || "<missing>"}`);
|
|
169
|
+
if (state.phase !== "complete") errors.push(`jobs.${preset} actual job phase must be complete, got ${state.phase || "<missing>"}`);
|
|
170
|
+
if (state.selection?.provider !== "chatgpt") errors.push(`jobs.${preset} actual provider must be chatgpt`);
|
|
171
|
+
if (state.selection?.preset !== preset) errors.push(`jobs.${preset} actual preset must be ${preset}, got ${state.selection?.preset || "<missing>"}`);
|
|
172
|
+
if (state.selection?.modelFamily !== canonicalPreset.modelFamily) errors.push(`jobs.${preset} actual modelFamily must be ${canonicalPreset.modelFamily}, got ${state.selection?.modelFamily || "<missing>"}`);
|
|
173
|
+
if ((state.selection?.effort || undefined) !== canonicalPreset.effort) errors.push(`jobs.${preset} actual effort must be ${canonicalPreset.effort || "<unset>"}, got ${state.selection?.effort || "<unset>"}`);
|
|
174
|
+
if (state.selection?.autoSwitchToThinking !== canonicalPreset.autoSwitchToThinking) errors.push(`jobs.${preset} actual autoSwitchToThinking must be ${canonicalPreset.autoSwitchToThinking}`);
|
|
175
|
+
if (state.cwd !== REPO_ROOT) errors.push(`jobs.${preset} actual cwd must be this repo (${REPO_ROOT}), got ${state.cwd || "<missing>"}`);
|
|
176
|
+
if (state.projectId !== REPO_ROOT) errors.push(`jobs.${preset} actual projectId must be this repo (${REPO_ROOT}), got ${state.projectId || "<missing>"}`);
|
|
177
|
+
if (state.requestSource !== "tool" && state.requestSource !== "command") errors.push(`jobs.${preset} actual requestSource must be tool or command`);
|
|
178
|
+
if (typeof state.sessionId !== "string" || !state.sessionId.trim()) errors.push(`jobs.${preset} actual job must record sessionId`);
|
|
179
|
+
if (typeof state.originSessionFile !== "string" || !existsSync(state.originSessionFile)) errors.push(`jobs.${preset} actual originSessionFile must exist`);
|
|
180
|
+
if (typeof state.promptPath !== "string" || !existsSync(state.promptPath)) errors.push(`jobs.${preset} actual promptPath must exist`);
|
|
181
|
+
if (typeof state.logsDir !== "string" || !existsSync(state.logsDir)) errors.push(`jobs.${preset} actual logsDir must exist`);
|
|
182
|
+
if (typeof state.runtimeId !== "string" || !state.runtimeId.trim()) errors.push(`jobs.${preset} actual job must record runtimeId`);
|
|
183
|
+
if (typeof state.runtimeSessionName !== "string" || !state.runtimeSessionName.trim()) errors.push(`jobs.${preset} actual job must record runtimeSessionName`);
|
|
184
|
+
if (!state.config?.browser || !state.config?.worker || !state.config?.cleanup) errors.push(`jobs.${preset} actual job must include persisted oracle config with browser, worker, and cleanup sections`);
|
|
185
|
+
const lifecycleKinds = new Set(Array.isArray(state.lifecycleEvents) ? state.lifecycleEvents.map((event) => event?.kind) : []);
|
|
186
|
+
const lifecyclePhases = new Set(Array.isArray(state.lifecycleEvents) ? state.lifecycleEvents.map((event) => event?.phase) : []);
|
|
187
|
+
if (!lifecycleKinds.has("created")) errors.push(`jobs.${preset} lifecycle events must include job creation`);
|
|
188
|
+
if (!lifecyclePhases.has("configuring_model")) errors.push(`jobs.${preset} lifecycle events must include configuring_model phase`);
|
|
189
|
+
if (!lifecyclePhases.has("complete")) errors.push(`jobs.${preset} lifecycle events must include complete phase`);
|
|
190
|
+
if (state.extensionProvenance?.schemaVersion !== 1) errors.push(`jobs.${preset} actual job must record extensionProvenance.schemaVersion=1`);
|
|
191
|
+
if (state.extensionProvenance?.packageName !== packageName) errors.push(`jobs.${preset} actual extension packageName must be ${packageName}`);
|
|
192
|
+
if (state.extensionProvenance?.packageVersion !== packageVersion) errors.push(`jobs.${preset} actual extension packageVersion must be ${packageVersion}`);
|
|
193
|
+
if (state.extensionProvenance?.gitHead !== gitHead) errors.push(`jobs.${preset} actual extension gitHead must be ${gitHead}`);
|
|
194
|
+
if (state.extensionProvenance?.sourcePath !== REPO_ROOT) errors.push(`jobs.${preset} actual extension sourcePath must be this repo (${REPO_ROOT}), got ${state.extensionProvenance?.sourcePath || "<missing>"}`);
|
|
195
|
+
if (typeof state.archivePath !== "string" || !state.archivePath.endsWith(".tar.zst")) errors.push(`jobs.${preset} actual archivePath must end with .tar.zst`);
|
|
196
|
+
if (typeof state.archiveSha256 !== "string" || !/^[0-9a-f]{64}$/i.test(state.archiveSha256)) errors.push(`jobs.${preset} actual job must record archiveSha256`);
|
|
197
|
+
if (typeof state.conversationId !== "string" || !state.conversationId.trim()) errors.push(`jobs.${preset} actual job must record conversationId`);
|
|
198
|
+
if (typeof state.chatUrl !== "string" || !state.chatUrl.startsWith("https://chatgpt.com/c/")) errors.push(`jobs.${preset} actual job must record a ChatGPT conversation URL`);
|
|
199
|
+
if (!responsePath || !existsSync(responsePath)) errors.push(`jobs.${preset} actual responsePath must exist`);
|
|
200
|
+
if (!workerLogPath || !existsSync(workerLogPath)) errors.push(`jobs.${preset} actual workerLogPath must exist`);
|
|
201
|
+
if (!response.includes(`PRESET ${preset} OK`)) errors.push(`jobs.${preset} actual response must include PRESET ${preset} OK`);
|
|
202
|
+
if (!response.includes(`PACKAGE ${packageName}`)) errors.push(`jobs.${preset} actual response must include PACKAGE ${packageName}`);
|
|
203
|
+
if (!workerLog.includes(`Configuring model family=${state.selection?.modelFamily}`) && !workerLog.includes("Model already appears configured")) {
|
|
204
|
+
errors.push(`jobs.${preset} worker log must show model configuration or an explicit already-configured skip`);
|
|
205
|
+
}
|
|
206
|
+
if (!workerLog.includes("Job completed successfully") && !workerLog.includes(`Job ${jobId} complete`)) errors.push(`jobs.${preset} worker log must show successful completion`);
|
|
207
|
+
|
|
208
|
+
if (completedAtMs === undefined) {
|
|
209
|
+
errors.push(`jobs.${preset} actual completedAt/phaseAt must be an ISO timestamp`);
|
|
210
|
+
} else {
|
|
211
|
+
if (completedAtMs <= gitHeadCommittedAtMs) errors.push(`jobs.${preset} must complete after current git HEAD commit time`);
|
|
212
|
+
if (proofValidatedAtMs !== undefined && completedAtMs > proofValidatedAtMs) errors.push(`jobs.${preset} completed after proof validatedAt`);
|
|
213
|
+
const maxAgeMs = maxAgeHours() * 60 * 60 * 1000;
|
|
214
|
+
if (Date.now() - completedAtMs > maxAgeMs) errors.push(`jobs.${preset} completedAt is older than ${maxAgeHours()} hours`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (typeof proofJob.conversation === "string" && proofJob.conversation.trim() && proofJob.conversation !== state.conversationId && proofJob.conversation !== state.chatUrl) {
|
|
218
|
+
errors.push(`jobs.${preset}.conversation does not match actual conversationId/chatUrl`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function validateProof(proof, path) {
|
|
223
|
+
const errors = [];
|
|
224
|
+
const { name, version } = packageMetadata();
|
|
225
|
+
const gitHead = currentGitHead();
|
|
226
|
+
const gitHeadCommittedAt = currentGitHeadCommittedAt();
|
|
227
|
+
const gitHeadCommittedAtMs = Date.parse(gitHeadCommittedAt);
|
|
228
|
+
const gitStatus = currentGitStatus();
|
|
229
|
+
const presetRegistry = canonicalPresets();
|
|
230
|
+
const requiredPresets = Object.keys(presetRegistry);
|
|
231
|
+
const allowedPresets = new Set(requiredPresets);
|
|
232
|
+
|
|
233
|
+
if (gitStatus) {
|
|
234
|
+
errors.push(`working tree must be clean before release proof is accepted; current changes:\n${gitStatus}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (!proof || typeof proof !== "object" || Array.isArray(proof)) {
|
|
238
|
+
errors.push("proof root must be a JSON object");
|
|
239
|
+
return errors;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (proof.schemaVersion !== 1) errors.push("schemaVersion must be 1");
|
|
243
|
+
if (proof.packageName !== name) errors.push(`packageName must be ${name}`);
|
|
244
|
+
if (proof.packageVersion !== version) errors.push(`packageVersion must match package.json version ${version}`);
|
|
245
|
+
if (proof.gitHead !== gitHead) errors.push(`gitHead must match current HEAD ${gitHead}`);
|
|
246
|
+
if (proof.provider !== "chatgpt") errors.push('provider must be "chatgpt"');
|
|
247
|
+
if (proof.extensionUnderTest !== "loaded-extension") errors.push('extensionUnderTest must be "loaded-extension"');
|
|
248
|
+
|
|
249
|
+
let proofValidatedAtMs;
|
|
250
|
+
if (!isIsoDate(proof.validatedAt)) {
|
|
251
|
+
errors.push("validatedAt must be an ISO-8601 UTC timestamp from new Date().toISOString()");
|
|
252
|
+
} else {
|
|
253
|
+
proofValidatedAtMs = Date.parse(proof.validatedAt);
|
|
254
|
+
const ageMs = Date.now() - proofValidatedAtMs;
|
|
255
|
+
const maxAgeMs = maxAgeHours() * 60 * 60 * 1000;
|
|
256
|
+
if (ageMs < 0) errors.push("validatedAt must not be in the future");
|
|
257
|
+
if (ageMs > maxAgeMs) errors.push(`validatedAt is older than ${maxAgeHours()} hours`);
|
|
258
|
+
if (proofValidatedAtMs <= gitHeadCommittedAtMs) errors.push("validatedAt must be after current git HEAD commit time");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const jobs = proof.jobs;
|
|
262
|
+
if (!jobs || typeof jobs !== "object" || Array.isArray(jobs)) {
|
|
263
|
+
errors.push("jobs must be an object keyed by canonical preset id");
|
|
264
|
+
return errors;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
for (const preset of requiredPresets) {
|
|
268
|
+
requireActualJobEvidence({
|
|
269
|
+
preset,
|
|
270
|
+
canonicalPreset: presetRegistry[preset],
|
|
271
|
+
proofJob: jobs[preset],
|
|
272
|
+
packageName: name,
|
|
273
|
+
packageVersion: version,
|
|
274
|
+
gitHead,
|
|
275
|
+
gitHeadCommittedAtMs,
|
|
276
|
+
proofValidatedAtMs,
|
|
277
|
+
errors,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
for (const preset of Object.keys(jobs)) {
|
|
282
|
+
if (!allowedPresets.has(preset)) errors.push(`jobs.${preset} is not a canonical ORACLE_SUBMIT_PRESETS id`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (errors.length === 0) {
|
|
286
|
+
console.log(`ChatGPT preset release proof accepted: ${path}`);
|
|
287
|
+
console.log(`Validated presets: ${requiredPresets.join(", ")}`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return errors;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function template() {
|
|
294
|
+
const { name, version } = packageMetadata();
|
|
295
|
+
const gitHead = currentGitHead();
|
|
296
|
+
const jobs = Object.fromEntries(canonicalPresetIds().map((preset) => [preset, {
|
|
297
|
+
preset,
|
|
298
|
+
provider: "chatgpt",
|
|
299
|
+
jobId: `replace-with-completed-${preset}-job-uuid`,
|
|
300
|
+
jobDir: `/tmp/oracle-replace-with-completed-${preset}-job-uuid`,
|
|
301
|
+
conversation: "replace-with-actual-conversation-id-or-chat-url",
|
|
302
|
+
}]));
|
|
303
|
+
|
|
304
|
+
console.log(JSON.stringify({
|
|
305
|
+
schemaVersion: 1,
|
|
306
|
+
packageName: name,
|
|
307
|
+
packageVersion: version,
|
|
308
|
+
gitHead,
|
|
309
|
+
provider: "chatgpt",
|
|
310
|
+
extensionUnderTest: "loaded-extension",
|
|
311
|
+
validatedAt: new Date().toISOString(),
|
|
312
|
+
jobs,
|
|
313
|
+
}, null, 2));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function main() {
|
|
317
|
+
const command = process.argv[2] || "check";
|
|
318
|
+
if (command === "--help" || command === "-h") {
|
|
319
|
+
usage();
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (command === "template") {
|
|
323
|
+
template();
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (command !== "check") {
|
|
327
|
+
usage();
|
|
328
|
+
fail(`Unknown command: ${command}`);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const path = proofPath();
|
|
333
|
+
if (!existsSync(path)) {
|
|
334
|
+
fail(`Missing ChatGPT preset release proof: ${path}\n\nRun live loaded-extension oracle jobs for every canonical ChatGPT preset, then save proof JSON.\nCreate a non-valid starting template with:\n mkdir -p .artifacts/chatgpt-preset-proof\n node scripts/oracle-chatgpt-preset-proof.mjs template > ${DEFAULT_PROOF_PATH}\n\nThis gate is intentional: releases are blocked until every preset has fresh live proof backed by actual oracle job state.`);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
let proof;
|
|
339
|
+
try {
|
|
340
|
+
proof = readJson(path);
|
|
341
|
+
} catch (error) {
|
|
342
|
+
fail(`Could not read proof JSON at ${path}: ${error.message}`);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const errors = validateProof(proof, path);
|
|
347
|
+
if (errors.length > 0) {
|
|
348
|
+
fail(`ChatGPT preset release proof rejected: ${path}\n- ${errors.join("\n- ")}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
main();
|