codex-chatgpt-control 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +21 -0
  3. package/README.md +54 -0
  4. package/contracts/v1/fixtures/backend-capabilities.json +54 -0
  5. package/contracts/v1/fixtures/backend-error-event-missing-stream-input.json +10 -0
  6. package/contracts/v1/fixtures/backend-error-missing-run-input.json +10 -0
  7. package/contracts/v1/fixtures/backend-runner-plan-request.json +12 -0
  8. package/contracts/v1/fixtures/backend-version.json +10 -0
  9. package/contracts/v1/fixtures/command-descriptors.json +803 -0
  10. package/contracts/v1/fixtures/describe-runner-run.json +31 -0
  11. package/contracts/v1/fixtures/doctor-bridge-upload.json +34 -0
  12. package/contracts/v1/fixtures/help-root.json +6 -0
  13. package/contracts/v1/fixtures/named-plan-two-turn.json +39 -0
  14. package/contracts/v1/fixtures/output-json-parse-success.json +115 -0
  15. package/contracts/v1/fixtures/primitive-bootstrap-blocker.json +43 -0
  16. package/contracts/v1/fixtures/report-redaction-default.json +16 -0
  17. package/contracts/v1/fixtures/reports-create-redacted.json +16 -0
  18. package/contracts/v1/fixtures/reports-summarize-redacted.json +26 -0
  19. package/contracts/v1/fixtures/responses-basic-success.json +27 -0
  20. package/contracts/v1/fixtures/responses-hidden-instructions-unsupported.json +22 -0
  21. package/contracts/v1/fixtures/responses-unknown-field-unsupported.json +22 -0
  22. package/contracts/v1/fixtures/responses-unsupported-previous-response-id.json +22 -0
  23. package/contracts/v1/fixtures/responses-unsupported-temperature.json +22 -0
  24. package/contracts/v1/fixtures/run-basic-success.json +36 -0
  25. package/contracts/v1/fixtures/run-browser-bridge-blocker.json +180 -0
  26. package/contracts/v1/fixtures/run-file-attach-success.json +63 -0
  27. package/contracts/v1/fixtures/run-report-redacted.json +52 -0
  28. package/contracts/v1/fixtures/run-selector-drift-blocker.json +60 -0
  29. package/contracts/v1/fixtures/run-timeout-partial.json +81 -0
  30. package/contracts/v1/fixtures/run-two-turn-success.json +83 -0
  31. package/contracts/v1/fixtures/run-upload-permission-blocker.json +56 -0
  32. package/contracts/v1/fixtures/runner-budget-blocker.json +106 -0
  33. package/contracts/v1/fixtures/runner-full-agent-config.json +50 -0
  34. package/contracts/v1/fixtures/runner-input-items-and-files-plan.json +53 -0
  35. package/contracts/v1/fixtures/runner-metadata-only-plan.json +31 -0
  36. package/contracts/v1/fixtures/runner-visible-prefix-plan.json +28 -0
  37. package/contracts/v1/fixtures/runner-visible-setup-plan.json +37 -0
  38. package/contracts/v1/fixtures/stream-basic.ndjson +3 -0
  39. package/contracts/v1/fixtures/stream-blocked.ndjson +2 -0
  40. package/contracts/v1/fixtures/stream-submitted-completed.ndjson +3 -0
  41. package/contracts/v1/fixtures/workflow-ask-success.json +67 -0
  42. package/contracts/v1/manifest.json +210 -0
  43. package/contracts/v1/parity-suite.json +635 -0
  44. package/contracts/v1/schemas/agent.schema.json +52 -0
  45. package/contracts/v1/schemas/backend-event.schema.json +74 -0
  46. package/contracts/v1/schemas/backend-request.schema.json +59 -0
  47. package/contracts/v1/schemas/backend-response.schema.json +42 -0
  48. package/contracts/v1/schemas/capabilities.schema.json +33 -0
  49. package/contracts/v1/schemas/command-descriptor.schema.json +31 -0
  50. package/contracts/v1/schemas/command-result.schema.json +99 -0
  51. package/contracts/v1/schemas/manifest.schema.json +69 -0
  52. package/contracts/v1/schemas/response.schema.json +51 -0
  53. package/contracts/v1/schemas/run-result.schema.json +138 -0
  54. package/contracts/v1/schemas/sequence-plan.schema.json +44 -0
  55. package/contracts/v1/schemas/stream-event.schema.json +25 -0
  56. package/dist/codex-chatgpt-control-backend.mjs +5284 -0
  57. package/dist/codex-chatgpt-control.bundle.mjs +6013 -0
  58. package/dist/src/backend/client.d.ts +129 -0
  59. package/dist/src/backend/client.js +415 -0
  60. package/dist/src/backend/protocol.d.ts +74 -0
  61. package/dist/src/backend/protocol.js +128 -0
  62. package/dist/src/backend/session.d.ts +11 -0
  63. package/dist/src/backend/session.js +223 -0
  64. package/dist/src/backend/stdio-server.d.ts +9 -0
  65. package/dist/src/backend/stdio-server.js +88 -0
  66. package/dist/src/browser/attach.d.ts +8 -0
  67. package/dist/src/browser/attach.js +456 -0
  68. package/dist/src/browser/clipboard.d.ts +2 -0
  69. package/dist/src/browser/clipboard.js +26 -0
  70. package/dist/src/browser/downloads.d.ts +7 -0
  71. package/dist/src/browser/downloads.js +29 -0
  72. package/dist/src/browser/page-state.d.ts +17 -0
  73. package/dist/src/browser/page-state.js +66 -0
  74. package/dist/src/client.d.ts +167 -0
  75. package/dist/src/client.js +666 -0
  76. package/dist/src/commands/confirmations.d.ts +9 -0
  77. package/dist/src/commands/confirmations.js +33 -0
  78. package/dist/src/commands/context.d.ts +2 -0
  79. package/dist/src/commands/context.js +32 -0
  80. package/dist/src/commands/doctor.d.ts +16 -0
  81. package/dist/src/commands/doctor.js +120 -0
  82. package/dist/src/commands/files.d.ts +4 -0
  83. package/dist/src/commands/files.js +314 -0
  84. package/dist/src/commands/helpers.d.ts +16 -0
  85. package/dist/src/commands/helpers.js +128 -0
  86. package/dist/src/commands/messages.d.ts +16 -0
  87. package/dist/src/commands/messages.js +538 -0
  88. package/dist/src/commands/modes.d.ts +9 -0
  89. package/dist/src/commands/modes.js +323 -0
  90. package/dist/src/commands/output.d.ts +5 -0
  91. package/dist/src/commands/output.js +31 -0
  92. package/dist/src/commands/registry.d.ts +17 -0
  93. package/dist/src/commands/registry.js +275 -0
  94. package/dist/src/commands/reports.d.ts +14 -0
  95. package/dist/src/commands/reports.js +47 -0
  96. package/dist/src/commands/response-actions.d.ts +2 -0
  97. package/dist/src/commands/response-actions.js +141 -0
  98. package/dist/src/commands/sequence.d.ts +9 -0
  99. package/dist/src/commands/sequence.js +228 -0
  100. package/dist/src/commands/session.d.ts +2 -0
  101. package/dist/src/commands/session.js +25 -0
  102. package/dist/src/commands/threads.d.ts +6 -0
  103. package/dist/src/commands/threads.js +300 -0
  104. package/dist/src/dom/fixtures.d.ts +1 -0
  105. package/dist/src/dom/fixtures.js +8 -0
  106. package/dist/src/dom/menus.d.ts +8 -0
  107. package/dist/src/dom/menus.js +40 -0
  108. package/dist/src/dom/message-format.d.ts +30 -0
  109. package/dist/src/dom/message-format.js +593 -0
  110. package/dist/src/dom/messages.d.ts +40 -0
  111. package/dist/src/dom/messages.js +127 -0
  112. package/dist/src/dom/selectors.d.ts +20 -0
  113. package/dist/src/dom/selectors.js +72 -0
  114. package/dist/src/dom/visible-text.d.ts +5 -0
  115. package/dist/src/dom/visible-text.js +27 -0
  116. package/dist/src/errors.d.ts +32 -0
  117. package/dist/src/errors.js +127 -0
  118. package/dist/src/index.d.ts +34 -0
  119. package/dist/src/index.js +34 -0
  120. package/dist/src/logger.d.ts +15 -0
  121. package/dist/src/logger.js +25 -0
  122. package/dist/src/runner/agent.d.ts +2 -0
  123. package/dist/src/runner/agent.js +17 -0
  124. package/dist/src/runner/index.d.ts +7 -0
  125. package/dist/src/runner/index.js +7 -0
  126. package/dist/src/runner/interruptions.d.ts +3 -0
  127. package/dist/src/runner/interruptions.js +65 -0
  128. package/dist/src/runner/responses.d.ts +30 -0
  129. package/dist/src/runner/responses.js +171 -0
  130. package/dist/src/runner/result.d.ts +3 -0
  131. package/dist/src/runner/result.js +128 -0
  132. package/dist/src/runner/resume.d.ts +11 -0
  133. package/dist/src/runner/resume.js +26 -0
  134. package/dist/src/runner/stream.d.ts +13 -0
  135. package/dist/src/runner/stream.js +66 -0
  136. package/dist/src/runner/types.d.ts +264 -0
  137. package/dist/src/runner/types.js +1 -0
  138. package/dist/src/safety/blockers.d.ts +7 -0
  139. package/dist/src/safety/blockers.js +60 -0
  140. package/dist/src/safety/redaction.d.ts +2 -0
  141. package/dist/src/safety/redaction.js +18 -0
  142. package/dist/src/safety/report-redaction.d.ts +8 -0
  143. package/dist/src/safety/report-redaction.js +74 -0
  144. package/dist/src/safety/risk.d.ts +26 -0
  145. package/dist/src/safety/risk.js +28 -0
  146. package/dist/src/scripts/backend-server.d.ts +2 -0
  147. package/dist/src/scripts/backend-server.js +7 -0
  148. package/dist/src/scripts/check-parity-suite.d.ts +1 -0
  149. package/dist/src/scripts/check-parity-suite.js +10 -0
  150. package/dist/src/scripts/continue-thread.d.ts +27 -0
  151. package/dist/src/scripts/continue-thread.js +388 -0
  152. package/dist/src/scripts/live-smoke/harness.d.ts +11 -0
  153. package/dist/src/scripts/live-smoke/harness.js +151 -0
  154. package/dist/src/scripts/live-smoke/scenarios.d.ts +3 -0
  155. package/dist/src/scripts/live-smoke/scenarios.js +648 -0
  156. package/dist/src/scripts/live-smoke/types.d.ts +53 -0
  157. package/dist/src/scripts/live-smoke/types.js +1 -0
  158. package/dist/src/scripts/live-smoke-module.d.ts +3 -0
  159. package/dist/src/scripts/live-smoke-module.js +2 -0
  160. package/dist/src/scripts/live-smoke.d.ts +1 -0
  161. package/dist/src/scripts/live-smoke.js +54 -0
  162. package/dist/src/scripts/smoke-attach-file.d.ts +1 -0
  163. package/dist/src/scripts/smoke-attach-file.js +39 -0
  164. package/dist/src/scripts/smoke-hi.d.ts +1 -0
  165. package/dist/src/scripts/smoke-hi.js +25 -0
  166. package/dist/src/scripts/smoke-search-open-read.d.ts +1 -0
  167. package/dist/src/scripts/smoke-search-open-read.js +41 -0
  168. package/dist/src/testing/parity-suite.d.ts +12 -0
  169. package/dist/src/testing/parity-suite.js +213 -0
  170. package/dist/src/types.d.ts +495 -0
  171. package/dist/src/types.js +1 -0
  172. package/package.json +81 -0
  173. package/references/agents-runner.md +32 -0
  174. package/references/backend-protocol.md +238 -0
  175. package/references/python-parity.md +168 -0
  176. package/references/responses-adapter.md +28 -0
  177. package/references/safety.md +14 -0
  178. package/references/streaming.md +15 -0
  179. package/references/troubleshooting.md +91 -0
@@ -0,0 +1,53 @@
1
+ import type { BrowserLike, CommandResult } from "../../types.js";
2
+ export type LiveSmokeStatus = "pass" | "fail" | "skip";
3
+ export type LiveSmokeCleanupResult = {
4
+ attempted: boolean;
5
+ ok: boolean;
6
+ reason?: string;
7
+ error?: {
8
+ name: string;
9
+ message: string;
10
+ };
11
+ };
12
+ export type LiveSmokeScenarioResult = {
13
+ name: string;
14
+ status: LiveSmokeStatus;
15
+ required: boolean;
16
+ startedAt: string;
17
+ endedAt: string;
18
+ durationMs: number;
19
+ command?: CommandResult<unknown>;
20
+ details?: Record<string, unknown>;
21
+ error?: {
22
+ name: string;
23
+ message: string;
24
+ };
25
+ cleanup?: LiveSmokeCleanupResult;
26
+ };
27
+ export type LiveSmokeBrowser = BrowserLike & {
28
+ tabs?: BrowserLike["tabs"] & {
29
+ finalize?: (options: {
30
+ keep?: unknown[];
31
+ }) => Promise<void>;
32
+ };
33
+ };
34
+ export type LiveSmokeContext = {
35
+ agent: unknown;
36
+ browser?: LiveSmokeBrowser;
37
+ env?: Record<string, string | undefined>;
38
+ reportDir: string;
39
+ knownThreadQuery?: string;
40
+ knownThreadUrl?: string;
41
+ knownConversationId?: string;
42
+ };
43
+ export type LiveSmokeScenario = {
44
+ name: string;
45
+ required: boolean;
46
+ enabled: (context: LiveSmokeContext) => boolean;
47
+ run: (context: LiveSmokeContext) => Promise<LiveSmokeScenarioResult>;
48
+ };
49
+ export type LiveSmokeRunResult = {
50
+ reportPath: string;
51
+ results: LiveSmokeScenarioResult[];
52
+ requiredFailures: LiveSmokeScenarioResult[];
53
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ export { contextEnvFlag, contextEnvText, envFlag, envText, filterScenarios, requiredFailures, runLiveSmoke, runScenario, writeReport } from "./live-smoke/harness.js";
2
+ export { optionalScenarios, requiredScenarios } from "./live-smoke/scenarios.js";
3
+ export type { LiveSmokeBrowser, LiveSmokeCleanupResult, LiveSmokeContext, LiveSmokeRunResult, LiveSmokeScenario, LiveSmokeScenarioResult, LiveSmokeStatus } from "./live-smoke/types.js";
@@ -0,0 +1,2 @@
1
+ export { contextEnvFlag, contextEnvText, envFlag, envText, filterScenarios, requiredFailures, runLiveSmoke, runScenario, writeReport } from "./live-smoke/harness.js";
2
+ export { optionalScenarios, requiredScenarios } from "./live-smoke/scenarios.js";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,54 @@
1
+ import { join } from "node:path";
2
+ import { BROWSER_BRIDGE_REMEDIATION, BROWSER_BRIDGE_UNAVAILABLE_MESSAGE } from "../errors.js";
3
+ import { envText, filterScenarios, runLiveSmoke } from "./live-smoke/harness.js";
4
+ import { optionalScenarios, requiredScenarios } from "./live-smoke/scenarios.js";
5
+ const globals = globalThis;
6
+ const agent = globals.agent;
7
+ if (agent === undefined) {
8
+ console.log(JSON.stringify({
9
+ ok: false,
10
+ status: "blocked",
11
+ blocker: {
12
+ kind: "browser_bridge_unavailable",
13
+ code: "codex_chrome_bridge_unavailable",
14
+ message: BROWSER_BRIDGE_UNAVAILABLE_MESSAGE,
15
+ remediation: BROWSER_BRIDGE_REMEDIATION
16
+ }
17
+ }, null, 2));
18
+ process.exitCode = 2;
19
+ }
20
+ else {
21
+ const context = {
22
+ agent,
23
+ reportDir: join(process.cwd(), "reports", "live-smoke")
24
+ };
25
+ const browser = globals.browser;
26
+ if (browser !== undefined) {
27
+ context.browser = browser;
28
+ }
29
+ const knownThreadQuery = envText("CHATGPT_SMOKE_QUERY");
30
+ if (knownThreadQuery !== undefined) {
31
+ context.knownThreadQuery = knownThreadQuery;
32
+ }
33
+ const knownThreadUrl = envText("CHATGPT_SMOKE_THREAD_URL");
34
+ if (knownThreadUrl !== undefined) {
35
+ context.knownThreadUrl = knownThreadUrl;
36
+ }
37
+ const knownConversationId = envText("CHATGPT_SMOKE_CONVERSATION_ID");
38
+ if (knownConversationId !== undefined) {
39
+ context.knownConversationId = knownConversationId;
40
+ }
41
+ context.env = {
42
+ CHATGPT_E2E_CREATE_IMAGE: envText("CHATGPT_E2E_CREATE_IMAGE"),
43
+ CHATGPT_E2E_DEEP_RESEARCH: envText("CHATGPT_E2E_DEEP_RESEARCH"),
44
+ CHATGPT_E2E_DOWNLOAD: envText("CHATGPT_E2E_DOWNLOAD"),
45
+ CHATGPT_E2E_LOGIN_PROFILE: envText("CHATGPT_E2E_LOGIN_PROFILE"),
46
+ CHATGPT_E2E_MODE_LABEL: envText("CHATGPT_E2E_MODE_LABEL"),
47
+ CHATGPT_E2E_STREAM: envText("CHATGPT_E2E_STREAM"),
48
+ CHATGPT_E2E_UPLOAD_PERMISSION_MANUAL: envText("CHATGPT_E2E_UPLOAD_PERMISSION_MANUAL"),
49
+ CHATGPT_E2E_WEB_SEARCH: envText("CHATGPT_E2E_WEB_SEARCH")
50
+ };
51
+ const scenarios = filterScenarios([...requiredScenarios, ...optionalScenarios], envText("CHATGPT_E2E_SCENARIOS"));
52
+ const result = await runLiveSmoke(context, scenarios);
53
+ process.exitCode = result.requiredFailures.length === 0 ? 0 : 1;
54
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,39 @@
1
+ import { mkdtemp, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { attachFiles } from "../commands/files.js";
5
+ import { askMessage } from "../commands/messages.js";
6
+ import { bootstrap } from "../commands/session.js";
7
+ import { newThread } from "../commands/threads.js";
8
+ const dir = await mkdtemp(join(tmpdir(), "chatgpt-control-smoke-"));
9
+ const file = join(dir, "chatgpt-control-smoke.txt");
10
+ await writeFile(file, "This is a ChatGPT control smoke fixture.\n");
11
+ const env = { agent: globalThis.agent };
12
+ const boot = await bootstrap(env);
13
+ if (!boot.ok) {
14
+ console.log(JSON.stringify(boot, null, 2));
15
+ process.exitCode = 2;
16
+ }
17
+ else {
18
+ const created = await newThread(env);
19
+ if (!created.ok) {
20
+ console.log(JSON.stringify(created, null, 2));
21
+ process.exitCode = 2;
22
+ }
23
+ else {
24
+ const attached = await attachFiles(env, { paths: [file], timeoutMs: 180000 });
25
+ if (!attached.ok) {
26
+ console.log(JSON.stringify(attached, null, 2));
27
+ process.exitCode = 1;
28
+ }
29
+ else {
30
+ const result = await askMessage(env, {
31
+ text: "Reply with the attached filename only.",
32
+ wait: { timeoutMs: 180000, stableMs: 2000 },
33
+ read: true
34
+ });
35
+ console.log(JSON.stringify(result, null, 2));
36
+ process.exitCode = result.ok && result.data?.responseText?.includes("chatgpt-control-smoke.txt") ? 0 : 1;
37
+ }
38
+ }
39
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ import { newThread } from "../commands/threads.js";
2
+ import { askMessage } from "../commands/messages.js";
3
+ import { bootstrap } from "../commands/session.js";
4
+ const env = { agent: globalThis.agent };
5
+ const boot = await bootstrap(env);
6
+ if (!boot.ok) {
7
+ console.log(JSON.stringify(boot, null, 2));
8
+ process.exitCode = 2;
9
+ }
10
+ else {
11
+ const created = await newThread(env);
12
+ if (!created.ok) {
13
+ console.log(JSON.stringify(created, null, 2));
14
+ process.exitCode = 2;
15
+ }
16
+ else {
17
+ const result = await askMessage(env, {
18
+ text: "reply with the word hi",
19
+ wait: { timeoutMs: 120000, stableMs: 2000 },
20
+ read: true
21
+ });
22
+ console.log(JSON.stringify(result, null, 2));
23
+ process.exitCode = result.ok && result.data?.responseText?.trim().toLowerCase() === "hi" ? 0 : 1;
24
+ }
25
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,41 @@
1
+ import { readLatest } from "../commands/messages.js";
2
+ import { bootstrap } from "../commands/session.js";
3
+ import { openThread, searchThreads } from "../commands/threads.js";
4
+ const query = process.env.CHATGPT_SMOKE_QUERY;
5
+ if (query === undefined || query.trim().length === 0) {
6
+ console.error("Set CHATGPT_SMOKE_QUERY to a known ChatGPT thread title or exact search phrase.");
7
+ process.exit(2);
8
+ }
9
+ const env = { agent: globalThis.agent };
10
+ const boot = await bootstrap(env);
11
+ if (!boot.ok) {
12
+ console.log(JSON.stringify(boot, null, 2));
13
+ process.exitCode = 2;
14
+ }
15
+ else {
16
+ const search = await searchThreads(env, { query, limit: 5 });
17
+ if (!search.ok || search.data?.results[0] === undefined) {
18
+ console.log(JSON.stringify(search, null, 2));
19
+ process.exitCode = 1;
20
+ }
21
+ else {
22
+ const opened = await openThread(env, { fromStep: "find", select: "first" }, new Map([["find", search]]));
23
+ if (!opened.ok) {
24
+ console.log(JSON.stringify(opened, null, 2));
25
+ process.exitCode = 1;
26
+ }
27
+ else {
28
+ const read = await readLatest(env, { role: "assistant", format: "normalized_text" });
29
+ console.log(JSON.stringify({
30
+ ok: read.ok,
31
+ status: read.status,
32
+ title: opened.data?.title,
33
+ url: opened.context.url,
34
+ preview: read.data?.text.slice(0, 200),
35
+ warnings: read.warnings,
36
+ blocker: read.blocker
37
+ }, null, 2));
38
+ process.exitCode = read.ok ? 0 : 1;
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,12 @@
1
+ export type ParitySuiteReport = {
2
+ evidenceMode: "full-repo" | "package-local";
3
+ surfaceCount: number;
4
+ fixtureCount: number;
5
+ commandCount: number;
6
+ gateCount: number;
7
+ manifestFixtures: string[];
8
+ coveredFixtures: string[];
9
+ sourceCommands: string[];
10
+ coveredCommands: string[];
11
+ };
12
+ export declare function validateParitySuite(packageRootInput?: URL | string): ParitySuiteReport;
@@ -0,0 +1,213 @@
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import ts from "typescript";
5
+ const PARITY_SUITE_SCHEMA_VERSION = "chatgpt.browser_control.parity_suite.v1";
6
+ export function validateParitySuite(packageRootInput = defaultPackageRoot()) {
7
+ const packageRoot = toPath(packageRootInput);
8
+ const repoLayout = resolveRepoLayout(packageRoot);
9
+ const contractRoot = join(packageRoot, "contracts", "v1");
10
+ const matrixPath = join(contractRoot, "parity-suite.json");
11
+ const manifestPath = join(contractRoot, "manifest.json");
12
+ const matrix = readJson(matrixPath);
13
+ const manifest = readJson(manifestPath);
14
+ const errors = [];
15
+ if (matrix.schemaVersion !== PARITY_SUITE_SCHEMA_VERSION) {
16
+ errors.push(`parity-suite.json has unsupported schemaVersion ${String(matrix.schemaVersion)}.`);
17
+ }
18
+ const surfaceIds = new Set(matrix.surfaces.map(surface => surface.id));
19
+ if (surfaceIds.size !== matrix.surfaces.length) {
20
+ errors.push("parity-suite.json contains duplicate surface ids.");
21
+ }
22
+ const manifestFixtures = sortedUnique(manifest.fixtures.map(fixture => fixture.file));
23
+ const sourceCommands = readBackendCommands(join(packageRoot, "src", "backend", "protocol.ts"));
24
+ const coveredFixtures = sortedUnique([
25
+ ...matrix.surfaces.flatMap(surface => surface.fixtures ?? []),
26
+ ...Object.values(matrix.backendCommands).flatMap(command => command.fixtures ?? [])
27
+ ]);
28
+ const coveredCommands = sortedUnique(Object.keys(matrix.backendCommands));
29
+ compareSets("fixture coverage", coveredFixtures, manifestFixtures, errors);
30
+ compareSets("backend command coverage", coveredCommands, sourceCommands, errors);
31
+ validateSurfaces(matrix.surfaces, surfaceIds, manifestFixtures, packageRoot, repoLayout, errors);
32
+ validateCommands(matrix.backendCommands, surfaceIds, manifestFixtures, packageRoot, repoLayout, errors);
33
+ validateGates(matrix.gates, packageRoot, repoLayout, errors);
34
+ if (errors.length > 0) {
35
+ throw new Error(`Parity suite validation failed:\n- ${errors.join("\n- ")}`);
36
+ }
37
+ return {
38
+ evidenceMode: repoLayout.fullRepo ? "full-repo" : "package-local",
39
+ surfaceCount: matrix.surfaces.length,
40
+ fixtureCount: manifestFixtures.length,
41
+ commandCount: sourceCommands.length,
42
+ gateCount: matrix.gates.length,
43
+ manifestFixtures,
44
+ coveredFixtures,
45
+ sourceCommands,
46
+ coveredCommands
47
+ };
48
+ }
49
+ function validateSurfaces(surfaces, surfaceIds, manifestFixtures, packageRoot, repoLayout, errors) {
50
+ for (const surface of surfaces) {
51
+ const label = `surface ${surface.id}`;
52
+ if (surface.summary.length === 0)
53
+ errors.push(`${label} must include a summary.`);
54
+ validateEvidence(label, surface, surfaceIds, manifestFixtures, packageRoot, repoLayout, errors);
55
+ }
56
+ }
57
+ function validateCommands(commands, surfaceIds, manifestFixtures, packageRoot, repoLayout, errors) {
58
+ for (const [command, coverage] of Object.entries(commands)) {
59
+ const label = `backend command ${command}`;
60
+ if (!surfaceIds.has(coverage.surface)) {
61
+ errors.push(`${label} references unknown surface ${coverage.surface}.`);
62
+ }
63
+ validateEvidence(label, coverage, surfaceIds, manifestFixtures, packageRoot, repoLayout, errors);
64
+ if ((coverage.nodeTests ?? []).length === 0)
65
+ errors.push(`${label} must include nodeTests evidence.`);
66
+ if ((coverage.pythonTests ?? []).length === 0)
67
+ errors.push(`${label} must include pythonTests evidence.`);
68
+ if ((coverage.docs ?? []).length === 0)
69
+ errors.push(`${label} must include docs evidence.`);
70
+ }
71
+ }
72
+ function validateEvidence(label, evidence, _surfaceIds, manifestFixtures, packageRoot, repoLayout, errors) {
73
+ for (const fixture of evidence.fixtures ?? []) {
74
+ if (!manifestFixtures.includes(fixture)) {
75
+ errors.push(`${label} references unknown fixture ${fixture}.`);
76
+ }
77
+ assertExists(`${label} fixture ${fixture}`, join(packageRoot, "contracts", "v1", "fixtures", fixture), errors);
78
+ }
79
+ for (const sourceFile of evidence.sourceFiles ?? []) {
80
+ assertExists(`${label} source ${sourceFile}`, join(packageRoot, sourceFile), errors);
81
+ }
82
+ for (const nodeTest of evidence.nodeTests ?? []) {
83
+ assertExists(`${label} node test ${nodeTest}`, join(packageRoot, nodeTest), errors);
84
+ }
85
+ for (const pythonTest of evidence.pythonTests ?? []) {
86
+ if (repoLayout.pythonRoot !== undefined) {
87
+ assertExists(`${label} python test ${pythonTest}`, join(repoLayout.pythonRoot, pythonTest), errors);
88
+ }
89
+ }
90
+ for (const doc of evidence.docs ?? []) {
91
+ if (!existsAtAny([join(packageRoot, doc), join(repoLayout.repoRoot, doc)])) {
92
+ if (!repoLayout.fullRepo && doc.startsWith("../")) {
93
+ continue;
94
+ }
95
+ errors.push(`${label} docs ${doc} does not exist.`);
96
+ }
97
+ }
98
+ }
99
+ function validateGates(gates, packageRoot, repoLayout, errors) {
100
+ const ids = new Set();
101
+ const packageJson = readJson(join(packageRoot, "package.json"));
102
+ const workflowText = repoLayout.fullRepo ? readWorkflowText(join(repoLayout.repoRoot, ".github", "workflows")) : "";
103
+ for (const gate of gates) {
104
+ if (ids.has(gate.id))
105
+ errors.push(`gate ${gate.id} is duplicated.`);
106
+ ids.add(gate.id);
107
+ if (repoLayout.fullRepo) {
108
+ assertExists(`gate ${gate.id} cwd ${gate.cwd}`, join(repoLayout.repoRoot, gate.cwd), errors);
109
+ }
110
+ const script = npmRunScriptName(gate.command);
111
+ if (script !== undefined && packageJson.scripts?.[script] === undefined) {
112
+ errors.push(`gate ${gate.id} references missing npm script ${script}.`);
113
+ }
114
+ if (repoLayout.fullRepo && gate.ciRequired !== false && !workflowText.includes(gate.command)) {
115
+ errors.push(`gate ${gate.id} command is missing from chatgpt-sdk-parity.yml: ${gate.command}`);
116
+ }
117
+ }
118
+ }
119
+ function readWorkflowText(workflowsDir) {
120
+ if (!existsSync(workflowsDir))
121
+ return "";
122
+ return readdirSync(workflowsDir)
123
+ .filter(file => /\.(ya?ml)$/i.test(file))
124
+ .map(file => readFileSync(join(workflowsDir, file), "utf8"))
125
+ .join("\n");
126
+ }
127
+ function resolveRepoLayout(packageRoot) {
128
+ const repoRoot = resolve(packageRoot, "..", "..");
129
+ const packageFromRepoRoot = join(repoRoot, "packages", "node");
130
+ const pythonFromRepoRoot = join(repoRoot, "packages", "python");
131
+ if (existsSync(packageFromRepoRoot) && existsSync(pythonFromRepoRoot)) {
132
+ return { repoRoot, pythonRoot: pythonFromRepoRoot, fullRepo: true };
133
+ }
134
+ const siblingPythonRoot = resolve(packageRoot, "..", "python");
135
+ if (existsSync(siblingPythonRoot)) {
136
+ return { repoRoot, pythonRoot: siblingPythonRoot, fullRepo: false };
137
+ }
138
+ return { repoRoot, fullRepo: false };
139
+ }
140
+ function readBackendCommands(protocolPath) {
141
+ const sourceText = readFileSync(protocolPath, "utf8");
142
+ const sourceFile = ts.createSourceFile(protocolPath, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
143
+ const commands = [];
144
+ function visit(node) {
145
+ if (ts.isVariableStatement(node)) {
146
+ for (const declaration of node.declarationList.declarations) {
147
+ if (ts.isIdentifier(declaration.name)
148
+ && declaration.name.text === "backendCommands"
149
+ && declaration.initializer !== undefined) {
150
+ const array = unwrapConstAssertion(declaration.initializer);
151
+ if (!ts.isArrayLiteralExpression(array)) {
152
+ throw new Error("backendCommands must be declared as an array literal.");
153
+ }
154
+ for (const element of array.elements) {
155
+ if (!ts.isStringLiteral(element)) {
156
+ throw new Error("backendCommands may only contain string literals.");
157
+ }
158
+ commands.push(element.text);
159
+ }
160
+ }
161
+ }
162
+ }
163
+ ts.forEachChild(node, visit);
164
+ }
165
+ visit(sourceFile);
166
+ if (commands.length === 0)
167
+ throw new Error("Unable to find backendCommands in backend protocol source.");
168
+ return sortedUnique(commands);
169
+ }
170
+ function unwrapConstAssertion(node) {
171
+ if (ts.isAsExpression(node))
172
+ return unwrapConstAssertion(node.expression);
173
+ return node;
174
+ }
175
+ function compareSets(label, actual, expected, errors) {
176
+ const actualSet = new Set(actual);
177
+ const expectedSet = new Set(expected);
178
+ const missing = expected.filter(item => !actualSet.has(item));
179
+ const extra = actual.filter(item => !expectedSet.has(item));
180
+ if (missing.length > 0)
181
+ errors.push(`${label} missing: ${missing.join(", ")}`);
182
+ if (extra.length > 0)
183
+ errors.push(`${label} extra: ${extra.join(", ")}`);
184
+ }
185
+ function assertExists(label, path, errors) {
186
+ if (!existsSync(path))
187
+ errors.push(`${label} does not exist at ${path}.`);
188
+ }
189
+ function existsAtAny(paths) {
190
+ return paths.some(path => existsSync(path));
191
+ }
192
+ function npmRunScriptName(command) {
193
+ const match = command.match(/^npm run ([^\s]+)$/);
194
+ if (match !== null)
195
+ return match[1];
196
+ if (command === "npm test")
197
+ return "test";
198
+ return undefined;
199
+ }
200
+ function sortedUnique(values) {
201
+ return [...new Set(values)].sort((left, right) => left.localeCompare(right));
202
+ }
203
+ function readJson(path) {
204
+ return JSON.parse(readFileSync(path, "utf8"));
205
+ }
206
+ function toPath(input) {
207
+ if (input instanceof URL)
208
+ return fileURLToPath(input);
209
+ return input;
210
+ }
211
+ function defaultPackageRoot() {
212
+ return resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
213
+ }