Files
OmniRoute/scripts/build/pack-artifact-policy.ts
Diego Rodrigues de Sa e Souza 97aac6ac6c fix(ci): clear base-reds on release/v3.8.50 (round 4) (#10260)
* fix(ci): clear base-reds on release/v3.8.50 (round 4)

Drains the HARD failures reported by Release-Green run 31693210948 on issue #9985
(ESLint errors: 2) plus the merge-integrity red every open PR is inheriting.

- ESLint error 1: @omniroute/opencode-plugin/src/index.ts had a stray extra
  '});' (introduced by #9316) that broke parsing with 'unexpected file in NFT list'
  on the build path.
- ESLint error 2: cli-env-inline-comment-10100.test.ts used new Function to extract
  parseEnvValue from the bin entrypoint (no-new-func, Hard Rule #3). Extracted the
  helper to bin/cli/utils/parseEnvValue.mjs and import it from both the entrypoint
  and the test (same behavior, no eval).
- open-sse-typecheck (Fast Quality Gates): open-sse/utils/stream.ts imported
  sseCommentsEnabled twice (#9378) causing TS2300 Duplicate identifier; removed the
  duplicate import.
- Merge integrity (changelog + generated skills): skills/omni-settings/SKILL.md was
  edited manually by #10169 without updating the generator source, so
  check:agent-skills-sync failed on every PR (Generated: 1). Moved the curated
  thinking-budget content into a <!-- skill:custom-start --> block (the documented
  preservation mechanism), which the generator now keeps in sync.

Refs #9985

* fix(tests): align wave1-a poolside test with #10216 probed catalog

#10216 published Poolside's two authenticated-probe models
(poolside/laguna-xs-2.1, poolside/laguna-s-2.1) as static seeds, but the
wave1-a free-tier test still asserted 'no invented static model ids'
(entry.models === []), failing every open PR. Separate poolside from the
empty-models assertion and pin its probed catalog explicitly so a future
catalog change is a deliberate update, not a silent drift.

* fix(pack): register parseEnvValue.mjs in PACK_ARTIFACT_REQUIRED_PATHS

The extract of parseEnvValue to bin/cli/utils/parseEnvValue.mjs added a new
direct import to bin/omniroute.mjs, which pack-artifact-entrypoint-closures
enforces against PACK_ARTIFACT_REQUIRED_PATHS. Register the module so a future
tarball omission fails loudly.

* fix(combo): restore default same-model retry semantics after #10217

#10217 wired config.failoverBeforeRetry into the same-model retry guard in
both the priority/auto and round-robin loops, but DEFAULT_COMBO_CONFIG
defaulted the flag to true — flipping same-model retry off for every combo
that never touched the setting, not just the opt-in case. Round-4 bisect
(06f41cda63 vs d2fd88dfbc) reproduced this against
tests/unit/combo-499-abort.test.ts, tests/unit/combo-quota-exhaustion-only-fallback.test.ts
and tests/unit/combo-stream-readiness-fallback.test.ts. Flip the default to
false so the historical retry-before-failover behavior returns for combos
that never set the flag, while explicit opt-in (the two new tests #10217
added to combo-routing-engine.test.ts) still works.

* fix(quality): register visionBridge-responses-9597 in stryker tap.testFiles

check-mutation-test-coverage.mjs flagged tests/unit/guardrails/visionBridge-responses-9597.test.ts
as covering open-sse/services/combo/comboStructure.ts without being listed
in stryker.conf.json's tap.testFiles array. Add it so mutation coverage
attribution stays accurate.

* test(pack): expect parseEnvValue.mjs in the missing-artifact-paths fixture

The prior commit on this branch registered bin/cli/utils/parseEnvValue.mjs
in PACK_ARTIFACT_REQUIRED_PATHS but the "findMissingArtifactPaths flags
missing root runtime files in the tarball" test still hardcoded the old
expected list, so it never accounted for the new required path being
absent from the simulated tarball. Add it in its alphabetical slot.

* chore(lint): prune stale no-explicit-any suppression for call-log-file-rotation

--prune-suppressions found tests/unit/call-log-file-rotation.test.ts no
longer produces the 5 suppressed @typescript-eslint/no-explicit-any
warnings recorded in config/quality/eslint-suppressions.json. Remove the
dead entry so a regression would be caught again. Full-tree run with
--max-warnings 0 is clean: 0 errors, 0 warnings.

* fix(combo): decouple failoverBeforeRetry same-model guard from the skipUpstreamRetry default

Audit found that DEFAULT_COMBO_CONFIG.failoverBeforeRetry has defaulted to
true since before #10217 (predates #2417), and that value also feeds the
independent skipUpstreamRetry mechanism (src/sse/handlers/chat.ts:859,1126).
The previous commit on this branch flipped that default to false to fix the
#10217 same-model retry guard, which silently disabled skipUpstreamRetry's
own default-on behavior for every combo without an opt-in — a regression in
the opposite direction (executor-level retries before the loop's own
failover, changing latency/failure behavior).

Revert the default back to true and decouple the two mechanisms instead:
resolveComboConfig/resolveComboSetupConfig now also compute
failoverBeforeRetryExplicit, true only when a cascade layer (combo/provider/
global) literally sets failoverBeforeRetry to true — not merely inherited
from the default. The #10217 same-model retry guards in combo.ts (priority/
auto and round-robin loops) now read failoverBeforeRetryExplicit instead of
config.failoverBeforeRetry, restoring opt-in-only behavior for that guard
while the skipUpstreamRetry pass-through (config.failoverBeforeRetry at
combo.ts:1297,2865) is untouched and keeps its historical default-on.

* fix(combo,i18n): align getDefaultComboConfig with 10217 explicit flag; pt denoRelay entities

Two round-4 follow-ups exposed by the combinated base-red PR run:

1. comboConfig.ts: #10217 round-4 fix (104afeda4e) added
   failoverBeforeRetryExplicit to resolveComboConfig/resolveComboSetupConfig
   but getDefaultComboConfig() returned only DEFAULT_COMBO_CONFIG, so the
   combo-config.test.ts deepEqual (resolveComboConfig(null) ===
   getDefaultComboConfig()) failed on the extra field. Mirror the opt-in flag
   as false in the default.

2. pt.json: denoRelayOrgDomainHint still carried raw <app-name>/<org-slug>
   (the UNCLOSED_TAG RSC regression) — encode as &lt;...&gt; like the other
   42 locales, greening i18n-deno-relay-unclosed-tag.test.ts.

* chore(lint): disable @next/next/no-location-assign-relative-destination pending per-case review (#10292)

The eslint-config-next bump in #10043 shipped this new rule, flagging 6
pre-existing window.location.href navigations — several are deliberate
full-page reloads (login/logout state reset). Off with tracking issue
rather than a blanket router.push rewrite.

* fix(i18n): fill 439 missing UI keys (thinkingMode ×39 locales + pt catch-up) to restore 100% coverage

The #10169 Thinking Budget keys existed only in en/pt-BR/vi and the pt (PT-PT)
catalog from #10250 lagged 88 recent keys, dropping i18nUiCoverage to 99.3%
vs the frozen 100% ratchet baseline. Translated via the i18n:sync-ui marker
pipeline; glossary + ICU placeholder post-pass clean.

* fix(i18n): zh-TW glossary — replace retired 默認 with canonical 預設 in new thinkingMode keys

* chore(quality): rebase dead-code baseline 248 -> 409 for knip 6.32 bump (#10043)

dependabot #10043 upgraded knip 6.27 -> 6.32, which detects 162 MORE
genuinely-unused exports (331 vs 169) that 6.27 missed; DEAD_FILES
unchanged (78). Reproduced identically on the clean release/v3.8.50 tip
266e39d3 with a fresh 6.32 node_modules, so every PR is born red until
the tool change is absorbed. Owner authorized rebaseline (2026-08-13 via
PR #10260). Structural cleanup of the newly-surfaced dead exports remains
separate debt.

---------

Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
Co-authored-by: backryun <bakryun0718@proton.me>
2026-08-13 23:02:48 -03:00

331 lines
13 KiB
TypeScript

/**
* Shared policy for OmniRoute npm publish artifact hygiene.
*
* The package publishes the standalone runtime under dist/ (Layer 1: renamed from app/).
* This policy keeps local backups, QA scratch files, and development-only
* directories out of the staged dist/ tree and out of the final tarball.
*/
const STAGING_FORBIDDEN_DIRECTORIES = [
"app.__qa_backup",
"coverage",
"electron",
"logs",
"scripts/scratch",
"tests",
"vscode-extension",
"_ideia",
"_mono_repo",
"_references",
"_tasks",
];
const STAGING_FORBIDDEN_FILES = ["audit-report.json", "package-lock.json"];
export const APP_STAGING_REMOVAL_PATHS: string[] = [
...STAGING_FORBIDDEN_DIRECTORIES,
...STAGING_FORBIDDEN_FILES,
// onnxruntime CUDA provider binary (~316 MB) inflates the npm tarball
// past the registry 413 limit for npm.org. It's only needed on systems
// with a CUDA GPU — users install CUDA providers separately.
"node_modules/onnxruntime-node/bin/napi-v6/linux/x64/libonnxruntime_providers_cuda.so",
];
export const APP_STAGING_ALLOWED_EXACT_PATHS: string[] = [
".env.example",
"BUILD_SHA",
"docs/openapi.yaml",
// #7065: imported by dist/server-ws.mjs; assembleStandalone copies it but without
// this bare entry the prepublish prune deleted it → every `omniroute` boot of the
// published 3.8.47 crashed with ERR_MODULE_NOT_FOUND (same class as tls-options/3.8.41).
"head-response-guard.cjs",
"http-method-guard.cjs",
"open-sse/mcp-server/server.js",
"open-sse/vendor/codex-chatgpt-web/adapters/chatgpt-web/mcp-server.js",
// LLMLingua ONNX worker — esbuild'd standalone .js spawned via worker_threads
// (the Next.js bundler can't trace the computed Worker path). Kept like the MCP server.
"open-sse/services/compression/engines/llmlingua/onnxWorker.js",
"package.json",
"peer-stamp.mjs",
"main-server-timeouts.mjs",
"responses-ws-proxy.mjs",
"bin/chatgpt-web-codex-mcp.mjs",
"scripts/dev/sync-env.mjs",
"scripts/dev/tls-options.mjs",
"server.js",
"server-ws.mjs",
// #5452: dist/tls-options.mjs is copied by assembleStandalone (EXTRA_MODULE_ENTRIES)
// and imported by dist/server-ws.mjs for opt-in native HTTPS/TLS (#5361). Without
// this bare entry the prepublish prune (Step 10.7) deletes it → `omniroute serve`
// crashes with ERR_MODULE_NOT_FOUND (regressed in the published 3.8.41 tarball).
"tls-options.mjs",
"webdav-handler.mjs",
];
export const APP_STAGING_ALLOWED_PATH_PREFIXES: string[] = [
// Layer 1: Next.js distDir changed from ".next" to ".build/next"; the server
// bundle now lives under .build/next/ inside the standalone output.
".build/next/",
".next/",
"data/",
"node_modules/",
"open-sse/services/compression/engines/rtk/filters/",
"open-sse/services/compression/rules/",
"public/",
"src/lib/db/migrations/",
"src/mitm/",
];
export const PACK_ARTIFACT_ALLOWED_EXACT_PATHS: string[] = APP_STAGING_ALLOWED_EXACT_PATHS.map(
(filePath: string) => `dist/${filePath}`
);
export const PACK_ARTIFACT_ALLOWED_PATH_PREFIXES: string[] = APP_STAGING_ALLOWED_PATH_PREFIXES.map(
(directoryPath: string) => `dist/${directoryPath}`
);
export const PACK_ARTIFACT_ROOT_ALLOWED_EXACT_PATHS: string[] = [
".env.example",
"LICENSE",
"README.md",
"THIRD_PARTY_NOTICES.md",
"bin/aliasResolver.mjs",
"bin/chatgpt-web-codex-mcp.mjs",
// #7808: ESM loader hook split out of bin/aliasResolver.mjs to silence CodeQL
// js/incomplete-url-substring-sanitization (the old code built a
// `data:text/javascript,...` URL dynamically). Loaded via pathToFileURL() at
// runtime; shipped via package.json "files", so it must be allowed here.
"bin/aliasResolverHook.mjs",
"bin/mcp-server.mjs",
// #9281: stdout/stderr console guard preloaded via `node --import` by
// bin/mcp-server.mjs before the MCP entry's module graph evaluates — without it
// the published CLI's `omniroute --mcp` crashes on the pathToFileURL() import.
"bin/mcpStdioConsoleGuard.mjs",
"bin/nodeRuntimeSupport.mjs",
"bin/omniroute.mjs",
"bin/reset-password.mjs",
// Operator incident-recovery / cold-start shell tooling (rollback, snapshot,
// restore, cold-start bench) shipped in bin/ for self-hosters — not imported by
// the runtime. Included via the package.json "files": ["bin/"] entry, so they
// must be allowed here. Each script is self-documenting via --help.
"bin/_ops-common.sh",
"bin/cold-start-bench.sh",
"bin/restore-data.sh",
"bin/restore-policies.sh",
"bin/rollback.sh",
"bin/snapshot-data.sh",
"open-sse/mcp-server/README.md",
"open-sse/mcp-server/audit.ts",
"open-sse/mcp-server/httpTransport.ts",
"open-sse/mcp-server/index.ts",
"open-sse/mcp-server/runtimeHeartbeat.ts",
"open-sse/mcp-server/scopeEnforcement.ts",
"open-sse/mcp-server/server.ts",
// Runtime polyfill eagerly imported by bin/omniroute.mjs (Node <22 compat);
// shipped via package.json "files", so it must be allowed in the tarball.
"open-sse/utils/setupPolyfill.ts",
"package.json",
"scripts/build/assembleStandalone.mjs",
"scripts/build/backendOnlyPages.mjs",
"scripts/build/build-tproxy-native.mjs",
"scripts/build/build-next-isolated.mjs",
"scripts/check/check-supported-node-runtime.ts",
"scripts/build/native-binary-compat.mjs",
"scripts/build/postinstall.mjs",
"scripts/build/postinstallSupport.mjs",
"scripts/build/colocateOptionals.mjs",
// #7802: imported by scripts/build/postinstall.mjs to repair tls-client-node's
// native binary (chatgpt-web/claude-web/grok-web/lmarena/perplexity-web transport).
"scripts/build/fixTlsClientNodeBinary.mjs",
// #8859: imported by scripts/build/postinstall.mjs to repair playwright-core's
// browser resolution on Termux/Android (no glibc, no bundled browsers).
"scripts/build/fixPlaywrightAndroid.mjs",
// #5227: imported at runtime by bin/cli/commands/serve.mjs (heap auto-calibration).
"scripts/build/runtime-env.mjs",
"scripts/build/sync-env.mjs",
"scripts/dev/responses-ws-proxy.mjs",
"scripts/dev/sync-env.mjs",
// #5361: imported at runtime by bin/cli/commands/serve.mjs + the standalone
// server wrapper for opt-in native HTTPS/TLS serving (kept dependency-light).
"scripts/dev/tls-options.mjs",
"scripts/postinstall.mjs",
"src/shared/utils/nodeRuntimeSupport.ts",
];
export const PACK_ARTIFACT_ROOT_ALLOWED_PATH_PREFIXES: string[] = [
"@omniroute/opencode-plugin/",
"@omniroute/opencode-provider/",
"bin/cli/",
// Broad open-sse + src source dirs added to package.json "files" in v3.8.21
// to allow TypeScript-first imports from the published package.
"open-sse/",
"src/domain/",
"src/lib/",
"src/models/",
"src/mitm/",
"src/server/",
"src/shared/",
"src/sse/",
"src/types/",
];
export const PACK_ARTIFACT_REQUIRED_PATHS: string[] = [
"dist/open-sse/services/compression/engines/rtk/filters/generic-output.json",
"dist/open-sse/vendor/codex-chatgpt-web/adapters/chatgpt-web/mcp-server.js",
"dist/open-sse/services/compression/rules/en/filler.json",
"dist/server.js",
"dist/server-ws.mjs",
"dist/responses-ws-proxy.mjs",
"dist/peer-stamp.mjs",
"dist/main-server-timeouts.mjs",
"dist/http-method-guard.cjs",
// #5452: regression guard — make check:pack-artifact fail loudly if the TLS
// opt-in sidecar (imported by dist/server-ws.mjs) ever vanishes from the tarball.
"dist/tls-options.mjs",
// #7065: regression guard for the HEAD response guard (dist/server-ws.mjs import).
"dist/head-response-guard.cjs",
"dist/webdav-handler.mjs",
"bin/cli/program.mjs",
// Direct imports of bin/omniroute.mjs — bin/cli/ is only an allowlist PREFIX, so a
// file vanishing from the tarball never fails the unexpected-paths check; only these
// required entries make its absence loud (#7065 class; derived + enforced by
// tests/unit/pack-artifact-entrypoint-closures.test.ts).
"bin/cli/data-dir.mjs",
"bin/cli/utils/ensureAndroidCacheDir.mjs",
"bin/cli/utils/parseEnvValue.mjs",
"bin/cli/utils/storageKeyProvision.mjs",
"bin/cli/utils/versionFastPath.mjs",
"bin/mcp-server.mjs",
// #9281: stdout/stderr console guard preloaded via `node --import` by
// bin/mcp-server.mjs before the MCP entry's module graph evaluates — without it
// the published CLI's `omniroute --mcp` crashes on the pathToFileURL() import.
"bin/mcpStdioConsoleGuard.mjs",
"bin/nodeRuntimeSupport.mjs",
"bin/omniroute.mjs",
// #7808: aliasResolver + its hook file. bin/omniroute.mjs imports
// bin/aliasResolver.mjs at startup, which in turn registers
// bin/aliasResolverHook.mjs as the ESM loader. Both must ship in the tarball
// or the CLI fails to boot — list them REQUIRED so a regression is loud.
"bin/aliasResolver.mjs",
"bin/aliasResolverHook.mjs",
"package.json",
"scripts/build/native-binary-compat.mjs",
"scripts/build/postinstall.mjs",
"scripts/build/postinstallSupport.mjs",
"scripts/build/colocateOptionals.mjs",
"scripts/build/fixTlsClientNodeBinary.mjs",
"scripts/build/runtime-env.mjs",
"src/shared/utils/nodeRuntimeSupport.ts",
];
PACK_ARTIFACT_ALLOWED_EXACT_PATHS.push(...PACK_ARTIFACT_ROOT_ALLOWED_EXACT_PATHS);
PACK_ARTIFACT_ALLOWED_PATH_PREFIXES.push(...PACK_ARTIFACT_ROOT_ALLOWED_PATH_PREFIXES);
export function normalizeArtifactPath(filePath: string): string {
return String(filePath || "")
.replace(/\\/g, "/")
.replace(/^\.\//, "")
.replace(/^\/+/, "")
.replace(/\/{2,}/g, "/");
}
/** Extract complete JSON values from npm's mixed stdout/stderr-style output. */
export function parseJsonValuesOutput(output: string): unknown[] {
const values: unknown[] = [];
for (let start = 0; start < output.length; start++) {
if (output[start] !== "[" && output[start] !== "{") continue;
const stack: string[] = [];
let inString = false;
let escaped = false;
for (let end = start; end < output.length; end++) {
const char = output[end];
if (inString) {
if (escaped) escaped = false;
else if (char === "\\") escaped = true;
else if (char === '"') inString = false;
continue;
}
if (char === '"') {
inString = true;
} else if (char === "[" || char === "{") {
stack.push(char);
} else if (char === "]" || char === "}") {
const expectedOpen = char === "]" ? "[" : "{";
if (stack.at(-1) !== expectedOpen) break;
stack.pop();
if (stack.length === 0) {
try {
const parsed: unknown = JSON.parse(output.slice(start, end + 1));
values.push(parsed);
start = end;
} catch {
// This bracket pair was not a complete JSON value; continue scanning.
}
break;
}
}
}
}
return values;
}
/** Extract the first matching JSON array from npm's mixed stdout/stderr-style output. */
export function parseJsonArrayOutput(
output: string,
matches: (parsed: unknown[]) => boolean = () => true
): unknown[] {
const parsed = parseJsonValuesOutput(output).find(
(value): value is unknown[] => Array.isArray(value) && matches(value)
);
if (!parsed) throw new Error("Expected a valid JSON array in command output.");
return parsed;
}
/**
* Paths that are NEVER publishable, whatever the allowlist says.
*
* Existence reason: the allowlist grants whole prefixes (e.g.
* `@omniroute/opencode-provider/`), so a nested `node_modules` inside an allowed
* prefix used to be authorized by it. That shipped 79 MB of devDependencies
* (tsup/esbuild/typescript) — 80% of the tarball — whenever the publish ran from
* a machine where someone had installed inside that subpackage. `files[]` in
* package.json now excludes it at the source; this is the gate that FAILS if it
* ever comes back instead of silently allowing it.
*/
export const PACK_ARTIFACT_NEVER_ALLOWED_SEGMENTS: string[] = ["node_modules"];
export function findUnexpectedArtifactPaths(
filePaths: string[],
{ exactPaths = [], prefixPaths = [] }: { exactPaths?: string[]; prefixPaths?: string[] } = {}
): string[] {
const normalizedExact = new Set(exactPaths.map(normalizeArtifactPath));
const normalizedPrefixes = prefixPaths.map(normalizeArtifactPath);
const hasForbiddenSegment = (filePath: string): boolean =>
filePath.split("/").some((segment) => PACK_ARTIFACT_NEVER_ALLOWED_SEGMENTS.includes(segment));
return filePaths
.map(normalizeArtifactPath)
.filter(Boolean)
.filter(
(filePath) =>
hasForbiddenSegment(filePath) ||
(!normalizedExact.has(filePath) &&
!normalizedPrefixes.some((prefix) => filePath.startsWith(prefix)))
)
.sort();
}
export function findMissingArtifactPaths(
filePaths: string[],
requiredPaths: string[] = []
): string[] {
const normalizedPaths = new Set(filePaths.map(normalizeArtifactPath).filter(Boolean));
return requiredPaths
.map(normalizeArtifactPath)
.filter(Boolean)
.filter((requiredPath) => !normalizedPaths.has(requiredPath))
.sort();
}