diff --git a/.coderabbit.yaml b/.coderabbit.yaml
deleted file mode 100644
index ab02b02657..0000000000
--- a/.coderabbit.yaml
+++ /dev/null
@@ -1,70 +0,0 @@
-# yaml-language-server: $schema=https://storage.googleapis.com/coderabbit_public_assets/schema.v2.json
-# CodeRabbit configuration for opencodex.
-# This file is the single source of truth for CodeRabbit behavior; prefer editing
-# this file over the dashboard so settings are versioned and reviewable.
-
-language: en-US
-tone_instructions: >-
- Always review in English. Be very detailed and specific: cite exact files and
- lines, explain the failure mode, and propose a concrete fix. Prefer
- evidence-backed findings over style nitpicks.
-
-reviews:
- profile: assertive
- high_level_summary: true
- auto_review:
- enabled: true
- drafts: false
- # Default branch (main) is included automatically; these are additional
- # base branches (anchored regex).
- base_branches:
- - "^dev$"
- - "^preview$"
- path_instructions:
- - path: "src/**"
- instructions: >-
- Runtime is Bun-native TypeScript (no separate compile step). Flag
- Node-only APIs that break under Bun, provider/adapter contract drift,
- and changes that bypass the shared routing/config layers. Watch for
- credential handling: tokens and OAuth material must never be logged or
- serialized into responses.
- - path: "tests/**"
- instructions: >-
- Tests are flat Bun tests under tests/. A behavior change in src/ should
- come with a focused regression test near the existing tests for that
- subsystem. Flag PRs that change shared routing, adapters, config, or
- server behavior without touching tests.
- - path: "gui/**"
- instructions: >-
- React dashboard built with Vite. Check that GUI state changes stay
- consistent with the management API responses and that user-visible
- strings go through the i18n locale files rather than hardcoded text.
- - path: ".github/**"
- instructions: >-
- Security boundary. Workflow changes, release automation, and dependency
- installation steps require explicit security review per MAINTAINERS.md.
- Flag any new secret usage, permission escalation, or third-party action
- pinned to a mutable ref.
- - path: "scripts/**"
- instructions: >-
- scripts/release.ts is the release authority and a security boundary.
- Flag changes that weaken CI gating, alter npm publish behavior, or
- bypass the release workflow's dry-run default.
- - path: "docs-site/**"
- instructions: >-
- Astro + Starlight docs site. Check that user-facing docs stay in sync
- with actual CLI/API behavior and that translated locale pages (ja, ko,
- ru, zh-cn) are not left contradicting the English source.
-
-issue_enrichment:
- auto_enrich:
- enabled: true
- planning:
- enabled: true
- auto_planning:
- enabled: true
- labels:
- - plan-me
-
-chat:
- auto_reply: true
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 1b2418c7fb..01da865adf 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1,31 +1,5 @@
-# Default reviewer
+# Single-owner repository: every path defaults to the owner. CODEOWNERS here
+# requests review; it is not a merge gate. Merge requirements live in the
+# GitHub ruleset on `main`, which must list repository admins as Always-allow
+# bypass actors (see MAINTAINERS.md).
* @pavelhov
-
-# High-impact runtime behavior
-/src/adapters/ @lidge-jun @Ingwannu @Wibias
-/src/providers/ @lidge-jun @Ingwannu @Wibias
-/src/codex/ @lidge-jun @Ingwannu @Wibias
-/src/server/ @lidge-jun @Ingwannu @Wibias
-
-# Repository automation and release security
-/.github/ @pavelhov
-/scripts/release.ts @pavelhov
-/scripts/build-macos-app.sh @pavelhov
-/scripts/package-macos-release.sh @pavelhov
-/package.json @pavelhov
-/bun.lock @pavelhov
-
-# Authentication, credentials, and management API
-/src/oauth/ @pavelhov
-/src/codex/auth-context.ts @pavelhov
-/src/server/auth-cors.ts @pavelhov
-/src/server/management-api.ts @pavelhov
-/app/Sources/MenuBarCore/Discovery.swift @pavelhov
-/app/Sources/MenuBarCore/ProxyClient.swift @pavelhov
-/app/Info.plist @pavelhov
-
-# Governance and security policy
-/AGENTS.md @pavelhov
-**/AGENTS.md @pavelhov
-/MAINTAINERS.md @pavelhov
-/SECURITY.md @pavelhov
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index 4a2503137e..c731707599 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -19,7 +19,7 @@ body:
- Codex SDK
- Claude Code
- Direct HTTP/API client
- - OpenCodex dashboard
+ - CodexCommander dashboard
- Other
validations:
required: true
@@ -28,7 +28,7 @@ body:
id: area
attributes:
label: Area
- description: Which part of OpenCodex is affected?
+ description: Which part of CodexCommander is affected?
options:
- CLI
- Proxy and routing
@@ -62,7 +62,7 @@ body:
description: |
Exact commands, steps, configuration shape, and inputs. If the problem is intermittent or not yet consistently reproducible, explain what you have observed and under which conditions.
placeholder: |
- 1. ocx start --port 10100
+ 1. ccx start --port 10100
2. Send a request to /v1/responses with ...
3. Observe ...
validations:
@@ -72,8 +72,8 @@ body:
id: version
attributes:
label: Version
- description: Installed `@bitkyc08/opencodex` version or commit SHA.
- placeholder: "2.7.31"
+ description: Installed `codexcommander` version or commit SHA.
+ placeholder: "0.1.0"
validations:
required: true
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 07fd1d1cf6..d587207795 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,11 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: Report a security vulnerability (private)
- url: https://github.com/lidge-jun/opencodex/security/advisories/new
+ url: https://github.com/pavelhov/CodexCommander/security/advisories/new
about: Report undisclosed vulnerabilities privately to the maintainers. Do not open a public issue.
- name: Security policy
- url: https://github.com/lidge-jun/opencodex/blob/main/SECURITY.md
+ url: https://github.com/pavelhov/CodexCommander/blob/main/SECURITY.md
about: Read the supported-version and reporting guidance before sharing security-sensitive details.
- name: Contributing guide
- url: https://opencodex.me/contributing/
+ url: https://github.com/pavelhov/CodexCommander/blob/main/CONTRIBUTING.md
about: Review setup, build, and verification guidance for contributors.
diff --git a/.github/ISSUE_TEMPLATE/documentation.yml b/.github/ISSUE_TEMPLATE/documentation.yml
index 358e5e889b..53c7cd8574 100644
--- a/.github/ISSUE_TEMPLATE/documentation.yml
+++ b/.github/ISSUE_TEMPLATE/documentation.yml
@@ -28,7 +28,7 @@ body:
attributes:
label: Documentation location
description: Public documentation URL or repository path.
- placeholder: "https://opencodex.me/providers/ or docs/providers.md"
+ placeholder: "docs-site/src/content/docs/guides/providers.md"
validations:
required: true
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
index 5ac7b510c3..3215665552 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -1,18 +1,18 @@
name: Feature proposal
-description: Propose a new capability or workflow improvement for OpenCodex.
+description: Propose a new capability or workflow improvement for CodexCommander.
labels:
- enhancement
body:
- type: markdown
attributes:
value: |
- Describe the workflow you need and what OpenCodex should do. Concrete examples help us evaluate and implement your proposal faster.
+ Describe the workflow you need and what CodexCommander should do. Concrete examples help us evaluate and implement your proposal faster.
- type: dropdown
id: area
attributes:
label: Area
- description: Which part of OpenCodex does this proposal affect?
+ description: Which part of CodexCommander does this proposal affect?
options:
- CLI
- Proxy and routing
@@ -52,7 +52,7 @@ body:
- type: textarea
id: behaviour
attributes:
- label: What should OpenCodex do?
+ label: What should CodexCommander do?
description: Describe the observable behaviour you expect, not internal implementation details.
placeholder: When I send ... the proxy should ...
validations:
@@ -66,7 +66,7 @@ body:
Provide at least one concrete example: a CLI command, configuration fragment, API exchange, UI workflow, or before/after comparison.
placeholder: |
```bash
- ocx config set routing.fallback_provider anthropic
+ ccx config set routing.fallback_provider anthropic
```
validations:
required: true
@@ -90,7 +90,7 @@ body:
options:
- label: I searched existing issues and documentation.
required: true
- - label: This request describes a concrete OpenCodex workflow rather than merely naming a desired technology.
+ - label: This request describes a concrete CodexCommander workflow rather than merely naming a desired technology.
required: true
- label: I removed secrets and personal data.
required: true
diff --git a/.github/ISSUE_TEMPLATE/provider_compatibility.yml b/.github/ISSUE_TEMPLATE/provider_compatibility.yml
index ab02cf3791..d071abfd08 100644
--- a/.github/ISSUE_TEMPLATE/provider_compatibility.yml
+++ b/.github/ISSUE_TEMPLATE/provider_compatibility.yml
@@ -7,7 +7,7 @@ body:
- type: markdown
attributes:
value: |
- Use this form when a provider endpoint, request format, response format, or client integration does not work correctly through the OpenCodex proxy.
+ Use this form when a provider endpoint, request format, response format, or client integration does not work correctly through the CodexCommander proxy.
- type: dropdown
id: client
@@ -37,9 +37,9 @@ body:
- type: input
id: version
attributes:
- label: OpenCodex version
- description: Installed `@bitkyc08/opencodex` version or commit SHA.
- placeholder: "2.7.31"
+ label: CodexCommander version
+ description: Installed `codexcommander` version or commit SHA.
+ placeholder: "0.1.0"
validations:
required: true
diff --git a/.github/release.yml b/.github/release.yml
deleted file mode 100644
index 7ecd38fb37..0000000000
--- a/.github/release.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-changelog:
- exclude:
- labels:
- - skip-changelog
- categories:
- - title: New Features
- labels:
- - enhancement
- - title: Bug Fixes
- labels:
- - bug
- - title: Documentation
- labels:
- - documentation
- - title: Chores
- labels:
- - chore
- - title: Other Changes
- labels:
- - "*"
diff --git a/.github/scripts/copilot-workflows.test.cjs b/.github/scripts/copilot-workflows.test.cjs
deleted file mode 100644
index 607911542d..0000000000
--- a/.github/scripts/copilot-workflows.test.cjs
+++ /dev/null
@@ -1,82 +0,0 @@
-const test = require('node:test');
-const assert = require('node:assert/strict');
-const fs = require('node:fs');
-const path = require('node:path');
-
-const ROOT = path.resolve(__dirname, '..', '..');
-const AI_ACTION = 'actions/ai-inference@2c43c91ae16266ca159d311430343c67a5ffa222';
-const CLI_INSTALL = 'npm install --global @github/copilot@1.0.74';
-const SETUP_NODE = 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e';
-
-function readWorkflow(name) {
- return fs.readFileSync(path.join(ROOT, '.github', 'workflows', name), 'utf8');
-}
-
-function count(text, fragment) {
- return text.split(fragment).length - 1;
-}
-
-test('issue automation uses pinned Copilot inference without tool access', () => {
- const quality = readWorkflow('enforce-issue-quality.yml');
- const triage = readWorkflow('issue-triage.yml');
- const combined = quality + '\n' + triage;
-
- assert.equal(count(quality, AI_ACTION), 2);
- assert.equal(count(triage, AI_ACTION), 1);
- assert.equal(count(quality, SETUP_NODE), 2);
- assert.equal(count(triage, SETUP_NODE), 1);
- assert.equal(count(quality, CLI_INSTALL), 2);
- assert.equal(count(triage, CLI_INSTALL), 1);
- assert.equal(count(quality, 'copilot-requests: write'), 2);
- assert.equal(count(triage, 'copilot-requests: write'), 1);
- assert.equal(count(quality, 'GITHUB_TOKEN: ${{ github.token }}'), 2);
- assert.equal(count(triage, 'GITHUB_TOKEN: ${{ github.token }}'), 1);
- assert.equal(count(quality, 'model: ""'), 2);
- assert.equal(count(triage, 'model: ""'), 1);
-
- assert.doesNotMatch(combined, /\bmodels:\s*read\b/);
- assert.doesNotMatch(combined, /max-tokens:/);
- assert.doesNotMatch(combined, /copilot-allow-tools:/);
- assert.doesNotMatch(combined, /--allow-tool/);
- assert.doesNotMatch(combined, /GitHub Models/);
-});
-
-test('Copilot failures leave issue enforcement and triage retryable', () => {
- const quality = readWorkflow('enforce-issue-quality.yml');
- const triage = readWorkflow('issue-triage.yml');
-
- assert.equal(count(quality, 'continue-on-error: true'), 6);
- assert.equal(count(triage, 'continue-on-error: true'), 3);
-
- assert.equal(
- count(
- quality,
- "if: steps.prepare.outputs.should_translate == 'true' && steps.copilot.outcome == 'success'",
- ),
- 2,
- );
- assert.equal(
- count(
- quality,
- "if: steps.prepare.outputs.should_translate == 'true' && steps.ai.outcome == 'success'",
- ),
- 2,
- );
- assert.equal(count(quality, "steps.ai.outcome == 'success' &&"), 2);
- assert.equal(count(quality, "steps.parse.outcome == 'success' &&"), 2);
- assert.match(quality, /leaving the issue unchanged and retryable/);
- assert.match(quality, /leaving the comment unchanged and retryable/);
-
- assert.equal(
- count(quality, "if: steps.prepare.outputs.should_translate == 'true' && steps.node.outcome == 'success'"),
- 2,
- );
- assert.match(triage, /if: steps\.node\.outcome == 'success'/);
- assert.match(triage, /if: steps\.copilot\.outcome == 'success'/);
- assert.match(triage, /if: steps\.infer\.outcome == 'success'/);
- assert.match(triage, /skipping duplicate suggestions for this issue/);
-
- // The deterministic quality gate must still run when translation fails.
- assert.match(quality, /needs: translate/);
- assert.match(quality, /always\(\) &&\n\s+needs\.translate\.result != 'cancelled'/);
-});
diff --git a/.github/scripts/enforce-pr-target.test.cjs b/.github/scripts/enforce-pr-target.test.cjs
deleted file mode 100644
index 5fc9455a7c..0000000000
--- a/.github/scripts/enforce-pr-target.test.cjs
+++ /dev/null
@@ -1,185 +0,0 @@
-"use strict";
-
-const fs = require("node:fs");
-const path = require("node:path");
-const { describe, it } = require("node:test");
-const assert = require("node:assert/strict");
-
-describe("enforce-pr-target workflow", () => {
- const workflowPath = path.join(__dirname, "../workflows/enforce-pr-target.yml");
- const workflow = fs.readFileSync(workflowPath, "utf8");
-
- it("uses pull_request_target without checking out PR head code", () => {
- assert.match(workflow, /pull_request_target:/);
- assert.doesNotMatch(
- workflow,
- /ref:\s*\$\{\{\s*github\.event\.pull_request\.head/,
- "enforcer must not check out untrusted PR head code",
- );
- });
-
- it("grants contents:write so draft GraphQL mutations work with GITHUB_TOKEN", () => {
- // convertPullRequestToDraft / markPullRequestReadyForReview fail with
- // "Resource not accessible by integration" when contents stays unset/read
- // (seen on #626). Assert the real permissions block, not comment text
- // that also mentions these scopes.
- const permissionsBlock = workflow.match(/^permissions:\n((?:[ \t]+.+\n)+)/m);
- assert.ok(permissionsBlock, "workflow must declare a top-level permissions block");
- const lines = permissionsBlock[1]
- .split("\n")
- .map((line) => line.trim())
- .filter(Boolean)
- .sort();
- assert.deepEqual(lines, ["contents: write", "pull-requests: write"]);
- });
-
- it("fails the required check on a wrong base even if draft conversion fails", () => {
- assert.match(workflow, /core\.setFailed\(/);
- assert.match(workflow, /draftConversionFailed/);
- assert.match(workflow, /Could not convert pull request to draft/);
- });
-
- it("soft-fails ready-for-review restoration the same way", () => {
- assert.match(workflow, /readyConversionFailed/);
- assert.match(workflow, /Could not mark pull request ready for review/);
- });
-
- it("listens for synchronize so rebase can clear ancestry failures", () => {
- assert.match(workflow, /synchronize/);
- });
-
- it("re-runs on issue_comment so a maintainer GUI waiver takes effect", () => {
- // The GUI-screenshot gate is waived by a maintainer issue comment
- // ("not touching gui"). `pull_request_target` types do not include issue
- // comments, so without this trigger the waiver sits unread until a PR
- // edit or push re-runs the gate.
- assert.match(workflow, /^ issue_comment:/m);
- assert.match(workflow, /- created/);
- assert.match(workflow, /- edited/);
- // The script resolves the PR number from the issue payload, which is what
- // an issue_comment event delivers instead of a pull_request object.
- assert.match(workflow, /context\.payload\.issue\?\.number/);
- });
-
- it("does not add review events that would break the trusted-base model", () => {
- // `pull_request_review` / `pull_request_review_comment` load the workflow
- // from the PR head branch (like `pull_request`), while this workflow's
- // checkout pins the base SHA — head YAML + base scripts mismatch, so the
- // gate crashes (`parseGateState is not a function`) and the head controls
- // the workflow definition under a write token. The findings claim runs on
- // every `pull_request_target` event instead (opened/edited/synchronize/
- // ready_for_review).
- assert.doesNotMatch(workflow, /^ pull_request_review:/m);
- assert.doesNotMatch(workflow, /^ pull_request_review_comment:/m);
- });
-
- it("queries review threads and feeds them to the findings claim check", () => {
- // Paginated read: `after: $cursor` + `pageInfo.hasNextPage`, so a busy PR
- // with more than 100 threads cannot hide unresolved bot threads (fail-open
- // gap in a fail-closed check).
- assert.match(workflow, /reviewThreads\(first: 100, after: \$cursor\)/);
- assert.match(workflow, /hasNextPage/);
- assert.match(workflow, /unresolvedFindingsClaim/);
- assert.match(workflow, /findingsClaim\.byBot/);
- assert.match(workflow, /review_findings/);
- });
-
- it("fails closed when review threads cannot be read", () => {
- assert.match(workflow, /findingsUnverifiable/);
- assert.match(workflow, /findings claim could not be verified/);
- });
-
- it("writes exactly one consolidated comment via a single upsert helper", () => {
- assert.match(workflow, /GATE_MARKER,/);
- assert.match(workflow, /comment\.body\?\.includes\(GATE_MARKER\)/);
- assert.match(workflow, /upsertGateComment/);
- assert.match(workflow, /buildGateCommentBody/);
- // No legacy two-comment write path remains.
- assert.doesNotMatch(workflow, /upsertReadinessComment/);
- assert.doesNotMatch(workflow, /buildReadinessCommentBody/);
- // No intermediate checkpoint comment writes.
- assert.doesNotMatch(workflow, /Draft conversion pending/);
- assert.doesNotMatch(workflow, /Recording ownership state/);
- });
-
- it("manages the review-ready status label at the ready moment", () => {
- assert.match(workflow, /REVIEW_READY_LABEL\s*=\s*"review-ready"/);
- assert.match(workflow, /github\.rest\.issues\.addLabels/);
- assert.match(workflow, /github\.rest\.issues\.removeLabel/);
- assert.match(workflow, /reviewReadyDesired/);
- });
-
- it("keeps CodeRabbit auto-review unfiltered so maintainer PRs are not starved", () => {
- // A positive `labels:` filter under `reviews.auto_review` in
- // `.coderabbit.yaml` would restrict ALL automatic reviews to PRs carrying
- // that label. Maintainer PRs never carry `review-ready` (no checklist), so
- // such a filter would silently stop CodeRabbit from reviewing maintainer
- // PRs. The label is a status marker only; assert the reviewer config
- // directly, since the workflow never writes a labels block.
- const coderabbit = fs.readFileSync(
- path.join(__dirname, "../../.coderabbit.yaml"),
- "utf8",
- );
- const autoReview = coderabbit.match(/auto_review:[\s\S]*?(?=\n\S|\n\s{2}\S)/);
- assert.ok(autoReview, ".coderabbit.yaml must declare auto_review");
- assert.doesNotMatch(autoReview[0], /labels:/);
- });
-
- it("migrates legacy two-comment PRs and deletes the old comments", () => {
- assert.match(workflow, /migrateLegacyCommentsIfNeeded/);
- assert.match(workflow, /migrateLegacyGateState/);
- assert.match(workflow, /github\.rest\.issues\.deleteComment/);
- assert.match(workflow, /legacyEnforcerComment/);
- assert.match(workflow, /legacyReadinessComment/);
- });
-
- it("checks out trusted base-branch scripts only (never PR head)", () => {
- // Scope the assertions to the checkout step itself, so a stray `ref:` on
- // another step cannot satisfy the pin while the checkout stays mutable.
- const checkoutStep = workflow
- .split("- name: Checkout trusted PR-quality scripts")[1]
- .split(/\n {6}- name:/)[0];
- assert.match(checkoutStep, /actions\/checkout@[0-9a-f]{40}/);
- // `pull_request_target` pins the PR's base SHA so the scripts match the
- // event's base revision. An `issue_comment` event has no PR payload, so
- // the ref falls back to the integration branch `dev` (the gate's only
- // allowed base) — still trusted, and never the PR head.
- assert.match(
- checkoutStep,
- /ref:\s*\$\{\{\s*github\.event\.pull_request\.base\.sha\s*\|\|\s*'dev'\s*\}\}/,
- );
- // The readiness ping reads MAINTAINERS.md from the same trusted checkout.
- assert.match(checkoutStep, /sparse-checkout:\s*\|\s*\n\s*\.github\/scripts\n\s*MAINTAINERS\.md/);
- assert.match(checkoutStep, /persist-credentials:\s*false/);
- assert.doesNotMatch(workflow, /ref:\s*\$\{\{\s*github\.event\.pull_request\.head/);
- });
-
- it("loads pr-quality via require from the checked-out scripts", () => {
- assert.match(workflow, /pr-quality\.cjs/);
- assert.match(workflow, /collectPrQualityFailures/);
- });
-
- it("checks stacked bases via open PR heads before wrong_base enforcement", () => {
- assert.match(workflow, /stackedBase/);
- assert.match(workflow, /github\.rest\.pulls\.list/);
- assert.match(workflow, /treating as stacked/);
- assert.match(workflow, /other\.base\?\.repo\?\.owner/);
- const qualityCall = workflow.match(
- /collectPrQualityFailures\(\{([\s\S]*?)\}\);/,
- );
- assert.ok(qualityCall, "must call collectPrQualityFailures");
- assert.match(qualityCall[1], /stackedBase/);
- });
-
- it("strips stale WRONG BRANCH prefix on failure when base is corrected", () => {
- const failureBlock = workflow.match(
- /if \(mustDraft\) \{([\s\S]*?)core\.setFailed\(/,
- );
- assert.ok(failureBlock, "workflow must have a draft path");
- const failurePath = failureBlock[1];
- assert.match(failurePath, /shouldStripTitlePrefix/);
- assert.match(failurePath, /!hasWrongBase/);
- assert.match(failurePath, /titlePrefixedByBot = false/);
- assert.match(failurePath, /pr\.title\.slice\(TITLE_PREFIX\.length\)/);
- });
-});
diff --git a/.github/scripts/issue-quality.cjs b/.github/scripts/issue-quality.cjs
deleted file mode 100644
index 1c0a6430a7..0000000000
--- a/.github/scripts/issue-quality.cjs
+++ /dev/null
@@ -1,1649 +0,0 @@
-"use strict";
-
-// ---------------------------------------------------------------------------
-// Pure issue-quality validation for OpenCodex.
-// CommonJS, zero runtime dependencies. No GitHub API calls.
-// ---------------------------------------------------------------------------
-
-/**
- * True when the entire meaningful value is a placeholder-only token.
- * Supports harmless Markdown emphasis/code markers and trailing punctuation.
- * Sentences that merely contain a placeholder phrase are not matches.
- */
-const PLACEHOLDER_ONLY_RE =
- /^[\s_*~`]*(?:no\s+response|n\/?a|not\s+applicable|not\s+available|none|todo|tbd)[\s_*~`]*[.!?]*$/i;
-
-/**
- * If `text` is exactly one enclosing fenced code block (``` or ~~~), return the
- * inner body; otherwise null. Real multi-statement fences are left alone by
- * the placeholder matcher after unwrap.
- */
-function unwrapSingleEnclosingFence(text) {
- const trimmed = text.trim();
- const match = trimmed.match(/^(```|~~~)[^\n]*\r?\n([\s\S]*?)\r?\n\1[ \t]*$/);
- if (!match) return null;
- return match[2];
-}
-
-/**
- * Shared strip/trim/unwrap used by placeholder and unusable-stand-in matchers.
- * Returns null when the value is absent after normalisation.
- */
-function normalizeRawSectionValue(raw) {
- if (typeof raw !== "string") return null;
- let value = raw.replace(//g, "").trim();
- if (!value) return null;
-
- // A lone fenced block whose entire body is a stand-in is still a stand-in
- // (e.g. ```text\nN/A\n```), not a real example.
- const unwrapped = unwrapSingleEnclosingFence(value);
- if (unwrapped !== null) {
- value = unwrapped.trim();
- if (!value) return null;
- }
-
- return value;
-}
-
-function isPlaceholderOnlyValue(raw) {
- const value = normalizeRawSectionValue(raw);
- if (value === null) return false;
- return PLACEHOLDER_ONLY_RE.test(value);
-}
-
-/**
- * Strip image/media-only content from a markdown or HTML fragment so that a
- * section whose only content is a screenshot or media embed is treated as
- * empty by the validators.
- *
- * Handles:
- * - Markdown images: ``, ``
- * - HTML
and ... blocks
- * - Common media embeds (video/audio) when they are the only content
- *
- * Text mixed with media (for example a caption or repro steps around an
- * image) is preserved; only the media tokens themselves are removed.
- */
-function stripMediaTokens(text) {
- if (typeof text !== "string") return "";
- // Indented code lines render as literal code in GitHub Markdown. Protect
- // them first so neither the HTML nor the Markdown media stripper can
- // remove example syntax; restore the lines afterwards.
- const protectedText = protectIndentedCodeLines(text);
- const markdownStripped = stripMarkdownImages(stripHtmlMedia(protectedText.text));
- const referenceStripped = stripReferenceImages(markdownStripped);
- return restoreIndentedCodeLines(referenceStripped, protectedText.lines);
-}
-
-/**
- * Replace every indented code line (4+ leading spaces or a tab) with a
- * placeholder of equal length so media stripping cannot touch it. Returns the
- * masked text plus the original lines for restoration.
- */
-function protectIndentedCodeLines(text) {
- const lines = [];
- const masked = text.split("\n").map((line) => {
- if (/^(?: {4,}|\t)/.test(line)) {
- lines.push(line);
- return "\u0000" + line.replace(/[^\n]/g, " ").slice(1);
- }
- lines.push(null);
- return line;
- });
- return { text: masked.join("\n"), lines };
-}
-
-/**
- * Restore masked indented-code lines from their original content. Placeholder
- * lines are identified by the leading \u0000 marker and matched positionally.
- */
-function restoreIndentedCodeLines(text, lines) {
- const out = text.split("\n").map((line, i) => {
- if (lines[i] !== null && line.startsWith("\u0000")) {
- return lines[i];
- }
- return line;
- });
- return out.join("\n");
-}
-
-/**
- * Strip HTML media blocks whose entire inner content is media markup (no
- * substantive text). A block that contains fallback/caption prose — for
- * example ``
- * — is left untouched so the prose survives the empty-section check.
- *
- * Handles
, ..., , and
- * .
- */
-function stripHtmlMedia(text) {
- if (typeof text !== "string") return "";
- let s = text
- .replace(/
]*>/gi, " ")
- .replace(//g, " ");
-
- // Whole media blocks: replace only when the inner content is not
- // substantive text (no word characters outside tags).
- s = s.replace(
- /<(picture|video|audio)\b[^>]*>([\s\S]*?)<\/\1>/gi,
- (match, tag, inner) => {
- const innerStripped = inner
- .replace(/<[^>]+>/g, " ")
- .replace(/[\s_*~`]+/g, " ")
- .trim();
- return innerStripped.length === 0 ? " " : match;
- },
- );
- return s;
-}
-
-/**
- * Remove Markdown image tokens `` using a small
- * balanced scanner instead of a regex, because destinations may contain
- * balanced parentheses (for example `image_(final).png`) and alt text may
- * contain balanced brackets (`![Image [screenshot]](url)`).
- *
- * A token is matched only when:
- * - it starts with `![` (not escaped);
- * - the alt text is balanced with respect to `[` / `]`;
- * - the destination is balanced with respect to `(`, `)` and `"` (an
- * optional title may follow); and
- * - the token closes with a `)`.
- *
- * Malformed tokens (unbalanced destination, e.g. `a)b.png)`) are left in
- * place — they are not valid Markdown images and must not be silently
- * dropped.
- */
-function stripMarkdownImages(text) {
- if (typeof text !== "string") return "";
- const out = [];
- let i = 0;
- while (i < text.length) {
- // Inside an indented code block (4+ leading spaces or a tab), image
- // syntax is literal code, not a rendered image. Leave it untouched so a
- // section that documents example syntax is not emptied.
- if (isInsideIndentedCode(text, i)) {
- out.push(text[i]);
- i += 1;
- continue;
- }
- // A backslash-escaped or code-fenced `![` is not an image token. We only
- // guard the common `\!` escape here; fenced blocks are handled by the
- // section extractor upstream, which does not include them in sections.
- if (text[i] === "!" && text[i + 1] === "[") {
- const end = scanMarkdownImage(text, i);
- if (end !== -1) {
- out.push(" ");
- i = end;
- continue;
- }
- }
- out.push(text[i]);
- i += 1;
- }
- return out.join("");
-}
-
-/**
- * True when `index` sits inside an indented code block, i.e. on a line that
- * starts with four or more spaces or a tab. Such lines render as literal
- * code in GitHub Markdown.
- */
-function isInsideIndentedCode(text, index) {
- const lineStart = text.lastIndexOf("\n", index - 1) + 1;
- const prefix = text.slice(lineStart, index);
- return /^(?: {4,}|\t)/.test(prefix);
-}
-
-/**
- * Strip reference-style Markdown images: inline references `![alt][ref]`
- * and the reference definitions `[ref]: https://...` they point at. These
- * are valid image syntax that a media-only section may use to embed a
- * screenshot.
- */
-function stripReferenceImages(text) {
- if (typeof text !== "string") return "";
- // Inline reference: ![alt][ref] or ![alt][] (implicit). Alt may contain
- // balanced brackets, so a balanced scan is used for the label part.
- let s = stripInlineReferences(text);
- // Reference definitions: [ref]: url "title" — only when the reference is
- // actually used by an image in the same text. A definition alone (or one
- // used by a text link) is not media and must stay.
- const refs = new Set();
- for (const ref of collectInlineReferenceLabels(text)) {
- refs.add(ref.toLowerCase());
- }
- if (refs.size > 0) {
- s = s.replace(
- /^\s*\[([^\]]+)\]:\s*\S+(?:\s+["'(][^"')]*["')])?\s*$/gm,
- (line, ref) => (refs.has(ref.toLowerCase()) ? " " : line),
- );
- }
- return s;
-}
-
-/**
- * Strip inline reference-style image tokens `![alt][ref]` / `![alt][]`
- * using a balanced scan for the alt text (which may contain nested brackets).
- */
-function stripInlineReferences(text) {
- const out = [];
- let i = 0;
- while (i < text.length) {
- if (text[i] === "!" && text[i + 1] === "[") {
- const end = scanReferenceImage(text, i);
- if (end !== -1) {
- out.push(" ");
- i = end;
- continue;
- }
- }
- out.push(text[i]);
- i += 1;
- }
- return out.join("");
-}
-
-/**
- * Scan an inline reference-style image `![alt][ref]` or `![alt][]` starting
- * at `start`. Returns the index just past the closing `]` on success, or -1.
- */
-function scanReferenceImage(text, start) {
- const altEnd = scanBalancedBrackets(text, start + 2);
- if (altEnd === -1 || text[altEnd] !== "]") return -1;
- if (text[altEnd + 1] !== "[") return -1;
- const refEnd = scanBalancedBrackets(text, altEnd + 2);
- if (refEnd === -1 || text[refEnd] !== "]") return -1;
- return refEnd + 1;
-}
-
-/**
- * Scan balanced bracket content starting at `start` (inside the opening `[`).
- * Returns the index of the matching closing `]`, or -1 when unbalanced.
- */
-function scanBalancedBrackets(text, start) {
- let depth = 0;
- for (let i = start; i < text.length; i += 1) {
- const ch = text[i];
- if (ch === "\\") {
- i += 1;
- continue;
- }
- if (ch === "[") {
- depth += 1;
- } else if (ch === "]") {
- if (depth === 0) return i;
- depth -= 1;
- }
- }
- return -1;
-}
-
-/**
- * Collect the reference labels used by inline reference-style images. For an
- * explicit `![alt][ref]` the label is `ref`; for an implicit `![alt][]` the
- * label is the alt text.
- */
-function collectInlineReferenceLabels(text) {
- const labels = [];
- let i = 0;
- while (i < text.length) {
- if (text[i] === "!" && text[i + 1] === "[") {
- const altStart = i + 2;
- const altEnd = scanBalancedBrackets(text, altStart);
- if (altEnd !== -1 && text[altEnd] === "]") {
- const alt = text.slice(altStart, altEnd);
- if (text[altEnd + 1] === "[") {
- const refStart = altEnd + 2;
- const refEnd = scanBalancedBrackets(text, refStart);
- if (refEnd !== -1 && text[refEnd] === "]") {
- const ref = text.slice(refStart, refEnd);
- labels.push(ref ? ref : alt);
- i = refEnd + 1;
- continue;
- }
- }
- }
- }
- i += 1;
- }
- return labels;
-}
-
-/**
- * Scan a Markdown image token starting at `start` (which points at `!`).
- * Returns the index just past the closing `)` on success, or -1 when the
- * token is malformed.
- */
-function scanMarkdownImage(text, start) {
- // Alt text: `![` ... `]` with balanced nested brackets.
- let i = start + 2;
- let bracketDepth = 0;
- for (; i < text.length; i += 1) {
- const ch = text[i];
- if (ch === "\\") {
- i += 1; // skip escaped character
- continue;
- }
- if (ch === "[") {
- bracketDepth += 1;
- } else if (ch === "]") {
- if (bracketDepth === 0) break;
- bracketDepth -= 1;
- }
- }
- if (i >= text.length || text[i] !== "]") return -1;
-
- // Destination: `(` ... `)` with balanced parentheses. An optional
- // whitespace-separated `"title"` may follow the destination.
- if (text[i + 1] !== "(") return -1;
- i += 2;
- let parenDepth = 1;
- let inQuotes = false;
- for (; i < text.length; i += 1) {
- const ch = text[i];
- if (ch === "\\") {
- i += 1; // skip escaped character
- continue;
- }
- if (ch === '"') {
- inQuotes = !inQuotes;
- continue;
- }
- if (inQuotes) continue;
- if (ch === "(") {
- parenDepth += 1;
- } else if (ch === ")") {
- parenDepth -= 1;
- if (parenDepth === 0) return i + 1;
- }
- }
- return -1;
-}
-
-/**
- * True when a section contains no substantive text after removing media
- * tokens and whitespace. Used to decide whether a media-only section should
- * count as empty for quality validation.
- */
-function isMediaOnly(text) {
- if (typeof text !== "string") return false;
- const stripped = stripMediaTokens(text);
- return stripped.replace(/\s+/g, "").length === 0;
-}
-
-/**
- * Strip HTML comments, placeholder-only values, and trim whitespace.
- */
-function clean(raw) {
- if (typeof raw !== "string") return "";
- let s = raw.replace(//g, "");
- // Media-only sections (a lone screenshot or embed) carry no reportable
- // text. Strip the media tokens so the section participates in emptiness and
- // duplicate detection like any other blank section. This closes the
- // image-only-section bypass (see #1098: an `
`-only goal hid repeated
- // prose in the other sections from duplicate detection).
- if (isMediaOnly(s)) {
- s = stripMediaTokens(s).replace(/\s+/g, " ").trim();
- }
- // Whole-value placeholders first (including a single enclosing fence), so
- // line-by-line stripping cannot leave bare fence markers behind.
- if (isPlaceholderOnlyValue(s)) return "";
- // Treat placeholder-only lines (GitHub "No response", N/A, etc.) as empty.
- s = s
- .split("\n")
- .map((line) => (isPlaceholderOnlyValue(line) ? "" : line))
- .join("\n");
- if (isPlaceholderOnlyValue(s)) return "";
- return s.trim();
-}
-
-/**
- * Lowercase, strip punctuation (Unicode-aware), collapse whitespace.
- */
-function normalise(raw) {
- return clean(raw)
- .toLowerCase()
- .replace(/[^\p{L}\p{N}\s]/gu, "")
- .replace(/\s+/g, " ")
- .trim();
-}
-
-/**
- * Canonical form for duplicate detection: normalise + strip common filler
- * phrases that do not add semantic content.
- */
-function canonicalise(raw) {
- let s = normalise(raw);
- const fillers = [
- /^i want to\s+/,
- /^we need to\s+/,
- /^would like to\s+/,
- /^i would like to\s+/,
- /^we would like to\s+/,
- /^please\s+/,
- ];
- for (const re of fillers) s = s.replace(re, "");
- return s.trim();
-}
-
-/**
- * Extract the text content of a markdown ### section by heading name.
- * Returns null when the heading is absent.
- */
-function extractSection(body, heading) {
- if (typeof body !== "string") return null;
- const lines = body.split("\n");
- const headingLower = heading.toLowerCase().trim();
- let capturing = false;
- let sectionDepth = 0;
- let fence = null;
- const out = [];
- for (const line of lines) {
- if (fence) {
- if (new RegExp(`^[ \\t]{0,3}${fence.marker}{${fence.length},}[ \\t]*$`).test(line)) {
- fence = null;
- }
- if (capturing) out.push(line);
- continue;
- }
-
- const fenceMatch = line.match(/^[ \t]{0,3}(`{3,}|~{3,})/);
- if (fenceMatch) {
- fence = { marker: fenceMatch[1][0], length: fenceMatch[1].length };
- if (capturing) out.push(line);
- continue;
- }
-
- const m = line.match(/^(#{2,4})\s+(.*)/);
- if (m) {
- const depth = m[1].length;
- if (capturing && depth <= sectionDepth) break;
- if (!capturing && m[2].toLowerCase().trim() === headingLower) {
- capturing = true;
- sectionDepth = depth;
- continue;
- }
- }
- if (capturing) out.push(line);
- }
- if (!capturing) return null;
- return out.join("\n").trim();
-}
-
-/**
- * Resolve a logical section from the first matching heading.
- * Prefers the first non-empty match; if every present heading is empty,
- * returns that empty string so callers can distinguish "missing" (null)
- * from "present but blank".
- */
-function resolveSection(body, headings) {
- let firstPresent = null;
- for (const heading of headings) {
- const section = extractSection(body, heading);
- if (section === null) continue;
- if (firstPresent === null) firstPresent = section;
- if (!isEmpty(section)) return section;
- }
- return firstPresent;
-}
-
-/**
- * True when the body has multiple non-empty h2–h4 sections with enough detail.
- * Soft-pass only — unstructured length alone is not enough, and a single
- * arbitrary heading must not bypass the quality gate (Codex on #564).
- */
-function hasSubstantialStructuredContent(body, minSectionLen = 40, minRichSections = 2) {
- if (typeof body !== "string") return false;
- const lines = body.split("\n");
- let capturing = false;
- let bucket = [];
- let richSections = 0;
- const flush = () => {
- if (clean(bucket.join("\n")).length >= minSectionLen) richSections += 1;
- bucket = [];
- };
- for (const line of lines) {
- const m = line.match(/^#{2,4}\s+(.*)/);
- if (m) {
- if (capturing) flush();
- capturing = true;
- continue;
- }
- if (capturing) bucket.push(line);
- }
- if (capturing) flush();
- return richSections >= minRichSections;
-}
-
-// ---------------------------------------------------------------------------
-// Issue kind detection
-// ---------------------------------------------------------------------------
-
-const FEATURE_NEW_HEADINGS = [
- "What are you trying to accomplish?",
- "What prevents this today?",
- "What should OpenCodex do?",
-];
-const FEATURE_LEGACY_HEADINGS = ["Problem to solve", "Proposed solution"];
-const FEATURE_GOAL_HEADINGS = [
- "What are you trying to accomplish?",
- "Goal / Problem",
- "Goal/Problem",
- "Problem to solve",
-];
-const FEATURE_BLOCKER_HEADINGS = [
- "What prevents this today?",
- "Current limitation",
- "Current workaround",
-];
-const FEATURE_BEHAVIOUR_HEADINGS = [
- "What should OpenCodex do?",
- "Expected behaviour",
- "Expected behavior",
- "Proposed solution",
-];
-const FEATURE_EXAMPLE_HEADINGS = [
- "Example usage or interface",
- "Example usage",
- "Example",
-];
-const FEATURE_ALIAS_DETECT_HEADINGS = [
- "Goal / Problem",
- "Goal/Problem",
- "Expected behaviour",
- "Expected behavior",
- "Current limitation",
- "Current workaround",
- "Example usage",
- // Intentionally omit bare "Example" — too common in freeform/bug reports.
-];
-const BUG_NEW_HEADINGS = ["Client or integration", "Summary", "Reproduction"];
-const BUG_LEGACY_HEADINGS = ["Summary", "Reproduction"];
-const PROVIDER_HEADINGS = [
- "Provider or upstream service",
- "Endpoint or capability",
- "Current behaviour",
- "Expected behaviour",
-];
-const DOCS_HEADINGS = [
- "Documentation problem type",
- "Documentation location",
- "What is wrong or missing?",
-];
-
-const KIND_TO_LABEL = {
- bug: "bug",
- feature: "enhancement",
- documentation: "documentation",
- "provider-compatibility": "provider-compatibility",
-};
-
-/**
- * Orthogonal product-area labels (additive beside kind/process labels).
- * Colors/descriptions are used when the workflow ensures labels exist.
- */
-const AREA_LABELS = {
- provider: {
- color: "1D76DB",
- description: "Provider adapters, OpenAI-compat presets, upstream API quirks",
- },
- "account-pool": {
- color: "5319E7",
- description: "OAuth, credentials, Codex pool, quota, failover, plans",
- },
- catalog: {
- color: "006B75",
- description: "Model catalog, slugs, visibility, routed entries",
- },
- gui: {
- color: "D93F0B",
- description: "Dashboard, tray, settings UI",
- },
- cli: {
- color: "FBCA04",
- description: "CLI, config inject, packaging flags",
- },
- proxy: {
- color: "0E8A16",
- description: "HTTP proxy, routing, reverse-proxy / management auth",
- },
- platform: {
- color: "BFDADC",
- description: "OS/service/tray/ACL (Windows-heavy, not Windows-only)",
- },
- streaming: {
- color: "C5DEF5",
- description: "SSE, WebSocket, terminal stream frames",
- },
- tools: {
- color: "F9D0C4",
- description: "tool_calls, MCP, web-search / sidecar tools",
- },
- install: {
- color: "EDEDED",
- description: "Installation or packaging",
- },
- service: {
- color: "EDEDED",
- description: "Service lifecycle (WinSW/launchd/scheduler)",
- },
-};
-
-/** Canonical Area dropdown text → area label(s). Keys are lowercased. */
-const AREA_FIELD_TO_LABELS = {
- cli: ["cli"],
- "proxy and routing": ["proxy"],
- dashboard: ["gui"],
- "provider adapter": ["provider"],
- "provider adapters": ["provider"],
- "authentication and account pool": ["account-pool"],
- "catalog / models": ["catalog"],
- streaming: ["streaming"],
- "tools / mcp / web search": ["tools"],
- "installation or packaging": ["install"],
- "service lifecycle": ["service"],
- "service lifecycle (config injection)": ["service"],
- "platform (windows / macos / linux)": ["platform"],
- // Do not map to kind label `documentation` — that collides with labelBasedKind
- // when a feature/bug form picks Area: Documentation. Docs form already seeds
- // the kind label; Area selection alone does not add an area tag.
- documentation: [],
- // No dedicated label; heuristics still run in detectAreaLabels.
- "multiple areas": [],
- other: [],
-};
-
-/** Body headings used for area heuristics (excludes Environment / OS metadata). */
-const AREA_HEURISTIC_BODY_HEADINGS = [
- "Summary",
- "Reproduction",
- "What are you trying to accomplish?",
- "What prevents this today?",
- "What should OpenCodex do?",
- "Example usage or interface",
- "Current behaviour",
- "Expected behaviour",
- "Minimal redacted request or reproduction",
- "What is wrong or missing?",
- "Documentation problem type",
- "Documentation location",
-];
-
-/**
- * Heuristic rules. `scope: "title"` avoids false hits from template Environment /
- * OS fields in the body; `scope: "full"` is for distinctive technical tokens.
- */
-const AREA_HEURISTICS = [
- {
- label: "account-pool",
- scope: "full",
- re: /\b(oauth|reauth|needsreauth|account pool|codex.?auth|auto[- ]?switch|account failover|refresh token|plan_type|chatgpt[- ]account|reset credit)\b/i,
- },
- {
- label: "account-pool",
- scope: "title",
- re: /\b(quota|failover|pool account|account switch)\b/i,
- },
- {
- label: "catalog",
- scope: "full",
- re: /\b(model catalog|opencodex-catalog|model list|model visibility|virtual model|routed (catalog|entries|slug)|model slug)\b/i,
- },
- {
- label: "catalog",
- scope: "title",
- re: /\bcatalog\b/i,
- },
- {
- label: "gui",
- scope: "title",
- re: /\b(dashboard|\bgui\b|tray|sidebar|settings (page|tab|ui))\b/i,
- },
- {
- label: "cli",
- scope: "title",
- re: /\b(ocx\b|config\.toml|config inject)\b/i,
- },
- {
- label: "proxy",
- scope: "full",
- re: /\b(reverse[- ]proxy|management api|admin[- ]token|\/api\/\*|bind(s)? the (old )?port)\b/i,
- },
- {
- label: "proxy",
- scope: "title",
- re: /\b(reverse[- ]proxy|management api|admin[- ]token)\b/i,
- },
- {
- label: "platform",
- scope: "full",
- re: /\b(winsw|launchd|schtasks|icacls|windows-latest|tray host|scheduler backend)\b/i,
- },
- {
- label: "platform",
- scope: "title",
- re: /\b(\[windows\]|\[macos\]|windows|macos|darwin|win32|wsl)\b/i,
- },
- {
- label: "streaming",
- scope: "full",
- re: /\b(sse|websocket|\bws\b|stream(ing)?\b.{0,40}\btruncat\w*|stream(ing)?\b.{0,40}\bterminal\b|terminal (sse )?frame|without a terminal)\b/i,
- },
- {
- label: "tools",
- scope: "full",
- re: /\b(tool_calls?|tool[- ]calls?|\bmcp\b|web[- ]search|tool[- ]recall)\b/i,
- },
- {
- label: "install",
- scope: "full",
- re: /\b(npm (global )?install|packaging|release asset|npx ocx)\b/i,
- },
- {
- label: "service",
- scope: "full",
- re: /\b(ocx service|winsw|scheduler backend|launchd service)\b/i,
- },
- {
- label: "provider",
- scope: "full",
- re: /\b(provider adapter|openai[- ]compatible|provider[- ]compat|adapter quirk|built[- ]in provider|provider preset)\b/i,
- },
- {
- label: "provider",
- scope: "title",
- re: /\b(\[provider\]|provider compat|openai[- ]compatible)\b/i,
- },
-];
-
-/**
- * Map a detected issue kind to its triage label. Returns null when unknown.
- */
-function labelForKind(kind) {
- if (!kind || typeof kind !== "string") return null;
- return KIND_TO_LABEL[kind] || null;
-}
-
-/**
- * Map a template Area dropdown value to orthogonal area label names.
- * Returns [] for Other / Multiple areas / unknown / empty.
- *
- * @param {unknown} areaText
- * @returns {string[]}
- */
-function mapAreaFieldToLabels(areaText) {
- if (typeof areaText !== "string") return [];
- const key = areaText.replace(/\s+/g, " ").trim().toLowerCase();
- if (!key) return [];
- return AREA_FIELD_TO_LABELS[key] ? [...AREA_FIELD_TO_LABELS[key]] : [];
-}
-
-/**
- * Build heuristic text from title-relevant semantic sections only — never from
- * Operating system / Version / Checks metadata that every template includes.
- *
- * @param {string} body
- * @returns {string}
- */
-function bodyForAreaHeuristics(body) {
- if (typeof body !== "string" || !body.trim()) return "";
- const parts = [];
- for (const heading of AREA_HEURISTIC_BODY_HEADINGS) {
- const section = extractSection(body, heading);
- if (section) parts.push(section);
- }
- return parts.join("\n\n");
-}
-
-/**
- * Conservative title/body heuristics for orthogonal area labels.
- *
- * @param {string} title
- * @param {string} body semantic body text (already filtered)
- * @returns {string[]}
- */
-function heuristicAreaLabels(title, body) {
- const titleText = title || "";
- const fullText = `${titleText}\n${body || ""}`;
- const seen = new Set();
- const out = [];
- for (const { label, re, scope } of AREA_HEURISTICS) {
- const text = scope === "title" ? titleText : fullText;
- if (!re.test(text) || seen.has(label)) continue;
- seen.add(label);
- out.push(label);
- }
- return out;
-}
-
-/**
- * Detect additive product-area labels from Area field, form defaults, and
- * title/body heuristics. Never invents per-provider labels.
- *
- * @param {{
- * title?: string,
- * body?: string,
- * labels?: string[],
- * heuristicBody?: string,
- * }} issue
- * `body` is the source form (for Area / provider headings).
- * `heuristicBody` may include English translation text for heuristics only.
- * @returns {string[]}
- */
-function detectAreaLabels(issue) {
- const title = typeof issue?.title === "string" ? issue.title : "";
- const body = typeof issue?.body === "string" ? issue.body : "";
- const labels = Array.isArray(issue?.labels) ? issue.labels : [];
- const heuristicSource = typeof issue?.heuristicBody === "string" ? issue.heuristicBody : body;
-
- const areaSection = extractSection(body, "Area");
- const fromArea = mapAreaFieldToLabels(areaSection);
- const fromHeur = heuristicAreaLabels(title, bodyForAreaHeuristics(heuristicSource));
- const fromForm = [];
- if (labels.includes("provider-compatibility")) fromForm.push("provider");
- // Provider-compat form uses this heading instead of Area.
- if (extractSection(body, "Provider or upstream service") !== null) {
- fromForm.push("provider");
- }
-
- const seen = new Set();
- const out = [];
- for (const label of [...fromArea, ...fromForm, ...fromHeur]) {
- if (!label || seen.has(label)) continue;
- if (!AREA_LABELS[label]) continue;
- seen.add(label);
- out.push(label);
- }
- return out;
-}
-
-function countHeadings(body, headings) {
- let n = 0;
- for (const h of headings) {
- if (extractSection(body, h) !== null) n++;
- }
- return n;
-}
-
-/**
- * Detect the issue kind from body headings, title prefix, labels, and
- * optional stored bot kind.
- *
- * @param {{ title: string, body: string, labels: string[], storedKind?: string|null }} issue
- * @returns {"feature"|"bug"|"provider-compatibility"|"documentation"|null}
- */
-function detectIssueKindFromContent(issue) {
- const { title = "", body = "", labels = [] } = issue;
- const titleLower = title.toLowerCase();
-
- // Provider compatibility: distinct headings.
- if (countHeadings(body, PROVIDER_HEADINGS) >= 3) return "provider-compatibility";
-
- // Documentation: distinct headings.
- if (countHeadings(body, DOCS_HEADINGS) >= 2) return "documentation";
-
- // New feature form: at least 2 of the 3 core headings.
- if (countHeadings(body, FEATURE_NEW_HEADINGS) >= 2) return "feature";
-
- // Translated / alternate feature headings (e.g. after issue-triage).
- // Require a feature-specific goal heading so common headings like
- // "Expected behaviour" cannot reclassify bug/freeform reports as features.
- // ([Feature]: prefix and enhancement labels are handled elsewhere.)
- if (
- countHeadings(body, FEATURE_ALIAS_DETECT_HEADINGS) >= 2 &&
- countHeadings(body, FEATURE_GOAL_HEADINGS) >= 1
- ) {
- return "feature";
- }
-
- // New bug form: Client or integration + Summary + Reproduction.
- if (
- extractSection(body, "Client or integration") !== null &&
- extractSection(body, "Summary") !== null &&
- extractSection(body, "Reproduction") !== null
- ) {
- return "bug";
- }
-
- // Legacy feature form: title prefix or old headings.
- if (titleLower.startsWith("[feature]:") || countHeadings(body, FEATURE_LEGACY_HEADINGS) >= 2) {
- return "feature";
- }
-
- // Legacy bug form: title prefix or old headings (Summary + Reproduction).
- if (titleLower.startsWith("[bug]:") || countHeadings(body, BUG_LEGACY_HEADINGS) >= 2) {
- // Only classify as bug when there is supporting evidence (label or prefix)
- // to avoid false positives on generic issues that happen to have those words.
- if (titleLower.startsWith("[bug]:") || labels.includes("bug")) return "bug";
- }
-
- return null;
-}
-
-/**
- * True when body evidence for `kind` is a full structured form, not merely a
- * title prefix or leftover label. Used to decide whether detected kind may
- * override a stored bot kind.
- */
-function hasStrongKindEvidence(kind, issue) {
- const { body = "" } = issue;
- switch (kind) {
- case "provider-compatibility":
- return countHeadings(body, PROVIDER_HEADINGS) >= 3;
- case "documentation":
- return countHeadings(body, DOCS_HEADINGS) >= 2;
- case "feature":
- return (
- countHeadings(body, FEATURE_NEW_HEADINGS) >= 2 ||
- countHeadings(body, FEATURE_LEGACY_HEADINGS) >= 2 ||
- (countHeadings(body, FEATURE_ALIAS_DETECT_HEADINGS) >= 2 &&
- countHeadings(body, FEATURE_GOAL_HEADINGS) >= 1)
- );
- case "bug":
- return (
- extractSection(body, "Client or integration") !== null &&
- extractSection(body, "Summary") !== null &&
- extractSection(body, "Reproduction") !== null
- );
- default:
- return false;
- }
-}
-
-/**
- * Detect the issue kind from body headings, title prefix, labels, and
- * optional stored bot kind.
- *
- * Stored kind survives heading removal (bypass protection). A different
- * detected kind overrides it only when the body has strong form evidence.
- *
- * @param {{ title: string, body: string, labels: string[], storedKind?: string|null }} issue
- * @returns {"feature"|"bug"|"provider-compatibility"|"documentation"|null}
- */
-function detectIssueKind(issue) {
- const { storedKind } = issue;
- const detected = detectIssueKindFromContent(issue);
-
- if (storedKind) {
- if (
- detected &&
- detected !== storedKind &&
- hasStrongKindEvidence(detected, issue)
- ) {
- return detected;
- }
- return storedKind;
- }
-
- return detected;
-}
-
-// ---------------------------------------------------------------------------
-// Validation
-// ---------------------------------------------------------------------------
-
-function isEmpty(text) {
- const c = clean(text);
- if (c.length === 0) return true;
- // Stand-ins like "...", "…", "---" are not actionable report content.
- return /^[\p{P}\p{S}\s]+$/u.test(c);
-}
-
-function allSameCanonical(sections) {
- const cans = sections.map(canonicalise).filter(Boolean);
- if (cans.length < 2) return false;
- return cans.every((c) => c === cans[0]);
-}
-
-function allRepeatTitle(sections, title) {
- const titleCan = canonicalise(title);
- if (!titleCan) return false;
- const cans = sections.map(canonicalise).filter(Boolean);
- if (cans.length === 0) return false;
- return cans.every((c) => c === titleCan);
-}
-
-function isPlaceholder(text) {
- return isPlaceholderOnlyValue(text);
-}
-
-/**
- * True when Version is an "I don't know" stand-in rather than an install id.
- * Kept separate from PLACEHOLDER_ONLY_RE so legacy N/A / No response soft-pass
- * behaviour is unchanged.
- */
-const UNUSABLE_VERSION_RE =
- /^[\s_*~`]*(?:unknown|unkown|uknown|don'?t\s+know|do\s+not\s+know|idk|dunno|not\s+sure|unsure|\?+|모름|잘\s*모름|모르겠(?:습니다|음)?|不明|わからない|分からない|不知道|不清楚|keine\s+ahnung|wei[sß]{1,2}\s+nicht)[\s_*~`]*[.!?]*$/i;
-
-function isUnusableVersion(raw) {
- const value = normalizeRawSectionValue(raw);
- if (value === null) return false;
- return UNUSABLE_VERSION_RE.test(value);
-}
-
-const CJK_RE =
- /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/gu;
-
-function countWords(text) {
- const c = clean(text);
- if (!c) return 0;
-
- // Count each CJK character as one unit, and non-CJK scripts as Unicode
- // word tokens. Mixing one CJK glyph into a Latin/Cyrillic word must not
- // inflate the count to letter-length.
- const cjkChars = c.match(CJK_RE) || [];
- const nonCjkText = c.replace(CJK_RE, " ");
- const nonCjkTokens = nonCjkText.match(/[\p{L}\p{N}']+/gu) || [];
-
- return cjkChars.length + nonCjkTokens.length;
-}
-
-function hasConcreteDetail(text) {
- const c = clean(text);
- if (!c) return false;
- return (
- /\d/.test(c) ||
- /[`{}\[\]<>/\\]/.test(c) ||
- /\b(ocx|config|api|cli|dashboard|provider|proxy|route|endpoint|workflow|command)\b/i.test(c)
- );
-}
-
-function isTooTerseFeatureSection(text) {
- if (isEmpty(text) || isPlaceholder(text)) return false;
- const words = countWords(text);
- if (words >= 8) return false;
- if (words >= 6 && hasConcreteDetail(text)) return false;
- return true;
-}
-
-/**
- * Bug Reproduction needs concrete signals that let a maintainer reproduce the
- * failure. Product keywords alone (e.g. "choose model deepseek" or "send a
- * message in the codex plugin") are not actionable: the report must name a
- * command, an error, a file/config path, or an exact observed output.
- */
-// Commands and exact technical actions, e.g. "ocx start", "run bun",
-// "send a streaming request", "curl https://...".
-const REPRO_COMMAND_RE = new RegExp([
- "\\b(?:run|start|stop|restart|install|launch|execute|reproduce|trigger|invoke)\\s+(?:(?:the|an|a)\\s+)?(?:ocx|bun|npm|pnpm|yarn|curl|node|codex|proxy|server|dashboard|plugin)\\b",
- "\\b(?:ocx|bun|npm|pnpm|yarn|curl|node|codex)\\s+(?:start|run|stop|restart|install|config|--[a-z-]+)\\b",
- "\\b(?:send|issue|make|post)\\s+(?:a|an|any)\\s+(?:streaming|api|http|json|completion|chat|config|auth|embedding|post|graphql|grpc)\\s+(?:request|call|command|prompt|query)\\b",
- "\\b(?:send|issue|make|post)\\s+(?:a|an|any)\\s+(?:api|curl|endpoint|url)\\b",
- "\\b(?:send|issue|make|post)\\s+(?:a|an|any)\\s+[\\w.-]+\\s+request\\s+to\\s+(?:the\\s+)?(?:endpoint|url|api|server|proxy|\\S+/\\S+)\\b",
- "\\b(?:pip|npm|bun)\\s+install\\b",
- "\\b(?:curl|wget)\\s+[^\\s]+",
-].join("|"), "i");
-
-// Error, exception, and failure tokens, plus status codes in status context
-// (bare 3-digit numbers can be ports or version numbers).
-const REPRO_FAILURE_RE = new RegExp([
- "\\b(?:segfault|sigsegv|panic|abort|exception|traceback|stack\\s*trace|timeout|timed\\s*out|refused|reset|denied|failed?|error|crash|hang|hangs?|stuck|spinning|empty\\s*response)\\b",
- "\\b(?:status\\s*(?:code\\s*)?|code\\s*|http\\s*)(?:is|of|:)?\\s*[1-5]\\d\\d\\b",
-].join("|"), "i");
-
-// File, config, and log paths such as ~/.codex/config.toml or C:\\logs\\ocx.log.
-const REPRO_PATH_RE = new RegExp([
- "~?/[\\w.@-]+(?:/[\\w.@-]+)+",
- "[A-Za-z]:\\\\(?:[\\w.@-]+\\\\)+[\\w.@-]+",
- "~?/[\\w.@-]+/[\\w.@-]+\\.(?:json|yaml|yml|toml|conf|log|env|txt|ts|js|tsx|jsx|sh|ps1|py)",
- "[\\w.@-]+\\.(?:json|yaml|yml|toml|conf|log|env)\\b",
-].join("|"));
-const ACTIONABLE_REPRO_RE = new RegExp(
- [REPRO_COMMAND_RE.source, REPRO_FAILURE_RE.source, REPRO_PATH_RE.source].join("|"),
- "i",
-);
-
-// Sigil-only fences with no body content are never actionable.
-const EMPTY_FENCE_RE = /^[ \t]{0,3}(?:```+|~~~+)\s*\n\s*\n[ \t]{0,3}(?:```+|~~~+)\s*$/;
-
-/**
- * True when a bug Reproduction names commands, error tokens, file/config
- * paths, or exact technical actions. Product/model mentions without any of
- * those signals (e.g. #977) are treated as unactionable. Fenced blocks only
- * count as actionable when their body contains non-whitespace content.
- */
-function hasActionableReproductionDetail(text) {
- const c = clean(text);
- if (!c) return false;
- if (ACTIONABLE_REPRO_RE.test(c)) return true;
- // Fenced blocks: only count when the body has non-whitespace content.
- if (/```|~~~/.test(c)) {
- const parts = stripFencedActionableContent(c);
- if (parts) return true;
- }
- return false;
-}
-
-/**
- * Walk the text looking for a fenced code block whose body contains
- * non-whitespace content. Returns the non-empty body or null.
- */
-function stripFencedActionableContent(text) {
- const fenceRe = /^[ \t]{0,3}(`{3,}|~{3,})/;
- const lines = text.split("\n");
- let i = 0;
- while (i < lines.length) {
- const m = lines[i].match(fenceRe);
- if (!m) { i++; continue; }
- const marker = m[1];
- const markerLen = marker.length;
- // Find the closing fence on a later line.
- let j = i + 1;
- const endRe = new RegExp(
- `^[ \\t]{0,3}${marker[0] === "`" ? "`" : "~"}{${markerLen},}[ \\t]*$`,
- );
- while (j < lines.length && !endRe.test(lines[j])) j++;
- if (j > i + 1) {
- const body = lines.slice(i + 1, j).join("\n");
- if (body.trim()) return body;
- }
- i = j + 1;
- }
- return null;
-}
-
-function isTooTerseBugReproduction(text) {
- if (isEmpty(text) || isPlaceholder(text)) return false;
- if (hasActionableReproductionDetail(text)) return false;
- return countWords(text) < 12;
-}
-
-/**
- * Check if raw section text is a placeholder-only variant without relying on
- * clean() first. Used to distinguish intentionally blank optional fields
- * (legacy "No response" / N/A) from actively cleared required fields.
- */
-function isRawPlaceholder(raw) {
- if (raw === null) return false;
- return isPlaceholderOnlyValue(raw);
-}
-
-/**
- * Near-miss freeform headings that often appear in API-opened or copy-pasted
- * bug reports instead of the Bug report template (e.g. Description / Log entry).
- */
-const FREEFORM_BUG_NEAR_MISS_HEADINGS = [
- "Description",
- "Steps to reproduce",
- "Steps to Reproduce",
- "How to reproduce",
- "Log",
- "Logs",
- "Log entry",
- "Error",
- "Error output",
- "Stack trace",
-];
-
-/**
- * True when an unclassified body looks like a bug report that skipped the
- * template (near-miss headings and/or repro/error signals).
- *
- * @param {{ title?: string, body?: string }} issue
- * @returns {boolean}
- */
-function looksLikeUntemplatedBugReport(issue) {
- const body = typeof issue?.body === "string" ? issue.body : "";
- if (!body.trim()) return false;
-
- const nearMissCount = countHeadings(body, FREEFORM_BUG_NEAR_MISS_HEADINGS);
- const hasReproduction =
- extractSection(body, "Reproduction") !== null ||
- extractSection(body, "Steps to reproduce") !== null ||
- extractSection(body, "Steps to Reproduce") !== null ||
- extractSection(body, "How to reproduce") !== null;
- const hasDescription = extractSection(body, "Description") !== null;
- const hasLogOrError =
- extractSection(body, "Log") !== null ||
- extractSection(body, "Logs") !== null ||
- extractSection(body, "Log entry") !== null ||
- extractSection(body, "Logs or error output") !== null ||
- extractSection(body, "Error") !== null ||
- extractSection(body, "Error output") !== null ||
- extractSection(body, "Stack trace") !== null;
-
- if (hasDescription && (hasReproduction || hasLogOrError)) return true;
- if (hasReproduction && hasLogOrError) return true;
- if (nearMissCount >= 2) return true;
-
- // Body-level signals for heading-free freeform dumps.
- const signalRe =
- /\b(repro(?:duce|duction| steps)?|stack\s*traces?|traceback|segfault|panic|exception|error\s*output|ECONNREFUSED|SIGSEGV)\b/i;
- if (signalRe.test(body) && (hasDescription || hasReproduction || hasLogOrError || nearMissCount >= 1)) {
- return true;
- }
- return false;
-}
-
-/**
- * Reasons/guidance when no structured issue kind was detected.
- *
- * @param {{ title?: string, body?: string }} issue
- * @returns {{ reasons: string[], guidance: string[] }}
- */
-function untemplatedIssueFailure(issue) {
- if (looksLikeUntemplatedBugReport(issue)) {
- return {
- reasons: [
- "This looks like a bug report but it does not use the Bug report template headings (for example Description/Log entry instead of Summary).",
- ],
- guidance: [
- "Use the Bug report template, or edit this issue to include: Client or integration, Summary, Reproduction, Version, and Operating system.",
- "Retitling with `[Bug]:` and applying the `bug` label alone is not enough without those section headings filled in.",
- ],
- };
- }
- return {
- reasons: [
- "This issue does not use a recognized issue template.",
- ],
- guidance: [
- "Open a new issue with the Bug report, Feature request, Documentation, or Provider compatibility template.",
- "Or edit this issue so the body uses the template section headings for the kind of report you are filing.",
- ],
- };
-}
-
-/**
- * Validate an issue body for its detected kind.
- *
- * Unclassified (freeform / non-template) issues are invalid so API-opened
- * reports cannot skip the quality gate. Trusted-author exemption is enforced
- * by the workflow, not here.
- *
- * @param {{ title: string, body: string, labels: string[], storedKind?: string|null }} issue
- * @returns {{ kind: string|null, valid: boolean, softPass: boolean, reasons: string[], guidance: string[] }}
- */
-function validateIssue(issue) {
- const { title = "", body = "" } = issue;
- const kind = detectIssueKind(issue);
- const reasons = [];
- const guidance = [];
- let softPass = false;
-
- if (!kind) {
- const failure = untemplatedIssueFailure(issue);
- return {
- kind: null,
- valid: false,
- softPass: false,
- reasons: failure.reasons,
- guidance: failure.guidance,
- };
- }
-
- if (kind === "feature") {
- const goal = resolveSection(body, FEATURE_GOAL_HEADINGS);
- const blocker = resolveSection(body, FEATURE_BLOCKER_HEADINGS);
- const behaviour = resolveSection(body, FEATURE_BEHAVIOUR_HEADINGS);
- const example = resolveSection(body, FEATURE_EXAMPLE_HEADINGS);
-
- const coreSections = [goal, blocker, behaviour, example];
- const emptyCore = [];
- if (isEmpty(goal)) emptyCore.push("goal / problem");
- // blocker and example are only required when those headings exist.
- // On the legacy / translated forms these sections may be absent (null).
- if (blocker !== null && isEmpty(blocker)) emptyCore.push("current limitation");
- if (isEmpty(behaviour)) emptyCore.push("expected behaviour");
- if (example !== null && isPlaceholder(example)) {
- reasons.push("Example usage or interface contains placeholder text instead of a concrete example.");
- guidance.push("Add a real CLI command, config snippet, API exchange, or before/after workflow example.");
- } else if (example !== null && isEmpty(example)) {
- emptyCore.push("example usage");
- }
-
- const mappedHeadingPresent =
- goal !== null || blocker !== null || behaviour !== null || example !== null;
-
- if (emptyCore.length > 0) {
- // Soft-pass rich non-template bodies once kind is already feature (title
- // prefix, enhancement label, or stored kind). Do not require the title to
- // keep a `[Feature]:` prefix — maintainer retitles must not re-arm closure.
- const canSoftPass =
- !mappedHeadingPresent &&
- hasSubstantialStructuredContent(body);
- if (canSoftPass) {
- softPass = true;
- } else {
- reasons.push(`Required sections are missing or empty: ${emptyCore.join(", ")}.`);
- guidance.push("Fill in each required section with specific detail about your workflow.");
- }
- }
-
- if (!softPass) {
- const nonEmpty = coreSections.filter((s) => !isEmpty(s));
- if (nonEmpty.length >= 2 && allSameCanonical(nonEmpty)) {
- reasons.push("All core sections contain the same content.");
- guidance.push("Each section should describe a different aspect: goal, limitation, expected behaviour, and a concrete example.");
- }
-
- if (nonEmpty.length >= 2 && allRepeatTitle(nonEmpty, title)) {
- reasons.push("All core sections merely repeat the issue title.");
- guidance.push("Expand each section with details beyond the title.");
- }
-
- if (nonEmpty.length > 0 && nonEmpty.every(isPlaceholder)) {
- reasons.push("Required sections contain only placeholder text.");
- guidance.push("Replace placeholder text with your actual proposal.");
- }
- }
-
- const terseSections = [];
- if (goal !== null && isTooTerseFeatureSection(goal)) terseSections.push("goal / problem");
- if (blocker !== null && isTooTerseFeatureSection(blocker)) terseSections.push("current limitation");
- if (behaviour !== null && isTooTerseFeatureSection(behaviour)) terseSections.push("expected behaviour");
- if (terseSections.length > 0) {
- reasons.push(`Required sections are too vague to act on: ${terseSections.join(", ")}.`);
- guidance.push("Describe the workflow, limitation, and expected behaviour with enough detail for someone to implement or evaluate the request.");
- }
- }
-
- if (kind === "bug") {
- const summary = extractSection(body, "Summary");
- const repro = extractSection(body, "Reproduction");
- const version = extractSection(body, "Version");
- const os = extractSection(body, "Operating system") ?? extractSection(body, "OS");
- // New Bug report template always includes Client or integration.
- const isNewBugForm = extractSection(body, "Client or integration") !== null;
-
- if (isEmpty(summary) && isEmpty(repro)) {
- // Soft-pass substantial non-English / freeform structured reports once
- // kind is already bug (label, stored kind, or prior `[Bug]:` detection).
- // Requiring the title to keep a `[Bug]:` prefix caused #545: a maintainer
- // retitle of an already detailed report was treated as empty Summary/
- // Reproduction and auto-closed.
- const canSoftPass =
- summary === null &&
- repro === null &&
- hasSubstantialStructuredContent(body);
- if (canSoftPass) {
- softPass = true;
- } else {
- reasons.push("Both Summary and Reproduction are empty.");
- guidance.push("Describe what happened and how to reproduce it.");
- }
- } else {
- // Each mapped field is required on its own — a filled Summary with an
- // empty / ellipsis Reproduction (e.g. #598) must not pass.
- if (isEmpty(summary)) {
- reasons.push("Summary is empty.");
- guidance.push("Describe what happened (the symptom or error).");
- }
- if (isEmpty(repro)) {
- reasons.push("Reproduction is empty.");
- guidance.push("List the exact steps to reproduce the problem.");
- } else if (!softPass && isTooTerseBugReproduction(repro)) {
- reasons.push("Reproduction is too vague to act on.");
- guidance.push("List exact steps, commands, and the observed failure — not only a short phrase.");
- }
- }
-
- // Version "Unknown" / "모름" / "idk" is never actionable, on any form.
- if (!softPass && version !== null && isUnusableVersion(version)) {
- reasons.push("Version is missing or unknown.");
- guidance.push("Report the installed `@bitkyc08/opencodex` version (for example `2.7.42`) or a commit SHA from `ocx --version`.");
- } else if (
- !softPass &&
- isNewBugForm &&
- (version === null || isEmpty(version) || isRawPlaceholder(version))
- ) {
- // New form requires Version (including when the heading was removed).
- // Legacy N/A / No response soft-pass stays only for bodies without
- // Client or integration.
- reasons.push("Version is missing.");
- guidance.push("Add your OpenCodex version so we can reproduce the environment.");
- }
-
- if (!softPass && isNewBugForm && os !== null && isUnusableVersion(os)) {
- reasons.push("Operating system is missing or unknown.");
- guidance.push("Add your OS name and version (for example Windows 11 24H2).");
- } else if (
- !softPass &&
- isNewBugForm &&
- (os === null || isEmpty(os) || isRawPlaceholder(os))
- ) {
- reasons.push("Operating system is missing.");
- guidance.push("Add your OS name and version (for example Windows 11 24H2).");
- }
-
- // Required environment fields removed after submission on bodies that are
- // not the new form (no Client or integration). Legacy reports never had
- // Version or OS fields, so null means absent, not removed. Skip when the
- // raw value is a "No response" placeholder — the old form had both fields
- // as optional. Only close when the field was actively cleared.
- if (
- !softPass &&
- !isNewBugForm &&
- version !== null &&
- os !== null &&
- isEmpty(version) &&
- isEmpty(os) &&
- !isRawPlaceholder(version) &&
- !isRawPlaceholder(os)
- ) {
- reasons.push("Version and Operating system are both missing.");
- guidance.push("Add your OpenCodex version and OS so we can reproduce the environment.");
- }
-
- if (!softPass) {
- const nonEmpty = [summary, repro].filter((s) => !isEmpty(s));
- if (nonEmpty.length >= 2 && allSameCanonical(nonEmpty)) {
- reasons.push("Summary and Reproduction contain the same content.");
- guidance.push("Summary should describe the symptom; Reproduction should list the exact steps.");
- }
-
- if (nonEmpty.length >= 1 && allRepeatTitle(nonEmpty, title)) {
- reasons.push("Summary and Reproduction merely repeat the title.");
- guidance.push("Add detail beyond the title: what you observed, what you expected, and the exact steps.");
- }
-
- if (nonEmpty.length > 0 && nonEmpty.every(isPlaceholder)) {
- reasons.push("Required sections contain only placeholder text.");
- guidance.push("Replace placeholder text with your actual report.");
- }
- }
- }
-
- if (kind === "provider-compatibility") {
- const current = extractSection(body, "Current behaviour");
- const expected = extractSection(body, "Expected behaviour");
- const repro = extractSection(body, "Minimal redacted request or reproduction");
- const response = extractSection(body, "Actual response or error");
- const docs = extractSection(body, "Upstream documentation");
-
- const emptyCore = [];
- if (isEmpty(current)) emptyCore.push("current behaviour");
- if (isEmpty(expected)) emptyCore.push("expected behaviour");
- // Metadata fields: provider, version, endpoint are required on the form.
- const provider = extractSection(body, "Provider or upstream service");
- const version = extractSection(body, "OpenCodex version");
- const endpoint = extractSection(body, "Endpoint or capability");
- if (provider !== null && isEmpty(provider)) emptyCore.push("provider or upstream service");
- if (version !== null && isRawPlaceholder(version) === false && isEmpty(version)) emptyCore.push("OpenCodex version");
- if (endpoint !== null && isEmpty(endpoint)) emptyCore.push("endpoint or capability");
- if (emptyCore.length > 0) {
- // Same soft-pass as bug/feature: label- or maintainer-scoped provider
- // reports often use non-English structured headings after a retitle.
- const mappedHeadingPresent =
- current !== null || expected !== null || repro !== null || response !== null || docs !== null ||
- provider !== null || version !== null || endpoint !== null;
- const canSoftPass =
- !mappedHeadingPresent &&
- hasSubstantialStructuredContent(body);
- if (canSoftPass) {
- softPass = true;
- } else {
- reasons.push(`Required sections are missing or empty: ${emptyCore.join(", ")}.`);
- guidance.push("Describe both the current and expected behaviour.");
- }
- }
-
- if (!softPass && !isEmpty(current) && !isEmpty(expected) && canonicalise(current) === canonicalise(expected)) {
- reasons.push("Current and expected behaviour are effectively identical.");
- guidance.push("Explain the difference between what happens now and what should happen.");
- }
-
- const allSections = [current, expected, repro, response].filter((s) => !isEmpty(s));
- if (!softPass && allSections.length >= 2 && allRepeatTitle(allSections, title)) {
- reasons.push("All sections merely repeat the issue title.");
- guidance.push("Add specific detail in each section.");
- }
-
- if (!softPass && isEmpty(repro) && isEmpty(response)) {
- reasons.push("Both the request/reproduction and the actual response/error are absent.");
- guidance.push("Include at least a minimal redacted request or the actual error output.");
- }
-
- if (!softPass && isEmpty(docs)) {
- reasons.push("Upstream documentation is empty without stating that no public specification exists.");
- guidance.push("Add a URL to the provider specification, or state that no public spec exists.");
- }
- }
-
- if (kind === "documentation") {
- const location = extractSection(body, "Documentation location");
- const problem = extractSection(body, "What is wrong or missing?");
- const expected = extractSection(body, "What should the documentation explain instead?");
-
- if (isEmpty(location) && isEmpty(problem)) {
- reasons.push("Documentation location and problem description are both missing.");
- guidance.push("Point to the exact documentation page and describe what is wrong.");
- }
-
- const nonEmpty = [location, problem, expected].filter((s) => !isEmpty(s));
- if (nonEmpty.length >= 1 && allRepeatTitle(nonEmpty, title)) {
- reasons.push("The body merely repeats the title.");
- guidance.push("Add detail: the exact URL or path, what is wrong, and what it should say.");
- }
-
- if (nonEmpty.length > 0 && nonEmpty.every(isPlaceholder)) {
- reasons.push("Required sections contain only placeholder text.");
- guidance.push("Replace placeholder text with the actual documentation problem.");
- }
- }
-
- return {
- kind,
- valid: reasons.length === 0 && !softPass,
- softPass,
- reasons,
- guidance,
- };
-}
-
-// ---------------------------------------------------------------------------
-// Closure ownership
-// ---------------------------------------------------------------------------
-
-/**
- * Decide whether the bot may auto-close an invalid issue.
- *
- * After a maintainer reopens and deactivates enforcement, later `edited`
- * events must not close the issue again.
- *
- * @param {{ active?: boolean, maintainerOverride?: boolean }|null|undefined} botState
- * @returns {boolean}
- */
-function shouldEnforceClosure(botState) {
- if (botState && botState.maintainerOverride === true) return false;
- return true;
-}
-
-/**
- * Decide whether the bot may reopen a closed issue.
- *
- * @param {{ active: boolean, closedAt: string|null, stateReason: string }} botState
- * @param {{ state: string, closed_at: string|null, state_reason: string|null, closed_by?: string|null }} issue
- * @param {boolean} maintainerOverride True when a maintainer changed the issue state after the bot.
- * @returns {boolean}
- */
-function shouldReopen(botState, issue, maintainerOverride) {
- if (!botState || !botState.active) return false;
- if (issue.state !== "closed") return false;
- if (maintainerOverride) return false;
- if (issue.closed_at !== botState.closedAt) return false;
- if (issue.state_reason !== botState.stateReason) return false;
- // Only reopen if the bot itself was the last actor to close the issue.
- // A human closing it (even with the same timestamp) means intentional closure.
- if (issue.closed_by && issue.closed_by !== "github-actions[bot]") return false;
- return true;
-}
-
-/**
- * workflow_dispatch accepts a bare issue number, but GitHub reuses the same
- * number namespace for issues and pull requests. Reject PR targets before any
- * validation or mutation runs.
- *
- * @param {{ pull_request?: unknown }} issue
- * @param {number|string} issueNumber
- * @param {string} eventName
- * @returns {string|null}
- */
-function rejectsWorkflowDispatchPullRequest(issue, issueNumber, eventName) {
- if (eventName !== "workflow_dispatch") return null;
- if (!issue?.pull_request) return null;
- return `#${issueNumber} is a pull request. This workflow only accepts issue numbers.`;
-}
-
-/**
- * workflow_dispatch can be started from a selected branch. Reject runs whose
- * selected ref is not the repository default branch so untrusted branch code
- * cannot drive issue mutations with issues:write.
- *
- * @param {string} eventName
- * @param {string|null|undefined} ref
- * @param {string|null|undefined} defaultBranch
- * @returns {string|null}
- */
-function rejectsWorkflowDispatchNonDefaultBranch(eventName, ref, defaultBranch) {
- if (eventName !== "workflow_dispatch") return null;
- if (!defaultBranch || typeof defaultBranch !== "string") {
- return "workflow_dispatch requires repository.default_branch to be available.";
- }
- const expected = `refs/heads/${defaultBranch}`;
- if (ref !== expected) {
- return (
- `workflow_dispatch must run from the default branch (${defaultBranch}); ` +
- `selected ref was ${ref || "(empty)"}.`
- );
- }
- return null;
-}
-
-// ---------------------------------------------------------------------------
-// Exports
-// ---------------------------------------------------------------------------
-
-module.exports = {
- clean,
- normalise,
- canonicalise,
- stripMediaTokens,
- isMediaOnly,
- extractSection,
- resolveSection,
- detectIssueKind,
- validateIssue,
- looksLikeUntemplatedBugReport,
- shouldReopen,
- shouldEnforceClosure,
- isPlaceholderOnlyValue,
- isPlaceholder,
- isRawPlaceholder,
- isUnusableVersion,
- countWords,
- hasConcreteDetail,
- hasActionableReproductionDetail,
- labelForKind,
- KIND_TO_LABEL,
- AREA_LABELS,
- AREA_FIELD_TO_LABELS,
- mapAreaFieldToLabels,
- bodyForAreaHeuristics,
- heuristicAreaLabels,
- detectAreaLabels,
- hasSubstantialStructuredContent,
- rejectsWorkflowDispatchPullRequest,
- rejectsWorkflowDispatchNonDefaultBranch,
-};
diff --git a/.github/scripts/issue-quality.test.cjs b/.github/scripts/issue-quality.test.cjs
deleted file mode 100644
index 4f22c4247c..0000000000
--- a/.github/scripts/issue-quality.test.cjs
+++ /dev/null
@@ -1,2028 +0,0 @@
-"use strict";
-
-const { describe, it } = require("node:test");
-const assert = require("node:assert/strict");
-const {
- clean,
- normalise,
- canonicalise,
- extractSection,
- detectIssueKind,
- validateIssue,
- looksLikeUntemplatedBugReport,
- shouldReopen,
- shouldEnforceClosure,
- labelForKind,
- AREA_LABELS,
- mapAreaFieldToLabels,
- detectAreaLabels,
- isPlaceholderOnlyValue,
- isPlaceholder,
- isRawPlaceholder,
- isUnusableVersion,
- stripMediaTokens,
- isMediaOnly,
- countWords,
- hasConcreteDetail,
- hasActionableReproductionDetail,
- rejectsWorkflowDispatchPullRequest,
- rejectsWorkflowDispatchNonDefaultBranch,
-} = require("./issue-quality.cjs");
-
-function featureBodyWithGoal(goal) {
- return [
- "### Area",
- "CLI",
- "### What are you trying to accomplish?",
- goal,
- "### What prevents this today?",
- "Port resets to 10100 after every ocx stop command.",
- "### What should OpenCodex do?",
- "Persist the last used port in config across restarts.",
- "### Example usage or interface",
- "ocx start --port 8080 && ocx stop && ocx start",
- ].join("\n");
-}
-
-function featureBodyWithExample(example) {
- return [
- "### What are you trying to accomplish?",
- "Route voice requests to a configured fallback provider when the primary quota is exhausted.",
- "### What prevents this today?",
- "Voice mode is hard-wired to the primary Codex quota and cannot switch providers.",
- "### What should OpenCodex do?",
- "Expose a setting to choose the fallback voice model and provider.",
- "### Example usage or interface",
- example,
- ].join("\n");
-}
-
-// ---------------------------------------------------------------------------
-// Detection
-// ---------------------------------------------------------------------------
-
-describe("detectIssueKind", () => {
- it("detects new feature form without [Feature]: prefix", () => {
- const body = [
- "### Area",
- "Proxy and routing",
- "### What are you trying to accomplish?",
- "Route requests to a fallback provider.",
- "### What prevents this today?",
- "No fallback support.",
- "### What should OpenCodex do?",
- "Fall back automatically.",
- "### Example usage or interface",
- "ocx config set routing.fallback anthropic",
- ].join("\n");
- assert.equal(detectIssueKind({ title: "Add fallback routing", body, labels: ["enhancement"] }), "feature");
- });
-
- it("detects legacy feature form with [Feature]: prefix", () => {
- const body = [
- "### Problem to solve",
- "I want opencodex to support streaming.",
- "### Proposed solution",
- "Add SSE passthrough.",
- ].join("\n");
- assert.equal(detectIssueKind({ title: "[Feature]: streaming support", body, labels: ["enhancement"] }), "feature");
- });
-
- it("detects new bug form without [Bug]: prefix", () => {
- const body = [
- "### Client or integration",
- "Codex CLI",
- "### Area",
- "Proxy and routing",
- "### Summary",
- "Proxy crashes on startup.",
- "### Reproduction",
- "1. ocx start",
- "### Version",
- "2.7.31",
- "### Operating system",
- "Windows 11",
- ].join("\n");
- assert.equal(detectIssueKind({ title: "Proxy crashes", body, labels: ["bug"] }), "bug");
- });
-
- it("detects legacy bug form with [Bug]: prefix", () => {
- const body = [
- "### Summary",
- "The proxy returns 502.",
- "### Reproduction",
- "Send a request to /v1/responses.",
- ].join("\n");
- assert.equal(detectIssueKind({ title: "[Bug]: 502 on responses", body, labels: ["bug"] }), "bug");
- });
-
- it("detects provider compatibility form", () => {
- const body = [
- "### Client or integration",
- "Codex CLI",
- "### Provider or upstream service",
- "anthropic",
- "### OpenCodex version",
- "2.7.31",
- "### Endpoint or capability",
- "/v1/messages",
- "### Current behaviour",
- "Returns 400.",
- "### Expected behaviour",
- "Returns 200 with a message.",
- "### Minimal redacted request or reproduction",
- "curl ...",
- "### Actual response or error",
- "400 Bad Request",
- "### Upstream documentation",
- "https://docs.anthropic.com/en/api/messages",
- ].join("\n");
- assert.equal(detectIssueKind({ title: "Anthropic messages 400", body, labels: ["enhancement"] }), "provider-compatibility");
- });
-
- it("detects documentation form", () => {
- const body = [
- "### Documentation problem type",
- "Missing documentation",
- "### Documentation location",
- "docs/providers.md",
- "### What is wrong or missing?",
- "No mention of the xai provider.",
- "### What should the documentation explain instead?",
- "How to configure xai.",
- ].join("\n");
- assert.equal(detectIssueKind({ title: "Missing xai docs", body, labels: ["documentation"] }), "documentation");
- });
-
- it("returns null for unrelated issue with manually applied enhancement label", () => {
- const body = "Just a random question about setup.";
- assert.equal(detectIssueKind({ title: "How do I configure?", body, labels: ["enhancement"] }), null);
- });
-
- it("uses stored bot kind when headings are removed", () => {
- const body = "Some edited text without headings.";
- assert.equal(detectIssueKind({ title: "My issue", body, labels: [], storedKind: "feature" }), "feature");
- });
-});
-
-// ---------------------------------------------------------------------------
-// Validation: feature
-// ---------------------------------------------------------------------------
-
-describe("validateIssue - feature", () => {
- it("keeps nested sub-headings and fenced heading text inside a section (#541)", () => {
- const body = [
- "### What are you trying to accomplish?",
- "Route Studio models through the user's WordPress.com account.",
- "### What prevents this today?",
- "The provider needs two wire formats and one shared OAuth identity.",
- "### What should OpenCodex do?",
- "Add a first-class provider with model-specific transport selection.",
- "### Example usage or interface",
- "#### CLI flow",
- "```bash",
- "# This heading-shaped shell comment must stay inside the fence",
- "ocx login wordpress-studio",
- "```",
- "#### Dashboard flow",
- "Providers -> WordPress Studio Code -> Log in",
- "### Alternatives or workarounds",
- "Use Studio directly.",
- ].join("\n");
-
- const example = extractSection(body, "Example usage or interface");
- assert.match(example, /^#### CLI flow/);
- assert.match(example, /# This heading-shaped shell comment/);
- assert.match(example, /#### Dashboard flow/);
- assert.doesNotMatch(example, /Alternatives or workarounds/);
-
- const result = validateIssue({ title: "Add WordPress Studio provider", body, labels: ["enhancement"] });
- assert.equal(result.kind, "feature");
- assert.equal(result.valid, true, `Expected valid but got reasons: ${result.reasons.join(", ")}`);
- });
-
- it("ignores markdown headings inside backtick and tilde fences when finding section boundaries (#541)", () => {
- for (const fence of ["```", "~~~~"]) {
- const body = [
- "### Example usage or interface",
- fence,
- "### pasted heading",
- "real example content",
- fence,
- "### Next sibling",
- "outside",
- ].join("\n");
- assert.equal(
- extractSection(body, "Example usage or interface"),
- [fence, "### pasted heading", "real example content", fence].join("\n"),
- );
- }
- });
-
- it("rejects issue #208-style duplicate content", () => {
- const repeated = "Add support for streaming responses in the proxy";
- const body = [
- "### What are you trying to accomplish?",
- repeated,
- "### What prevents this today?",
- repeated,
- "### What should OpenCodex do?",
- repeated,
- "### Example usage or interface",
- repeated,
- ].join("\n");
- const result = validateIssue({ title: repeated, body, labels: ["enhancement"] });
- assert.equal(result.kind, "feature");
- assert.equal(result.valid, false);
- assert.ok(result.reasons.length > 0);
- });
-
- it("rejects an image-only goal section that hides repeated prose (#1098)", () => {
- // Regression for #1098: an HTML
in the goal section made the goal
- // look non-empty, so the repeated identical sentences in the other three
- // sections were not caught as duplicates and the issue passed validation.
- const repeated =
- "It is hoped that the usage query will support time-based queries and statistics, as well as key-based queries and statistics";
- const img =
- '
';
- const body = [
- "### Area",
- "CLI",
- "### What are you trying to accomplish?",
- img,
- "### What prevents this today?",
- repeated,
- "### What should OpenCodex do?",
- repeated,
- "### Example usage or interface",
- repeated,
- ].join("\n");
- const result = validateIssue({ title: repeated, body, labels: ["enhancement"] });
- assert.equal(result.kind, "feature");
- assert.equal(result.valid, false);
- assert.ok(
- result.reasons.some((r) => /missing or empty/i.test(r)),
- `Expected missing/empty reason, got: ${result.reasons.join("; ")}`,
- );
- assert.ok(
- result.reasons.some((r) => /same content/i.test(r)),
- `Expected duplicate-content reason, got: ${result.reasons.join("; ")}`,
- );
- assert.ok(
- result.reasons.some((r) => /repeat the issue title/i.test(r)),
- `Expected repeated-title reason, got: ${result.reasons.join("; ")}`,
- );
- });
-
- it("rejects a markdown-image-only goal section with repeated prose (#1098)", () => {
- const repeated =
- "It is hoped that the usage query will support time-based queries and statistics, as well as key-based queries and statistics";
- const mdImg = "";
- const body = [
- "### What are you trying to accomplish?",
- mdImg,
- "### What prevents this today?",
- repeated,
- "### What should OpenCodex do?",
- repeated,
- "### Example usage or interface",
- repeated,
- ].join("\n");
- const result = validateIssue({ title: repeated, body, labels: ["enhancement"] });
- assert.equal(result.kind, "feature");
- assert.equal(result.valid, false);
- assert.ok(
- result.reasons.some((r) => /missing or empty/i.test(r)),
- `Expected missing/empty reason, got: ${result.reasons.join("; ")}`,
- );
- });
-
- it("rejects a markdown image with bracketed alt text in the goal (#1098)", () => {
- const repeated =
- "It is hoped that the usage query will support time-based queries and statistics, as well as key-based queries and statistics";
- // GitHub permits balanced brackets inside image alt text, e.g.
- // ![Image [screenshot]](url). The stripper must still treat it as
- // media-only so it cannot hide repeated prose.
- const mdImg = "![Image [screenshot]](https://example.com/x.png)";
- const body = [
- "### What are you trying to accomplish?",
- mdImg,
- "### What prevents this today?",
- repeated,
- "### What should OpenCodex do?",
- repeated,
- "### Example usage or interface",
- repeated,
- ].join("\n");
- const result = validateIssue({ title: repeated, body, labels: ["enhancement"] });
- assert.equal(result.kind, "feature");
- assert.equal(result.valid, false);
- assert.ok(
- result.reasons.some((r) => /missing or empty/i.test(r)),
- `Expected missing/empty reason, got: ${result.reasons.join("; ")}`,
- );
- });
-
- it("rejects a markdown image whose URL contains balanced parentheses (#1098)", () => {
- const repeated =
- "It is hoped that the usage query will support time-based queries and statistics, as well as key-based queries and statistics";
- // Markdown destinations may contain balanced parentheses, e.g.
- // .png). The stripper must
- // still treat it as media-only so it cannot hide repeated prose.
- const mdImg = ".png)";
- const body = [
- "### What are you trying to accomplish?",
- mdImg,
- "### What prevents this today?",
- repeated,
- "### What should OpenCodex do?",
- repeated,
- "### Example usage or interface",
- repeated,
- ].join("\n");
- const result = validateIssue({ title: repeated, body, labels: ["enhancement"] });
- assert.equal(result.kind, "feature");
- assert.equal(result.valid, false);
- assert.ok(
- result.reasons.some((r) => /missing or empty/i.test(r)),
- `Expected missing/empty reason, got: ${result.reasons.join("; ")}`,
- );
- });
-
- it("preserves a goal section that mixes an image with real text", () => {
- const goal = [
- "",
- "Route voice requests to a configured fallback provider when the primary quota is exhausted.",
- ].join("\n");
- const result = validateIssue({
- title: "Voice fallback routing",
- body: featureBodyWithGoal(goal),
- labels: ["enhancement"],
- });
- assert.equal(result.kind, "feature");
- assert.equal(result.valid, true);
- });
-
- it("treats image/media-only sections as empty via isMediaOnly", () => {
- assert.equal(isMediaOnly('
'), true);
- assert.equal(isMediaOnly(""), true);
- assert.equal(isMediaOnly("![alt [with bracket]](https://example.com/x.png)"), true);
- assert.equal(isMediaOnly(".png)"), true);
- assert.equal(isMediaOnly('.png "title")'), true);
- assert.equal(isMediaOnly("b.png)"), false);
- assert.equal(isMediaOnly("\\"), false);
- // Reference-style images (Codex bot finding): inline ref + definition.
- assert.equal(isMediaOnly("![Image][shot]\n\n[shot]: https://example.com/x.png"), true);
- assert.equal(isMediaOnly("![Image][]\n\n[Image]: https://example.com/x.png"), true);
- assert.equal(isMediaOnly("![Image][shot]\n\n[shot]: https://example.com/x.png\ncaption"), false);
- // Fallback prose inside media blocks is preserved (Codex bot finding).
- assert.equal(
- isMediaOnly(""),
- false,
- );
- assert.equal(isMediaOnly(''), true);
- assert.equal(isMediaOnly("Fallback image description"), false);
- // Indented code blocks render as literal code, not images (Codex bot finding).
- assert.equal(isMediaOnly(" "), false);
- assert.equal(isMediaOnly("\t"), false);
- // HTML media inside indented code is also literal code (CodeRabbit finding).
- assert.equal(isMediaOnly('
'), false);
- assert.equal(isMediaOnly(' '), false);
- assert.equal(isMediaOnly('\t
'), false);
- // Reference labels with nested alt brackets (CodeRabbit finding).
- assert.equal(isMediaOnly("![Image [screenshot]][shot]\n\n[shot]: https://example.com/x.png"), true);
- assert.equal(
- isMediaOnly("![Image [screenshot]][shot]\n\n[shot]: https://example.com/x.png\ncaption"),
- false,
- );
- assert.equal(isMediaOnly('
'), true);
- assert.equal(isMediaOnly(''), true);
- assert.equal(isMediaOnly('
\nCaption text'), false);
- assert.equal(isMediaOnly("Some real description."), false);
- assert.equal(stripMediaTokens('
').trim(), "");
- assert.equal(stripMediaTokens('').trim(), "");
- assert.equal(stripMediaTokens('before  after').replace(/\s+/g, " ").trim(), "before after");
- });
-
- it("accepts a concise but actionable feature", () => {
- const body = [
- "### Area",
- "CLI",
- "### What are you trying to accomplish?",
- "Pin the proxy port across restarts.",
- "### What prevents this today?",
- "Port resets to 10100 after ocx stop.",
- "### What should OpenCodex do?",
- "Remember the last used port in config.",
- "### Example usage or interface",
- "ocx start --port 8080 && ocx stop && ocx start # still 8080",
- ].join("\n");
- const result = validateIssue({ title: "Persist port across restarts", body, labels: ["enhancement"] });
- assert.equal(result.kind, "feature");
- assert.equal(result.valid, true);
- });
-
- it("rejects issue #401-style low-effort feature with placeholder example", () => {
- const body = [
- "### Area",
- "Proxy and routing",
- "### What are you trying to accomplish?",
- "Quota for Chatgpt running out that can no longer use voice mode. Would like to change other model for that",
- "### What prevents this today?",
- "No usage without codex quota",
- "### What should OpenCodex do?",
- "Change another voice model",
- "### Example usage or interface",
- "NA",
- ].join("\n");
- const result = validateIssue({
- title: "Change voice chat to different model",
- body,
- labels: ["enhancement"],
- });
- assert.equal(result.kind, "feature");
- assert.equal(result.valid, false);
- assert.ok(result.reasons.some((r) => r.includes("placeholder")));
- assert.ok(result.reasons.some((r) => r.includes("expected behaviour")));
- });
-
- it("rejects feature reports with placeholder example variants", () => {
- const placeholders = [
- "NA",
- "N/A",
- "_N/A_",
- "NA.",
- "N/A.",
- "Not applicable",
- "Not applicable.",
- "Not available!",
- ];
- for (const example of placeholders) {
- const body = [
- "### What are you trying to accomplish?",
- "Route voice requests to a configured fallback provider when the primary quota is exhausted.",
- "### What prevents this today?",
- "Voice mode is hard-wired to the primary Codex quota and cannot switch providers.",
- "### What should OpenCodex do?",
- "Expose a setting to choose the fallback voice model and provider.",
- "### Example usage or interface",
- example,
- ].join("\n");
- const result = validateIssue({ title: "Voice fallback routing", body, labels: ["enhancement"] });
- assert.equal(result.valid, false, `Expected placeholder example "${example}" to be invalid`);
- assert.ok(
- result.reasons.some((r) => r.includes("placeholder")),
- `Expected placeholder reason for "${example}", got: ${result.reasons.join("; ")}`,
- );
- assert.ok(!result.reasons.some((r) => r.includes("example usage")));
- }
- });
-
- it("reports blank example usage as missing, not placeholder", () => {
- const body = [
- "### What are you trying to accomplish?",
- "Route voice requests to a configured fallback provider when the primary quota is exhausted.",
- "### What prevents this today?",
- "Voice mode is hard-wired to the primary Codex quota and cannot switch providers.",
- "### What should OpenCodex do?",
- "Expose a setting to choose the fallback voice model and provider.",
- "### Example usage or interface",
- "",
- ].join("\n");
- const result = validateIssue({ title: "Voice fallback routing", body, labels: ["enhancement"] });
- assert.equal(result.valid, false);
- assert.ok(result.reasons.some((r) => r.includes("example usage")));
- assert.ok(!result.reasons.some((r) => r.includes("placeholder")));
- });
-
- it("accepts a valid legacy feature request without blocker/example headings", () => {
- const body = [
- "### Problem to solve",
- "No way to set a custom timeout per provider in the proxy config.",
- "### Proposed solution",
- "Add a per-provider timeout field in the config JSON.",
- ].join("\n");
- const result = validateIssue({ title: "[Feature]: per-provider timeout", body, labels: ["enhancement"] });
- assert.equal(result.kind, "feature");
- assert.equal(result.valid, true, `Expected valid but got reasons: ${result.reasons.join(", ")}`);
- });
-
- it("accepts a detailed CJK submission", () => {
- const body = [
- "### Area",
- "Proxy and routing",
- "### What are you trying to accomplish?",
- "\u4ee3\u7406\u670d\u52a1\u5668\u9700\u8981\u652f\u6301\u591a\u4e2a\u4e0a\u6e38\u63d0\u4f9b\u5546\u7684\u81ea\u52a8\u6545\u969c\u8f6c\u79fb\uff0c\u5f53\u4e3b\u63d0\u4f9b\u5546\u8fd4\u56de\u9519\u8bef\u65f6\u81ea\u52a8\u5207\u6362\u5230\u5907\u7528\u63d0\u4f9b\u5546\u3002",
- "### What prevents this today?",
- "\u76ee\u524d\u4ee3\u7406\u4e0d\u652f\u6301\u6545\u969c\u8f6c\u79fb\uff0c\u9700\u8981\u624b\u52a8\u91cd\u542f\u5e76\u66f4\u6539\u914d\u7f6e\u3002",
- "### What should OpenCodex do?",
- "\u5f53\u4e3b\u63d0\u4f9b\u5546\u8fd4\u56de 5xx \u6216\u8d85\u65f6\u65f6\uff0c\u81ea\u52a8\u5c06\u8bf7\u6c42\u8f6c\u53d1\u5230\u914d\u7f6e\u7684\u5907\u7528\u63d0\u4f9b\u5546\u3002",
- "### Example usage or interface",
- "```json\n{\"routing\":{\"fallback_provider\":\"anthropic\"}}\n```",
- ].join("\n");
- const result = validateIssue({ title: "\u652f\u6301\u591a\u63d0\u4f9b\u5546\u6545\u969c\u8f6c\u79fb", body, labels: ["enhancement"] });
- assert.equal(result.kind, "feature");
- assert.equal(result.valid, true);
- });
-
- it("rejects terse goal sections that only contain a keyword, digit, or punctuation", () => {
- const terseGoals = ["API", "provider", "1", "/", "use CLI", "route 1"];
- for (const goal of terseGoals) {
- const result = validateIssue({
- title: "Improve feature request quality",
- body: featureBodyWithGoal(goal),
- labels: ["enhancement"],
- });
- assert.equal(result.kind, "feature");
- assert.equal(result.valid, false, `Expected terse goal "${goal}" to be invalid`);
- assert.ok(
- result.reasons.some((r) => r.includes("too vague") || /missing or empty/i.test(r)),
- `Expected too vague or empty reason for "${goal}", got: ${result.reasons.join(", ")}`,
- );
- }
- });
-
- it("rejects a single long non-CJK word as overly terse", () => {
- const terseGoals = [
- "провайдер",
- "маршрут",
- "πάροχος",
- "واجهة",
- ];
- for (const goal of terseGoals) {
- assert.equal(countWords(goal), 1, `Expected "${goal}" to count as one word`);
- const result = validateIssue({
- title: "Improve feature request quality",
- body: featureBodyWithGoal(goal),
- labels: ["enhancement"],
- });
- assert.equal(result.kind, "feature");
- assert.equal(result.valid, false, `Expected terse goal "${goal}" to be invalid`);
- assert.ok(
- result.reasons.some((r) => r.includes("too vague")),
- `Expected too vague reason for "${goal}", got: ${result.reasons.join(", ")}`,
- );
- }
- });
-
- it("counts mixed-script CJK text without inflating non-CJK letter length", () => {
- assert.equal(countWords("provider中"), 2);
- assert.equal(countWords("провайдер中"), 2);
- assert.equal(countWords("configuration中"), 2);
- assert.equal(countWords("中provider文"), 3);
- });
-
- it("rejects mixed-script CJK stubs that only inflate letter counts", () => {
- const terseGoals = ["provider中", "провайдер中", "configuration中"];
- for (const goal of terseGoals) {
- assert.ok(countWords(goal) < 8, `Expected "${goal}" to stay under 8 units`);
- const result = validateIssue({
- title: "Improve feature request quality",
- body: featureBodyWithGoal(goal),
- labels: ["enhancement"],
- });
- assert.equal(result.kind, "feature");
- assert.equal(result.valid, false, `Expected terse goal "${goal}" to be invalid`);
- assert.ok(
- result.reasons.some((r) => r.includes("too vague")),
- `Expected too vague reason for "${goal}", got: ${result.reasons.join(", ")}`,
- );
- }
- });
-
- it("accepts sufficiently detailed goal sections", () => {
- const detailedGoal =
- "Expose a dashboard setting to choose the fallback voice model and provider.";
- const detailedResult = validateIssue({
- title: "Voice fallback routing",
- body: featureBodyWithGoal(detailedGoal),
- labels: ["enhancement"],
- });
- assert.equal(detailedResult.kind, "feature");
- assert.equal(detailedResult.valid, true);
- assert.ok(countWords(detailedGoal) >= 8);
- });
-
- it("accepts a 6-7 word goal when it includes concrete technical detail", () => {
- const concreteGoal = "Route requests through the configured API provider.";
- assert.equal(countWords(concreteGoal), 7);
- assert.ok(countWords(concreteGoal) < 8);
- assert.ok(countWords(concreteGoal) >= 6);
- assert.equal(hasConcreteDetail(concreteGoal), true);
-
- const concreteResult = validateIssue({
- title: "Voice fallback routing",
- body: featureBodyWithGoal(concreteGoal),
- labels: ["enhancement"],
- });
- assert.equal(concreteResult.kind, "feature");
- assert.equal(concreteResult.valid, true);
- });
-
- it("rejects a 6-7 word goal that lacks concrete technical detail", () => {
- // Avoid keywords that count as concrete detail (api/provider/workflow/…).
- const vagueGoal = "Make this process easier for all users.";
- assert.equal(countWords(vagueGoal), 7);
- assert.equal(hasConcreteDetail(vagueGoal), false);
-
- const vagueResult = validateIssue({
- title: "Voice fallback routing",
- body: featureBodyWithGoal(vagueGoal),
- labels: ["enhancement"],
- });
- assert.equal(vagueResult.kind, "feature");
- assert.equal(vagueResult.valid, false);
- assert.ok(vagueResult.reasons.some((r) => r.includes("too vague")));
- });
-
- it("treats only commands, errors, paths, or exact actions as actionable reproduction detail", () => {
- assert.equal(hasActionableReproductionDetail("1. choose model deepseek\n2. send a message in codex plugin"), false);
- assert.equal(hasActionableReproductionDetail("I want to work with deepseek in VSCode, but it dont reply"), false);
- assert.equal(hasActionableReproductionDetail("1. ocx start --port 10100\n2. Send a request"), true);
- assert.equal(hasActionableReproductionDetail("Run ocx start and send any streaming request."), true);
- assert.equal(hasActionableReproductionDetail("ocx start on Raspberry Pi 4, send any streaming request."), true);
- assert.equal(hasActionableReproductionDetail("send a request"), false);
- assert.equal(hasActionableReproductionDetail("make a call"), false);
- assert.equal(hasActionableReproductionDetail("post a command"), false);
- assert.equal(hasActionableReproductionDetail("make an API call"), true);
- assert.equal(hasActionableReproductionDetail("send an HTTP request"), true);
- assert.equal(hasActionableReproductionDetail("send a request to /v1/responses"), true);
- assert.equal(hasActionableReproductionDetail("The proxy returns HTTP 502 after the first streaming chunk."), true);
- assert.equal(hasActionableReproductionDetail("Paste ~/.codex/config.toml, then restart the proxy."), true);
- assert.equal(hasActionableReproductionDetail("```\n\n```"), false);
- assert.equal(hasActionableReproductionDetail("~~~\n\n~~~"), false);
- assert.equal(hasActionableReproductionDetail("```\nSIGSEGV at 0x0000\n```"), true);
- });
-
- it("rejects fenced placeholder-only examples", () => {
- const fencedPlaceholders = [
- "```\nN/A\n```",
- "```text\nN/A\n```",
- "```json\nNot applicable.\n```",
- "~~~text\nN/A\n~~~",
- ];
- for (const example of fencedPlaceholders) {
- assert.equal(
- isPlaceholderOnlyValue(example),
- true,
- `Expected fenced placeholder to match: ${JSON.stringify(example)}`,
- );
- const result = validateIssue({
- title: "Voice fallback routing",
- body: featureBodyWithExample(example),
- labels: ["enhancement"],
- });
- assert.equal(result.valid, false, `Expected fenced placeholder to be invalid: ${JSON.stringify(example)}`);
- assert.ok(
- result.reasons.some((r) => r.includes("placeholder")),
- `Expected placeholder reason for ${JSON.stringify(example)}, got: ${result.reasons.join("; ")}`,
- );
- }
- });
-
- it("accepts real fenced examples that merely mention N/A", () => {
- const realExamples = [
- "```text\nThe API returns N/A when no provider is configured.\n```",
- '```json\n{"provider":"N/A","fallback":"anthropic"}\n```',
- ];
- for (const example of realExamples) {
- assert.equal(
- isPlaceholderOnlyValue(example),
- false,
- `Expected real example not to be placeholder-only: ${JSON.stringify(example)}`,
- );
- const result = validateIssue({
- title: "Voice fallback routing",
- body: featureBodyWithExample(example),
- labels: ["enhancement"],
- });
- assert.equal(
- result.valid,
- true,
- `Expected real fenced example to remain valid, got: ${result.reasons.join("; ")}`,
- );
- }
- });
-});
-// ---------------------------------------------------------------------------
-// Validation: bug
-// ---------------------------------------------------------------------------
-
-describe("validateIssue - bug", () => {
- it("rejects an empty bug report", () => {
- const body = [
- "### Client or integration",
- "Codex CLI",
- "### Area",
- "CLI",
- "### Summary",
- "No response",
- "### Reproduction",
- "No response",
- "### Version",
- "No response",
- "### Operating system",
- "No response",
- ].join("\n");
- const result = validateIssue({ title: "Bug", body, labels: ["bug"] });
- assert.equal(result.kind, "bug");
- assert.equal(result.valid, false);
- });
-
- it("rejects a bug with Summary filled but Reproduction empty", () => {
- const body = [
- "### Client or integration",
- "Codex CLI",
- "### Area",
- "CLI",
- "### Summary",
- "The 'gpt-5.6-sol' model is not supported when using Codex with a ChatGPT account.",
- "### Reproduction",
- "No response",
- "### Version",
- "2.7.42",
- "### Operating system",
- "macOS",
- ].join("\n");
- const result = validateIssue({ title: "Open Codex Error", body, labels: ["bug"] });
- assert.equal(result.kind, "bug");
- assert.equal(result.valid, false);
- assert.ok(result.reasons.some((r) => /Reproduction is empty/i.test(r)));
- assert.ok(!result.reasons.some((r) => /Summary is empty/i.test(r)));
- });
-
- it("rejects a bug whose Reproduction is only an ellipsis (#598)", () => {
- const body = [
- "### Client or integration",
- "Codex CLI",
- "### Area",
- "CLI",
- "### Summary",
- "{\"detail\":\"The 'gpt-5.6-sol' model is not supported when using Codex with a ChatGPT account.\"}",
- "### Reproduction",
- "...",
- "### Version",
- "2.7.42",
- "### Operating system",
- "mac os",
- ].join("\n");
- const result = validateIssue({ title: "Open Codex Error", body, labels: ["bug"] });
- assert.equal(result.kind, "bug");
- assert.equal(result.valid, false);
- assert.ok(result.reasons.some((r) => /Reproduction is empty/i.test(r)));
- });
-
- it("rejects a bug with Reproduction filled but Summary empty", () => {
- const body = [
- "### Client or integration",
- "Codex CLI",
- "### Area",
- "CLI",
- "### Summary",
- "",
- "### Reproduction",
- "1. Run ocx start\n2. Send a request",
- "### Version",
- "2.7.42",
- "### Operating system",
- "macOS",
- ].join("\n");
- const result = validateIssue({ title: "Crash", body, labels: ["bug"] });
- assert.equal(result.kind, "bug");
- assert.equal(result.valid, false);
- assert.ok(result.reasons.some((r) => /Summary is empty/i.test(r)));
- });
-
- it("accepts a terse real crash report", () => {
- const body = [
- "### Client or integration",
- "Codex CLI",
- "### Area",
- "Proxy and routing",
- "### Summary",
- "Proxy segfaults on ARM64 when streaming is enabled.",
- "### Reproduction",
- "ocx start on Raspberry Pi 4, send any streaming request.",
- "### Version",
- "2.7.30",
- "### Operating system",
- "Debian 12 aarch64",
- "### Logs or error output",
- "```",
- "SIGSEGV at 0x0000 in bun_runtime",
- "```",
- ].join("\n");
- const result = validateIssue({ title: "Segfault on ARM64 streaming", body, labels: ["bug"] });
- assert.equal(result.kind, "bug");
- assert.equal(result.valid, true);
- });
-
- it("accepts a valid legacy bug report without version/OS headings", () => {
- const body = [
- "### Summary",
- "The proxy crashes when streaming is enabled.",
- "### Reproduction",
- "Run ocx start and send a streaming request.",
- ].join("\n");
- const result = validateIssue({ title: "[Bug]: crash on streaming", body, labels: ["bug"] });
- assert.equal(result.kind, "bug");
- assert.equal(result.valid, true, `Expected valid but got reasons: ${result.reasons.join(", ")}`);
- });
-
- it("accepts a legacy bug with _No response_ in old optional env fields", () => {
- const body = [
- "### Summary",
- "Proxy crashes on startup.",
- "### Reproduction",
- "Run ocx start.",
- "### Version",
- "_No response_",
- "### OS",
- "_No response_",
- ].join("\n");
- const result = validateIssue({ title: "[Bug]: crash", body, labels: ["bug"] });
- assert.equal(result.kind, "bug");
- assert.equal(result.valid, true, `Expected valid but got: ${result.reasons.join(", ")}`);
- });
-
- it("accepts a legacy bug with N/A-style placeholders in Version and Operating system", () => {
- for (const placeholder of ["N/A", "NA", "Not applicable.", "Not available!"]) {
- const body = [
- "### Summary",
- "Proxy crashes on startup when streaming is enabled.",
- "### Reproduction",
- "Run ocx start and send any streaming request.",
- "### Version",
- placeholder,
- "### Operating system",
- placeholder,
- ].join("\n");
- const result = validateIssue({ title: "[Bug]: crash", body, labels: ["bug"] });
- assert.equal(result.kind, "bug");
- assert.equal(
- result.valid,
- true,
- `Expected legacy env placeholder "${placeholder}" to remain valid, got: ${result.reasons.join(", ")}`,
- );
- assert.ok(!result.reasons.some((r) => r.includes("Version")));
- }
- });
-
- it("rejects a new-form bug where env fields were actively cleared", () => {
- const body = [
- "### Client or integration",
- "Codex CLI",
- "### Summary",
- "Proxy crashes.",
- "### Reproduction",
- "Run ocx start.",
- "### Version",
- "",
- "### Operating system",
- "",
- ].join("\n");
- const result = validateIssue({ title: "Crash", body, labels: ["bug"] });
- assert.equal(result.kind, "bug");
- assert.equal(result.valid, false);
- assert.ok(result.reasons.some((r) => r.includes("Version")));
- });
-
- it("rejects unknown / don't-know Version values (#624)", () => {
- const versions = [
- "Unknown",
- "Uknown",
- "unkown",
- "Don't know",
- "dont know",
- "idk",
- "모름",
- "잘 모름",
- "?",
- "???",
- ];
- for (const version of versions) {
- const body = [
- "### Client or integration",
- "Codex CLI",
- "### Area",
- "CLI",
- "### Summary",
- "The OpenCodex proxy keeps dropping the Codex CLI connection mid-request.",
- "### Reproduction",
- "1. ocx start --port 10100",
- "2. Send any Codex CLI request through the proxy",
- "3. Observe the connection drop",
- "### Version",
- version,
- "### Operating system",
- "Windows 11",
- ].join("\n");
- const result = validateIssue({
- title: "Unexpected interruption continues to occur",
- body,
- labels: ["bug"],
- });
- assert.equal(result.kind, "bug");
- assert.equal(
- result.valid,
- false,
- `Expected unusable Version "${version}" to be invalid, got: ${result.reasons.join("; ")}`,
- );
- assert.ok(
- result.reasons.some((r) => /Version/i.test(r) && /unknown|missing/i.test(r)),
- `Expected Version unknown/missing reason for "${version}", got: ${result.reasons.join("; ")}`,
- );
- }
- });
-
- it("rejects issue #624-style low-effort new-form bug", () => {
- const body = [
- "### Client or integration",
- "Codex CLI",
- "### Area",
- "CLI",
- "### Summary",
- "CLI로 확인해봤는데 오픈코덱스 프록시가 중간에 자꾸 연결이 끊어져서 그런거라고 합니다.",
- "",
- "수정 바랍니다.",
- "### Reproduction",
- "예기치않게중단됨",
- "### Version",
- "모름",
- "### Operating system",
- "윈11",
- "### Provider and model",
- "_No response_",
- "### Logs or error output",
- "```shell",
- "",
- "```",
- ].join("\n");
- const result = validateIssue({
- title: "Unexpected interruption continues to occur",
- body,
- labels: ["bug"],
- });
- assert.equal(result.kind, "bug");
- assert.equal(result.valid, false);
- assert.ok(result.reasons.some((r) => /Version/i.test(r)));
- assert.ok(result.reasons.some((r) => /Reproduction/i.test(r) && /vague|empty/i.test(r)));
- });
-
- it("rejects a new-form bug with a usable Version but placeholder OS", () => {
- const body = [
- "### Client or integration",
- "Codex CLI",
- "### Area",
- "CLI",
- "### Summary",
- "Proxy returns 502 when streaming is enabled on Windows.",
- "### Reproduction",
- "1. ocx start",
- "2. Send a streaming /v1/responses request",
- "### Version",
- "2.7.42",
- "### Operating system",
- "No response",
- ].join("\n");
- const result = validateIssue({ title: "Streaming 502", body, labels: ["bug"] });
- assert.equal(result.kind, "bug");
- assert.equal(result.valid, false);
- assert.ok(result.reasons.some((r) => /Operating system/i.test(r)));
- });
-
- it("rejects a new-form bug when the Version heading was removed", () => {
- const body = [
- "### Client or integration",
- "Codex CLI",
- "### Area",
- "CLI",
- "### Summary",
- "Proxy returns 502 when streaming is enabled on Windows.",
- "### Reproduction",
- "1. ocx start",
- "2. Send a streaming /v1/responses request",
- "### Operating system",
- "Windows 11",
- ].join("\n");
- const result = validateIssue({ title: "Streaming 502", body, labels: ["bug"] });
- assert.equal(result.kind, "bug");
- assert.equal(result.valid, false);
- assert.ok(result.reasons.some((r) => /Version/i.test(r) && /missing/i.test(r)));
- });
-
- it("rejects a new-form bug when the Operating system heading was removed", () => {
- const body = [
- "### Client or integration",
- "Codex CLI",
- "### Area",
- "CLI",
- "### Summary",
- "Proxy returns 502 when streaming is enabled on Windows.",
- "### Reproduction",
- "1. ocx start",
- "2. Send a streaming /v1/responses request",
- "### Version",
- "2.7.42",
- ].join("\n");
- const result = validateIssue({ title: "Streaming 502", body, labels: ["bug"] });
- assert.equal(result.kind, "bug");
- assert.equal(result.valid, false);
- assert.ok(result.reasons.some((r) => /Operating system/i.test(r) && /missing/i.test(r)));
- });
-
- it("rejects a new-form bug whose Reproduction is only a vague phrase", () => {
- const body = [
- "### Client or integration",
- "Codex CLI",
- "### Area",
- "CLI",
- "### Summary",
- "The OpenCodex proxy keeps dropping the Codex CLI connection mid-request.",
- "### Reproduction",
- "Unexpected interruption",
- "### Version",
- "2.7.42",
- "### Operating system",
- "Windows 11",
- ].join("\n");
- const result = validateIssue({
- title: "Unexpected interruption continues to occur",
- body,
- labels: ["bug"],
- });
- assert.equal(result.kind, "bug");
- assert.equal(result.valid, false);
- assert.ok(result.reasons.some((r) => /Reproduction/i.test(r) && /vague/i.test(r)));
- });
-
- it("rejects a #977-shaped bug with product keywords but no actionable reproduction", () => {
- const body = [
- "### Client or integration",
- "Other",
- "### Area",
- "Proxy and routing",
- "### Summary",
- "I want to work with deepseek in VSCode, but it dont reply,just thinking",
- "### Reproduction",
- "1.choose model deepseek",
- "2.send a message in codex plugin",
- "### Version",
- "2.10.0",
- "### Operating system",
- "Ubuntu 24.04",
- "### Provider and model",
- "deepseek",
- ].join("\n");
- const result = validateIssue({
- title: "Dont work in VSCode Codex plugin",
- body,
- labels: ["bug", "proxy"],
- });
- assert.equal(result.kind, "bug");
- assert.equal(result.valid, false);
- assert.ok(
- result.reasons.some((r) => /Reproduction/i.test(r) && /vague/i.test(r)),
- `Expected a vague Reproduction reason, got: ${result.reasons.join("; ")}`,
- );
- assert.ok(
- result.guidance.some((g) => /commands|steps/i.test(g)),
- `Expected reproduction guidance, got: ${result.guidance.join("; ")}`,
- );
- });
-
- it("rejects unknown Operating system stand-ins on the new bug form", () => {
- const body = [
- "### Client or integration",
- "Codex CLI",
- "### Area",
- "CLI",
- "### Summary",
- "The OpenCodex proxy keeps dropping the Codex CLI connection mid-request.",
- "### Reproduction",
- "1. ocx start --port 10100",
- "2. Send any Codex CLI request through the proxy",
- "3. Observe the connection drop",
- "### Version",
- "2.7.42",
- "### Operating system",
- "Unknown",
- ].join("\n");
- const result = validateIssue({
- title: "Unexpected interruption continues to occur",
- body,
- labels: ["bug"],
- });
- assert.equal(result.kind, "bug");
- assert.equal(result.valid, false);
- assert.ok(result.reasons.some((r) => /Operating system/i.test(r)));
- });
-});
-
-// ---------------------------------------------------------------------------
-// Validation: provider-compatibility
-// ---------------------------------------------------------------------------
-
-describe("validateIssue - provider-compatibility", () => {
- it("rejects when request and response are both absent", () => {
- const body = [
- "### Client or integration",
- "Codex CLI",
- "### Provider or upstream service",
- "mistral",
- "### OpenCodex version",
- "2.7.31",
- "### Endpoint or capability",
- "/v1/chat/completions",
- "### Current behaviour",
- "Returns 500.",
- "### Expected behaviour",
- "Returns 200.",
- "### Minimal redacted request or reproduction",
- "No response",
- "### Actual response or error",
- "No response",
- "### Upstream documentation",
- "https://docs.mistral.ai/api/",
- ].join("\n");
- const result = validateIssue({ title: "Mistral 500", body, labels: ["enhancement"] });
- assert.equal(result.kind, "provider-compatibility");
- assert.equal(result.valid, false);
- });
-
- it("accepts a complete provider compatibility report", () => {
- const body = [
- "### Client or integration",
- "Codex CLI",
- "### Provider or upstream service",
- "anthropic",
- "### OpenCodex version",
- "2.7.31",
- "### Endpoint or capability",
- "/v1/messages",
- "### Current behaviour",
- "Proxy strips the system field from the request.",
- "### Expected behaviour",
- "Proxy preserves the system field as documented.",
- "### Minimal redacted request or reproduction",
- "curl -X POST http://localhost:10100/v1/messages -d '{\"model\":\"claude-sonnet-4-20250514\",\"system\":\"You are helpful.\",\"messages\":[]}'",
- "### Actual response or error",
- "400: system is required",
- "### Upstream documentation",
- "https://docs.anthropic.com/en/api/messages",
- ].join("\n");
- const result = validateIssue({ title: "System field stripped", body, labels: ["enhancement"] });
- assert.equal(result.kind, "provider-compatibility");
- assert.equal(result.valid, true);
- });
-
- it("rejects provider compat report when provider/endpoint fields are cleared", () => {
- const body = [
- "### Client or integration",
- "Codex CLI",
- "### Provider or upstream service",
- "",
- "### OpenCodex version",
- "2.7.31",
- "### Endpoint or capability",
- "",
- "### Current behaviour",
- "Returns 400.",
- "### Expected behaviour",
- "Returns 200.",
- "### Minimal redacted request or reproduction",
- "curl ...",
- "### Actual response or error",
- "400 Bad Request",
- "### Upstream documentation",
- "https://docs.example.com",
- ].join("\n");
- const result = validateIssue({ title: "400 error", body, labels: ["provider-compatibility"] });
- assert.equal(result.kind, "provider-compatibility");
- assert.equal(result.valid, false);
- assert.ok(result.reasons.some((r) => r.includes("provider")));
- });
-});
-
-// ---------------------------------------------------------------------------
-// Validation: documentation
-// ---------------------------------------------------------------------------
-
-describe("validateIssue - documentation", () => {
- it("rejects an empty documentation report", () => {
- const body = [
- "### Documentation problem type",
- "Missing documentation",
- "### Documentation location",
- "No response",
- "### What is wrong or missing?",
- "No response",
- "### What should the documentation explain instead?",
- "No response",
- ].join("\n");
- const result = validateIssue({ title: "Docs", body, labels: ["documentation"] });
- assert.equal(result.kind, "documentation");
- assert.equal(result.valid, false);
- });
-
- it("accepts a complete documentation correction", () => {
- const body = [
- "### Documentation problem type",
- "Incorrect documentation",
- "### Documentation location",
- "https://lidge-jun.github.io/opencodex/providers/",
- "### What is wrong or missing?",
- "The page says kimi uses /v1/chat/completions but it actually uses /v1/responses.",
- "### What should the documentation explain instead?",
- "Update the endpoint to /v1/responses and add a note about the model discovery step.",
- ].join("\n");
- const result = validateIssue({ title: "Wrong kimi endpoint in docs", body, labels: ["documentation"] });
- assert.equal(result.kind, "documentation");
- assert.equal(result.valid, true);
- });
-});
-
-// ---------------------------------------------------------------------------
-// Normalisation
-// ---------------------------------------------------------------------------
-
-describe("normalisation", () => {
- it("treats 'No response' as empty", () => {
- assert.equal(clean("No response"), "");
- assert.equal(clean("_No response_"), "");
- });
-
- it("treats NA and not applicable as placeholders", () => {
- assert.equal(clean("NA"), "");
- assert.equal(clean("N/A"), "");
- assert.equal(clean("_N/A_"), "");
- assert.equal(clean("NA."), "");
- assert.equal(clean("N/A."), "");
- assert.equal(clean("not applicable"), "");
- assert.equal(clean("Not applicable."), "");
- assert.equal(clean("Not available!"), "");
- });
-
- it("detects unusable Version stand-ins without treating them as generic placeholders", () => {
- for (const value of ["Unknown", "Uknown", "모름", "idk", "don't know"]) {
- assert.equal(isUnusableVersion(value), true, value);
- assert.equal(isPlaceholderOnlyValue(value), false, value);
- }
- for (const value of ["2.7.42", "N/A", "No response", "main@abc1234"]) {
- assert.equal(isUnusableVersion(value), false, value);
- }
- });
-
- it("does not treat sentences containing placeholder phrases as empty", () => {
- assert.equal(clean("This is N/A for voice mode today."), "This is N/A for voice mode today.");
- assert.equal(clean("Not applicable to Claude Code."), "Not applicable to Claude Code.");
- });
-
- it("shares one placeholder matcher across clean, isPlaceholder, and isRawPlaceholder", () => {
- const placeholders = [
- "No response",
- "NA",
- "N/A",
- "_N/A_",
- "NA.",
- "N/A.",
- "None",
- "Todo",
- "TBD",
- "Not applicable",
- "Not applicable.",
- "Not available!",
- "```\nN/A\n```",
- "```text\nN/A\n```",
- "```json\nNot applicable.\n```",
- "~~~text\nN/A\n~~~",
- ];
- for (const value of placeholders) {
- assert.equal(isPlaceholderOnlyValue(value), true, value);
- assert.equal(isPlaceholder(value), true, value);
- assert.equal(isRawPlaceholder(value), true, value);
- assert.equal(clean(value), "", value);
- }
- assert.equal(isPlaceholderOnlyValue("Route voice traffic to provider N/A fallback"), false);
- assert.equal(isPlaceholderOnlyValue("```text\nThe API returns N/A when no provider is configured.\n```"), false);
- assert.equal(isPlaceholderOnlyValue('```json\n{"provider":"N/A","fallback":"anthropic"}\n```'), false);
- assert.equal(isPlaceholder("use CLI"), false);
- assert.equal(isRawPlaceholder(""), false);
- assert.equal(isRawPlaceholder(null), false);
- });
-
- it("strips HTML comments", () => {
- assert.equal(clean("Hello world"), "Hello world");
- });
-
- it("normalises punctuation and capitalisation", () => {
- assert.equal(normalise("Hello, World!"), normalise("hello world"));
- });
-
- it("removes filler phrases", () => {
- const a = canonicalise("I want to add streaming support");
- const b = canonicalise("add streaming support");
- assert.equal(a, b);
- });
-});
-
-// ---------------------------------------------------------------------------
-// extractSection
-// ---------------------------------------------------------------------------
-
-describe("extractSection", () => {
- it("extracts content between headings", () => {
- const body = "### Summary\nProxy crashes.\n### Reproduction\nRun ocx start.";
- assert.equal(extractSection(body, "Summary"), "Proxy crashes.");
- assert.equal(extractSection(body, "Reproduction"), "Run ocx start.");
- });
-
- it("returns null for missing sections", () => {
- assert.equal(extractSection("### Summary\nHello", "Reproduction"), null);
- });
-});
-
-// ---------------------------------------------------------------------------
-// Closure ownership (shouldReopen)
-// ---------------------------------------------------------------------------
-
-describe("shouldReopen", () => {
- const baseBotState = {
- version: 2,
- active: true,
- kind: "feature",
- closedAt: "2026-07-20T10:00:00Z",
- stateReason: "not_planned",
- };
-
- it("allows reopen when timestamps and state match", () => {
- const issue = { state: "closed", closed_at: "2026-07-20T10:00:00Z", state_reason: "not_planned" };
- assert.equal(shouldReopen(baseBotState, issue, false), true);
- });
-
- it("forbids reopen when timestamp differs", () => {
- const issue = { state: "closed", closed_at: "2026-07-21T12:00:00Z", state_reason: "not_planned" };
- assert.equal(shouldReopen(baseBotState, issue, false), false);
- });
-
- it("forbids reopen when state reason differs", () => {
- const issue = { state: "closed", closed_at: "2026-07-20T10:00:00Z", state_reason: "completed" };
- assert.equal(shouldReopen(baseBotState, issue, false), false);
- });
-
- it("forbids reopen when bot state is inactive", () => {
- const inactive = { ...baseBotState, active: false };
- const issue = { state: "closed", closed_at: "2026-07-20T10:00:00Z", state_reason: "not_planned" };
- assert.equal(shouldReopen(inactive, issue, false), false);
- });
-
- it("returns false when issue is already open", () => {
- const issue = { state: "open", closed_at: null, state_reason: null };
- assert.equal(shouldReopen(baseBotState, issue, false), false);
- });
-
- it("forbids reopen on maintainer override", () => {
- const issue = { state: "closed", closed_at: "2026-07-20T10:00:00Z", state_reason: "not_planned" };
- assert.equal(shouldReopen(baseBotState, issue, true), false);
- });
-
- it("forbids reopen when a human closed the issue (closed_by is not the bot)", () => {
- const issue = {
- state: "closed",
- closed_at: "2026-07-20T10:00:00Z",
- state_reason: "not_planned",
- closed_by: "lidge-jun",
- };
- assert.equal(shouldReopen(baseBotState, issue, false), false);
- });
-
- it("allows reopen when the bot is the recorded closer", () => {
- const issue = {
- state: "closed",
- closed_at: "2026-07-20T10:00:00Z",
- state_reason: "not_planned",
- closed_by: "github-actions[bot]",
- };
- assert.equal(shouldReopen(baseBotState, issue, false), true);
- });
-});
-
-describe("shouldEnforceClosure", () => {
- it("enforces when there is no bot state yet", () => {
- assert.equal(shouldEnforceClosure(null), true);
- });
-
- it("enforces while the bot still owns an active closure", () => {
- assert.equal(
- shouldEnforceClosure({
- version: 2,
- active: true,
- kind: "feature",
- closedAt: "2026-07-20T10:00:00Z",
- stateReason: "not_planned",
- }),
- true,
- );
- });
-
- it("does not enforce after a maintainer override", () => {
- assert.equal(
- shouldEnforceClosure({
- version: 2,
- active: false,
- kind: "feature",
- closedAt: "2026-07-20T10:00:00Z",
- stateReason: "not_planned",
- maintainerOverride: true,
- }),
- false,
- );
- });
-
- it("still enforces after a normal active:false without maintainer override", () => {
- assert.equal(
- shouldEnforceClosure({
- version: 2,
- active: false,
- kind: "feature",
- closedAt: "2026-07-20T10:00:00Z",
- stateReason: "not_planned",
- }),
- true,
- );
- });
-});
-
-// ---------------------------------------------------------------------------
-// Translated / soft-pass / labels
-// ---------------------------------------------------------------------------
-
-describe("translated feature headings and soft-pass", () => {
- it("accepts Goal / Problem + Expected behaviour as a valid feature", () => {
- const body = [
- "### Goal / Problem",
- "Codex App rejects image paste for noVisionModels before the vision sidecar can run.",
- "### Expected behaviour",
- "Catalog should advertise image input when the vision sidecar covers the model.",
- "### Environment",
- "opencodex 2.7.36 on macOS with Codex App.",
- ].join("\n");
- const result = validateIssue({
- title: "[Feature]: Auto-advertise image inputModalities for noVisionModels",
- body,
- labels: [],
- });
- assert.equal(result.kind, "feature");
- assert.equal(result.valid, true, `Expected valid but got: ${result.reasons.join("; ")}`);
- assert.equal(result.softPass, false);
- });
-
- it("soft-passes [Feature]: with rich custom headings outside the alias map", () => {
- const body = [
- "### Concrete user workflow that fails",
- "User pastes an image in Codex App while a text-only routed model is selected and the App blocks upload.",
- "### Why this matters",
- "Vision sidecar is advertised but never reached from the App client path.",
- "### Verification",
- "Same proxy config works end-to-end in Claude Code with the sidecar describing the image.",
- ].join("\n");
- const result = validateIssue({
- title: "[Feature]: Vision sidecar unusable from Codex App",
- body,
- labels: [],
- });
- assert.equal(result.kind, "feature");
- assert.equal(result.softPass, true);
- assert.equal(result.valid, false);
- });
-
- it("soft-passes retitled feature reports that drop the [Feature]: prefix", () => {
- const body = [
- "### Concrete user workflow that fails",
- "User pastes an image in Codex App while a text-only routed model is selected and the App blocks upload.",
- "### Why this matters",
- "Vision sidecar is advertised but never reached from the App client path.",
- "### Verification",
- "Same proxy config works end-to-end in Claude Code with the sidecar describing the image.",
- ].join("\n");
- const result = validateIssue({
- title: "Vision sidecar unusable from Codex App",
- body,
- labels: ["enhancement"],
- storedKind: "feature",
- });
- assert.equal(result.kind, "feature");
- assert.equal(result.softPass, true);
- });
-
- it("soft-passes retitled bug reports with substantial non-English structure (#545)", () => {
- // Maintainer retitle removed `[Bug]:`; Korean structured body has no English
- // Summary/Reproduction headings but is clearly actionable.
- const body = [
- "## 환경",
- "- opencodex 2.7.41 (launchd, port 10100)",
- "- Claude Desktop 3P + Anthropic OAuth (Pro/Max)",
- "- Auto Mode classifier model = `claude-sonnet-5`",
- "",
- "## 증상",
- "Auto Mode classifier requests truncate at outputTokens=64 with max_output_tokens,",
- "then retry the same payload up to 5 times. Dashboard previously showed 502.",
- "",
- "## 재현",
- "1. `ocx login anthropic` and enable Claude Desktop 3P gateway key mode",
- "2. Enable Auto Mode and trigger a tool permission classifier turn",
- "3. Observe five identical 64-token incomplete terminals for one approval",
- "",
- "## 증거",
- "Inbound+outbound correlated captures show max_tokens:64 and stop_sequences preserved.",
- ].join("\n");
- const result = validateIssue({
- title: "Claude Desktop 3P Auto Mode classifier retries after 64-token Anthropic OAuth outputs",
- body,
- labels: ["bug", "provider-compatibility"],
- storedKind: "bug",
- });
- assert.equal(result.kind, "bug");
- assert.equal(result.softPass, true, `Expected soft-pass but got: ${result.reasons.join("; ")}`);
- assert.equal(result.valid, false);
- });
-
- it("does not soft-pass a single arbitrary rich heading (Codex #564)", () => {
- const result = validateIssue({
- title: "Something broke after upgrade",
- body: [
- "## Notes",
- "x".repeat(80),
- ].join("\n"),
- labels: ["bug"],
- storedKind: "bug",
- });
- assert.equal(result.kind, "bug");
- assert.equal(result.softPass, false);
- assert.equal(result.valid, false);
- assert.match(result.reasons.join(" "), /Summary and Reproduction are empty/);
- });
-
- it("does not soft-pass provider reports that only fill mapped metadata headings", () => {
- const result = validateIssue({
- title: "Provider X fails on Responses",
- body: [
- "### Provider or upstream service",
- "custom-openai-compatible gateway hosted on our internal mesh",
- "### OpenCodex version",
- "2.7.41",
- "### Endpoint or capability",
- "`POST /v1/responses` with streaming tool calls",
- "## Extra notes",
- "We see intermittent 502s after rotating the upstream API key for this gateway.",
- ].join("\n"),
- labels: ["provider-compatibility"],
- storedKind: "provider-compatibility",
- });
- assert.equal(result.kind, "provider-compatibility");
- assert.equal(result.softPass, false);
- assert.equal(result.valid, false);
- assert.match(result.reasons.join(" "), /current behaviour|expected behaviour/i);
- });
-
- it("still rejects empty [Feature]: bodies", () => {
- const result = validateIssue({
- title: "[Feature]: do something cool",
- body: "please add this",
- labels: [],
- });
- assert.equal(result.kind, "feature");
- assert.equal(result.valid, false);
- assert.equal(result.softPass, false);
- });
-
- it("does not treat a title containing problem as a bug", () => {
- assert.equal(
- detectIssueKind({
- title: "Problem with documentation wording",
- body: "The docs are confusing about install.",
- labels: [],
- }),
- null,
- );
- });
-
- it("does not soft-pass long unstructured bodies without headings", () => {
- const result = validateIssue({
- title: "[Feature]: please add thing",
- body: "x".repeat(250),
- labels: [],
- });
- assert.equal(result.kind, "feature");
- assert.equal(result.softPass, false);
- assert.equal(result.valid, false);
- });
-
- it("does not classify Expected behaviour + Example as feature without a feature hint", () => {
- assert.equal(
- detectIssueKind({
- title: "Something broke in the proxy",
- body: [
- "### Expected behaviour",
- "Proxy should return 200.",
- "### Example",
- "curl localhost:10100/v1/responses",
- ].join("\n"),
- labels: [],
- }),
- null,
- );
- });
-
- it("classifies alias headings as feature when a goal heading is present", () => {
- assert.equal(
- detectIssueKind({
- title: "Advertise image input for sidecar models",
- body: [
- "### Goal / Problem",
- "App blocks images before the sidecar runs.",
- "### Expected behaviour",
- "Catalog should advertise image input.",
- ].join("\n"),
- labels: [],
- }),
- "feature",
- );
- });
-
- it("lets a strong bug form override a stale stored feature kind", () => {
- const result = validateIssue({
- title: "Crash on start",
- body: [
- "### Client or integration",
- "Codex CLI",
- "### Summary",
- "Proxy segfaults on ARM64 when streaming is enabled.",
- "### Reproduction",
- "ocx start on Raspberry Pi 4, send any streaming request.",
- "### Version",
- "2.7.36",
- "### Operating system",
- "Linux",
- ].join("\n"),
- labels: ["bug"],
- storedKind: "feature",
- });
- assert.equal(result.kind, "bug");
- assert.equal(result.valid, true, `Expected valid bug but got: ${result.reasons.join("; ")}`);
- });
-
- it("accepts US spelling Expected behavior as a behaviour alias", () => {
- const result = validateIssue({
- title: "[Feature]: Auto-advertise image inputModalities",
- body: [
- "### Goal / Problem",
- "App blocks images before the vision sidecar can run.",
- "### Expected behavior",
- "Catalog should advertise image input when the sidecar covers the model.",
- ].join("\n"),
- labels: [],
- });
- assert.equal(result.kind, "feature");
- assert.equal(result.valid, true, `Expected valid but got: ${result.reasons.join("; ")}`);
- });
-
- it("does not treat enhancement + non-goal aliases as a feature detect hit", () => {
- assert.equal(
- detectIssueKind({
- title: "Something odd in the proxy",
- body: [
- "### Current limitation",
- "No fallback provider today for upstream 5xx responses.",
- "### Expected behaviour",
- "Auto failover to a backup provider.",
- ].join("\n"),
- labels: ["enhancement"],
- }),
- null,
- );
- });
-
- it("does not let a weak title-prefix detection override stored documentation kind", () => {
- assert.equal(
- detectIssueKind({
- title: "[Feature]: rewrite the docs",
- body: "Still working on the write-up.",
- labels: [],
- storedKind: "documentation",
- }),
- "documentation",
- );
- });
-});
-
-describe("labelForKind", () => {
- it("maps kinds to triage labels", () => {
- assert.equal(labelForKind("bug"), "bug");
- assert.equal(labelForKind("feature"), "enhancement");
- assert.equal(labelForKind("documentation"), "documentation");
- assert.equal(labelForKind("provider-compatibility"), "provider-compatibility");
- assert.equal(labelForKind(null), null);
- assert.equal(labelForKind("unknown"), null);
- });
-});
-
-// ---------------------------------------------------------------------------
-// Freeform / non-template bypass (e.g. issue #521)
-// ---------------------------------------------------------------------------
-
-describe("validateIssue - freeform / non-template", () => {
- it("rejects a plain freeform body that previously skipped validation", () => {
- const result = validateIssue({
- title: "How do I configure?",
- body: "Just a random question about setup.",
- labels: [],
- });
- assert.equal(result.kind, null);
- assert.equal(result.valid, false);
- assert.equal(result.softPass, false);
- assert.ok(result.reasons.some((r) => /recognized issue template/i.test(r)));
- assert.ok(result.guidance.some((g) => /Bug report|Feature request/i.test(g)));
- });
-
- it("rejects a #521-shaped Description/Reproduction/Log entry body with a clear message", () => {
- const body = [
- "### Description",
- "Proxy returns 502 when streaming is enabled on Windows.",
- "### Reproduction",
- "1. ocx start",
- "2. Send a streaming request",
- "3. Observe 502",
- "### Log entry",
- "```",
- "upstream connect error or disconnect/reset before headers",
- "```",
- ].join("\n");
- assert.equal(
- detectIssueKind({ title: "Proxy 502 on streaming", body, labels: [] }),
- null,
- "near-miss headings must not silently classify as a structured bug",
- );
- assert.equal(looksLikeUntemplatedBugReport({ title: "Proxy 502 on streaming", body }), true);
-
- const result = validateIssue({
- title: "Proxy 502 on streaming",
- body,
- labels: [],
- });
- assert.equal(result.kind, null);
- assert.equal(result.valid, false);
- assert.ok(
- result.reasons.some((r) => /bug report/i.test(r) && /template/i.test(r)),
- `Expected bug-template reason, got: ${result.reasons.join("; ")}`,
- );
- assert.ok(
- result.guidance.some((g) => /Client or integration|Summary|Reproduction/i.test(g)),
- `Expected template-heading guidance, got: ${result.guidance.join("; ")}`,
- );
- });
-
- it("still detects and validates a real structured bug as before", () => {
- const body = [
- "### Client or integration",
- "Codex CLI",
- "### Summary",
- "Proxy segfaults on ARM64 when streaming is enabled.",
- "### Reproduction",
- "ocx start on Raspberry Pi 4, send any streaming request.",
- "### Version",
- "2.7.30",
- "### Operating system",
- "Debian 12 aarch64",
- ].join("\n");
- const result = validateIssue({
- title: "Segfault on ARM64 streaming",
- body,
- labels: ["bug"],
- });
- assert.equal(result.kind, "bug");
- assert.equal(result.valid, true);
- });
-
- it("does not treat Summary+Reproduction alone as a bug without prefix or label", () => {
- // Existing anti-false-positive rule; freeform gate still fails these as untemplated.
- const body = [
- "### Summary",
- "Something went wrong in the proxy.",
- "### Reproduction",
- "Run ocx start.",
- ].join("\n");
- assert.equal(detectIssueKind({ title: "Something went wrong", body, labels: [] }), null);
- const result = validateIssue({ title: "Something went wrong", body, labels: [] });
- assert.equal(result.kind, null);
- assert.equal(result.valid, false);
- });
-
- it("keeps label-backed storedKind validation for enhancement freeform", () => {
- // Workflow passes storedKind from the enhancement label; empty feature form still fails.
- const result = validateIssue({
- title: "How do I configure?",
- body: "Just a random question about setup.",
- labels: ["enhancement"],
- storedKind: "feature",
- });
- assert.equal(result.kind, "feature");
- assert.equal(result.valid, false);
- assert.ok(result.reasons.some((r) => /missing or empty/i.test(r)));
- });
-});
-
-// ---------------------------------------------------------------------------
-// workflow_dispatch guards
-// ---------------------------------------------------------------------------
-
-describe("rejectsWorkflowDispatchPullRequest", () => {
- it("rejects pull request numbers on workflow_dispatch", () => {
- assert.equal(
- rejectsWorkflowDispatchPullRequest({ pull_request: {} }, 423, "workflow_dispatch"),
- "#423 is a pull request. This workflow only accepts issue numbers.",
- );
- });
-
- it("allows issues and non-dispatch events", () => {
- assert.equal(rejectsWorkflowDispatchPullRequest({ pull_request: {} }, 423, "issues"), null);
- assert.equal(rejectsWorkflowDispatchPullRequest({}, 42, "workflow_dispatch"), null);
- });
-});
-
-describe("rejectsWorkflowDispatchNonDefaultBranch", () => {
- it("rejects workflow_dispatch runs that are not on the default branch", () => {
- assert.equal(
- rejectsWorkflowDispatchNonDefaultBranch(
- "workflow_dispatch",
- "refs/heads/fix/issue-quality-low-effort-reports",
- "main",
- ),
- "workflow_dispatch must run from the default branch (main); selected ref was refs/heads/fix/issue-quality-low-effort-reports.",
- );
- });
-
- it("allows default-branch dispatches and normal issue events", () => {
- assert.equal(
- rejectsWorkflowDispatchNonDefaultBranch("workflow_dispatch", "refs/heads/main", "main"),
- null,
- );
- assert.equal(
- rejectsWorkflowDispatchNonDefaultBranch(
- "issues",
- "refs/heads/fix/issue-quality-low-effort-reports",
- "main",
- ),
- null,
- );
- });
-});
-
-// ---------------------------------------------------------------------------
-// Orthogonal area labels
-// ---------------------------------------------------------------------------
-
-describe("mapAreaFieldToLabels", () => {
- it("maps canonical Area dropdown values", () => {
- assert.deepEqual(mapAreaFieldToLabels("CLI"), ["cli"]);
- assert.deepEqual(mapAreaFieldToLabels("Proxy and routing"), ["proxy"]);
- assert.deepEqual(mapAreaFieldToLabels("Dashboard"), ["gui"]);
- assert.deepEqual(mapAreaFieldToLabels("Provider adapter"), ["provider"]);
- assert.deepEqual(mapAreaFieldToLabels("Provider adapters"), ["provider"]);
- assert.deepEqual(mapAreaFieldToLabels("Authentication and account pool"), ["account-pool"]);
- assert.deepEqual(mapAreaFieldToLabels("Catalog / models"), ["catalog"]);
- assert.deepEqual(mapAreaFieldToLabels("Streaming"), ["streaming"]);
- assert.deepEqual(mapAreaFieldToLabels("Tools / MCP / web search"), ["tools"]);
- assert.deepEqual(mapAreaFieldToLabels("Installation or packaging"), ["install"]);
- assert.deepEqual(mapAreaFieldToLabels("Service lifecycle"), ["service"]);
- assert.deepEqual(mapAreaFieldToLabels("Platform (Windows / macOS / Linux)"), ["platform"]);
- assert.deepEqual(mapAreaFieldToLabels("Documentation"), []);
- });
-
- it("maps legacy Service lifecycle wording and ignores Other / Multiple areas", () => {
- assert.deepEqual(mapAreaFieldToLabels("Service lifecycle (config injection)"), ["service"]);
- assert.deepEqual(mapAreaFieldToLabels("Other"), []);
- assert.deepEqual(mapAreaFieldToLabels("Multiple areas"), []);
- assert.deepEqual(mapAreaFieldToLabels(""), []);
- assert.deepEqual(mapAreaFieldToLabels(null), []);
- });
-
- it("exposes metadata for every non-documentation area label", () => {
- for (const name of Object.keys(AREA_LABELS)) {
- assert.ok(AREA_LABELS[name].color, name);
- assert.ok(AREA_LABELS[name].description, name);
- }
- });
-});
-
-describe("detectAreaLabels", () => {
- it("applies Area mapping plus orthogonal heuristics", () => {
- const labels = detectAreaLabels({
- title: "Pool failover stalls on SSE without terminal frame",
- body: [
- "### Area",
- "Authentication and account pool",
- "### Summary",
- "Account pool failover waits forever when the upstream SSE stream ends without a terminal frame.",
- ].join("\n"),
- labels: ["bug"],
- });
- assert.ok(labels.includes("account-pool"));
- assert.ok(labels.includes("streaming"));
- });
-
- it("adds provider for provider-compatibility form and label", () => {
- const fromLabel = detectAreaLabels({
- title: "AgentRouter Anthropic streams can end without terminal SSE frames",
- body: "### Summary\nStream ends early.",
- labels: ["provider-compatibility"],
- });
- assert.ok(fromLabel.includes("provider"));
- assert.ok(fromLabel.includes("streaming"));
-
- const fromHeading = detectAreaLabels({
- title: "Custom relay rejects tool_calls",
- body: [
- "### Provider or upstream service",
- "Volcengine Ark",
- "### Current behaviour",
- "tool_calls with empty content return 400.",
- ].join("\n"),
- labels: [],
- });
- assert.ok(fromHeading.includes("provider"));
- assert.ok(fromHeading.includes("tools"));
- });
-
- it("runs heuristics for Multiple areas / Other without inventing per-provider labels", () => {
- const labels = detectAreaLabels({
- title: "Dashboard ACL hardening blocks management API on Windows",
- body: [
- "### Area",
- "Multiple areas",
- "### Summary",
- "Management API fails closed when icacls hardening cannot be verified.",
- ].join("\n"),
- labels: ["bug"],
- });
- assert.ok(labels.includes("gui"), `got ${labels.join(",")}`);
- assert.ok(labels.includes("platform"), `got ${labels.join(",")}`);
- assert.ok(labels.includes("proxy"), `got ${labels.join(",")}`);
- assert.equal(labels.includes("kiro"), false);
- assert.equal(labels.includes("gemini"), false);
- assert.equal(labels.includes("windows"), false);
- });
-
- it("does not map Documentation Area onto the documentation kind label", () => {
- const labels = detectAreaLabels({
- title: "Codex Auth UI/docs conflate usage-based switching",
- body: ["### Area", "Documentation", "### Summary", "Docs misdefine new session."].join("\n"),
- labels: ["enhancement"],
- });
- assert.equal(labels.includes("documentation"), false);
- assert.equal(labels.includes("docs"), false);
- });
-
- it("ignores Operating system metadata for platform heuristics", () => {
- const labels = detectAreaLabels({
- title: "Dashboard shows empty providers tab",
- body: [
- "### Area",
- "Dashboard",
- "### Summary",
- "Providers tab is blank after login.",
- "### Operating system",
- "Windows 11",
- "### Reproduction",
- "1. Open the dashboard",
- ].join("\n"),
- labels: ["bug"],
- });
- assert.ok(labels.includes("gui"));
- assert.equal(labels.includes("platform"), false);
- });
-
- it("uses heuristicBody translation text when Area is Other", () => {
- const labels = detectAreaLabels({
- title: "问题报告",
- body: ["### Area", "Other", "### Summary", "原始描述"].join("\n"),
- heuristicBody: [
- "### Area",
- "Other",
- "### Summary",
- "Account pool failover fails when refresh token is already used.",
- ].join("\n"),
- labels: ["bug"],
- });
- assert.ok(labels.includes("account-pool"), `got ${labels.join(",")}`);
- });
-
- it("matches truncated streaming wording via truncat stem", () => {
- const labels = detectAreaLabels({
- title: "Upstream streaming response truncated mid-turn",
- body: ["### Area", "Other", "### Summary", "The streaming response was truncated."].join("\n"),
- labels: ["bug"],
- });
- assert.ok(labels.includes("streaming"), `got ${labels.join(",")}`);
- });
-});
diff --git a/.github/scripts/issue-translation.cjs b/.github/scripts/issue-translation.cjs
deleted file mode 100644
index c7d546bc23..0000000000
--- a/.github/scripts/issue-translation.cjs
+++ /dev/null
@@ -1,911 +0,0 @@
-"use strict";
-
-const crypto = require("crypto");
-
-const MARKER = "";
-const END_MARKER = "";
-const LEGACY_STATE_RE = /\s*/;
-const CONTROL_MARKER = "";
-const CONTROL_STATE_V2_RE =
- //;
-const CONTROL_STATE_LEGACY_RE =
- //;
-/** Trailing standalone marker (+ optional final whitespace). Never mid-body. */
-const TRAILING_ORPHAN_BODY_STATE_RE =
- /[ \t]*(?:\r?\n)?[ \t]*$/;
-const ISSUE_BODY_MAX = 65536;
-const BOT_LOGIN = "github-actions[bot]";
-const SOURCE_HASH_RE = /^[a-f0-9]{16}$/;
-const ISSUE_SOURCE_KEY = "issue";
-const MAX_SOURCE_HASHES = 64;
-const MAX_RECENT = 32;
-/** Allow small clock skew; far-future timestamps are rejected. */
-const MAX_CLOCK_SKEW_MS = 5 * 60 * 1000;
-
-const DEFAULT_RATE_LIMIT = {
- minIntervalMs: 60_000,
- maxPerHour: 10,
- minSourceChars: 20,
-};
-
-/**
- * Deterministic fingerprint of the original issue source (title + stripped body).
- */
-function hashTranslationSource({ title = "", body = "" } = {}) {
- const payload = [
- "title:",
- String(title || ""),
- "\nbody:\n",
- String(body || ""),
- ].join("");
- return crypto.createHash("sha256").update(payload, "utf8").digest("hex").slice(0, 16);
-}
-
-/**
- * Locate the first generated inline translation block.
- * @returns {{ start: number, end: number } | null}
- */
-function findTranslationBlockRange(text) {
- const markerIdx = String(text || "").indexOf(MARKER);
- if (markerIdx === -1) return null;
-
- let cursor = markerIdx + MARKER.length;
- const afterMarker = String(text).slice(cursor);
- const legacyState = afterMarker.match(/^\s*\s*/);
- if (legacyState) {
- cursor += legacyState.index + legacyState[0].length;
- }
-
- const rest = String(text).slice(cursor);
- const endRel = rest.indexOf(END_MARKER);
- if (endRel !== -1) {
- return { start: markerIdx, end: cursor + endRel + END_MARKER.length };
- }
-
- // Legacy blocks (pre-END_MARKER): fall back to first .
- if (/^\s*/i.test(rest)) {
- const closeRel = rest.search(/<\/details>/i);
- if (closeRel !== -1) {
- return { start: markerIdx, end: cursor + closeRel + " ".length };
- }
- return { start: markerIdx, end: cursor };
- }
-
- if (legacyState) {
- return { start: markerIdx, end: cursor };
- }
-
- return { start: markerIdx, end: markerIdx + MARKER.length };
-}
-
-/**
- * Split an issue body into user prefix/suffix and the generated translation block.
- */
-function splitTranslationBlock(body) {
- const text = String(body || "");
- const range = findTranslationBlockRange(text);
- if (!range) {
- const sourceBody = text.replace(/\s+$/, "");
- return {
- found: false,
- prefix: sourceBody,
- block: "",
- suffix: "",
- sourceBody,
- };
- }
-
- const prefix = text.slice(0, range.start).replace(/\s+$/, "");
- const block = text.slice(range.start, range.end);
- const suffix = text.slice(range.end).replace(/^\s+/, "");
- const sourceBody = suffix
- ? (prefix ? `${prefix}\n\n${suffix}` : suffix).replace(/\s+$/, "")
- : prefix;
-
- return { found: true, prefix, block, suffix, sourceBody };
-}
-
-function stripTranslationBlock(body) {
- return splitTranslationBlock(body).sourceBody;
-}
-
-/** Legacy body-embedded state (ignored for rate limits). */
-function extractTranslationState(body) {
- const match = String(body || "").match(LEGACY_STATE_RE);
- if (!match) return null;
- try {
- const parsed = JSON.parse(match[1]);
- if (!parsed || typeof parsed !== "object") return null;
- return parsed;
- } catch {
- return null;
- }
-}
-
-function scrubDetectedLanguage(value) {
- return (
- String(value || "")
- .replace(/[^\p{L}\p{N}\s\-()]/gu, "")
- .replace(/\s+/g, " ")
- .trim()
- .slice(0, 64) || "non-English"
- );
-}
-
-/**
- * True when the model (or caller) reported English / no translation needed.
- */
-function isEnglishDetectedLanguage(value) {
- const lang = scrubDetectedLanguage(value).toLowerCase();
- return lang === "english" || lang === "en" || lang === "eng";
-}
-
-/**
- * Language written into control-state on the no-translation persist path.
- *
- * Confirmed English only when `sourceComplete` is true (valid parsed
- * `requires_translation: false`). Incomplete AI/parse/action failures always
- * record `unknown` — never retain a language label that could look confirmed.
- */
-function detectedLanguageForControlPersist({ detectedLanguage, sourceComplete } = {}) {
- if (sourceComplete !== true) return "unknown";
- return scrubDetectedLanguage(detectedLanguage || "English");
-}
-
-/**
- * Visible bookkeeping language label for the sticky control comment.
- * Always non-empty so the bot bubble never renders as a blank ghost comment.
- * Missing language is `unknown` — never invent a confirmed English label.
- */
-function bookkeepingLanguageLabel(state) {
- if (state?.detectedLanguage) return scrubDetectedLanguage(state.detectedLanguage);
- return "unknown";
-}
-
-/**
- * Strip obsolete bot-owned body control markers from the legacy trailing
- * storage position only. Markers inside fenced code, quotes, or prose are
- * left untouched. Surrounding author whitespace is preserved byte-for-byte.
- */
-function stripOrphanBodyControlState(body) {
- let text = String(body || "");
- // Only remove exact trailing tokens (legacy bot storage). Repeat in case
- // multiple obsolete markers were appended at EOF.
- while (TRAILING_ORPHAN_BODY_STATE_RE.test(text)) {
- text = text.replace(TRAILING_ORPHAN_BODY_STATE_RE, "");
- }
- return text;
-}
-
-function isValidControlTimestamp(ts, now = Date.now()) {
- return typeof ts === "number"
- && Number.isFinite(ts)
- && ts <= now + MAX_CLOCK_SKEW_MS;
-}
-
-function findAllControlComments(comments) {
- return (Array.isArray(comments) ? comments : []).filter(
- (comment) => comment?.user?.login === BOT_LOGIN && comment?.body?.includes(CONTROL_MARKER),
- );
-}
-
-function encodeControlState(state) {
- return Buffer.from(JSON.stringify(state), "utf8").toString("base64url");
-}
-
-function isValidSourceKey(key) {
- return key === ISSUE_SOURCE_KEY || /^comment:[1-9][0-9]*$/.test(String(key || ""));
-}
-
-/**
- * Per-source completed hashes. Legacy flat `sourceHash` maps only to the issue key.
- */
-function migrateSourceHashes(state) {
- if (!state || typeof state !== "object") return {};
- const out = {};
- if (state.sourceHashes && typeof state.sourceHashes === "object" && !Array.isArray(state.sourceHashes)) {
- for (const [key, value] of Object.entries(state.sourceHashes)) {
- if (isValidSourceKey(key) && typeof value === "string" && SOURCE_HASH_RE.test(value)) {
- out[key] = value;
- }
- }
- return out;
- }
- if (
- typeof state.sourceHash === "string"
- && SOURCE_HASH_RE.test(state.sourceHash)
- && state.sourceHash !== "0000000000000000"
- ) {
- out[ISSUE_SOURCE_KEY] = state.sourceHash;
- }
- return out;
-}
-
-function completedHashFor(state, sourceKey) {
- const key = isValidSourceKey(sourceKey) ? sourceKey : ISSUE_SOURCE_KEY;
- const hashes = migrateSourceHashes(state);
- return hashes[key] || null;
-}
-
-function withCompletedSourceHash(hashes, sourceKey, sourceHash) {
- const next = { ...hashes };
- if (isValidSourceKey(sourceKey) && typeof sourceHash === "string" && SOURCE_HASH_RE.test(sourceHash)) {
- next[sourceKey] = sourceHash;
- }
- const keys = Object.keys(next);
- if (keys.length <= MAX_SOURCE_HASHES) return next;
- // Prefer keeping the issue key; drop oldest-inserted comment keys first.
- const commentKeys = keys.filter((k) => k !== ISSUE_SOURCE_KEY);
- while (Object.keys(next).length > MAX_SOURCE_HASHES && commentKeys.length) {
- delete next[commentKeys.shift()];
- }
- return next;
-}
-
-function validateControlState(parsed, now = Date.now()) {
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
- if (parsed.v !== 2) return null;
- if (typeof parsed.sourceHash !== "string" || !SOURCE_HASH_RE.test(parsed.sourceHash)) {
- return null;
- }
- if (!isValidControlTimestamp(parsed.attemptedAt, now)) {
- return null;
- }
- if (!Array.isArray(parsed.recent)) return null;
- const recent = parsed.recent
- .filter((ts) => isValidControlTimestamp(ts, now))
- .slice(-MAX_RECENT);
- if (typeof parsed.requiresTranslation !== "boolean") return null;
-
- let detectedLanguage = null;
- if (parsed.detectedLanguage != null) {
- if (typeof parsed.detectedLanguage !== "string") return null;
- detectedLanguage = scrubDetectedLanguage(parsed.detectedLanguage);
- }
-
- return {
- v: 2,
- sourceHash: parsed.sourceHash,
- sourceHashes: migrateSourceHashes(parsed),
- attemptedAt: parsed.attemptedAt,
- recent,
- requiresTranslation: parsed.requiresTranslation,
- detectedLanguage,
- };
-}
-
-function decodeControlState(encoded, now = Date.now()) {
- try {
- const json = Buffer.from(String(encoded || ""), "base64url").toString("utf8");
- return validateControlState(JSON.parse(json), now);
- } catch {
- return null;
- }
-}
-
-/** Legacy JSON-in-HTML-comment state (read-only migration). */
-function parseLegacyControlState(raw, now = Date.now()) {
- try {
- return validateControlState(JSON.parse(raw), now);
- } catch {
- return null;
- }
-}
-
-function parseControlStateFromCommentBody(body, now = Date.now()) {
- const text = String(body || "");
- const v2 = text.match(CONTROL_STATE_V2_RE);
- if (v2) return decodeControlState(v2[1], now);
- const legacy = text.match(CONTROL_STATE_LEGACY_RE);
- if (legacy) return parseLegacyControlState(legacy[1], now);
- return null;
-}
-
-/**
- * Newest github-actions control comment with a valid decoded state.
- * Author-forged comments and far-future poisoned payloads are ignored.
- * Used for *reading* authoritative rate-limit state.
- */
-function findControlComment(comments, now = Date.now()) {
- let best = null;
- let bestState = null;
- for (const comment of findAllControlComments(comments)) {
- const state = parseControlStateFromCommentBody(comment.body, now);
- if (!state) continue;
- if (!bestState || state.attemptedAt >= bestState.attemptedAt) {
- best = comment;
- bestState = state;
- }
- }
- return best;
-}
-
-/**
- * Sticky upsert target: the oldest bot-owned control comment (by id).
- * Prefer updating this in place so the bubble stays near the top of the
- * thread instead of creating a new comment at the bottom after every
- * English classification. Corrupt/unparseable bodies still qualify — we
- * overwrite them — so a bad decode never forces a duplicate create.
- */
-function findStickyControlComment(comments) {
- let sticky = null;
- for (const comment of findAllControlComments(comments)) {
- if (!Number.isSafeInteger(comment?.id) || comment.id <= 0) continue;
- if (!sticky || comment.id < sticky.id) sticky = comment;
- }
- return sticky;
-}
-
-function extractTranslationControlState(comments, now = Date.now()) {
- const newest = findControlComment(comments, now);
- if (!newest) return null;
- return parseControlStateFromCommentBody(newest.body, now);
-}
-
-/**
- * Authoritative control state comes only from verified bot-owned comments.
- * Issue body markers and author comments are never consulted.
- * The optional second argument is ignored (kept for call-site compatibility).
- */
-function resolveControlState(comments, _issueNumber, now = Date.now()) {
- return extractTranslationControlState(comments, now);
-}
-
-/**
- * Always false: control comments always include a visible bookkeeping line
- * so GitHub never renders an HTML-comment-only ghost bubble.
- * Kept as an exported predicate for workflow/tests that assert the contract.
- */
-function shouldOmitVisibleBookkeeping(_state) {
- return false;
-}
-
-function buildTranslationControlComment(state) {
- const safe = validateControlState(state) || {
- v: 2,
- sourceHash: "0000000000000000",
- sourceHashes: {},
- attemptedAt: Date.now(),
- recent: [],
- requiresTranslation: false,
- detectedLanguage: null,
- };
- const encoded = encodeControlState(safe);
- const lang = bookkeepingLanguageLabel(safe);
- return [
- CONTROL_MARKER,
- ``,
- "",
- `Automated translation bookkeeping — detected language: ${lang}.`,
- ].join("\n");
-}
-
-function pruneRecent(recent, now, windowMs = 3_600_000) {
- const cutoff = now - windowMs;
- const maxTs = now + MAX_CLOCK_SKEW_MS;
- return (Array.isArray(recent) ? recent : []).filter(
- (ts) => typeof ts === "number" && Number.isFinite(ts) && ts > cutoff && ts <= maxTs,
- );
-}
-
-function countRecentAttempts(recent, now, windowMs = 3_600_000) {
- return pruneRecent(recent, now, windowMs).length;
-}
-
-/**
- * Merge bounded recent-attempt histories from every valid bot control comment
- * so canonicalisation does not drop hourly-limit evidence.
- */
-function collectMergedRecentFromComments(comments, priorState = null, now = Date.now()) {
- const collected = [];
- if (Array.isArray(priorState?.recent)) collected.push(...priorState.recent);
- for (const comment of findAllControlComments(comments)) {
- const state = parseControlStateFromCommentBody(comment.body, now);
- if (state?.recent) collected.push(...state.recent);
- }
- return [...new Set(pruneRecent(collected, now))].sort((a, b) => a - b).slice(-MAX_RECENT);
-}
-
-/**
- * Record a new attempt. Far-future poisoned prior state is ignored/healed.
- * New attemptedAt always uses wall-clock `now` so skew cannot stick forever.
- *
- * Completed hashes are stored per `sourceKey` (`issue` vs `comment:`) so
- * issue and comment paths do not clobber each other's unchanged_source checks.
- * Rate-limit fields (`attemptedAt`, `recent`) stay shared across the issue.
- * Pass `sourceComplete: true` only after a valid no-translation decision or a
- * successful issue/comment translation apply — never for invalid/empty model
- * output or GitHub update failures (those must remain retryable after cooldown).
- */
-function mergeTranslationAttemptState({ priorState = null, attempt, now = Date.now() }) {
- let prior = null;
- if (priorState && isValidControlTimestamp(priorState.attemptedAt, now)) {
- prior = {
- ...priorState,
- recent: (priorState.recent || []).filter((ts) => isValidControlTimestamp(ts, now)),
- sourceHashes: migrateSourceHashes(priorState),
- };
- }
-
- const priorRecent = pruneRecent(prior?.recent, now);
- const recent = pruneRecent([...priorRecent, now], now);
- const sourceComplete = attempt?.sourceComplete === true;
- const sourceKey = isValidSourceKey(attempt?.sourceKey) ? attempt.sourceKey : ISSUE_SOURCE_KEY;
- let sourceHashes = migrateSourceHashes(prior);
- let completedHash = prior?.sourceHash && SOURCE_HASH_RE.test(prior.sourceHash)
- ? prior.sourceHash
- : "0000000000000000";
-
- if (
- sourceComplete
- && typeof attempt.sourceHash === "string"
- && SOURCE_HASH_RE.test(attempt.sourceHash)
- ) {
- sourceHashes = withCompletedSourceHash(sourceHashes, sourceKey, attempt.sourceHash);
- completedHash = attempt.sourceHash;
- }
-
- return {
- v: 2,
- sourceHash: completedHash,
- sourceHashes,
- attemptedAt: now,
- recent,
- requiresTranslation: Boolean(attempt.requiresTranslation),
- detectedLanguage: attempt.detectedLanguage == null
- ? null
- : scrubDetectedLanguage(attempt.detectedLanguage),
- };
-}
-
-/**
- * Delete verified bot control comments by ID.
- * Re-checks bot authorship + CONTROL_MARKER before each delete.
- * Deletion failures are reported, not thrown.
- */
-async function deleteVerifiedControlComments({
- github,
- owner,
- repo,
- issue_number,
- commentIds,
- comments = null,
- keepCommentId = null,
-}) {
- const keepId = Number.isSafeInteger(keepCommentId) && keepCommentId > 0
- ? keepCommentId
- : null;
- const ids = [...new Set(
- (Array.isArray(commentIds) ? commentIds : [])
- .map((id) => Number(id))
- .filter((id) => Number.isSafeInteger(id) && id > 0 && id !== keepId),
- )];
- if (!ids.length) {
- return { deleted: [], skipped: [], failed: [] };
- }
-
- let liveComments = comments;
- if (!Array.isArray(liveComments)) {
- liveComments = await github.paginate(github.rest.issues.listComments, {
- owner,
- repo,
- issue_number,
- per_page: 100,
- });
- }
- const byId = new Map(
- (Array.isArray(liveComments) ? liveComments : [])
- .filter((c) => Number.isSafeInteger(c?.id))
- .map((c) => [c.id, c]),
- );
-
- const deleted = [];
- const skipped = [];
- const failed = [];
- for (const id of ids) {
- const comment = byId.get(id);
- if (
- !comment
- || comment.user?.login !== BOT_LOGIN
- || !String(comment.body || "").includes(CONTROL_MARKER)
- ) {
- skipped.push(id);
- continue;
- }
- try {
- await github.rest.issues.deleteComment({
- owner,
- repo,
- comment_id: id,
- });
- deleted.push(id);
- } catch (err) {
- failed.push({
- id,
- error: err instanceof Error ? err.message : String(err),
- });
- }
- }
- return { deleted, skipped, failed };
-}
-
-/**
- * Upsert the canonical bot-owned control comment.
- * Always includes a visible detected-language bookkeeping line (English too).
- * Updates the oldest sticky bot control comment in place when one exists —
- * including corrupt bodies — so classification never spams a new bottom bubble.
- * Never mutates the issue title or body.
- */
-async function upsertTranslationControlComment({
- github,
- owner,
- repo,
- issue_number,
- comments,
- priorState = null,
- attempt,
- now = Date.now(),
-}) {
- const merged = mergeTranslationAttemptState({ priorState, attempt, now });
- const body = buildTranslationControlComment(merged);
- // Sticky target ≠ newest valid state: prefer oldest marker comment so the
- // thread position stays stable even when state on that comment is corrupt.
- const existing = findStickyControlComment(comments);
-
- if (existing) {
- if (existing.body !== body) {
- await github.rest.issues.updateComment({
- owner,
- repo,
- comment_id: existing.id,
- body,
- });
- }
- return { comment: { ...existing, body }, state: merged, created: false };
- }
-
- const created = await github.rest.issues.createComment({
- owner,
- repo,
- issue_number,
- body,
- });
- return { comment: created.data, state: merged, created: true };
-}
-
-/**
- * Persist rate-limit / cooldown state in a bot-owned issue comment.
- * Writes/updates the canonical comment first; only then deletes redundant
- * older bot control comments. Create/update failure preserves prior comments.
- * Never uses the issue body/title or author-created comments as storage.
- */
-async function persistTranslationControlState({
- github,
- owner,
- repo,
- issue_number,
- comments,
- priorState = null,
- attempt,
- now = Date.now(),
-}) {
- const mergedRecent = collectMergedRecentFromComments(comments, priorState, now);
- const effectivePrior = priorState && isValidControlTimestamp(priorState.attemptedAt, now)
- ? { ...priorState, recent: mergedRecent }
- : (mergedRecent.length
- ? {
- v: 2,
- // Incomplete synthetic prior: do not treat the current attempt hash as completed.
- sourceHash: "0000000000000000",
- sourceHashes: {},
- attemptedAt: Math.min(...mergedRecent),
- recent: mergedRecent,
- requiresTranslation: false,
- detectedLanguage: null,
- }
- : null);
-
- let upserted;
- try {
- upserted = await upsertTranslationControlComment({
- github,
- owner,
- repo,
- issue_number,
- comments,
- priorState: effectivePrior,
- attempt,
- now,
- });
- } catch (err) {
- const error = new Error(
- `translation control comment persistence failed: ${err instanceof Error ? err.message : String(err)}`,
- );
- error.cause = err;
- throw error;
- }
-
- const canonicalId = upserted.comment?.id;
- const redundantIds = findAllControlComments(comments)
- .map((comment) => comment.id)
- .filter((id) => Number.isSafeInteger(id) && id > 0 && id !== canonicalId);
-
- let cleanup = { deleted: [], skipped: [], failed: [] };
- if (redundantIds.length) {
- cleanup = await deleteVerifiedControlComments({
- github,
- owner,
- repo,
- issue_number,
- commentIds: redundantIds,
- comments,
- keepCommentId: canonicalId,
- });
- }
-
- return {
- storage: "comment",
- state: upserted.state,
- comment: upserted.comment,
- // Always false: bookkeeping line is always visible (no ghost HTML-only bubble).
- markerOnly: false,
- cleanup,
- };
-}
-
-function isPreparedSourceStillCurrent({ preparedHash, liveTitle, liveBody }) {
- const liveHash = hashTranslationSource({
- title: liveTitle || "",
- body: liveBody || "",
- });
- return liveHash === preparedHash;
-}
-
-/** Stable title key so comment hashes never collide with issue title+body hashes. */
-function commentSourceTitle(commentId) {
- return `comment:${commentId}`;
-}
-
-/**
- * Hard skips before rate-limit / hash checks.
- * @returns {string | null} skip reason, or null when eligible for shouldTranslate
- */
-function shouldSkipCommentTranslation(comment, issue = null) {
- if (issue?.pull_request) return "pull_request";
- const login = String(comment?.user?.login || "");
- const userType = String(comment?.user?.type || "");
- if (userType === "Bot" || /\[bot\]$/i.test(login) || login === BOT_LOGIN) {
- return "bot_author";
- }
- const body = String(comment?.body || "");
- if (body.includes(CONTROL_MARKER)) return "control_comment";
- return null;
-}
-
-/**
- * Decide whether a user issue comment should be sent to the translator.
- * Reuses issue rate limits via the shared per-issue control comment.
- * `comment:` is only a hash namespace — minSourceChars applies to the
- * stripped comment body alone so short comments cannot burn model quota.
- */
-function shouldTranslateComment({
- comment,
- issue = null,
- priorState = null,
- now = Date.now(),
- rateLimit = DEFAULT_RATE_LIMIT,
-}) {
- const skip = shouldSkipCommentTranslation(comment, issue);
- if (skip) return { ok: false, reason: skip };
-
- const commentId = comment?.id;
- if (!Number.isSafeInteger(commentId) || commentId <= 0) {
- return { ok: false, reason: "invalid_comment_id" };
- }
-
- const sourceBody = stripTranslationBlock(comment.body || "");
- const minChars = rateLimit.minSourceChars ?? DEFAULT_RATE_LIMIT.minSourceChars;
- if (String(sourceBody).trim().length < minChars) {
- return { ok: false, reason: "source_too_short" };
- }
-
- const decision = shouldTranslate({
- sourceTitle: commentSourceTitle(commentId),
- sourceBody,
- sourceKey: commentSourceTitle(commentId),
- priorState,
- now,
- // Length already enforced on the body; title is namespace-only.
- rateLimit: { ...rateLimit, minSourceChars: 0 },
- });
- if (!decision.ok) return decision;
- return {
- ...decision,
- sourceBody,
- sourceTitle: commentSourceTitle(commentId),
- commentId,
- };
-}
-
-/**
- * Build the in-place comment body: original + folded English translation.
- */
-function buildTranslatedCommentBody(sourceBody, translatedBody, detectedLanguage) {
- const lang = scrubDetectedLanguage(detectedLanguage);
- const translationText = [
- `*Original language: ${lang}*`,
- "",
- String(translatedBody || ""),
- ].join("\n");
- return appendTranslationBlock(sourceBody, translationText);
-}
-
-/**
- * Required translated fields for a successful apply.
- * Nonempty source title/body each require a nonempty translated counterpart.
- * @returns {string[]} missing field names (`title` / `body`)
- */
-function missingRequiredTranslationFields({
- sourceTitle = "",
- sourceBody = "",
- translatedTitle = "",
- translatedBody = "",
-} = {}) {
- const missing = [];
- if (String(sourceTitle || "").trim() && !String(translatedTitle || "").trim()) {
- missing.push("title");
- }
- if (String(sourceBody || "").trim() && !String(translatedBody || "").trim()) {
- missing.push("body");
- }
- return missing;
-}
-
-function shouldTranslate({
- sourceTitle = "",
- sourceBody,
- sourceKey = ISSUE_SOURCE_KEY,
- priorState = null,
- now = Date.now(),
- rateLimit = DEFAULT_RATE_LIMIT,
-}) {
- const title = String(sourceTitle || "").trim();
- const body = String(sourceBody || "").trim();
- const combined = `${title}\n${body}`.trim();
- const minChars = rateLimit.minSourceChars ?? DEFAULT_RATE_LIMIT.minSourceChars;
-
- if (combined.length < minChars) {
- return { ok: false, reason: "source_too_short" };
- }
-
- const key = isValidSourceKey(sourceKey) ? sourceKey : ISSUE_SOURCE_KEY;
- const sourceHash = hashTranslationSource({ title, body });
- if (completedHashFor(priorState, key) === sourceHash) {
- return { ok: false, reason: "unchanged_source" };
- }
-
- const minInterval = rateLimit.minIntervalMs ?? DEFAULT_RATE_LIMIT.minIntervalMs;
- const maxPerHour = rateLimit.maxPerHour ?? DEFAULT_RATE_LIMIT.maxPerHour;
- const attemptedAt = typeof priorState?.attemptedAt === "number" ? priorState.attemptedAt : 0;
- const recent = pruneRecent(priorState?.recent, now);
-
- if (attemptedAt && now - attemptedAt < minInterval) {
- return { ok: false, reason: "rate_limited_interval" };
- }
-
- if (countRecentAttempts(recent, now) >= maxPerHour) {
- return { ok: false, reason: "rate_limited_hourly" };
- }
-
- return { ok: true, sourceHash, sourceKey: key, recent };
-}
-
-function sanitizeTranslationBody(raw, maxChars = 60000) {
- return String(raw || "")
- .split(MARKER).join("")
- .split(END_MARKER).join("")
- .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "")
- // Defuse pings only: @login / @org/team — not emails, scopes, or decorators.
- .replace(
- /(^|[\s(])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,38})(?:\/[A-Za-z0-9._-]+)?)/g,
- "$1@\u200b$2",
- )
- .trim()
- .slice(0, maxChars);
-}
-
-function buildTranslationBlock(translatedBody) {
- const safeBody = sanitizeTranslationBody(translatedBody);
- return [
- "",
- MARKER,
- "",
- "",
- "",
- "Translated Message
",
- "",
- safeBody,
- "",
- " ",
- END_MARKER,
- "",
- ].join("\n");
-}
-
-function maxTranslationChars(sourceBody) {
- const base = stripTranslationBlock(sourceBody);
- const emptyBlock = buildTranslationBlock("");
- return Math.max(0, ISSUE_BODY_MAX - base.length - emptyBlock.length - 64);
-}
-
-function fitTranslationBody(sourceBody, translatedBody) {
- let safe = sanitizeTranslationBody(translatedBody);
- const maxChars = maxTranslationChars(sourceBody);
- if (safe.length <= maxChars) return safe;
- const note = "\n\n_(Translation truncated to fit GitHub issue body limit.)_";
- const budget = Math.max(0, maxChars - note.length);
- return safe.slice(0, budget).trimEnd() + note;
-}
-
-function appendTranslationBlock(sourceBody, translatedBody) {
- const base = stripTranslationBlock(sourceBody);
- const fitted = fitTranslationBody(base, translatedBody);
- const next = base + buildTranslationBlock(fitted);
- if (next.length > ISSUE_BODY_MAX) {
- throw new Error("Translated issue body exceeds GitHub limit after truncation.");
- }
- return next;
-}
-
-module.exports = {
- MARKER,
- END_MARKER,
- CONTROL_MARKER,
- BOT_LOGIN,
- ISSUE_BODY_MAX,
- ISSUE_SOURCE_KEY,
- DEFAULT_RATE_LIMIT,
- MAX_CLOCK_SKEW_MS,
- hashTranslationSource,
- findTranslationBlockRange,
- splitTranslationBlock,
- stripTranslationBlock,
- extractTranslationState,
- findControlComment,
- findStickyControlComment,
- findAllControlComments,
- deleteVerifiedControlComments,
- extractTranslationControlState,
- resolveControlState,
- encodeControlState,
- decodeControlState,
- validateControlState,
- isValidControlTimestamp,
- buildTranslationControlComment,
- mergeTranslationAttemptState,
- collectMergedRecentFromComments,
- upsertTranslationControlComment,
- persistTranslationControlState,
- shouldOmitVisibleBookkeeping,
- isPreparedSourceStillCurrent,
- commentSourceTitle,
- shouldSkipCommentTranslation,
- shouldTranslateComment,
- buildTranslatedCommentBody,
- missingRequiredTranslationFields,
- shouldTranslate,
- completedHashFor,
- migrateSourceHashes,
- sanitizeTranslationBody,
- scrubDetectedLanguage,
- isEnglishDetectedLanguage,
- detectedLanguageForControlPersist,
- bookkeepingLanguageLabel,
- stripOrphanBodyControlState,
- buildTranslationBlock,
- maxTranslationChars,
- fitTranslationBody,
- appendTranslationBlock,
- pruneRecent,
- countRecentAttempts,
-};
diff --git a/.github/scripts/issue-translation.test.cjs b/.github/scripts/issue-translation.test.cjs
deleted file mode 100644
index f6c754fb9e..0000000000
--- a/.github/scripts/issue-translation.test.cjs
+++ /dev/null
@@ -1,1688 +0,0 @@
-"use strict";
-
-const { describe, it } = require("node:test");
-const assert = require("node:assert/strict");
-const {
- MARKER,
- END_MARKER,
- CONTROL_MARKER,
- BOT_LOGIN,
- ISSUE_BODY_MAX,
- DEFAULT_RATE_LIMIT,
- hashTranslationSource,
- splitTranslationBlock,
- stripTranslationBlock,
- appendTranslationBlock,
- buildTranslationBlock,
- buildTranslationControlComment,
- findControlComment,
- findStickyControlComment,
- extractTranslationControlState,
- resolveControlState,
- encodeControlState,
- decodeControlState,
- validateControlState,
- mergeTranslationAttemptState,
- persistTranslationControlState,
- upsertTranslationControlComment,
- isPreparedSourceStillCurrent,
- shouldTranslate,
- commentSourceTitle,
- shouldSkipCommentTranslation,
- shouldTranslateComment,
- buildTranslatedCommentBody,
- missingRequiredTranslationFields,
- sanitizeTranslationBody,
- scrubDetectedLanguage,
- isEnglishDetectedLanguage,
- detectedLanguageForControlPersist,
- bookkeepingLanguageLabel,
- stripOrphanBodyControlState,
- fitTranslationBody,
- shouldOmitVisibleBookkeeping,
- deleteVerifiedControlComments,
- MAX_CLOCK_SKEW_MS,
- collectMergedRecentFromComments,
- isValidControlTimestamp,
- pruneRecent,
- completedHashFor,
-} = require("./issue-translation.cjs");
-
-const HASH_A = "aaaaaaaaaaaaaaaa";
-const HASH_B = "bbbbbbbbbbbbbbbb";
-const ORPHAN_MARKER = ``;
-
-const SOURCE = [
- "### Was funktioniert nicht?",
- "Der Proxy startet nicht nach dem Update.",
- "### Schritte",
- "1. ocx start",
- "2. Fehler in der Konsole",
-].join("\n");
-
-function botComment(body, id = 1) {
- return { id, user: { login: BOT_LOGIN }, body };
-}
-
-function mockGithub(handlers = {}) {
- const calls = [];
- const github = {
- paginate: async (_fn, args) => {
- calls.push(["paginate", args]);
- return handlers.listComments || [];
- },
- rest: {
- issues: {
- listComments: async (args) => {
- calls.push(["list", args]);
- return { data: handlers.listComments || [] };
- },
- createComment: async (args) => {
- calls.push(["create", args]);
- if (handlers.create) return handlers.create(args);
- return { data: { id: handlers.nextId || 99, body: args.body, user: { login: BOT_LOGIN } } };
- },
- updateComment: async (args) => {
- calls.push(["update", args]);
- if (handlers.update) return handlers.update(args);
- return { data: { id: args.comment_id, body: args.body, user: { login: BOT_LOGIN } } };
- },
- deleteComment: async (args) => {
- calls.push(["delete", args]);
- if (handlers.delete) return handlers.delete(args);
- return {};
- },
- update: async (args) => {
- calls.push(["issueUpdate", args]);
- if (handlers.issueUpdate) return handlers.issueUpdate(args);
- return { data: args };
- },
- },
- },
- };
- return { github, calls };
-}
-
-describe("hashTranslationSource", () => {
- it("changes when only the title changes", () => {
- const bodyOnly = hashTranslationSource({ body: SOURCE });
- const withTitle = hashTranslationSource({ title: "Neuer Titel", body: SOURCE });
- assert.notEqual(bodyOnly, withTitle);
- });
-
- it("changes when only the body changes", () => {
- const base = hashTranslationSource({ title: "Titel", body: SOURCE });
- const edited = hashTranslationSource({ title: "Titel", body: SOURCE + "\nmehr" });
- assert.notEqual(base, edited);
- });
-
- it("is stable for unchanged title and body", () => {
- const a = hashTranslationSource({ title: "T", body: SOURCE });
- const b = hashTranslationSource({ title: "T", body: SOURCE });
- assert.equal(a, b);
- });
-});
-
-describe("splitTranslationBlock", () => {
- it("handles generated block at end", () => {
- const translated = appendTranslationBlock(SOURCE, "English");
- const split = splitTranslationBlock(translated);
- assert.equal(split.sourceBody, SOURCE);
- assert.ok(split.block.includes(MARKER));
- });
-
- it("preserves suffix after generated block", () => {
- const suffix = "Extra logs added by contributor.";
- const translated = appendTranslationBlock(SOURCE, "English") + "\n\n" + suffix;
- const split = splitTranslationBlock(translated);
- assert.equal(split.suffix, suffix);
- assert.equal(split.sourceBody, `${SOURCE}\n\n${suffix}`);
- });
-
- it("preserves prefix before generated block", () => {
- const prefix = "Preface";
- const translated = prefix + "\n\n" + appendTranslationBlock(SOURCE, "English").trimStart();
- const split = splitTranslationBlock(translated);
- assert.equal(split.prefix, `${prefix}\n\n${SOURCE}`);
- assert.equal(split.sourceBody, `${prefix}\n\n${SOURCE}`);
- });
-
- it("does not remove contributor-authored details elsewhere", () => {
- const contributorDetails = [
- "My notes
",
- "private repro notes",
- " ",
- ].join("\n");
- const body = contributorDetails + "\n\n" + appendTranslationBlock(SOURCE, "English").trimStart();
- const split = splitTranslationBlock(body);
- assert.ok(split.sourceBody.includes("private repro notes"));
- assert.ok(split.sourceBody.includes("My notes"));
- });
-
- it("fails safely when closing details is missing", () => {
- const malformed = `${SOURCE}\n\n${MARKER}\n\nTranslated Message
\n\noops`;
- const split = splitTranslationBlock(malformed);
- assert.ok(split.sourceBody.includes("oops"));
- assert.ok(split.sourceBody.includes(SOURCE));
- });
-
- it("preserves nested details inside translated content via end marker", () => {
- const nested = [
- "Outer translation",
- "logs
",
- "inner",
- " ",
- "still translation",
- ].join("\n");
- const body = appendTranslationBlock(SOURCE, nested) + "\n\nuser suffix";
- assert.ok(body.includes(END_MARKER));
- const split = splitTranslationBlock(body);
- assert.equal(split.suffix, "user suffix");
- assert.equal(split.sourceBody, `${SOURCE}\n\nuser suffix`);
- assert.ok(split.block.includes("inner"));
- assert.ok(split.block.includes("still translation"));
- });
-
- it("removes multi-level nested details only inside the generated block", () => {
- const before = "before
\nbefore-log\n ";
- const after = "after
\nafter-log\n ";
- const nested = [
- "top",
- "L1
",
- "L2
",
- "deep",
- " ",
- " ",
- "tail",
- ].join("\n");
- const body = [
- before,
- "",
- appendTranslationBlock(SOURCE, nested).trimStart(),
- "",
- after,
- ].join("\n");
- const stripped = stripTranslationBlock(body);
- assert.ok(stripped.includes("before-log"));
- assert.ok(stripped.includes("after-log"));
- assert.ok(!stripped.includes("deep"));
- assert.ok(!stripped.includes("top"));
- assert.ok(!stripped.includes(MARKER));
- assert.ok(!stripped.includes(END_MARKER));
- });
-
- it("migrates legacy blocks that close on first details end", () => {
- const legacy = [
- SOURCE,
- "",
- MARKER,
- "",
- "",
- "",
- "Translated Message
",
- "",
- "legacy english",
- "",
- " ",
- "",
- "user after",
- ].join("\n");
- const split = splitTranslationBlock(legacy);
- assert.equal(split.suffix, "user after");
- assert.equal(split.sourceBody, `${SOURCE}\n\nuser after`);
- const migrated = appendTranslationBlock(split.sourceBody, "fresh");
- assert.ok(migrated.includes(END_MARKER));
- assert.equal((migrated.match(new RegExp(MARKER, "g")) || []).length, 1);
- assert.ok(migrated.includes("user after"));
- });
-
- it("does not greedily erase across duplicate end markers", () => {
- const block = buildTranslationBlock("one");
- const forged = `${SOURCE}${block}\n${END_MARKER}\nkeep me`;
- const split = splitTranslationBlock(forged);
- assert.ok(split.sourceBody.includes("keep me"));
- assert.ok(!split.block.includes("keep me"));
- assert.equal(split.suffix, `${END_MARKER}\nkeep me`);
- });
-});
-
-describe("isPreparedSourceStillCurrent", () => {
- it("detects body changes between prepare and apply", () => {
- const prepared = hashTranslationSource({ title: "T", body: SOURCE });
- assert.equal(
- isPreparedSourceStillCurrent({
- preparedHash: prepared,
- liveTitle: "T",
- liveBody: SOURCE + "\nnew logs",
- }),
- false,
- );
- });
-
- it("detects title changes between prepare and apply", () => {
- const prepared = hashTranslationSource({ title: "Alt", body: SOURCE });
- assert.equal(
- isPreparedSourceStillCurrent({
- preparedHash: prepared,
- liveTitle: "Neu",
- liveBody: SOURCE,
- }),
- false,
- );
- });
-
- it("allows apply when only generated translation changed", () => {
- const prepared = hashTranslationSource({ title: "T", body: SOURCE });
- const withBlock = appendTranslationBlock(SOURCE, "English");
- assert.equal(
- isPreparedSourceStillCurrent({
- preparedHash: prepared,
- liveTitle: "T",
- liveBody: stripTranslationBlock(withBlock),
- }),
- true,
- );
- });
-});
-
-describe("bot-owned control state", () => {
- it("always includes visible bookkeeping, including English", () => {
- const german = buildTranslationControlComment({
- v: 2,
- sourceHash: HASH_A,
- attemptedAt: 1,
- recent: [1],
- requiresTranslation: true,
- detectedLanguage: "German",
- });
- assert.match(german, /Automated translation bookkeeping — detected language: German/);
- assert.equal(shouldOmitVisibleBookkeeping({
- requiresTranslation: true,
- detectedLanguage: "German",
- }), false);
-
- const english = buildTranslationControlComment({
- v: 2,
- sourceHash: HASH_A,
- attemptedAt: 1,
- recent: [1],
- requiresTranslation: false,
- detectedLanguage: "English",
- });
- assert.match(english, /Automated translation bookkeeping — detected language: English/);
- assert.equal(shouldOmitVisibleBookkeeping({
- requiresTranslation: false,
- detectedLanguage: "English",
- }), false);
- });
-
- it("incomplete AI/parse failures bookkeep as unknown, never false-English", async () => {
- assert.equal(
- detectedLanguageForControlPersist({ detectedLanguage: "", sourceComplete: false }),
- "unknown",
- );
- assert.equal(
- detectedLanguageForControlPersist({ detectedLanguage: undefined, sourceComplete: false }),
- "unknown",
- );
- assert.equal(
- detectedLanguageForControlPersist({ detectedLanguage: "unknown", sourceComplete: false }),
- "unknown",
- );
- assert.equal(
- detectedLanguageForControlPersist({ detectedLanguage: "English", sourceComplete: false }),
- "unknown",
- );
- assert.equal(
- detectedLanguageForControlPersist({ detectedLanguage: "", sourceComplete: true }),
- "English",
- );
- assert.equal(
- detectedLanguageForControlPersist({ detectedLanguage: "English", sourceComplete: true }),
- "English",
- );
- assert.equal(bookkeepingLanguageLabel({ requiresTranslation: false, detectedLanguage: null }), "unknown");
- assert.equal(bookkeepingLanguageLabel({ requiresTranslation: false, detectedLanguage: "English" }), "English");
-
- const { github, calls } = mockGithub({ nextId: 77 });
- const result = await persistTranslationControlState({
- github,
- owner: "o",
- repo: "r",
- issue_number: 9,
- comments: [],
- attempt: {
- sourceHash: HASH_A,
- requiresTranslation: false,
- detectedLanguage: detectedLanguageForControlPersist({
- detectedLanguage: "",
- sourceComplete: false,
- }),
- sourceComplete: false,
- },
- now: 100,
- });
- assert.equal(result.state.sourceHash, "0000000000000000");
- assert.equal(result.state.detectedLanguage, "unknown");
- assert.match(calls[0][1].body, /detected language: unknown/);
- assert.doesNotMatch(calls[0][1].body, /detected language: English/);
- // Attempt counted (recent) but source stays retryable.
- assert.deepEqual(result.state.recent, [100]);
- assert.equal(completedHashFor(result.state, "issue"), null);
- });
-
- it("English creates a bot comment with visible English bookkeeping", async () => {
- const { github, calls } = mockGithub({ nextId: 42 });
- const result = await persistTranslationControlState({
- github,
- owner: "o",
- repo: "r",
- issue_number: 7,
- comments: [],
- attempt: {
- sourceHash: HASH_A,
- requiresTranslation: false,
- detectedLanguage: "English",
- sourceComplete: true,
- },
- now: 100,
- });
- assert.equal(result.storage, "comment");
- assert.equal(result.markerOnly, false);
- assert.equal(result.comment.id, 42);
- assert.deepEqual(calls.map((c) => c[0]), ["create"]);
- assert.match(calls[0][1].body, new RegExp(CONTROL_MARKER));
- assert.match(calls[0][1].body, /Automated translation bookkeeping — detected language: English/);
- assert.equal(extractTranslationControlState([botComment(calls[0][1].body, 42)]).sourceHash, HASH_A);
- });
-
- it("English updates the canonical bot comment instead of creating duplicates", async () => {
- const priorBody = buildTranslationControlComment({
- v: 2,
- sourceHash: HASH_B,
- attemptedAt: 1,
- recent: [1],
- requiresTranslation: false,
- detectedLanguage: "English",
- });
- const prior = botComment(priorBody, 11);
- const { github, calls } = mockGithub();
- const result = await persistTranslationControlState({
- github,
- owner: "o",
- repo: "r",
- issue_number: 7,
- comments: [prior],
- priorState: extractTranslationControlState([prior]),
- attempt: {
- sourceHash: HASH_A,
- requiresTranslation: false,
- detectedLanguage: "English",
- sourceComplete: true,
- },
- now: 200,
- });
- assert.equal(result.comment.id, 11);
- assert.deepEqual(calls.map((c) => c[0]), ["update"]);
- assert.equal(calls[0][1].comment_id, 11);
- assert.match(calls[0][1].body, /Automated translation bookkeeping — detected language: English/);
- });
-
- it("updates the oldest sticky control comment even when its state is corrupt", async () => {
- const corrupt = botComment(
- `${CONTROL_MARKER}\n`,
- 3,
- );
- const validNewer = botComment(buildTranslationControlComment({
- v: 2,
- sourceHash: HASH_B,
- attemptedAt: 50,
- recent: [50],
- requiresTranslation: false,
- detectedLanguage: "English",
- }), 9);
- const { github, calls } = mockGithub();
- const result = await persistTranslationControlState({
- github,
- owner: "o",
- repo: "r",
- issue_number: 7,
- comments: [corrupt, validNewer],
- priorState: extractTranslationControlState([corrupt, validNewer]),
- attempt: {
- sourceHash: HASH_A,
- requiresTranslation: false,
- detectedLanguage: "English",
- sourceComplete: true,
- },
- now: 100,
- });
- assert.equal(findStickyControlComment([corrupt, validNewer]).id, 3);
- assert.equal(result.comment.id, 3);
- assert.deepEqual(calls.map((c) => c[0]), ["update", "delete"]);
- assert.equal(calls[0][1].comment_id, 3);
- assert.match(calls[0][1].body, /detected language: English/);
- assert.equal(calls[1][1].comment_id, 9);
- });
-
- it("non-English persist writes or updates a visible bot-owned comment", async () => {
- const { github, calls } = mockGithub({ nextId: 50 });
- const created = await persistTranslationControlState({
- github,
- owner: "o",
- repo: "r",
- issue_number: 11,
- comments: [],
- attempt: {
- sourceHash: HASH_A,
- requiresTranslation: true,
- detectedLanguage: "German",
- sourceComplete: true,
- },
- now: 100,
- });
- assert.equal(created.storage, "comment");
- assert.equal(created.markerOnly, false);
- assert.equal(created.comment.id, 50);
- assert.match(calls[0][1].body, /detected language: German/);
-
- const prior = botComment(calls[0][1].body, 50);
- calls.length = 0;
- const updated = await persistTranslationControlState({
- github,
- owner: "o",
- repo: "r",
- issue_number: 11,
- comments: [prior],
- priorState: extractTranslationControlState([prior]),
- attempt: {
- sourceHash: HASH_B,
- requiresTranslation: true,
- detectedLanguage: "French",
- sourceComplete: true,
- },
- now: 200,
- });
- assert.equal(updated.storage, "comment");
- assert.deepEqual(calls.map((c) => c[0]), ["update"]);
- assert.equal(calls[0][1].comment_id, 50);
- assert.match(calls[0][1].body, /detected language: French/);
- });
-
- it("ignores author comments containing the control marker", () => {
- const forged = {
- id: 9,
- user: { login: "attacker" },
- body: buildTranslationControlComment({
- v: 2,
- sourceHash: HASH_B,
- attemptedAt: 99,
- recent: [99],
- requiresTranslation: false,
- detectedLanguage: "English",
- }),
- };
- const bot = botComment(buildTranslationControlComment({
- v: 2,
- sourceHash: HASH_A,
- attemptedAt: 1,
- recent: [1],
- requiresTranslation: true,
- detectedLanguage: "German",
- }), 10);
- assert.equal(resolveControlState([forged, bot]).sourceHash, HASH_A);
- assert.equal(findControlComment([forged, bot]).id, 10);
- });
-
- it("treats corrupt control state as missing", () => {
- const comments = [
- botComment(`${CONTROL_MARKER}\n`),
- ];
- assert.equal(extractTranslationControlState(comments), null);
- assert.equal(resolveControlState(comments), null);
- });
-
- it("never treats the issue body as authoritative control state", () => {
- const orphan = `${SOURCE}\n\n\n`;
- assert.equal(resolveControlState([], 1), null);
- assert.equal(extractTranslationControlState([]), null);
- assert.equal(stripOrphanBodyControlState(orphan).includes("control-state-v2:"), false);
- });
-
- it("failed sticky update preserves existing control comments and does not delete", async () => {
- // Corrupt sticky comment is still the upsert target; update failure must
- // not cascade into deleting it (or any sibling) as "redundant."
- const stale = botComment(
- `${CONTROL_MARKER}\n`,
- 5,
- );
- const { github, calls } = mockGithub({
- update: async () => {
- throw new Error("API update failed");
- },
- });
- await assert.rejects(
- () => persistTranslationControlState({
- github,
- owner: "o",
- repo: "r",
- issue_number: 1,
- comments: [stale],
- attempt: {
- sourceHash: HASH_A,
- requiresTranslation: false,
- detectedLanguage: "English",
- sourceComplete: true,
- },
- }),
- /persistence failed/,
- );
- assert.deepEqual(calls.map((c) => c[0]), ["update"]);
- assert.equal(calls[0][1].comment_id, 5);
- assert.ok(!calls.some((c) => c[0] === "delete"));
- assert.equal(findControlComment([stale]), null);
- assert.equal(findStickyControlComment([stale]).id, 5);
- });
-
- it("re-fetches comments when none are supplied and still verifies authorship", async () => {
- const forged = {
- id: 9,
- user: { login: "attacker" },
- body: `please ignore ${CONTROL_MARKER} forged`,
- };
- const bot = botComment(buildTranslationControlComment({
- v: 2,
- sourceHash: HASH_A,
- attemptedAt: 1,
- recent: [1],
- requiresTranslation: true,
- detectedLanguage: "German",
- }), 10);
- const { github, calls } = mockGithub({ listComments: [forged, bot] });
- const result = await deleteVerifiedControlComments({
- github,
- owner: "o",
- repo: "r",
- issue_number: 1,
- commentIds: [9, 10],
- });
- assert.deepEqual(result.deleted, [10]);
- assert.deepEqual(result.skipped, [9]);
- assert.ok(calls.some((c) => c[0] === "paginate"));
- });
-
- it("failed comment update preserves the previous comment", async () => {
- const priorBody = buildTranslationControlComment({
- v: 2,
- sourceHash: HASH_B,
- attemptedAt: 1,
- recent: [1],
- requiresTranslation: false,
- detectedLanguage: "English",
- });
- const prior = botComment(priorBody, 8);
- const { github, calls } = mockGithub({
- update: async () => {
- throw new Error("API update failed");
- },
- });
- await assert.rejects(
- () => persistTranslationControlState({
- github,
- owner: "o",
- repo: "r",
- issue_number: 1,
- comments: [prior],
- priorState: extractTranslationControlState([prior]),
- attempt: {
- sourceHash: HASH_A,
- requiresTranslation: false,
- detectedLanguage: "English",
- sourceComplete: true,
- },
- now: 200,
- }),
- /persistence failed/,
- );
- assert.deepEqual(calls.map((c) => c[0]), ["update"]);
- assert.ok(!calls.some((c) => c[0] === "delete"));
- assert.equal(extractTranslationControlState([prior]).sourceHash, HASH_B);
- });
-
- it("deletes redundant bot comments only after sticky replacement succeeds", async () => {
- const older = botComment(buildTranslationControlComment({
- v: 2,
- sourceHash: HASH_B,
- attemptedAt: 1,
- recent: [1],
- requiresTranslation: true,
- detectedLanguage: "German",
- }), 1);
- const newer = botComment(buildTranslationControlComment({
- v: 2,
- sourceHash: HASH_A,
- attemptedAt: 2,
- recent: [1, 2],
- requiresTranslation: false,
- detectedLanguage: "English",
- }), 2);
- const { github, calls } = mockGithub();
- const result = await persistTranslationControlState({
- github,
- owner: "o",
- repo: "r",
- issue_number: 1,
- comments: [older, newer],
- priorState: extractTranslationControlState([older, newer]),
- attempt: {
- sourceHash: HASH_A,
- requiresTranslation: false,
- detectedLanguage: "English",
- sourceComplete: true,
- },
- now: 300,
- });
- // Sticky = oldest id; update it in place and delete the newer duplicate.
- assert.equal(result.comment.id, 1);
- assert.deepEqual(calls.map((c) => c[0]), ["update", "delete"]);
- assert.equal(calls[0][1].comment_id, 1);
- assert.equal(calls[1][1].comment_id, 2);
- assert.deepEqual(result.cleanup.deleted, [2]);
- });
-
- it("cleanup failure leaves valid fallback comments intact", async () => {
- const older = botComment(buildTranslationControlComment({
- v: 2,
- sourceHash: HASH_B,
- attemptedAt: 1,
- recent: [1],
- requiresTranslation: true,
- detectedLanguage: "German",
- }), 1);
- const newer = botComment(buildTranslationControlComment({
- v: 2,
- sourceHash: HASH_A,
- attemptedAt: 2,
- recent: [1, 2],
- requiresTranslation: false,
- detectedLanguage: "English",
- }), 2);
- const { github, calls } = mockGithub({
- delete: async () => {
- throw new Error("delete denied");
- },
- });
- const result = await persistTranslationControlState({
- github,
- owner: "o",
- repo: "r",
- issue_number: 1,
- comments: [older, newer],
- priorState: extractTranslationControlState([older, newer]),
- attempt: {
- sourceHash: HASH_A,
- requiresTranslation: false,
- detectedLanguage: "English",
- sourceComplete: true,
- },
- now: 300,
- });
- assert.equal(result.comment.id, 1);
- assert.equal(result.cleanup.failed.length, 1);
- assert.equal(result.cleanup.failed[0].id, 2);
- assert.ok(calls.some((c) => c[0] === "update"));
- // Newer duplicate remains when delete fails — durable fallback remains.
- assert.equal(extractTranslationControlState([older, newer]).sourceHash, HASH_A);
- assert.equal(extractTranslationControlState([older]).sourceHash, HASH_B);
- });
-
- it("cooldown and hourly limits survive repeated issue events via bot comments", () => {
- const now = 1_700_000_000_000;
- const body = buildTranslationControlComment({
- v: 2,
- sourceHash: HASH_A,
- attemptedAt: now,
- recent: [now],
- requiresTranslation: false,
- detectedLanguage: "English",
- });
- const priorState = resolveControlState([botComment(body)]);
- const decision = shouldTranslate({
- sourceTitle: "Hello",
- sourceBody: "Still English but edited enough to change the hash.",
- priorState,
- now: now + 5_000,
- });
- assert.equal(decision.ok, false);
- assert.equal(decision.reason, "rate_limited_interval");
-
- const hourly = resolveControlState([botComment(buildTranslationControlComment({
- v: 2,
- sourceHash: HASH_A,
- attemptedAt: now,
- recent: Array.from({ length: 10 }, (_, i) => now - i * 60_000),
- requiresTranslation: false,
- detectedLanguage: "English",
- }))]);
- const hourlyDecision = shouldTranslate({
- sourceTitle: "Hello again",
- sourceBody: "Another English edit that would otherwise probe the model.",
- priorState: hourly,
- now: now + 120_000,
- });
- assert.equal(hourlyDecision.ok, false);
- assert.equal(hourlyDecision.reason, "rate_limited_hourly");
- });
-
- it("rapid sequential edits cannot invoke the model repeatedly", async () => {
- const { github, calls } = mockGithub({ nextId: 3 });
- const first = await persistTranslationControlState({
- github,
- owner: "o",
- repo: "r",
- issue_number: 9,
- comments: [],
- attempt: {
- sourceHash: HASH_A,
- requiresTranslation: false,
- detectedLanguage: "English",
- sourceComplete: true,
- },
- now: 1_000,
- });
- const comments = [botComment(first.comment.body, first.comment.id)];
- const priorState = resolveControlState(comments);
- const second = shouldTranslate({
- sourceTitle: "Edit two",
- sourceBody: "Changed body content that must still be rate limited.",
- priorState,
- now: 1_000 + 10_000,
- });
- assert.equal(second.ok, false);
- assert.equal(second.reason, "rate_limited_interval");
- assert.equal(calls.filter((c) => c[0] === "create").length, 1);
- });
-
- it("persistence never mutates the issue title or body", async () => {
- const { github, calls } = mockGithub();
- await persistTranslationControlState({
- github,
- owner: "o",
- repo: "r",
- issue_number: 9,
- comments: [],
- attempt: {
- sourceHash: HASH_A,
- requiresTranslation: false,
- detectedLanguage: "English",
- sourceComplete: true,
- },
- });
- assert.ok(!calls.some((c) => c[0] === "issueUpdate"));
- });
-
- it("selects only github-actions control comments", () => {
- const state = {
- v: 2,
- sourceHash: HASH_A,
- sourceHashes: { issue: HASH_A },
- attemptedAt: 1,
- recent: [1],
- requiresTranslation: true,
- detectedLanguage: "German",
- };
- const comments = [
- botComment("random bot comment"),
- botComment(buildTranslationControlComment(state)),
- { user: { login: "contributor" }, body: buildTranslationControlComment(state) },
- ];
- assert.deepEqual(extractTranslationControlState(comments), state);
- });
-
- it("reader and selector agree on the newest control comment", () => {
- const older = {
- v: 2,
- sourceHash: HASH_A,
- sourceHashes: { issue: HASH_A },
- attemptedAt: 1,
- recent: [1],
- requiresTranslation: true,
- detectedLanguage: "German",
- };
- const newer = {
- v: 2,
- sourceHash: HASH_B,
- sourceHashes: { issue: HASH_B },
- attemptedAt: 2,
- recent: [1, 2],
- requiresTranslation: true,
- detectedLanguage: "Japanese",
- };
- const comments = [
- { id: 1, user: { login: BOT_LOGIN }, body: buildTranslationControlComment(older) },
- { id: 2, user: { login: BOT_LOGIN }, body: buildTranslationControlComment(newer) },
- ];
- const selected = findControlComment(comments);
- assert.equal(selected.id, 2);
- assert.deepEqual(extractTranslationControlState(comments), newer);
- });
-
- it("round-trips base64url control state without HTML breakout", () => {
- const state = {
- v: 2,
- sourceHash: HASH_A,
- attemptedAt: 42,
- recent: [40, 42],
- requiresTranslation: true,
- detectedLanguage: "German --> @username