diff --git a/.oxlintrc.json b/.oxlintrc.json index 0a760d3d6b..1a8119d65e 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -1,7 +1,6 @@ { "$schema": "./node_modules/oxlint/configuration_schema.json", - "plugins": ["typescript", "import", "unicorn", "oxc"], - "jsPlugins": [{ "name": "rea", "specifier": "./scripts/lint/index.mjs" }], + "plugins": ["typescript", "import", "unicorn", "oxc", "vitest"], "rules": { "rea/no-buffer-deep-equality": "error", "rea/cancellation-cause": "error", @@ -29,7 +28,37 @@ "max-params": ["warn", 4], "no-var": "warn", "no-var-requires": "error", - "typescript/no-import-type-side-effects": "error" + "typescript/no-import-type-side-effects": "error", + "vitest/no-standalone-expect": "off", + "vitest/no-conditional-expect": "off", + "vitest/expect-expect": "off", + "vitest/no-disabled-tests": "off", + "vitest/require-mock-type-parameters": "off", + "vitest/require-to-throw-message": "off", + "import/no-duplicates": [ + "warn", + { + "preferInline": false + } + ], + "vitest/valid-expect": [ + "warn", + { + "maxArgs": 2 + } + ], + "rea/no-evidence-json-preadmission": "error", + "rea/no-positional-tool-contract": "error", + "rea/no-fixture-root-cleanup": "error", + "typescript/no-floating-promises": "warn", + "typescript/no-misused-promises": "warn", + "typescript/return-await": ["warn", "error-handling-correctness-only"], + "typescript/switch-exhaustiveness-check": [ + "warn", + { + "considerDefaultExhaustiveForUnions": false + } + ] }, "overrides": [ { @@ -42,6 +71,12 @@ "no-var-requires": "off" } }, + { + "files": ["src/**/*.ts"], + "rules": { + "typescript/no-misused-promises": "error" + } + }, { "files": ["tests/**/*.ts", "src/**/*.test.ts", "src/**/*.fixture.ts"], "rules": { @@ -53,7 +88,8 @@ "skipBlankLines": true, "skipComments": true } - ] + ], + "typescript/no-misused-promises": "warn" } }, { @@ -97,6 +133,25 @@ } ] } + }, + { + "files": ["tests/**/*.test.ts", "src/**/*.test.ts"], + "rules": { + "vitest/valid-expect": [ + "warn", + { + "maxArgs": 2 + } + ], + "vitest/valid-expect-in-promise": "error", + "vitest/no-focused-tests": "error" + } + }, + { + "files": ["src/**/*.test.ts"], + "rules": { + "typescript/no-floating-promises": "error" + } } ], "env": { @@ -115,5 +170,14 @@ ".devin", ".factory", "third_party" + ], + "options": { + "reportUnusedDisableDirectives": "error" + }, + "jsPlugins": [ + { + "name": "rea", + "specifier": "./scripts/lint/index.mjs" + } ] } diff --git a/docs/testing.md b/docs/testing.md index 7af365f6f2..f9dc40c84a 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -159,13 +159,31 @@ cause when a directly checked caller `AbortSignal` is converted to an follow indirect cancellation helpers, or prove that an explicit cause equals the caller's reason. Keep focused boundary and lifecycle tests for those facts. -Type-aware linting remains outside the routine gate: a full source, test, and -script scan exceeded the bounded process-family memory envelope, including -with Go runtime limits. Future scoped evaluation can consider -`typescript/no-floating-promises`, `typescript/return-await` with -`error-handling-correctness-only`, `typescript/no-misused-promises`, and -`typescript/switch-exhaustiveness-check`. None replaces behavioral checks for -source identity, omission versus unknown, or Evidence provenance. +`rea/no-evidence-json-preadmission` rejects direct generic JSON parses in +`createEvidence` payload expressions; it does not analyze intermediate values. +`rea/no-positional-tool-contract` rejects catalog indices used to select +operation names, schemas or types; display-only selection remains allowed. +`rea/no-fixture-root-cleanup` follows `const` fixture-root aliases into teardown +deletion; mutable bindings and mixed root arrays remain outside its scope. + +Focused `.only` declarations, broken assertion promise chains, and unused disable +directives are errors. `vitest/valid-expect` warns (`maxArgs: 2`) because valid stored and +collected assertions can be awaited later. `import/no-duplicates` warns with +`preferInline: false`, preserving separate type imports. + +Fast lint stays syntax-based. `npm run lint:typed` runs serially with pinned +`oxlint-tsgolint` 7.0.2005 and records native plus typed diagnostics in +`.cache/static-checks/typed-oxlint.json`. Production `src` treats +`no-misused-promises` as an error; tests/fixtures warn. `no-floating-promises` is +an error in `src/**/*.test.ts`; other scopes, return-await and exhaustiveness +findings warn pending triage. + +`npm run knip:production` writes advisory candidates to +`.cache/static-checks/production-reachability.json`; execution/report errors fail. +Ordinary Knip remains. `check`, `check:pr`, and `check:ci:deep` run reachability +then typed lint sequentially after cached work. Typed lint builds the project +type graph and stays separate from fast feedback. Its default Go settings are +`GOMAXPROCS=2` and `GOMEMLIMIT=1024MiB`; caller-selected settings take precedence. Routine iterations and rebases need focused regressions and relevant checks. Before handing off a PR, run `npm run check` and generated-document checks when diff --git a/knip.production.json b/knip.production.json new file mode 100644 index 0000000000..5ccf4fd2ad --- /dev/null +++ b/knip.production.json @@ -0,0 +1,35 @@ +{ + "$schema": "./node_modules/knip/schema.json", + "entry": [ + "src/main.ts!", + "src/cli.ts!", + "src/cliOutput.ts!", + "src/cliBuiltinArguments.ts!", + "src/cli/cliResultOutput.ts!", + "src/mcpDoctor.ts!", + "src/composition/javascriptAnalysisWorkerProcess.ts!", + "src/composition/webBundleAnalysisWorkerProcess.ts!", + "src/sqlite/SqliteDatabaseWorker.ts!", + "src/evm/EvmoleInterfaceWorker.ts!", + "src/browser/history/NodeCaptureProcess.ts!", + "src/javascript/sourceMaps/SourceMapCodecProcess.ts!", + "scripts/rea.mjs!", + "scripts/rea-android-workspace-guardian.mjs!", + "scripts/electron-active-hook.cjs!", + "scripts/prepack.mjs!", + "scripts/prepare.mjs!", + "scripts/verify-windows-native-artifact.mjs!" + ], + "project": ["src/**/*.ts!", "!src/**/*.test.ts", "!src/**/*.fixture.ts"], + "ignoreExportsUsedInFile": true, + "include": [ + "files", + "exports", + "nsExports", + "types", + "nsTypes", + "enumMembers", + "namespaceMembers", + "duplicates" + ] +} diff --git a/licenses/npm-dependencies.txt b/licenses/npm-dependencies.txt index aaf5608cba..df798b062d 100644 --- a/licenses/npm-dependencies.txt +++ b/licenses/npm-dependencies.txt @@ -2,7 +2,7 @@ REA npm dependency inventory Prepared from the root package-lock.json; refresh this inventory when locked dependencies change. License identifiers are upstream lockfile declarations, not a legal assessment. Runtime includes transitive and optional platform packages; development includes build, test and website tooling. -Locked package entries: 956; runtime: 180. +Locked package entries: 963; runtime: 180. Full runtime copyright/license/NOTICE texts: npm-runtime-notices.txt. @algolia/abtesting@1.25.0 | development | MIT @@ -1100,6 +1100,36 @@ commander@13.1.0 | runtime | MIT Package: https://www.npmjs.com/package/@oxfmt/binding-win32-x64-msvc/v/0.72.0 Archive: https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.72.0.tgz Integrity: sha512-uhooIN+DCzWX5uH7mQkSVCsODqsGluGJ4n3ch4+mITtWKpESMDjUJUSDBAVFuYQDwqY2McPDKdKJbM0oZVW/Qw== +@oxlint-tsgolint/darwin-arm64@7.0.2005 | development, optional | MIT + Lockfile path: node_modules/@oxlint-tsgolint/darwin-arm64 + Package: https://www.npmjs.com/package/@oxlint-tsgolint/darwin-arm64/v/7.0.2005 + Archive: https://registry.npmjs.org/@oxlint-tsgolint/darwin-arm64/-/darwin-arm64-7.0.2005.tgz + Integrity: sha512-VqDZxcZVjlonNLuuvraVNgDQt+1ndwa4yJo8OqsK4HYh/61ba9IlMStCVfi/aaIqRoDhNbAKC8gdcTrXqnb+Ug== +@oxlint-tsgolint/darwin-x64@7.0.2005 | development, optional | MIT + Lockfile path: node_modules/@oxlint-tsgolint/darwin-x64 + Package: https://www.npmjs.com/package/@oxlint-tsgolint/darwin-x64/v/7.0.2005 + Archive: https://registry.npmjs.org/@oxlint-tsgolint/darwin-x64/-/darwin-x64-7.0.2005.tgz + Integrity: sha512-4kUfLRNYoVpv0Tm3jPYrvxyfOc/LmdvFC3pKZ4tL6K7lA7K/VS81SAtmxRzbYXYSyb3SBWhwsuX+df4neqzxxA== +@oxlint-tsgolint/linux-arm64@7.0.2005 | development, optional | MIT + Lockfile path: node_modules/@oxlint-tsgolint/linux-arm64 + Package: https://www.npmjs.com/package/@oxlint-tsgolint/linux-arm64/v/7.0.2005 + Archive: https://registry.npmjs.org/@oxlint-tsgolint/linux-arm64/-/linux-arm64-7.0.2005.tgz + Integrity: sha512-rhUNcPsBlKvhXtrbhbBUidtRBRdJ8fbEHN9H5Q5N59gby18ruGuDA7KUxl7oRnUzp797uykYQhkBDm+0KpfK/Q== +@oxlint-tsgolint/linux-x64@7.0.2005 | development, optional | MIT + Lockfile path: node_modules/@oxlint-tsgolint/linux-x64 + Package: https://www.npmjs.com/package/@oxlint-tsgolint/linux-x64/v/7.0.2005 + Archive: https://registry.npmjs.org/@oxlint-tsgolint/linux-x64/-/linux-x64-7.0.2005.tgz + Integrity: sha512-tKs9yinDQZOHygM0e8EJnjTU3mzJtdfkOZP6Udu82iLMePUSHOhTJ9zzU/mSd1hQxURI4LTRRiFhfzC+PzwdNA== +@oxlint-tsgolint/win32-arm64@7.0.2005 | development, optional | MIT + Lockfile path: node_modules/@oxlint-tsgolint/win32-arm64 + Package: https://www.npmjs.com/package/@oxlint-tsgolint/win32-arm64/v/7.0.2005 + Archive: https://registry.npmjs.org/@oxlint-tsgolint/win32-arm64/-/win32-arm64-7.0.2005.tgz + Integrity: sha512-VYPnu1gjfbSv+ROhuyNfMs382D4zQU2ZRc8Uc3nuEZo1k2kf+1KfTrg4hrlWh73h9GI+dUWyo+DYCQzUBlPwcQ== +@oxlint-tsgolint/win32-x64@7.0.2005 | development, optional | MIT + Lockfile path: node_modules/@oxlint-tsgolint/win32-x64 + Package: https://www.npmjs.com/package/@oxlint-tsgolint/win32-x64/v/7.0.2005 + Archive: https://registry.npmjs.org/@oxlint-tsgolint/win32-x64/-/win32-x64-7.0.2005.tgz + Integrity: sha512-ekU89cTGy9fpQzuvxIsFHrnoP8XoFjGIRgPEW6eqgY2NYjGy9pwpjF79o04WXRMJFKRrcaVqkZXUUBdLsqpQDw== @oxlint/binding-android-arm-eabi@1.87.0 | development, optional | MIT Lockfile path: node_modules/@oxlint/binding-android-arm-eabi Package: https://www.npmjs.com/package/@oxlint/binding-android-arm-eabi/v/1.87.0 @@ -3540,6 +3570,11 @@ oxlint@1.87.0 | development | MIT Package: https://www.npmjs.com/package/oxlint/v/1.87.0 Archive: https://registry.npmjs.org/oxlint/-/oxlint-1.87.0.tgz Integrity: sha512-drxVlaW7Oqq7wSy1CN25znRu00aRq8n57Q2wniJRxCmtYVz07ufCm7EVDPQFZURrTgQIjzKrVEHAD/HcvJXObg== +oxlint-tsgolint@7.0.2005 | development | MIT + Lockfile path: node_modules/oxlint-tsgolint + Package: https://www.npmjs.com/package/oxlint-tsgolint/v/7.0.2005 + Archive: https://registry.npmjs.org/oxlint-tsgolint/-/oxlint-tsgolint-7.0.2005.tgz + Integrity: sha512-6ccPGu4u4J9bwYPHXBFPee3RZrX3BboRdU7k14zRDDB1lvN3O7J/vPRFdaEfa1+CWx4xnQQOp4ctoaF66oO0uw== p-limit@2.3.0 | development | MIT Lockfile path: node_modules/p-limit Package: https://www.npmjs.com/package/p-limit/v/2.3.0 diff --git a/package-lock.json b/package-lock.json index 170aee203d..0efee88df8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,6 +73,7 @@ "lint-staged": "16.4.0", "oxfmt": "0.72.0", "oxlint": "1.87.0", + "oxlint-tsgolint": "7.0.2005", "release-please": "17.3.0", "rolldown": "1.2.12", "tsrs": "0.18.0-ts7.1.0-dev.20260929", @@ -3491,6 +3492,90 @@ "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/@oxlint-tsgolint/darwin-arm64": { + "version": "7.0.2005", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-arm64/-/darwin-arm64-7.0.2005.tgz", + "integrity": "sha512-VqDZxcZVjlonNLuuvraVNgDQt+1ndwa4yJo8OqsK4HYh/61ba9IlMStCVfi/aaIqRoDhNbAKC8gdcTrXqnb+Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxlint-tsgolint/darwin-x64": { + "version": "7.0.2005", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-x64/-/darwin-x64-7.0.2005.tgz", + "integrity": "sha512-4kUfLRNYoVpv0Tm3jPYrvxyfOc/LmdvFC3pKZ4tL6K7lA7K/VS81SAtmxRzbYXYSyb3SBWhwsuX+df4neqzxxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxlint-tsgolint/linux-arm64": { + "version": "7.0.2005", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-arm64/-/linux-arm64-7.0.2005.tgz", + "integrity": "sha512-rhUNcPsBlKvhXtrbhbBUidtRBRdJ8fbEHN9H5Q5N59gby18ruGuDA7KUxl7oRnUzp797uykYQhkBDm+0KpfK/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint-tsgolint/linux-x64": { + "version": "7.0.2005", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-x64/-/linux-x64-7.0.2005.tgz", + "integrity": "sha512-tKs9yinDQZOHygM0e8EJnjTU3mzJtdfkOZP6Udu82iLMePUSHOhTJ9zzU/mSd1hQxURI4LTRRiFhfzC+PzwdNA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint-tsgolint/win32-arm64": { + "version": "7.0.2005", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-arm64/-/win32-arm64-7.0.2005.tgz", + "integrity": "sha512-VYPnu1gjfbSv+ROhuyNfMs382D4zQU2ZRc8Uc3nuEZo1k2kf+1KfTrg4hrlWh73h9GI+dUWyo+DYCQzUBlPwcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxlint-tsgolint/win32-x64": { + "version": "7.0.2005", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-x64/-/win32-x64-7.0.2005.tgz", + "integrity": "sha512-ekU89cTGy9fpQzuvxIsFHrnoP8XoFjGIRgPEW6eqgY2NYjGy9pwpjF79o04WXRMJFKRrcaVqkZXUUBdLsqpQDw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@oxlint/binding-android-arm-eabi": { "version": "1.87.0", "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.87.0.tgz", @@ -10137,6 +10222,24 @@ } } }, + "node_modules/oxlint-tsgolint": { + "version": "7.0.2005", + "resolved": "https://registry.npmjs.org/oxlint-tsgolint/-/oxlint-tsgolint-7.0.2005.tgz", + "integrity": "sha512-6ccPGu4u4J9bwYPHXBFPee3RZrX3BboRdU7k14zRDDB1lvN3O7J/vPRFdaEfa1+CWx4xnQQOp4ctoaF66oO0uw==", + "dev": true, + "license": "MIT", + "bin": { + "tsgolint": "bin/tsgolint.js" + }, + "optionalDependencies": { + "@oxlint-tsgolint/darwin-arm64": "7.0.2005", + "@oxlint-tsgolint/darwin-x64": "7.0.2005", + "@oxlint-tsgolint/linux-arm64": "7.0.2005", + "@oxlint-tsgolint/linux-x64": "7.0.2005", + "@oxlint-tsgolint/win32-arm64": "7.0.2005", + "@oxlint-tsgolint/win32-x64": "7.0.2005" + } + }, "node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", diff --git a/package.json b/package.json index 28356cfa87..a1e8ae5d6c 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "prebuild": "npm run deps:check", "build": "node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run artifacts:skills --force", "precheck": "npm run deps:check", - "check": "node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run typecheck lint:built format:check knip metadata:check && npm run verify:architecture-guards", + "check": "node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run typecheck lint:built format:check knip metadata:check && npm run verify:architecture-guards && npm run knip:production && npm run lint:typed", "precheck:fast": "npm run deps:check", "check:fast": "node scripts/run-turbo.mjs run typecheck lint --output-logs=errors-only", "precheck:changed": "npm run deps:check", @@ -120,14 +120,14 @@ "precheck:test": "npm run deps:check", "check:test": "node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run typecheck lint:built format:check knip metadata:check test:run", "precheck:pr": "npm run deps:check", - "check:pr": "node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run typecheck lint:built format:check knip metadata:check test:run && npm run test:python && npm run docs:check && npm run jscpd && npm run scan:todos", + "check:pr": "node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run typecheck lint:built format:check knip metadata:check test:run && npm run test:python && npm run docs:check && npm run jscpd && npm run scan:todos && npm run knip:production && npm run lint:typed", "precheck:ci": "npm run deps:check", "precheck:ci:fast": "npm run deps:check", "precheck:ci:deep": "npm run deps:check", "check:ci": "npm run check:ci:fast && npm run check:ci:deep", "check:ci:fast": "node scripts/run-exclusive.mjs artifacts npm run check:ci:fast:tasks", "check:ci:fast:tasks": "node scripts/run-turbo.mjs run typecheck lint:built format:check", - "check:ci:deep": "node scripts/run-exclusive.mjs artifacts npm run check:ci:deep:tasks && npm run verify:architecture-guards && npm run verify:test-discovery && npm run test:python", + "check:ci:deep": "node scripts/run-exclusive.mjs artifacts npm run check:ci:deep:tasks && npm run verify:architecture-guards && npm run verify:test-discovery && npm run test:python && npm run knip:production && npm run lint:typed", "check:ci:deep:tasks": "node scripts/run-turbo.mjs run knip metadata:check jscpd scan:todos", "check:ci:tasks": "node scripts/run-turbo.mjs run typecheck lint:built format:check knip metadata:check jscpd scan:todos", "ci:static-context": "node scripts/ci/static-task-context.mjs", @@ -326,7 +326,9 @@ "docs:prepare": "node scripts/generate-package-metadata.mjs && node scripts/run-turbo.mjs run artifacts:product-catalog artifacts:managed-evidence artifacts:error-schema", "verify:web:qlog": "npm run build:cached && node scripts/verify-qlog-captures.mjs", "licenses:generate": "node scripts/generate-third-party-inventory.mjs", - "licenses:check": "node scripts/generate-third-party-inventory.mjs --check" + "licenses:check": "node scripts/generate-third-party-inventory.mjs --check", + "lint:typed": "node scripts/ci/static-check-report.mjs typed-lint", + "knip:production": "node scripts/ci/production-reachability-report.mjs" }, "lint-staged": { "*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}": [ @@ -404,6 +406,7 @@ "lint-staged": "16.4.0", "oxfmt": "0.72.0", "oxlint": "1.87.0", + "oxlint-tsgolint": "7.0.2005", "release-please": "17.3.0", "rolldown": "1.2.12", "tsrs": "0.18.0-ts7.1.0-dev.20260929", diff --git a/scripts/ci/plan.mjs b/scripts/ci/plan.mjs index 7865e1157f..a93872181b 100644 --- a/scripts/ci/plan.mjs +++ b/scripts/ci/plan.mjs @@ -108,7 +108,7 @@ function classifyPackage(base, head) { continue; } if ( - /^(test(?::|$)|check(?::|$)|lint(?::|$)|format(?::|$)|knip$|jscpd$)/.test( + /^(test(?::|$)|check(?::|$)|lint(?::|$)|format(?::|$)|knip(?::|$)|jscpd$)/.test( script, ) ) diff --git a/scripts/ci/production-reachability-report.d.mts b/scripts/ci/production-reachability-report.d.mts new file mode 100644 index 0000000000..3970ca390f --- /dev/null +++ b/scripts/ci/production-reachability-report.d.mts @@ -0,0 +1,33 @@ +export interface ProductionReachabilityFinding { + readonly name: string; + readonly namespace?: string; + readonly kind?: string; + readonly specifier?: string; + readonly line?: number; + readonly col?: number; + readonly pos?: number; +} + +export interface ProductionReachabilityIssue { + readonly file: string; + readonly owners?: readonly { readonly name: string }[]; + readonly files?: readonly ProductionReachabilityFinding[]; + readonly exports?: readonly ProductionReachabilityFinding[]; + readonly nsExports?: readonly ProductionReachabilityFinding[]; + readonly types?: readonly ProductionReachabilityFinding[]; + readonly nsTypes?: readonly ProductionReachabilityFinding[]; + readonly enumMembers?: readonly ProductionReachabilityFinding[]; + readonly namespaceMembers?: readonly ProductionReachabilityFinding[]; + readonly duplicates?: readonly (readonly ProductionReachabilityFinding[])[]; +} + +export interface ProductionReachabilityReport { + readonly issues: readonly ProductionReachabilityIssue[]; +} + +export function parseProductionReachabilityReport( + text: string, +): ProductionReachabilityReport; +export function recordProductionReachabilityReport( + root?: string, +): Promise; diff --git a/scripts/ci/production-reachability-report.mjs b/scripts/ci/production-reachability-report.mjs new file mode 100644 index 0000000000..8758e6a126 --- /dev/null +++ b/scripts/ci/production-reachability-report.mjs @@ -0,0 +1,86 @@ +import { spawnSync } from "node:child_process"; +import { mkdir, rm, writeFile } from "node:fs/promises"; +import { createRequire } from "node:module"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { z } from "zod"; + +const require = createRequire(import.meta.url); +const finding = z.object({ + name: z.string(), + namespace: z.string().optional(), + kind: z.string().optional(), + specifier: z.string().optional(), + line: z.number().optional(), + col: z.number().optional(), + pos: z.number().optional(), +}); +const issue = z.strictObject({ + file: z.string(), + owners: z.array(z.object({ name: z.string() })).optional(), + files: z.array(finding).optional(), + exports: z.array(finding).optional(), + nsExports: z.array(finding).optional(), + types: z.array(finding).optional(), + nsTypes: z.array(finding).optional(), + enumMembers: z.array(finding).optional(), + namespaceMembers: z.array(finding).optional(), + duplicates: z.array(z.array(finding)).optional(), +}); +const reportSchema = z.strictObject({ issues: z.array(issue) }); + +/** Admit the installed Knip JSON reporter's candidate records. */ +export const parseProductionReachabilityReport = (text) => + reportSchema.parse(JSON.parse(text)); + +/** Run production reachability without turning candidates into failures. */ +export const recordProductionReachabilityReport = async ( + root = process.cwd(), +) => { + const reportDirectory = join(root, ".cache", "static-checks"); + const reportPath = join(reportDirectory, "production-reachability.json"); + + // Findings are advisory. Execution, configuration and report-admission failures + // still fail, and must never leave an older successful report at this path. + await mkdir(reportDirectory, { recursive: true }); + await rm(reportPath, { force: true }); + const result = spawnSync( + process.execPath, + [ + join(dirname(require.resolve("knip")), "..", "bin", "knip.js"), + "--config", + "knip.production.json", + "--production", + "--reporter", + "json", + "--no-exit-code", + ], + { cwd: root, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 }, + ); + if (result.stderr) process.stderr.write(result.stderr); + if (result.error !== undefined) throw result.error; + if (result.status !== 0) { + if (result.stdout) process.stderr.write(result.stdout); + throw new Error( + result.status === null + ? `Production reachability analysis terminated by ${result.signal}` + : `Production reachability analysis failed with status ${result.status}`, + ); + } + const report = parseProductionReachabilityReport(result.stdout); + await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`); + console.log( + `Production reachability: ${report.issues.length} files with advisory candidates.`, + ); + console.log(`Full report: ${reportPath}`); + console.log( + "Candidates require runtime, verifier and published-contract review before removal; ordinary Knip remains separate.", + ); + return report; +}; + +if ( + process.argv[1] !== undefined && + resolve(process.argv[1]) === fileURLToPath(import.meta.url) +) + await recordProductionReachabilityReport(); diff --git a/scripts/ci/static-check-report.mjs b/scripts/ci/static-check-report.mjs index eecaf750af..6654f9a60b 100644 --- a/scripts/ci/static-check-report.mjs +++ b/scripts/ci/static-check-report.mjs @@ -48,7 +48,12 @@ const duplicateReport = z }) .passthrough(); -const run = (packageName, entrypoint, arguments_) => { +const run = ( + packageName, + entrypoint, + arguments_, + environment = process.env, +) => { const executable = join( dirname(require.resolve(`${packageName}/package.json`)), entrypoint, @@ -56,6 +61,7 @@ const run = (packageName, entrypoint, arguments_) => { const result = spawnSync(process.execPath, [executable, ...arguments_], { encoding: "utf8", maxBuffer: 32 * 1024 * 1024, + env: environment, }); if (result.stderr) process.stderr.write(result.stderr); if (result.error !== undefined) throw result.error; @@ -66,14 +72,30 @@ const run = (packageName, entrypoint, arguments_) => { try { await mkdir(reportRoot, { recursive: true }); - if (mode === "lint") { - const reportPath = join(reportRoot, "oxlint.json"); + if (mode === "lint" || mode === "typed-lint") { + const typed = mode === "typed-lint"; + const reportPath = join( + reportRoot, + typed ? "typed-oxlint.json" : "oxlint.json", + ); await rm(reportPath, { force: true }); - const result = run("oxlint", "bin/oxlint", [ - ...extraArguments, - "--format", - "json", - ]); + const result = run( + "oxlint", + "bin/oxlint", + [ + ...(typed ? ["--type-aware", "--threads", "1"] : []), + ...extraArguments, + "--format", + "json", + ], + typed + ? { + ...process.env, + GOMAXPROCS: process.env.GOMAXPROCS ?? "2", + GOMEMLIMIT: process.env.GOMEMLIMIT ?? "1024MiB", + } + : process.env, + ); let report; try { report = lintReport.parse(JSON.parse(result.stdout)); @@ -89,7 +111,7 @@ try { ({ severity }) => severity === "error", ); console.log( - `Lint: ${report.number_of_files} files, ${errors.length} errors, ${warnings.length} warnings.`, + `${typed ? "Typed lint" : "Lint"}: ${report.number_of_files} files, ${errors.length} errors, ${warnings.length} warnings.`, ); for (const diagnostic of errors) { const location = diagnostic.labels[0]?.span; @@ -106,7 +128,9 @@ try { ) .slice(0, 8)) console.log(` ${rule}: ${count} warnings`); - console.log(`Full diagnostics: ${reportPath}; npm run lint:details`); + console.log( + `Full diagnostics: ${reportPath}${typed ? "" : "; npm run lint:details"}`, + ); process.exitCode = result.status; } else if (mode === "duplicates") { const output = join(reportRoot, "jscpd"); @@ -143,7 +167,7 @@ try { console.log(`Full diagnostics: ${reportPath}; npm run jscpd:details`); process.exitCode = result.status; } else { - throw new Error("Expected lint or duplicates"); + throw new Error("Expected lint, typed-lint or duplicates"); } } catch (cause) { console.error(cause instanceof Error ? cause.message : String(cause)); diff --git a/scripts/lint/index.mjs b/scripts/lint/index.mjs index c04cbd95c4..db0365acfd 100644 --- a/scripts/lint/index.mjs +++ b/scripts/lint/index.mjs @@ -1,10 +1,17 @@ import noBufferDeepEquality from "./no-buffer-deep-equality.mjs"; import cancellationCause from "./cancellation-cause.mjs"; +import noEvidenceJsonPreadmission from "./rules/no-evidence-json-preadmission.mjs"; +import noFixtureRootCleanup from "./rules/no-fixture-root-cleanup.mjs"; +import noPositionalToolContract from "./rules/no-positional-tool-contract.mjs"; + export default { meta: { name: "rea" }, rules: { "no-buffer-deep-equality": noBufferDeepEquality, "cancellation-cause": cancellationCause, + "no-evidence-json-preadmission": noEvidenceJsonPreadmission, + "no-fixture-root-cleanup": noFixtureRootCleanup, + "no-positional-tool-contract": noPositionalToolContract, }, }; diff --git a/scripts/lint/rules/import-source.d.mts b/scripts/lint/rules/import-source.d.mts new file mode 100644 index 0000000000..e7d68389b9 --- /dev/null +++ b/scripts/lint/rules/import-source.d.mts @@ -0,0 +1,4 @@ +export declare const resolveImportSource: ( + filename: string, + specifier: unknown, +) => string | undefined; diff --git a/scripts/lint/rules/import-source.mjs b/scripts/lint/rules/import-source.mjs new file mode 100644 index 0000000000..ec524cefc7 --- /dev/null +++ b/scripts/lint/rules/import-source.mjs @@ -0,0 +1,20 @@ +import { dirname, isAbsolute, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +/** Resolve local import spellings without assigning local meaning to bare or HTTP imports. */ +export const resolveImportSource = (filename, specifier) => { + if (typeof specifier !== "string") return undefined; + if (isAbsolute(specifier)) return resolve(specifier); + if (specifier.startsWith("file:")) { + try { + const url = new URL(specifier); + if (url.search !== "" || url.hash !== "") return undefined; + return fileURLToPath(url); + } catch { + return undefined; + } + } + if (specifier.startsWith("./") || specifier.startsWith("../")) + return resolve(dirname(filename), specifier); + return undefined; +}; diff --git a/scripts/lint/rules/no-evidence-json-preadmission.d.mts b/scripts/lint/rules/no-evidence-json-preadmission.d.mts new file mode 100644 index 0000000000..86d61729e2 --- /dev/null +++ b/scripts/lint/rules/no-evidence-json-preadmission.d.mts @@ -0,0 +1,4 @@ +import type { RuleTester } from "oxlint/plugins-dev"; + +declare const rule: Parameters[1]; +export default rule; diff --git a/scripts/lint/rules/no-evidence-json-preadmission.mjs b/scripts/lint/rules/no-evidence-json-preadmission.mjs new file mode 100644 index 0000000000..d591d54ad5 --- /dev/null +++ b/scripts/lint/rules/no-evidence-json-preadmission.mjs @@ -0,0 +1,137 @@ +import { findBinding } from "../bindings.mjs"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { resolveImportSource } from "./import-source.mjs"; + +const domain = fileURLToPath(new URL("../../../src/domain/", import.meta.url)); +const payloadFields = new Set(["parameters", "result", "rawResult"]); + +const propertyName = (node) => { + if (!node.computed && node.property?.type === "Identifier") + return node.property.name; + if ( + node.property?.type === "Literal" && + typeof node.property.value === "string" + ) + return node.property.value; + return undefined; +}; + +const fieldName = (node) => { + if (node.type !== "Property") return undefined; + if (!node.computed && node.key.type === "Identifier") return node.key.name; + if (node.key.type === "Literal" && typeof node.key.value === "string") + return node.key.value; + return undefined; +}; + +const importedBinding = (sourceCode, node) => { + if (node.type !== "Identifier") return undefined; + const variable = findBinding(sourceCode, node); + if (!variable || variable.defs.length !== 1) return undefined; + const definition = variable.defs[0]; + return definition.type === "ImportBinding" ? definition : undefined; +}; + +const isImported = (context, node, module, names) => { + const member = node.type === "MemberExpression" && !node.optional; + const binding = importedBinding( + context.sourceCode, + member ? node.object : node, + ); + if (!binding || binding.parent?.type !== "ImportDeclaration") return false; + const declaration = binding.parent; + if (declaration.importKind === "type" || binding.node.importKind === "type") + return false; + const specifier = declaration.source.value; + const target = resolveImportSource( + context.physicalFilename ?? context.filename, + specifier, + ); + if ( + target !== resolve(domain, `${module}.js`) && + target !== resolve(domain, `${module}.ts`) + ) + return false; + if (member) + return ( + binding.node.type === "ImportNamespaceSpecifier" && + names.has(propertyName(node)) + ); + if (binding.node.type !== "ImportSpecifier") return false; + return names.has(binding.node.imported.name ?? binding.node.imported.value); +}; + +const evidenceCreator = new Set(["createEvidence"]); +const genericJsonSchemas = new Set(["jsonValueSchema", "jsonObjectSchema"]); + +export default { + meta: { + type: "suggestion", + docs: { + description: + "Leave ordinary Evidence JSON payload admission with createEvidence.", + }, + schema: [], + messages: { + redundantJsonAdmission: + "Pass the {{field}} payload directly to createEvidence; it owns generic JSON admission. Keep semantic validation at its own boundary.", + }, + }, + create(context) { + return { + CallExpression(node) { + if ( + node.optional || + !isImported(context, node.callee, "evidence", evidenceCreator) + ) + return; + const observation = node.arguments[2]; + if (observation?.type !== "ObjectExpression") return; + for (const [index, property] of observation.properties.entries()) { + const field = fieldName(property); + if ( + !payloadFields.has(field) || + property.kind !== "init" || + property.method + ) + continue; + // A later spread or unknown key may replace this field before admission. + if ( + observation.properties + .slice(index + 1) + .some( + (later) => + later.type !== "Property" || + fieldName(later) === undefined || + fieldName(later) === field, + ) + ) + continue; + const parsed = property.value; + if ( + parsed.type !== "CallExpression" || + parsed.optional || + parsed.arguments.length !== 1 + ) + continue; + const parse = parsed.callee; + if ( + parse.type !== "MemberExpression" || + parse.optional || + propertyName(parse) !== "parse" + ) + continue; + if ( + isImported(context, parse.object, "jsonValue", genericJsonSchemas) + ) + context.report({ + node: parsed, + messageId: "redundantJsonAdmission", + data: { field }, + }); + } + }, + }; + }, +}; diff --git a/scripts/lint/rules/no-fixture-root-cleanup.d.mts b/scripts/lint/rules/no-fixture-root-cleanup.d.mts new file mode 100644 index 0000000000..b5656592e8 --- /dev/null +++ b/scripts/lint/rules/no-fixture-root-cleanup.d.mts @@ -0,0 +1,3 @@ +import type { RuleTester } from "oxlint/plugins-dev"; +declare const rule: Parameters[1]; +export default rule; diff --git a/scripts/lint/rules/no-fixture-root-cleanup.mjs b/scripts/lint/rules/no-fixture-root-cleanup.mjs new file mode 100644 index 0000000000..bf97ae1300 --- /dev/null +++ b/scripts/lint/rules/no-fixture-root-cleanup.mjs @@ -0,0 +1,149 @@ +import { findBinding } from "../bindings.mjs"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { resolveImportSource } from "./import-source.mjs"; + +const root = fileURLToPath(new URL("../../../", import.meta.url)); +const fixtureModule = resolve(root, "tests/fixtures/temporaryDirectory"); +const workspaceModule = resolve( + root, + "tests/support/workspace/workspaceFixture", +); +const hooks = new Set(["afterEach", "afterAll", "onTestFinished"]); +const unwrap = (node) => { + while ( + node && + [ + "AwaitExpression", + "TSAsExpression", + "TSNonNullExpression", + "ChainExpression", + ].includes(node.type) + ) + node = node.argument ?? node.expression; + return node; +}; +const isFunction = (node) => + node && + [ + "FunctionDeclaration", + "FunctionExpression", + "ArrowFunctionExpression", + ].includes(node.type); + +export default { + meta: { + type: "problem", + docs: { + description: "Let createTestTempDirectory own removal of its root.", + }, + schema: [], + messages: { + duplicate: + "createTestTempDirectory already owns root cleanup; remove the duplicate teardown deletion, retaining cleanup of other resources.", + }, + }, + create(context) { + const sourceCode = context.sourceCode; + const filename = context.physicalFilename ?? context.filename; + const calls = []; + const variable = (node) => findBinding(sourceCode, node); + const imported = (node) => { + const member = node?.type === "MemberExpression" ? node : undefined; + const binding = member ? member.object : node; + if (binding?.type !== "Identifier") return undefined; + const definition = variable(binding)?.defs[0]; + if ( + definition?.type !== "ImportBinding" || + (member + ? definition.node.type !== "ImportNamespaceSpecifier" + : definition.node.type !== "ImportSpecifier") + ) + return undefined; + const declaration = definition.parent ?? definition.node.parent; + const specifier = declaration.source?.value; + if (typeof specifier !== "string") return undefined; + return { + name: member + ? member.computed + ? member.property.value + : member.property.name + : (definition.node.imported.name ?? definition.node.imported.value), + module: + resolveImportSource(filename, specifier)?.replace( + /\.(?:[cm]?[jt]s)$/u, + "", + ) ?? specifier, + }; + }; + const fixtureRoot = (raw, seen = new Set()) => { + const node = unwrap(raw); + if (node?.type === "CallExpression") { + const identity = imported(node.callee); + return ( + identity?.module === fixtureModule && + identity.name === "createTestTempDirectory" + ); + } + if (node?.type !== "Identifier") return false; + const binding = variable(node); + if (!binding || seen.has(binding)) return false; + seen.add(binding); + const definition = binding.defs[0]; + // Mutable ownership and mixed root registries require flow analysis, not + // a guess based on the variable name or one assignment. + return ( + definition?.type === "Variable" && + definition.parent.kind === "const" && + definition.node.id.type === "Identifier" && + fixtureRoot(definition.node.init, seen) + ); + }; + const callback = (raw) => { + const node = unwrap(raw); + if (isFunction(node)) return node; + if (node?.type !== "Identifier") return undefined; + const definition = variable(node)?.defs[0]; + if (definition?.type === "FunctionName") return definition.node; + if ( + definition?.type === "Variable" && + definition.parent.kind === "const" + ) { + const value = unwrap(definition.node.init); + if (isFunction(value)) return value; + } + return undefined; + }; + return { + CallExpression(node) { + calls.push(node); + }, + "Program:exit"() { + const teardowns = new Set(); + for (const call of calls) { + const identity = imported(call.callee); + if (identity?.module === "vitest" && hooks.has(identity.name)) { + const body = callback(call.arguments[0]); + if (body) teardowns.add(body); + } + } + for (const call of calls) { + const identity = imported(call.callee); + if (!identity) continue; + const removesRoot = + (identity.module === "node:fs/promises" && + ["rm", "rmdir"].includes(identity.name)) || + (identity.module === "node:fs" && + ["rm", "rmSync", "rmdir", "rmdirSync"].includes(identity.name)) || + (identity.module === workspaceModule && + identity.name === "removeTestWorkspace"); + if (!removesRoot || !fixtureRoot(call.arguments[0])) continue; + let parent = call.parent; + while (parent && !isFunction(parent)) parent = parent.parent; + if (teardowns.has(parent)) + context.report({ node: call, messageId: "duplicate" }); + } + }, + }; + }, +}; diff --git a/scripts/lint/rules/no-positional-tool-contract.d.mts b/scripts/lint/rules/no-positional-tool-contract.d.mts new file mode 100644 index 0000000000..b5656592e8 --- /dev/null +++ b/scripts/lint/rules/no-positional-tool-contract.d.mts @@ -0,0 +1,3 @@ +import type { RuleTester } from "oxlint/plugins-dev"; +declare const rule: Parameters[1]; +export default rule; diff --git a/scripts/lint/rules/no-positional-tool-contract.mjs b/scripts/lint/rules/no-positional-tool-contract.mjs new file mode 100644 index 0000000000..9a8276db6e --- /dev/null +++ b/scripts/lint/rules/no-positional-tool-contract.mjs @@ -0,0 +1,218 @@ +import { findBinding } from "../bindings.mjs"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { collectToolCatalogSources } from "./tool-catalog-sources.mjs"; +import { resolveImportSource } from "./import-source.mjs"; + +const root = fileURLToPath(new URL("../../../", import.meta.url)); +const catalogs = collectToolCatalogSources( + resolve(root, "src/contracts/toolContracts.ts"), +); + +const unwrap = (node) => { + while ( + node && + [ + "TSAsExpression", + "TSSatisfiesExpression", + "TSNonNullExpression", + "ChainExpression", + ].includes(node.type) + ) + node = node.expression; + return node; +}; +const numeric = (node) => + (node?.type === "Literal" && + (typeof node.value === "number" || + (typeof node.value === "string" && + /^(?:0|[1-9][0-9]*)$/u.test(node.value)))) || + (node?.type === "UnaryExpression" && + ["+", "-"].includes(node.operator) && + node.argument.type === "Literal" && + typeof node.argument.value === "number"); + +export default { + meta: { + type: "problem", + docs: { + description: + "Select canonical tool contracts by name, never catalog position.", + }, + schema: [], + messages: { + positional: + "Select a named tool contract; catalog ordering must not select an operation or schema.", + }, + }, + create(context) { + const sourceCode = context.sourceCode; + const filename = context.physicalFilename ?? context.filename; + const variable = (node) => findBinding(sourceCode, node); + const imported = (node) => { + if (node?.type !== "Identifier") return undefined; + const definition = variable(node)?.defs[0]; + if (definition?.type !== "ImportBinding") return undefined; + const declaration = definition.parent ?? definition.node.parent; + const specifier = declaration.source?.value; + const path = resolveImportSource(filename, specifier)?.replace( + /\.(?:[cm]?[jt]s)$/u, + "", + ); + const expected = catalogs.get(path); + if (expected === undefined) return undefined; + return { definition, expected }; + }; + const isCatalog = (raw, seen = new Set()) => { + const node = unwrap(raw); + if (!node) return false; + if (node.type === "Identifier") { + const binding = variable(node); + if (!binding || seen.has(binding)) return false; + seen.add(binding); + const identity = imported(node); + if (identity) { + const specifier = identity.definition.node; + return ( + specifier.type === "ImportSpecifier" && + identity.expected.has( + specifier.imported.name ?? specifier.imported.value, + ) + ); + } + const definition = binding.defs[0]; + return ( + definition?.type === "Variable" && + definition.parent.kind === "const" && + definition.node.id.type === "Identifier" && + isCatalog(definition.node.init, seen) + ); + } + if (node.type === "MemberExpression" || node.type === "TSQualifiedName") { + const object = + node.type === "MemberExpression" ? node.object : node.left; + const identity = imported(object); + if (identity?.definition.node.type !== "ImportNamespaceSpecifier") + return false; + const property = + node.type === "MemberExpression" ? node.property : node.right; + const name = node.computed ? property.value : property.name; + return identity.expected.has(name); + } + return false; + }; + const selectedContract = (raw, seen = new Set()) => { + const node = unwrap(raw); + if (!node) return undefined; + if ( + node.type === "MemberExpression" && + node.computed && + numeric(node.property) && + isCatalog(node.object) + ) + return node; + if (node.type === "CallExpression") { + const callee = unwrap(node.callee); + if ( + callee?.type === "MemberExpression" && + (callee.computed ? callee.property.value : callee.property.name) === + "at" && + numeric(node.arguments[0]) && + isCatalog(callee.object) + ) + return node; + } + if (node.type !== "Identifier") return undefined; + const binding = variable(node); + if (!binding || seen.has(binding)) return undefined; + seen.add(binding); + const definition = binding.defs[0]; + if (definition?.type !== "Variable" || definition.parent.kind !== "const") + return undefined; + if (definition.node.id.type === "Identifier") + return selectedContract(definition.node.init, seen); + if ( + definition.node.id.type === "ArrayPattern" && + definition.node.id.elements.some( + (element) => + element?.type === "Identifier" && element.name === node.name, + ) && + isCatalog(definition.node.init) + ) + return definition.node; + return undefined; + }; + const bindingFields = new Set(["name", "inputSchema", "outputSchema"]); + const isRegistrationOptions = (node) => { + if (node?.type !== "Identifier") return false; + const definition = variable(node)?.defs[0]; + if ( + definition?.type !== "ImportBinding" || + definition.node.type !== "ImportSpecifier" + ) + return false; + const declaration = definition.parent ?? definition.node.parent; + const specifier = declaration.source?.value; + const path = resolveImportSource(filename, specifier)?.replace( + /\.(?:[cm]?[jt]s)$/u, + "", + ); + const name = + definition.node.imported.name ?? definition.node.imported.value; + return ( + path === resolve(root, "src/server/toolRegistrationOptions") && + [ + "toolRegistrationOptions", + "applicationToolRegistrationOptions", + ].includes(name) + ); + }; + const reported = new Set(); + const reportSelection = (raw) => { + const node = selectedContract(raw); + if (node && !reported.has(node)) { + reported.add(node); + context.report({ node, messageId: "positional" }); + } + }; + return { + MemberExpression(node) { + const name = node.computed ? node.property.value : node.property.name; + const parent = node.parent; + const writesProperty = + (parent?.type === "AssignmentExpression" && parent.left === node) || + parent?.type === "UpdateExpression" || + (parent?.type === "UnaryExpression" && parent.operator === "delete"); + if (bindingFields.has(name) && !writesProperty) + reportSelection(node.object); + }, + CallExpression(node) { + if (isRegistrationOptions(unwrap(node.callee))) + reportSelection(node.arguments[0]); + }, + VariableDeclarator(node) { + if ( + node.id.type === "ObjectPattern" && + node.id.properties.some( + (property) => + property.type === "Property" && + bindingFields.has( + property.computed ? property.key.value : property.key.name, + ), + ) + ) + reportSelection(node.init); + }, + TSIndexedAccessType(node) { + if ( + node.objectType.type === "TSTypeQuery" && + node.indexType.type === "TSLiteralType" && + numeric(node.indexType.literal) && + isCatalog(node.objectType.exprName) + ) + context.report({ node, messageId: "positional" }); + }, + }; + }, +}; diff --git a/scripts/lint/rules/tool-catalog-sources.d.mts b/scripts/lint/rules/tool-catalog-sources.d.mts new file mode 100644 index 0000000000..6d45ede868 --- /dev/null +++ b/scripts/lint/rules/tool-catalog-sources.d.mts @@ -0,0 +1,4 @@ +export declare const collectToolCatalogSources: ( + entryPath: string, + readSource?: (path: string) => string, +) => ReadonlyMap>; diff --git a/scripts/lint/rules/tool-catalog-sources.mjs b/scripts/lint/rules/tool-catalog-sources.mjs new file mode 100644 index 0000000000..da07422f86 --- /dev/null +++ b/scripts/lint/rules/tool-catalog-sources.mjs @@ -0,0 +1,118 @@ +import { parse } from "@babel/parser"; +import { readFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; + +const withoutExtension = (path) => path.replace(/\.(?:[cm]?[jt]s)$/u, ""); +const arrayExpression = (raw) => { + let node = raw; + while ( + node && + ["TSAsExpression", "TSSatisfiesExpression"].includes(node.type) + ) + node = node.expression; + return node; +}; + +/** Read only source composition, never execute the runtime contract catalog. */ +export const collectToolCatalogSources = ( + entryPath, + readSource = (path) => readFileSync(path, "utf8"), +) => { + const catalogs = new Map(); + const modules = new Map(); + const active = new Set(); + const complete = new Set(); + const moduleFacts = (path) => { + let facts = modules.get(path); + if (facts) return facts; + // Read/parse errors intentionally escape: a broken inventory cannot disable + // the rule by presenting an empty set of catalog bindings. + const ast = parse(readSource(path), { + sourceType: "module", + plugins: ["typescript"], + }); + const imports = new Map(); + const exports = new Map(); + const locals = new Map(); + for (const statement of ast.program.body) { + if (statement.type === "ImportDeclaration") { + for (const specifier of statement.specifiers) { + if (specifier.type === "ImportSpecifier") + imports.set(specifier.local.name, { + source: statement.source.value, + name: specifier.imported.name ?? specifier.imported.value, + }); + } + } + const declarations = + statement.type === "ExportNamedDeclaration" + ? statement.declaration + : statement; + if (declarations?.type === "VariableDeclaration") + for (const declaration of declarations.declarations) + if (declaration.id.type === "Identifier") { + locals.set(declaration.id.name, declaration.init); + if (statement.type === "ExportNamedDeclaration") + exports.set(declaration.id.name, declaration.init); + } + } + facts = { imports, exports, locals }; + modules.set(path, facts); + return facts; + }; + const visit = (path, name) => { + const key = JSON.stringify([path, name]); + if (complete.has(key)) return; + if (active.has(key)) + throw new Error(`Cyclic tool catalog composition: ${path}#${name}`); + active.add(key); + const facts = moduleFacts(path); + const array = arrayExpression(facts.exports.get(name)); + if (array?.type !== "ArrayExpression") + throw new Error(`Expected exported tool catalog array: ${path}#${name}`); + const source = withoutExtension(path); + const names = catalogs.get(source) ?? new Set(); + names.add(name); + catalogs.set(source, names); + const composition = (raw, localPath = new Set()) => { + const expression = arrayExpression(raw); + if (expression?.type === "ArrayExpression") { + for (const element of expression.elements) + if (element?.type === "SpreadElement") + composition(element.argument, localPath); + return; + } + // Calls such as HOPPER_MEMORY_TOOL_DEFINITIONS.map(official) construct + // entries locally; they are not imported catalog arrays. Do not execute + // or infer catalog ownership from arbitrary computed expressions. + if (expression?.type !== "Identifier") return; + const binding = facts.imports.get(expression.name); + if (binding) { + if (!binding.source.startsWith(".")) + throw new Error(`Expected local catalog module: ${path}#${name}`); + visit( + `${withoutExtension(resolve(dirname(path), binding.source))}.ts`, + binding.name, + ); + return; + } + if (localPath.has(expression.name)) + throw new Error( + `Cyclic local catalog composition: ${path}#${expression.name}`, + ); + if (!facts.locals.has(expression.name)) + throw new Error( + `Unresolved local catalog spread: ${path}#${expression.name}`, + ); + composition( + facts.locals.get(expression.name), + new Set([...localPath, expression.name]), + ); + }; + composition(array); + active.delete(key); + complete.add(key); + }; + visit(entryPath, "TOOL_CONTRACTS"); + return catalogs; +}; diff --git a/src/application/ReconstructionObligationCandidates.semantic.test.ts b/src/application/ReconstructionObligationCandidates.semantic.test.ts index a56652033f..9b11b56f5f 100644 --- a/src/application/ReconstructionObligationCandidates.semantic.test.ts +++ b/src/application/ReconstructionObligationCandidates.semantic.test.ts @@ -3,7 +3,6 @@ import { describe, expect, it } from "vitest"; import { JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE } from "../contracts/javascript/javascriptRuntimeReconciliationExample.js"; import { createEvidence } from "../domain/evidence.js"; import { createEvidenceBundle } from "../domain/evidenceBundle.js"; -import { jsonValueSchema } from "../domain/jsonValue.js"; import { javascriptApplicationAnalysisResultSchema } from "../domain/javascript/javascriptApplicationAnalysis.js"; import { artifactEvidence } from "../domain/javascript/javascriptApplicationGraph.fixture.js"; import { @@ -108,7 +107,7 @@ const semanticApplication = ( predicateType: example.predicate_type, operation: example.operation, parameters: example.parameters, - result: jsonValueSchema.parse({ ...base, semantic_graph: graph }), + result: { ...base, semantic_graph: graph }, confidence: "derived", authority: "shipped-artifact", }, diff --git a/src/application/ReconstructionObligationCandidates.test.ts b/src/application/ReconstructionObligationCandidates.test.ts index cc15e6b290..759a060e8f 100644 --- a/src/application/ReconstructionObligationCandidates.test.ts +++ b/src/application/ReconstructionObligationCandidates.test.ts @@ -33,7 +33,6 @@ import { createJavaScriptApplicationGraph, createJavaScriptApplicationNode, } from "../domain/javascript/javascriptApplicationGraph.js"; -import { jsonValueSchema } from "../domain/jsonValue.js"; import { MANAGED_WORKFLOW_PROVIDER } from "../domain/investigationProviders.js"; import { createProcessCaptureEvidence } from "./process/ProcessEvidence.js"; @@ -90,7 +89,7 @@ const applicationWithUnknowns = ( predicateType: "rea.managed-application-graph", operation: "project_managed_application_graph", parameters: {}, - result: jsonValueSchema.parse({ + result: { projection_id: `magp_${"a".repeat(64)}`, root_artifact_sha256: APPLICATION_GRAPH_DIGESTS.asar, source_evidence: { @@ -113,7 +112,7 @@ const applicationWithUnknowns = ( coverage: { status: "complete-within-inputs" }, evidence_links: [source.evidence_id], limitations: [], - }), + }, confidence: "inferred", authority: "analyst-inference", evidenceLinks: [source.evidence_id], @@ -212,7 +211,7 @@ describe("reconstruction obligation candidates", () => { predicateType: "rea.process-capture", operation: "capture_process_scenario", parameters: {}, - result: jsonValueSchema.parse({ + result: { ...rest, manifest, exit: { @@ -224,7 +223,7 @@ describe("reconstruction obligation candidates", () => { ], }, }, - }), + }, }); const generated = deriveCandidates(createEvidenceBundle([evidence]), []); @@ -308,7 +307,7 @@ it("keeps each semantic candidate bound to its own file and authority context", predicateType: example.predicate_type, operation: example.operation, parameters: example.parameters, - result: jsonValueSchema.parse({ ...base, semantic_graph: semanticGraph }), + result: { ...base, semantic_graph: semanticGraph }, confidence: "derived", authority: "shipped-artifact", evidenceLinks: sourceRecords.map(({ evidence_id }) => evidence_id), @@ -415,7 +414,7 @@ it("falls back to a kind-based title when a semantic node label is blank", () => predicateType: example.predicate_type, operation: example.operation, parameters: example.parameters, - result: jsonValueSchema.parse({ ...base, semantic_graph: semanticGraph }), + result: { ...base, semantic_graph: semanticGraph }, confidence: "derived", authority: "shipped-artifact", evidenceLinks: [source.evidence_id], diff --git a/src/artifacts/inventory/ArtifactOccurrenceIdentity.test.ts b/src/artifacts/inventory/ArtifactOccurrenceIdentity.test.ts index 1d2ea42ae0..9d7fe276f6 100644 --- a/src/artifacts/inventory/ArtifactOccurrenceIdentity.test.ts +++ b/src/artifacts/inventory/ArtifactOccurrenceIdentity.test.ts @@ -9,7 +9,6 @@ import { compareArtifacts } from "../../domain/artifactComparison.js"; import { parseArtifactInventoryEvidence } from "../../domain/artifactInventoryEvidence.js"; import { projectAppleInventory } from "../../domain/apple/appleApplication.js"; import { createEvidence } from "../../domain/evidence.js"; -import { jsonValueSchema } from "../../domain/jsonValue.js"; import { thinMach } from "../../domain/binaryTarget.fixture.js"; import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js"; @@ -32,7 +31,7 @@ const observeArchive = async (order: readonly string[]) => { { operation: "inventory_artifact", parameters: {}, - result: jsonValueSchema.parse(inventory), + result: inventory, confidence: "observed", authority: "shipped-artifact", }, @@ -128,7 +127,7 @@ describe("content identity and occurrence facts", () => { { operation: "inventory_artifact", parameters: {}, - result: jsonValueSchema.parse(inventory), + result: inventory, confidence: "observed", authority: "shipped-artifact", }, diff --git a/src/artifacts/pyinstaller/PyInstallerFormatBoundaries.test.ts b/src/artifacts/pyinstaller/PyInstallerFormatBoundaries.test.ts index 898a0995fc..95bd2de054 100644 --- a/src/artifacts/pyinstaller/PyInstallerFormatBoundaries.test.ts +++ b/src/artifacts/pyinstaller/PyInstallerFormatBoundaries.test.ts @@ -19,7 +19,6 @@ import { import { readPyzArchive } from "./PyzArchive.js"; import { marshalCode } from "../../python/PythonPyc.fixture.js"; import { createEvidence } from "../../domain/evidence.js"; -import { jsonValueSchema } from "../../domain/jsonValue.js"; import { parseArtifactInventoryEvidence } from "../../domain/artifactInventoryEvidence.js"; import { ArtifactReaderFailure, @@ -365,7 +364,7 @@ it.each([true, false])( parameters: { page: index }, confidence: "observed", authority: "shipped-artifact", - result: jsonValueSchema.parse({ + result: { ...inventory, nodes: index === 0 ? inventory.nodes : [], edges: index === 0 ? inventory.edges : [], @@ -378,7 +377,7 @@ it.each([true, false])( row, ] : [row], - }), + }, }, ), ); diff --git a/src/browser/PlaywrightElectronActiveActions.ts b/src/browser/PlaywrightElectronActiveActions.ts index d06657e86b..3826e64824 100644 --- a/src/browser/PlaywrightElectronActiveActions.ts +++ b/src/browser/PlaywrightElectronActiveActions.ts @@ -373,7 +373,6 @@ const emitDeepLink = ( ); /** Read the current Electron windows, metrics, and hook snapshot. */ -// oxlint-disable-next-line max-lines-per-function -- one state read must share one capture boundary. export const readApplicationState = async ( application: ElectronApplication, ): Promise<{ diff --git a/src/domain/apple/appleApplication.test.ts b/src/domain/apple/appleApplication.test.ts index ccb00c333d..8a9685a887 100644 --- a/src/domain/apple/appleApplication.test.ts +++ b/src/domain/apple/appleApplication.test.ts @@ -10,7 +10,6 @@ import { AnalysisInputError } from "../analysisErrorCore.js"; import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import { digestCanonicalValue } from "../canonicalDigest.js"; import { createEvidence, type Evidence } from "../evidence.js"; -import { jsonValueSchema } from "../jsonValue.js"; type EntryKind = "file" | "directory" | "symlink" | "slice"; type RootFormat = "ipa" | "zip" | "dmg" | "directory" | "asar"; @@ -136,7 +135,7 @@ const inventoryEvidence = ( parameters: {}, confidence: "observed", authority: "shipped-artifact", - result: jsonValueSchema.parse({ + result: { manifest: { manifest_id: `agm_${digestCanonicalValue( { root_artifact_id: rootId, graph_sha256: graphSha256 }, @@ -157,7 +156,7 @@ const inventoryEvidence = ( integrity_contradictions: [], coverage: { scope: "container-entries", status: "complete" }, limitations: [], - }), + }, }, ); }; diff --git a/src/domain/artifactInventoryEvidence.test.ts b/src/domain/artifactInventoryEvidence.test.ts index 9ab18f2d51..0a5508a876 100644 --- a/src/domain/artifactInventoryEvidence.test.ts +++ b/src/domain/artifactInventoryEvidence.test.ts @@ -14,7 +14,6 @@ import { parseArtifactInspectionInventoryResult, } from "./artifactInventoryEvidence.js"; import { createEvidence } from "./evidence.js"; -import { jsonValueSchema } from "./jsonValue.js"; const inventoryEvidence = ARTIFACT_COMPARISON_EXAMPLE.left; const target = inventoryEvidence.subject; @@ -31,7 +30,7 @@ const inspectionEvidence = (result: unknown, subject = target) => { operation: "inspect_artifact", parameters: {}, - result: jsonValueSchema.parse(result), + result, confidence: "observed", authority: "shipped-artifact", }, @@ -152,12 +151,12 @@ it("keeps a commitment failure discovered across individually incomplete pages a parameters: {}, confidence: "observed", authority: "shipped-artifact", - result: jsonValueSchema.parse({ + result: { ...complete, manifest, nodes: includeNodes ? complete.nodes : [], occurrences: includeNodes ? [] : complete.occurrences, - }), + }, }, ); const nodes = page(true); diff --git a/src/domain/changedBehavior.test.ts b/src/domain/changedBehavior.test.ts index 8686065a43..4142623361 100644 --- a/src/domain/changedBehavior.test.ts +++ b/src/domain/changedBehavior.test.ts @@ -7,7 +7,7 @@ import { } from "./changedBehavior.js"; import { createEvidence, type Evidence } from "./evidence.js"; import { functionComparisonResultSchema } from "./functionComparisonSchemas.js"; -import { jsonValueSchema, type JsonValue } from "./jsonValue.js"; +import type { JsonValue } from "./jsonValue.js"; const source = (digit: string): Evidence => createEvidence( @@ -350,13 +350,11 @@ describe("changed-behavior input and Evidence validation", () => { predicateType: base.predicate_type, operation: base.operation, parameters: base.parameters, - result: jsonValueSchema.parse( - functionComparisonResultSchema.parse({ - ...result, - dimensions, - changes: dimensions.filter(({ status }) => status !== "unchanged"), - }), - ), + result: functionComparisonResultSchema.parse({ + ...result, + dimensions, + changes: dimensions.filter(({ status }) => status !== "unchanged"), + }), confidence: base.confidence, authority: base.authority, evidenceLinks: base.evidence_links, diff --git a/src/domain/functionComparison.test.ts b/src/domain/functionComparison.test.ts index f61a6babd8..6f2f767cdb 100644 --- a/src/domain/functionComparison.test.ts +++ b/src/domain/functionComparison.test.ts @@ -664,7 +664,7 @@ describe("function comparison ordering controls", () => { parameters: enhancedInputSchemas.analyze_function.parse({ procedure: "dispatch", }), - result: jsonValueSchema.parse(result), + result, confidence: "derived", authority: "shipped-artifact", relationshipCoverage: { status: "complete" }, diff --git a/src/domain/staticRuntimeCorrelation.test.ts b/src/domain/staticRuntimeCorrelation.test.ts index 0081495239..1fe58af428 100644 --- a/src/domain/staticRuntimeCorrelation.test.ts +++ b/src/domain/staticRuntimeCorrelation.test.ts @@ -35,11 +35,9 @@ const artifactComparison = createEvidence( left_evidence_ids: [ARTIFACT_COMPARISON_EXAMPLE.left.evidence_id], right_evidence_ids: [ARTIFACT_COMPARISON_EXAMPLE.right.evidence_id], }, - result: jsonValueSchema.parse( - compareArtifacts( - ARTIFACT_COMPARISON_EXAMPLE.left, - ARTIFACT_COMPARISON_EXAMPLE.right, - ), + result: compareArtifacts( + ARTIFACT_COMPARISON_EXAMPLE.left, + ARTIFACT_COMPARISON_EXAMPLE.right, ), confidence: "derived", authority: "analyst-inference", @@ -157,10 +155,10 @@ it("keeps text, assembly and unavailable CFG caveats on their selected correlati { operation: source.operation, parameters: source.parameters, - result: jsonValueSchema.parse({ + result: { ...functionDossierSchema.parse(source.normalized_result), basic_blocks: [{ start: "0x1000", end: "0x1000", successors: [] }], - }), + }, confidence: source.confidence, authority: source.authority, }, @@ -178,7 +176,7 @@ it("keeps text, assembly and unavailable CFG caveats on their selected correlati left_evidence_id: left.evidence_id, right_evidence_id: right.evidence_id, }, - result: jsonValueSchema.parse(computed), + result: computed, confidence: "derived", authority: "analyst-inference", limitations: computed.limitations, diff --git a/src/domain/webMcpDiscovery.test.ts b/src/domain/webMcpDiscovery.test.ts index 452038d6b5..29c57973a0 100644 --- a/src/domain/webMcpDiscovery.test.ts +++ b/src/domain/webMcpDiscovery.test.ts @@ -6,7 +6,6 @@ import { parseEvidenceBundle, serializeEvidenceBundle, } from "./evidenceBundle.js"; -import { jsonValueSchema } from "./jsonValue.js"; import { webMcpDiscoverySchema, type WebMcpDiscovery, @@ -106,7 +105,7 @@ describe("WebMCP discovery consequential annotation", () => { predicateType: "rea.webmcp-discovery", operation: "discover_webmcp_tools", parameters: {}, - result: jsonValueSchema.parse(result), + result, }); const bundle = createEvidenceBundle([evidence]); const reread = parseEvidenceBundle( diff --git a/tests/acceptance/applications/artifactComparisonCoverage.test.ts b/tests/acceptance/applications/artifactComparisonCoverage.test.ts index 94c0ceab1b..b60d46da19 100644 --- a/tests/acceptance/applications/artifactComparisonCoverage.test.ts +++ b/tests/acceptance/applications/artifactComparisonCoverage.test.ts @@ -14,7 +14,7 @@ import { parseEvidence, type Evidence, } from "../../../src/domain/evidence.js"; -import { jsonValueSchema } from "../../../src/domain/jsonValue.js"; + import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { cliTest, type TestCli } from "../../support/cli/cliFixture.js"; @@ -75,7 +75,7 @@ const assertCoverageControls = async ({ { operation: "inventory_artifact", parameters: {}, - result: jsonValueSchema.parse(result), + result: result, confidence: "derived", authority: "shipped-artifact", }, diff --git a/tests/boundary/browser/cdpCleanupCancellation.test.ts b/tests/boundary/browser/cdpCleanupCancellation.test.ts index 1c31341d48..8c59661d8d 100644 --- a/tests/boundary/browser/cdpCleanupCancellation.test.ts +++ b/tests/boundary/browser/cdpCleanupCancellation.test.ts @@ -1,4 +1,4 @@ -import { rm, writeFile } from "node:fs/promises"; +import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { pathToFileURL } from "node:url"; @@ -145,7 +145,6 @@ it("closes an Electron inspection session when cleanup is cancelled", async () = }); onTestFinished(async () => { await browser.close(); - await rm(root, { recursive: true, force: true }); }); const controller = new AbortController(); const pending = new CdpElectronProvider().inspectPage( @@ -204,7 +203,6 @@ it("classifies cancellation that arrives during final Electron detach", async () }); onTestFinished(async () => { await browser.close(); - await rm(root, { recursive: true, force: true }); }); const result = await new CdpElectronProvider().inspectPage( @@ -283,7 +281,6 @@ it.each( closing.mockRestore(); await provider.close(); await browser.close(); - await rm(root, { recursive: true, force: true }); }); const result = provider instanceof CdpBrowserProvider diff --git a/tests/boundary/browser/cdpProviderCleanupOwnership.test.ts b/tests/boundary/browser/cdpProviderCleanupOwnership.test.ts index 9a3582bf5b..edb801456b 100644 --- a/tests/boundary/browser/cdpProviderCleanupOwnership.test.ts +++ b/tests/boundary/browser/cdpProviderCleanupOwnership.test.ts @@ -1,4 +1,4 @@ -import { rm, writeFile } from "node:fs/promises"; +import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { pathToFileURL } from "node:url"; @@ -52,7 +52,6 @@ it.each([ | undefined; if (closeProvider !== undefined) await closeProvider.call(provider); await browser.close(); - await rm(root, { recursive: true, force: true }); }); const close = vi diff --git a/tests/boundary/filesystem/artifactComparison.test.ts b/tests/boundary/filesystem/artifactComparison.test.ts index 4b9b07321f..e15fdbf4b6 100644 --- a/tests/boundary/filesystem/artifactComparison.test.ts +++ b/tests/boundary/filesystem/artifactComparison.test.ts @@ -36,7 +36,7 @@ const observe = async (path: string) => { { operation: "inventory_artifact", parameters: {}, - result: jsonValueSchema.parse(inventory), + result: inventory, confidence: "observed", authority: "shipped-artifact", }, @@ -62,7 +62,7 @@ const inspectPages = ( { operation: "inspect_artifact", parameters: {}, - result: jsonValueSchema.parse({ + result: { inspection_id: `ai_${"f".repeat(64)}`, subject: { manifest_id: inventory.manifest.manifest_id, @@ -96,7 +96,7 @@ const inspectPages = ( }, evidence_links: pages.map(({ evidence_id }) => evidence_id), limitations: [], - }), + }, confidence: "observed", authority: "shipped-artifact", }, @@ -144,7 +144,7 @@ describe("leaf artifact comparison", () => { { operation: "inventory_artifact", parameters: {}, - result: jsonValueSchema.parse(withoutCoverage), + result: withoutCoverage, confidence: "observed", authority: "shipped-artifact", }, @@ -306,11 +306,11 @@ describe("artifact inventory coverage", () => { { operation: "inventory_artifact", parameters: {}, - result: jsonValueSchema.parse({ + result: { ...graph, coverage: { scope: "root", status: "unknown" }, limitations: ["No extra child entries were observed."], - }), + }, confidence: "derived", authority: "shipped-artifact", }, @@ -348,10 +348,10 @@ describe("artifact inventory coverage", () => { { operation: "inventory_artifact", parameters: fragment.parameters, - result: jsonValueSchema.parse({ + result: { ...fragmentGraph, coverage: { ...fragmentGraph.coverage, status: "unknown" }, - }), + }, confidence: "derived", authority: "shipped-artifact", }, @@ -448,10 +448,10 @@ describe("artifact comparison", () => { { operation: "inventory_artifact", parameters: {}, - result: jsonValueSchema.parse({ + result: { ...completeInventory, nodes: [], - }), + }, confidence: "observed", authority: "shipped-artifact", limitations: ["Inventory node observations are unavailable."], @@ -496,10 +496,10 @@ describe("artifact comparison completeness", () => { { operation: "inventory_artifact", parameters: {}, - result: jsonValueSchema.parse({ + result: { ...inventory, coverage: { scope: "root", status: "complete" }, - }), + }, confidence: "derived", authority: "shipped-artifact", }, @@ -862,10 +862,10 @@ describe("artifact comparison fragment integrity", () => { { operation: "inventory_artifact", parameters: {}, - result: jsonValueSchema.parse({ + result: { ...artifactInventoryResultSchema.parse(fragment.normalized_result), coverage: { scope: "root", status: "complete" }, - }), + }, }, ); expect(() => diff --git a/tests/boundary/filesystem/artifactComparisonSummary.test.ts b/tests/boundary/filesystem/artifactComparisonSummary.test.ts index 5042c916c3..ef1e911a60 100644 --- a/tests/boundary/filesystem/artifactComparisonSummary.test.ts +++ b/tests/boundary/filesystem/artifactComparisonSummary.test.ts @@ -6,7 +6,7 @@ import { describe, expect, it } from "vitest"; import { inventoryArtifact } from "../../fixtures/artifactInventory.js"; import { compareArtifacts } from "../../../src/domain/artifactComparison.js"; import { createEvidence } from "../../../src/domain/evidence.js"; -import { jsonValueSchema } from "../../../src/domain/jsonValue.js"; + import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; const observe = async ( @@ -30,7 +30,7 @@ const observe = async ( { operation: "inventory_artifact", parameters: {}, - result: jsonValueSchema.parse(inventory), + result: inventory, confidence: partial ? "derived" : "observed", authority: "shipped-artifact", }, diff --git a/tests/boundary/filesystem/artifactDirectoryIdentity.test.ts b/tests/boundary/filesystem/artifactDirectoryIdentity.test.ts index 3d1d252100..258f1fe098 100644 --- a/tests/boundary/filesystem/artifactDirectoryIdentity.test.ts +++ b/tests/boundary/filesystem/artifactDirectoryIdentity.test.ts @@ -10,7 +10,7 @@ import { inventoryArtifact } from "../../fixtures/artifactInventory.js"; import { compareArtifacts } from "../../../src/domain/artifactComparison.js"; import type { ArtifactInventoryResult } from "../../../src/domain/artifactGraph.js"; import { createEvidence } from "../../../src/domain/evidence.js"; -import { jsonValueSchema } from "../../../src/domain/jsonValue.js"; + import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; const observeZip = async (entries: readonly (readonly [string, string])[]) => { @@ -32,7 +32,7 @@ const observeZip = async (entries: readonly (readonly [string, string])[]) => { { operation: "inventory_artifact", parameters: {}, - result: jsonValueSchema.parse(inventory), + result: inventory, confidence: "observed", authority: "shipped-artifact", }, diff --git a/tests/boundary/filesystem/ciChangeScope.test.ts b/tests/boundary/filesystem/ciChangeScope.test.ts index ce3217f79b..87b472c1fe 100644 --- a/tests/boundary/filesystem/ciChangeScope.test.ts +++ b/tests/boundary/filesystem/ciChangeScope.test.ts @@ -1360,6 +1360,30 @@ it.each([ }, ); +it.each(["knip:production", "lint:typed"])( + "keeps the %s analysis command within static checks", + async (script) => { + const { directory } = await fixture(); + const base = await change( + directory, + ["package.json"], + JSON.stringify({ scripts: { [script]: "node scripts/old.mjs" } }), + ); + const head = await change( + directory, + ["package.json"], + JSON.stringify({ scripts: { [script]: "node scripts/new.mjs" } }), + ); + expect(await classify(directory, base, head)).toMatchObject({ + code: true, + runtime: false, + package: false, + evm: false, + browser: false, + }); + }, +); + it("retains a broad fallback when a provider script adds shared shell work", async () => { const { directory } = await fixture(); const before = { diff --git a/tests/boundary/filesystem/javascriptApplicationWorkflows.test.ts b/tests/boundary/filesystem/javascriptApplicationWorkflows.test.ts index 0c48a5ef06..3a442708bd 100644 --- a/tests/boundary/filesystem/javascriptApplicationWorkflows.test.ts +++ b/tests/boundary/filesystem/javascriptApplicationWorkflows.test.ts @@ -14,7 +14,7 @@ import { import { reconcileJavaScriptRuntimeEvidence } from "../../../src/application/javascript/JavaScriptRuntimeReconciliationService.js"; import { createEvidence } from "../../../src/domain/evidence.js"; import { AnalysisInputError } from "../../../src/domain/analysisErrorCore.js"; -import { jsonValueSchema } from "../../../src/domain/jsonValue.js"; + import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js"; import { applicationVersionComparisonResultSchema, @@ -377,10 +377,10 @@ describe("Application graph Evidence envelopes", () => { predicateType: source.predicate_type, operation: source.operation, parameters: source.parameters, - result: jsonValueSchema.parse({ + result: { ...result, evidence_links: [...result.evidence_links, extraLink], - }), + }, rawResult: source.raw_result, confidence: source.confidence, authority: source.authority, @@ -479,7 +479,7 @@ describe("JavaScript application workflow error diagnostics", () => { predicateType: evidence.predicate_type, operation: evidence.operation, parameters: evidence.parameters, - result: jsonValueSchema.parse(malformedResult), + result: malformedResult, confidence: evidence.confidence, authority: evidence.authority, limitations: evidence.limitations, diff --git a/tests/boundary/filesystem/lintRuleConfiguration.test.ts b/tests/boundary/filesystem/lintRuleConfiguration.test.ts new file mode 100644 index 0000000000..fb5dc363cf --- /dev/null +++ b/tests/boundary/filesystem/lintRuleConfiguration.test.ts @@ -0,0 +1,270 @@ +import { parse } from "@babel/parser"; +import { mkdir, writeFile } from "node:fs/promises"; +import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { expect } from "vitest"; +import { z } from "zod"; + +import { writeRepositoryOxlintConfiguration } from "../../fixtures/oxlintConfiguration.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { processTest } from "../../support/process/processFixture.js"; + +const repository = fileURLToPath(new URL("../../../", import.meta.url)); +const oxlint = join(repository, "node_modules/oxlint/bin/oxlint"); +const applicationSource = "src/application/lintRuleAcceptance.ts"; +const testSource = "tests/boundary/lintRuleAcceptance.test.ts"; + +interface AcceptanceCase { + readonly name: string; + readonly filename: string; + readonly source: string; + readonly exitCode: number; + readonly diagnostics: readonly { + readonly code?: string; + readonly severity: "error" | "warning"; + readonly message?: string; + }[]; +} + +const cases: readonly AcceptanceCase[] = [ + { + name: "enables imported Evidence JSON admission errors through the repository plugin", + filename: applicationSource, + source: ` + import { createEvidence as record } from "../domain/evidence.js"; + import { jsonValueSchema as json } from "../domain/jsonValue.js"; + export const evidence = record(undefined, {}, { result: json.parse({ ready: true }) }); + `, + exitCode: 1, + diagnostics: [ + { code: "rea(no-evidence-json-preadmission)", severity: "error" }, + ], + }, + { + name: "recognizes canonical Evidence owners imported through file URLs", + filename: applicationSource, + source: ` + import { createEvidence as record } from ${JSON.stringify(pathToFileURL(join(repository, "src/domain/evidence.js")).href)}; + import { jsonValueSchema as json } from ${JSON.stringify(pathToFileURL(join(repository, "src/domain/jsonValue.js")).href)}; + export const evidence = record(undefined, {}, { result: json.parse({ ready: true }) }); + `, + exitCode: 1, + diagnostics: [ + { code: "rea(no-evidence-json-preadmission)", severity: "error" }, + ], + }, + { + name: "allows semantic schema admission and stored JSON contracts", + filename: applicationSource, + source: ` + import { z } from "zod"; + import { createEvidence } from "../domain/evidence.js"; + import { jsonValueSchema } from "../domain/jsonValue.js"; + const semanticSchema = z.object({ ready: z.boolean() }); + export const semantic = createEvidence(undefined, {}, { result: semanticSchema.parse({ ready: true }) }); + const result = jsonValueSchema.parse({ ready: true }); + export const stored = createEvidence(undefined, {}, { result }); + `, + exitCode: 0, + diagnostics: [], + }, + { + name: "enables positional catalog type errors through the repository plugin", + filename: applicationSource, + source: ` + import { GPU_TOOL_CONTRACTS as contracts } from "../contracts/gpu/gpuToolContracts.js"; + export type Selected = (typeof contracts)[0]; + `, + exitCode: 1, + diagnostics: [ + { code: "rea(no-positional-tool-contract)", severity: "error" }, + ], + }, + { + name: "enables duplicate fixture root teardown errors through the repository plugin", + filename: testSource, + source: ` + import { afterEach } from "vitest"; + import { rm } from "node:fs/promises"; + import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; + const root = await createTestTempDirectory("rea-lint-rule-"); + afterEach(() => rm(root, { recursive: true, force: true })); + `, + exitCode: 1, + diagnostics: [{ code: "rea(no-fixture-root-cleanup)", severity: "error" }], + }, + { + name: "allows explicit root deletion outside fixture teardown", + filename: testSource, + source: ` + import { rm } from "node:fs/promises"; + import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; + const root = await createTestTempDirectory("rea-lint-rule-"); + await rm(root, { recursive: true, force: true }); + `, + exitCode: 0, + diagnostics: [], + }, + { + name: "rejects an unreturned promise callback containing assertions", + filename: testSource, + source: ` + import { expect, it } from "vitest"; + it("unjoined assertion", () => { + Promise.resolve("ready").then(value => { expect(value).toBe("ready"); }); + }); + `, + exitCode: 1, + diagnostics: [ + { code: "vitest(valid-expect-in-promise)", severity: "error" }, + ], + }, + { + name: "rejects focused tests through the repository Vitest configuration", + filename: testSource, + source: ` + import { expect, it } from "vitest"; + it.only("focused assertion", () => { expect("ready").toBe("ready"); }); + `, + exitCode: 1, + diagnostics: [{ code: "vitest(no-focused-tests)", severity: "error" }], + }, + { + name: "keeps native stored assertion limitations at warning severity", + filename: testSource, + source: ` + import { expect, it } from "vitest"; + it("joins a stored assertion", async () => { + const assertion = expect(Promise.reject(new Error("controlled"))).rejects.toThrow("controlled"); + await assertion; + }); + `, + exitCode: 0, + diagnostics: [{ code: "vitest(valid-expect)", severity: "warning" }], + }, + { + name: "allows an assertion diagnostic message", + filename: testSource, + source: ` + import { expect, it } from "vitest"; + it("keeps diagnostic context", () => { + expect("ready", "controlled diagnostic context").toBe("ready"); + }); + `, + exitCode: 0, + diagnostics: [], + }, + { + name: "rejects unused suppression directives through the repository configuration", + filename: testSource, + source: ` + // oxlint-disable-next-line no-var + export const ready = true; + `, + exitCode: 1, + diagnostics: [ + { + severity: "error", + message: "Unused oxlint-disable directive (no problems were reported).", + }, + ], + }, +]; + +const reportSchema = z.object({ + diagnostics: z.array( + z.object({ + code: z.string().optional(), + severity: z.enum(["error", "warning"]), + message: z.string(), + }), + ), +}); + +const relocateImports = (scenario: AcceptanceCase, path: string): string => { + const source = scenario.source; + const parsed = parse(source, { + sourceType: "module", + plugins: ["typescript"], + }); + const replacements = []; + for (const statement of parsed.program.body) { + if ( + statement.type !== "ImportDeclaration" || + !statement.source.value.startsWith(".") + ) + continue; + const target = resolve( + repository, + dirname(scenario.filename), + statement.source.value, + ); + const relativePath = relative(dirname(path), target); + const specifier = isAbsolute(relativePath) + ? pathToFileURL(target).href + : relativePath.startsWith(".") + ? relativePath.split(sep).join("/") + : `./${relativePath.split(sep).join("/")}`; + const { start, end } = statement.source; + if ( + start === null || + start === undefined || + end === null || + end === undefined + ) + throw new Error("Authored lint fixture import has no source range"); + replacements.push({ + start, + end, + specifier, + }); + } + let relocated = source; + for (const replacement of replacements.reverse()) + relocated = + relocated.slice(0, replacement.start) + + JSON.stringify(replacement.specifier) + + relocated.slice(replacement.end); + return relocated; +}; + +for (const scenario of cases) { + processTest(scenario.name, async ({ processes }) => { + const project = await createTestTempDirectory("rea-lint-configuration-"); + const configuration = await writeRepositoryOxlintConfiguration(project); + const path = join(project, scenario.filename); + await mkdir(dirname(path), { recursive: true }); + await writeFile(path, relocateImports(scenario, path)); + const result = await processes.run( + process.execPath, + [ + oxlint, + "--config", + configuration, + "--threads", + "1", + "--format", + "json", + scenario.filename, + ], + { + cwd: project, + timeoutMs: 15_000, + }, + ); + expect(result.signal, result.stderr).toBeNull(); + expect(result.exitCode, result.stderr || result.stdout).toBe( + scenario.exitCode, + ); + const { diagnostics } = reportSchema.parse(JSON.parse(result.stdout)); + expect( + diagnostics.map(({ code, severity }) => ({ code, severity })), + ).toEqual( + scenario.diagnostics.map(({ code, severity }) => ({ code, severity })), + ); + for (const [index, expected] of scenario.diagnostics.entries()) { + if (expected.message !== undefined) + expect(diagnostics[index]?.message).toBe(expected.message); + } + }); +} diff --git a/tests/boundary/filesystem/productionReachabilityReport.test.ts b/tests/boundary/filesystem/productionReachabilityReport.test.ts new file mode 100644 index 0000000000..b7c6c6a71d --- /dev/null +++ b/tests/boundary/filesystem/productionReachabilityReport.test.ts @@ -0,0 +1,117 @@ +import { spawnSync } from "node:child_process"; +import { access, mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { expect, it } from "vitest"; + +import { parseProductionReachabilityReport } from "../../../scripts/ci/production-reachability-report.mjs"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +const script = fileURLToPath( + new URL( + "../../../scripts/ci/production-reachability-report.mjs", + import.meta.url, + ), +); +const configuration = new URL("../../../knip.production.json", import.meta.url); +const reportPath = (root: string) => + join(root, ".cache/static-checks/production-reachability.json"); +const invoke = (root: string) => + spawnSync(process.execPath, [script], { + cwd: root, + encoding: "utf8", + timeout: 15_000, + }); + +it("reports test-only candidates without losing dynamic, worker or published roots", async () => { + const root = await createTestTempDirectory("rea-production-reachability-"); + const files = { + "package.json": JSON.stringify({ + name: "reachability-fixture", + type: "module", + bin: { fixture: "scripts/rea.mjs" }, + exports: { "./published": "./src/published.ts" }, + }), + "src/main.ts": + 'import { live } from "./facts.js"; console.log(live); void import("./lazy.js");', + "src/facts.ts": "export const live = 1; export const testOnly = 2;", + "src/lazy.ts": "console.log('dynamic production module');", + "src/testOnly.ts": "export const retired = 3;", + "src/published.ts": "export const publicContract = 4;", + "src/composition/javascriptAnalysisWorkerProcess.ts": + 'import { workerFact } from "../workerFacts.js"; console.log(workerFact);', + "src/workerFacts.ts": "export const workerFact = 5;", + "scripts/rea.mjs": 'import "../src/main.js";', + "tests/facts.test.ts": + 'import { testOnly } from "../src/facts.js"; import { retired } from "../src/testOnly.js"; console.log(testOnly, retired);', + }; + for (const [path, content] of Object.entries(files)) { + const destination = join(root, path); + await mkdir(dirname(destination), { recursive: true }); + await writeFile(destination, content); + } + await writeFile( + join(root, "knip.production.json"), + await readFile(configuration, "utf8"), + ); + + const result = invoke(root); + expect(result.error).toBeUndefined(); + expect(result.status, result.stderr).toBe(0); + expect(result.stdout).toContain("advisory candidates"); + const { issues } = parseProductionReachabilityReport( + await readFile(reportPath(root), "utf8"), + ); + expect(issues).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + file: "src/facts.ts", + exports: expect.arrayContaining([ + expect.objectContaining({ name: "testOnly" }), + ]), + }), + expect.objectContaining({ file: "src/testOnly.ts" }), + ]), + ); + const reached = [ + "src/main.ts", + "src/lazy.ts", + "src/published.ts", + "src/workerFacts.ts", + "src/composition/javascriptAnalysisWorkerProcess.ts", + "tests/facts.test.ts", + ]; + expect(issues.filter(({ file }) => reached.includes(file))).toEqual([]); + expect( + issues.flatMap(({ exports: findings = [] }) => + findings.map(({ name }) => name), + ), + ).not.toContain("live"); +}); + +it("fails on Knip configuration errors and removes a stale candidate report", async () => { + const root = await createTestTempDirectory( + "rea-production-reachability-error-", + ); + await writeFile(join(root, "package.json"), '{"name":"broken-config"}'); + await writeFile(join(root, "knip.production.json"), "{"); + await mkdir(dirname(reportPath(root)), { recursive: true }); + await writeFile(reportPath(root), '{"issues":[]}'); + + const result = invoke(root); + expect(result.error).toBeUndefined(); + expect(result.status).not.toBe(0); + expect(result.stderr).toContain("Production reachability analysis failed"); + await expect(access(reportPath(root))).rejects.toMatchObject({ + code: "ENOENT", + }); +}); + +it.each([ + "not JSON", + '{"issues":[{"file":"src/facts.ts","exports":[{"name":42}]}]}', + '{"issues":[{"file":"src/facts.ts","unrecognized":[]}]}', +])("rejects malformed checker output: %s", (text) => { + expect(() => parseProductionReachabilityReport(text)).toThrow(); +}); diff --git a/tests/boundary/filesystem/staticCheckReports.test.ts b/tests/boundary/filesystem/staticCheckReports.test.ts index c133e21cfd..b433dc9f55 100644 --- a/tests/boundary/filesystem/staticCheckReports.test.ts +++ b/tests/boundary/filesystem/staticCheckReports.test.ts @@ -1,10 +1,11 @@ import { spawnSync } from "node:child_process"; -import { access, cp, mkdir, readFile, writeFile } from "node:fs/promises"; +import { access, mkdir, readFile, writeFile } from "node:fs/promises"; import { dirname, join, sep } from "node:path"; import { fileURLToPath } from "node:url"; import { expect, it } from "vitest"; import { z } from "zod"; +import { writeRepositoryOxlintConfiguration } from "../../fixtures/oxlintConfiguration.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; const script = fileURLToPath( @@ -149,11 +150,11 @@ it("keeps lint errors failing and visible alongside the retained warnings", asyn it("excludes translation staging while retaining authored website errors", async () => { const root = await fixture(); - for (const name of [".gitignore", ".oxlintrc.json"]) - await writeFile(join(root, name), await readFile(join(repository, name))); - await cp(join(repository, "scripts/lint"), join(root, "scripts/lint"), { - recursive: true, - }); + await writeFile( + join(root, ".gitignore"), + await readFile(join(repository, ".gitignore")), + ); + await writeRepositoryOxlintConfiguration(root); const authored = "website/public/assets/authored.js"; const staged = "website/.i18n-regression/site/examples/notes-electron/main.js"; @@ -166,7 +167,7 @@ it("excludes translation staging while retaining authored website errors", async ); } - const result = invoke(root, "lint", "website"); + const result = invoke(root, "lint"); expect(result.error).toBeUndefined(); expect(result.status).toBe(1); expect(result.stdout).toContain("1 errors, 0 warnings"); diff --git a/tests/boundary/filesystem/typedLintRules.test.ts b/tests/boundary/filesystem/typedLintRules.test.ts new file mode 100644 index 0000000000..74694abb22 --- /dev/null +++ b/tests/boundary/filesystem/typedLintRules.test.ts @@ -0,0 +1,192 @@ +import { mkdir, readFile, symlink, writeFile } from "node:fs/promises"; +import { dirname, isAbsolute, join, relative, sep } from "node:path"; +import { fileURLToPath } from "node:url"; +import { expect } from "vitest"; +import { z } from "zod"; + +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { writeRepositoryOxlintConfiguration } from "../../fixtures/oxlintConfiguration.js"; +import { processTest } from "../../support/process/processFixture.js"; + +const dependencies = fileURLToPath( + new URL("../../../node_modules", import.meta.url), +); +const reporter = fileURLToPath( + new URL("../../../scripts/ci/static-check-report.mjs", import.meta.url), +); +const sources = { + "src/unjoined.test.ts": ` +import { expect, it } from "vitest"; + +const cliTest = it.extend<{ label: string }>({ label: "owned" }); + +it("forgets the assertion promise", () => { + expect(Promise.resolve("ready")).resolves.toBe("ready"); +}); + +cliTest("forgets the assertion in an extended callback", ({ label }) => { + expect(Promise.resolve(label)).resolves.toBe("owned"); +}); +`, + "src/joined.test.ts": ` +import { expect, it } from "vitest"; + +const cliTest = it.extend<{ label: string }>({ label: "owned" }); + +it("joins a stored assertion promise", async () => { + const assertion = expect(Promise.resolve("ready")).resolves.toBe("ready"); + await assertion; +}); + +cliTest("joins a stored assertion in an extended callback", async ({ label }) => { + const assertion = expect(Promise.resolve(label)).resolves.toBe("owned"); + await assertion; +}); + +export async function finishBeforeCleanup(): Promise { + try { + return await Promise.resolve("ready"); + } finally { + await Promise.resolve(); + } +} +`, + "src/misused.ts": ` +export function promiseCondition(): string { + if (Promise.resolve(false)) return "ready"; + return "pending"; +} +`, + "src/nonexhaustive.ts": ` +type Outcome = { kind: "open" } | { kind: "closed" }; + +export function describeOutcome(outcome: Outcome): string { + switch (outcome.kind) { + case "open": return "open"; + default: return "unknown"; + } +} +`, + "src/unawaited-finally.ts": ` +export async function cleanupBeforeCompletion(): Promise { + try { + return Promise.resolve("ready"); + } finally { + await Promise.resolve(); + } +} +`, + "src/test-misused.test.ts": ` +import { it } from "vitest"; +it("keeps test-only promise conditions advisory", () => { + if (Promise.resolve(false)) throw new Error("test fixture"); +}); +`, + "tests/unjoined.test.ts": ` +import { expect, it } from "vitest"; +const cliTest = it.extend<{ label: string }>({ label: "owned" }); +it("warns about an unjoined boundary-test assertion", () => { + expect(Promise.resolve("ready")).resolves.toBe("ready"); +}); +cliTest("warns in an extended boundary-test callback", ({ label }) => { + expect(Promise.resolve(label)).resolves.toBe("owned"); +}); +`, +}; + +processTest( + "runs typed promise and exhaustiveness rules on an isolated real TypeScript project", + async ({ processes }) => { + const root = await createTestTempDirectory("rea-typed-lint-"); + await symlink( + dependencies, + join(root, "node_modules"), + process.platform === "win32" ? "junction" : "dir", + ); + await Promise.all( + Object.keys(sources).map((name) => + mkdir(dirname(join(root, name)), { recursive: true }), + ), + ); + await writeRepositoryOxlintConfiguration(root); + await Promise.all([ + writeFile(join(root, "package.json"), '{"type":"module"}\n'), + writeFile( + join(root, "tsconfig.json"), + JSON.stringify({ + compilerOptions: { + strict: true, + noEmit: true, + skipLibCheck: true, + module: "NodeNext", + moduleResolution: "NodeNext", + target: "ES2023", + types: ["node"], + }, + files: Object.keys(sources), + }), + ), + ...Object.entries(sources).map(([name, source]) => + writeFile(join(root, name), source), + ), + ]); + + const result = await processes.run( + process.execPath, + [reporter, "typed-lint", ...Object.keys(sources)], + { + cwd: root, + timeoutMs: 20_000, + env: { ...process.env, GOMAXPROCS: "2", GOMEMLIMIT: "1GiB" }, + }, + ); + expect(result.signal, result.stderr).toBeNull(); + expect(result.exitCode, result.stderr || result.stdout).toBe(1); + const { diagnostics } = z + .object({ + diagnostics: z.array( + z.object({ + code: z.string(), + severity: z.enum(["error", "warning"]), + filename: z.string(), + }), + ), + }) + .parse( + JSON.parse( + await readFile( + join(root, ".cache/static-checks/typed-oxlint.json"), + "utf8", + ), + ), + ); + expect( + diagnostics + .filter(({ code }) => + [ + "typescript(no-floating-promises)", + "typescript(no-misused-promises)", + "typescript(return-await)", + "typescript(switch-exhaustiveness-check)", + ].includes(code), + ) + .map( + ({ code, filename, severity }) => + `${(isAbsolute(filename) ? relative(root, filename) : filename).split(sep).join("/")}: ${severity}: ${code}`, + ) + .sort(), + ).toEqual([ + "src/misused.ts: error: typescript(no-misused-promises)", + "src/nonexhaustive.ts: warning: typescript(switch-exhaustiveness-check)", + "src/test-misused.test.ts: warning: typescript(no-misused-promises)", + "src/unawaited-finally.ts: warning: typescript(return-await)", + "src/unjoined.test.ts: error: typescript(no-floating-promises)", + "src/unjoined.test.ts: error: typescript(no-floating-promises)", + "tests/unjoined.test.ts: warning: typescript(no-floating-promises)", + "tests/unjoined.test.ts: warning: typescript(no-floating-promises)", + ]); + expect( + diagnostics.filter(({ severity }) => severity === "error"), + ).toHaveLength(3); + }, +); diff --git a/tests/boundary/filesystem/webArtifactReadOwnership.test.ts b/tests/boundary/filesystem/webArtifactReadOwnership.test.ts index e378d7a38c..1caf6ee3d1 100644 --- a/tests/boundary/filesystem/webArtifactReadOwnership.test.ts +++ b/tests/boundary/filesystem/webArtifactReadOwnership.test.ts @@ -1,4 +1,4 @@ -import { mkdir, rm, writeFile } from "node:fs/promises"; +import { mkdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect, it, onTestFinished, vi } from "vitest"; @@ -43,12 +43,11 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; it("retains cancelled source read ownership until service close retries it", async () => { const root = await createTestTempDirectory("rea-web-artifact-read-owner-"); - onTestFinished(async () => { + onTestFinished(() => { injected.sourcePath = undefined; injected.abortController = undefined; injected.failClose = false; injected.closeAttempts = 0; - await rm(root, { recursive: true, force: true }); }); const source = 'import "./dep.js";'; const fixture = webModuleArtifactsFixture(source); diff --git a/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts b/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts index 33be84d85e..842c086e18 100644 --- a/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts +++ b/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts @@ -20,7 +20,7 @@ import { } from "../../../src/domain/evidence.js"; import { artifactInspectionResultSchema } from "../../../src/domain/artifactInspection.js"; import { artifactInventoryResultSchema } from "../../../src/domain/artifactGraph.js"; -import { jsonValueSchema } from "../../../src/domain/jsonValue.js"; + import { analysisViewResultSchema, type AnalysisViewResult, @@ -446,10 +446,10 @@ it.each(["full", "compact"] as const)( { operation: "inventory_artifact", parameters: {}, - result: jsonValueSchema.parse({ + result: { ...graph, occurrences: graph.occurrences.slice(0, 1), - }), + }, }, ); const partialReply = await client.callTool({ diff --git a/tests/boundary/mcp/artifactMcp.test.ts b/tests/boundary/mcp/artifactMcp.test.ts index 2227cec5df..4930077f4d 100644 --- a/tests/boundary/mcp/artifactMcp.test.ts +++ b/tests/boundary/mcp/artifactMcp.test.ts @@ -12,7 +12,7 @@ import { } from "../../../src/domain/evidence.js"; import { EvidenceIntegrityError } from "../../../src/domain/evidenceErrors.js"; import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js"; -import { jsonValueSchema } from "../../../src/domain/jsonValue.js"; + import { artifactInventoryResultSchema } from "../../../src/domain/artifactGraph.js"; import { artifactEdgeId, @@ -468,7 +468,7 @@ it("records an explicitly continued mismatch, preserves verified siblings, and n { operation: "inventory_artifact", parameters: {}, - result: jsonValueSchema.parse(inventoryWithoutCoverage), + result: inventoryWithoutCoverage, }, ); const retainedComparison = await client.callTool({ @@ -913,7 +913,7 @@ const withForeignMetadataAndNestedZipMember = ( parameters: evidence.parameters, confidence: evidence.confidence, authority: evidence.authority, - result: jsonValueSchema.parse({ + result: { ...inventory, occurrences, edges, @@ -924,7 +924,7 @@ const withForeignMetadataAndNestedZipMember = ( graph_sha256: graphSha256, manifest_id: manifestId, }, - }), + }, }, ); }; diff --git a/tests/boundary/mcp/evidenceBundleInline.test.ts b/tests/boundary/mcp/evidenceBundleInline.test.ts index a27d2af1b6..060a1687a5 100644 --- a/tests/boundary/mcp/evidenceBundleInline.test.ts +++ b/tests/boundary/mcp/evidenceBundleInline.test.ts @@ -12,7 +12,7 @@ import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js"; import { createEvidence } from "../../../src/domain/evidence.js"; import { ANALYSIS_SNAPSHOT_TARGET } from "../../../src/domain/analysisSnapshot.fixture.js"; -import { jsonValueSchema } from "../../../src/domain/jsonValue.js"; + import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../../../src/domain/process/processCaptureExample.js"; import { PROCESS_PROVIDER } from "../../../src/domain/process/processEvidenceProvider.js"; import { createServer } from "../../../src/server/createServer.js"; @@ -195,7 +195,7 @@ const createBundleSourceSession = () => { predicateType: "rea.process-capture", operation: "capture_process_scenario", parameters: {}, - result: jsonValueSchema.parse(EMPTY_PROCESS_CAPTURE_EXAMPLE), + result: EMPTY_PROCESS_CAPTURE_EXAMPLE, confidence: "observed", authority: "controlled-replay", }), diff --git a/tests/boundary/mcp/functionComparisonIntegrityMcp.test.ts b/tests/boundary/mcp/functionComparisonIntegrityMcp.test.ts index 8615d92d7f..fb9dbbd409 100644 --- a/tests/boundary/mcp/functionComparisonIntegrityMcp.test.ts +++ b/tests/boundary/mcp/functionComparisonIntegrityMcp.test.ts @@ -14,7 +14,7 @@ import { type FunctionComparisonResult, } from "../../../src/domain/functionComparisonSchemas.js"; import { summarize } from "../../../src/domain/functionComparisonResults.js"; -import { jsonValueSchema } from "../../../src/domain/jsonValue.js"; + import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; @@ -165,7 +165,7 @@ it.each([ predicateType: base.predicate_type, operation: base.operation, parameters: base.parameters, - result: jsonValueSchema.parse(scenario.result(result)), + result: scenario.result(result), confidence: base.confidence, authority: base.authority, limitations: base.limitations, diff --git a/tests/boundary/mcp/functionComparisonMcp.test.ts b/tests/boundary/mcp/functionComparisonMcp.test.ts index 75bf38e384..c9380f8b97 100644 --- a/tests/boundary/mcp/functionComparisonMcp.test.ts +++ b/tests/boundary/mcp/functionComparisonMcp.test.ts @@ -9,7 +9,6 @@ import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarnes import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import { createEvidenceBundle } from "../../../src/domain/evidenceBundle.js"; import { compareFunctions } from "../../../src/domain/functionComparison.js"; -import { jsonValueSchema } from "../../../src/domain/jsonValue.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { FUNCTION_COMPARISON_EXAMPLE } from "../../../src/contracts/functionComparisonExample.js"; @@ -74,7 +73,7 @@ it.each([ { operation: "analyze_function", parameters: { procedure: entry }, - result: jsonValueSchema.parse(value), + result: value, confidence: "derived", relationshipCoverage: { status: "complete" }, }, @@ -532,14 +531,14 @@ it.each([ }; const consistent = createEvidence(undefined, provider, { ...observation, - result: jsonValueSchema.parse(result), + result: result, }); const inconsistent = status === "unchanged" ? null : createEvidence(undefined, provider, { ...observation, - result: jsonValueSchema.parse({ ...result, status: "unchanged" }), + result: { ...result, status: "unchanged" }, }); const directory = await createTestTempDirectory("rea-function-status-"); const path = join(directory, "bundle.json"); diff --git a/tests/boundary/mcp/functionComparisonReferenceFactsMcp.test.ts b/tests/boundary/mcp/functionComparisonReferenceFactsMcp.test.ts index 842517d2fd..4e38faf6f5 100644 --- a/tests/boundary/mcp/functionComparisonReferenceFactsMcp.test.ts +++ b/tests/boundary/mcp/functionComparisonReferenceFactsMcp.test.ts @@ -11,7 +11,7 @@ import { import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; import { functionComparisonResultSchema } from "../../../src/domain/functionComparisonSchemas.js"; import { functionDossierSchema } from "../../../src/domain/hopperValues.js"; -import { jsonValueSchema } from "../../../src/domain/jsonValue.js"; + import { reconstructionVerificationResultSchema } from "../../../src/domain/reconstructionVerificationSchemas.js"; import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; @@ -147,7 +147,7 @@ const evidence = ( ? {} : { analysisProfile: options.profile ?? profile }), parameters: { ...example.parameters, observation: id }, - result: jsonValueSchema.parse({ + result: { ...dossier, procedure: { ...dossier.procedure, @@ -162,7 +162,7 @@ const evidence = ( callers: [...(options.callers ?? [])], incoming_references: [...(options.incoming ?? [])], outgoing_references: [...outgoing], - }), + }, confidence: "derived", authority: "shipped-artifact", relationshipCoverage: { status: "complete" }, diff --git a/tests/boundary/mcp/functionComparisonStringDecodingMcp.test.ts b/tests/boundary/mcp/functionComparisonStringDecodingMcp.test.ts index 6957f417c4..9f2aba5bef 100644 --- a/tests/boundary/mcp/functionComparisonStringDecodingMcp.test.ts +++ b/tests/boundary/mcp/functionComparisonStringDecodingMcp.test.ts @@ -8,7 +8,7 @@ import { functionComparisonResultSchema, type FunctionComparisonResult as Comparison, } from "../../../src/domain/functionComparisonSchemas.js"; -import { jsonValueSchema } from "../../../src/domain/jsonValue.js"; + import { createServer } from "../../../src/server/createServer.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; @@ -30,7 +30,7 @@ const evidence = ( { operation: "analyze_function", parameters: { ...template.parameters, observation: id }, - result: jsonValueSchema.parse({ + result: { ...base, referenced_strings: [...strings], swift_string_literals: @@ -47,7 +47,7 @@ const evidence = ( }, ], swift_string_literal_omissions: [], - }), + }, confidence: template.confidence, authority: template.authority, relationshipCoverage: { status: "complete" }, diff --git a/tests/boundary/mcp/investigationAnalysisMcp.test.ts b/tests/boundary/mcp/investigationAnalysisMcp.test.ts index 5040c665af..25ca5d93da 100644 --- a/tests/boundary/mcp/investigationAnalysisMcp.test.ts +++ b/tests/boundary/mcp/investigationAnalysisMcp.test.ts @@ -1,4 +1,4 @@ -import { parseEvidence } from "../../../src/domain/evidence.js"; +import { parseEvidence, createEvidence } from "../../../src/domain/evidence.js"; import { ok as resultOk } from "../../../src/domain/result.js"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { expect, it } from "vitest"; @@ -14,18 +14,14 @@ import { } from "../../../src/application/AnalysisProvider.js"; import type { BinarySession } from "../../../src/application/binary/BinarySession.js"; import { FUNCTION_COMPARISON_EXAMPLE } from "../../../src/contracts/functionComparisonExample.js"; -import { INVESTIGATION_EXAMPLES } from "../../../src/contracts/investigationExamples.js"; import { + INVESTIGATION_EXAMPLES, FUNCTION_COMPARISON_EVIDENCE, PROCESS_CAPTURE_RECONSTRUCTION, PROCESS_CAPTURE_REFERENCE, PROCESS_COMPARISON_EVIDENCE, } from "../../../src/contracts/investigationExamples.js"; -import { createEvidence } from "../../../src/domain/evidence.js"; -import { - jsonObjectSchema, - jsonValueSchema, -} from "../../../src/domain/jsonValue.js"; +import { jsonObjectSchema } from "../../../src/domain/jsonValue.js"; import { createServer } from "../../../src/server/createServer.js"; import { observed } from "../../fixtures/analysisExecution.js"; import type { RelationshipCoverage } from "../../../src/domain/relationshipCoverage.js"; @@ -217,7 +213,7 @@ const dossierWithCallees = (addresses: readonly string[]) => { predicateType: base.predicate_type, operation: base.operation, parameters: base.parameters, - result: jsonValueSchema.parse({ + result: { ...jsonObjectSchema.parse(base.normalized_result), callees: addresses.map((address) => ({ address, @@ -228,7 +224,7 @@ const dossierWithCallees = (addresses: readonly string[]) => { reason: "The fixture has no callee body-extent observation.", }, })), - }), + }, rawResult: base.raw_result, confidence: base.confidence, authority: base.authority, diff --git a/tests/boundary/mcp/managedWorkflowUnknownIdentityMcp.test.ts b/tests/boundary/mcp/managedWorkflowUnknownIdentityMcp.test.ts index 152f6379d6..6bf579b9c8 100644 --- a/tests/boundary/mcp/managedWorkflowUnknownIdentityMcp.test.ts +++ b/tests/boundary/mcp/managedWorkflowUnknownIdentityMcp.test.ts @@ -5,7 +5,7 @@ import { MANAGED_RECONSTRUCTION_IMPORT_EXAMPLE, } from "../../../src/contracts/managed/managedWorkflowExamples.js"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; -import { jsonValueSchema } from "../../../src/domain/jsonValue.js"; + import { managedMemberInspectionSchema, managedNativeBoundaryInspectionSchema, @@ -34,7 +34,7 @@ const emptyExportEvidence = () => { { operation: "inspect_macho", parameters: original.parameters, - result: jsonValueSchema.parse({ + result: { ...result, exports: { items: [], @@ -42,7 +42,7 @@ const emptyExportEvidence = () => { exhaustive: true, limitations: [], }, - }), + }, rawResult: null, limitations: [], }, @@ -63,7 +63,7 @@ const secondDeclaration = () => { return createEvidence(undefined, original.provider, { operation: "inspect_managed_native_boundaries", parameters: original.parameters, - result: jsonValueSchema.parse({ + result: { ...result, pinvoke_imports: [ { @@ -74,7 +74,7 @@ const secondDeclaration = () => { import_name: "open_native_again", }, ], - }), + }, rawResult: null, limitations: original.limitations, }); @@ -128,7 +128,7 @@ const memberEvidence = ( { operation: "inspect_managed_members", parameters: original.parameters, - result: jsonValueSchema.parse(normalized), + result: normalized, rawResult: null, limitations: normalized.limitations, }, diff --git a/tests/boundary/mcp/processComparisonDiagnosticsMcp.test.ts b/tests/boundary/mcp/processComparisonDiagnosticsMcp.test.ts index a404dad098..b4fe9c8fdf 100644 --- a/tests/boundary/mcp/processComparisonDiagnosticsMcp.test.ts +++ b/tests/boundary/mcp/processComparisonDiagnosticsMcp.test.ts @@ -27,7 +27,7 @@ import { parseEvidence, type EvidenceProvider, } from "../../../src/domain/evidence.js"; -import { jsonValueSchema } from "../../../src/domain/jsonValue.js"; + import { digestProcessCommitment } from "../../../src/domain/process/processScenario.js"; import { createServer } from "../../../src/server/createServer.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; @@ -45,14 +45,14 @@ const captureEvidence = ( predicateType: "rea.process-capture", operation: "capture_process_scenario", parameters: { side }, - result: jsonValueSchema.parse({ + result: { ...observation, manifest: { ...observation.manifest, comparison_contract: comparisonContract, comparison_contract_sha256: digestProcessCommitment(comparisonContract), }, - }), + }, confidence: "observed", authority: "controlled-replay", }); @@ -523,7 +523,7 @@ it("names the side and constraint when Evidence is not a usable capture", async predicateType: "rea.process-capture", operation: "capture_process_scenario", parameters: {}, - result: jsonValueSchema.parse(result), + result: result, }); expect(parseEvidence(invalidResult)).toEqual(invalidResult); const malformed = await client.callTool({ @@ -565,7 +565,7 @@ it("keeps structural capture issues alongside semantic ones through MCP and the predicateType: "rea.process-capture", operation: "capture_process_scenario", parameters: {}, - result: jsonValueSchema.parse({ + result: { ...rest, manifest, exit: { @@ -577,7 +577,7 @@ it("keeps structural capture issues alongside semantic ones through MCP and the ], }, }, - }), + }, }); const described = "(at manifest.completed_at: Invalid input: expected string, received undefined (1 more issues))"; diff --git a/tests/boundary/mcp/processComparisonVerificationMcp.test.ts b/tests/boundary/mcp/processComparisonVerificationMcp.test.ts index e906811d76..bac31269d7 100644 --- a/tests/boundary/mcp/processComparisonVerificationMcp.test.ts +++ b/tests/boundary/mcp/processComparisonVerificationMcp.test.ts @@ -5,7 +5,7 @@ import { expect, it, onTestFinished } from "vitest"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; import { createEvidenceBundle } from "../../../src/domain/evidenceBundle.js"; -import { jsonValueSchema } from "../../../src/domain/jsonValue.js"; + import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../../../src/domain/process/processCaptureExample.js"; import { parseProcessCapture } from "../../../src/domain/process/processCaptureParsing.js"; import { compareProcessCaptures } from "../../../src/domain/process/processComparison.js"; @@ -48,11 +48,11 @@ it("rejects an inconsistent imported process comparison while retaining its Evid left_evidence_id: left.evidence_id, right_evidence_id: right.evidence_id, }, - result: jsonValueSchema.parse({ + result: { ...nativeResult, [dimension]: "contradiction", status, - }), + }, confidence: "derived", authority: "analyst-inference", evidenceLinks: [left.evidence_id, right.evidence_id], diff --git a/tests/boundary/mcp/processTraceAdaptersParity.test.ts b/tests/boundary/mcp/processTraceAdaptersParity.test.ts index 0fa2f948d3..e781ee9c30 100644 --- a/tests/boundary/mcp/processTraceAdaptersParity.test.ts +++ b/tests/boundary/mcp/processTraceAdaptersParity.test.ts @@ -11,7 +11,7 @@ import { compareProcessEvidenceFiles } from "../../../src/application/process/Pr import { PROCESS_PROVIDER } from "../../../src/domain/process/processEvidenceProvider.js"; import { processTraceCaptureFixture } from "../../../src/domain/process/processCapture.fixture.js"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; -import { jsonValueSchema } from "../../../src/domain/jsonValue.js"; + import { createServer } from "../../../src/server/createServer.js"; import { observed as ok } from "../../fixtures/analysisExecution.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; @@ -25,7 +25,7 @@ const captureEvidence = (side: "left" | "right") => predicateType: "rea.process-capture", operation: "capture_process_scenario", parameters: { side }, - result: jsonValueSchema.parse(capture), + result: capture, confidence: "observed", authority: "controlled-replay", locations: [{ kind: "artifact-path", path: `/fixture/${side}` }], diff --git a/tests/boundary/mcp/promptCompletion.test.ts b/tests/boundary/mcp/promptCompletion.test.ts index 40dd6a370f..9bd76f71dd 100644 --- a/tests/boundary/mcp/promptCompletion.test.ts +++ b/tests/boundary/mcp/promptCompletion.test.ts @@ -184,7 +184,7 @@ describe("guided prompt completion from investigation records", () => { const inspectionEvidence = createEvidence(undefined, fixtureProvider, { operation: "inspect_artifact", parameters: {}, - result: jsonValueSchema.parse(inventoryInspection), + result: inventoryInspection, confidence: "observed", authority: "shipped-artifact", evidenceLinks: inventoryInspection.evidence_links, @@ -426,7 +426,7 @@ const createPagedInspectionEvidence = async ( { operation: "inventory_artifact", parameters: {}, - result: jsonValueSchema.parse(inventoryResult), + result: inventoryResult, confidence: "observed", authority: "shipped-artifact", }, @@ -530,7 +530,7 @@ const createPagedInspectionEvidence = async ( { operation: "inspect_artifact", parameters: {}, - result: jsonValueSchema.parse(result), + result: result, confidence: "observed", authority: "shipped-artifact", evidenceLinks: result.evidence_links, diff --git a/tests/boundary/mcp/serverUnknownsMcp.test.ts b/tests/boundary/mcp/serverUnknownsMcp.test.ts index f11d19b2d4..7c9b157db7 100644 --- a/tests/boundary/mcp/serverUnknownsMcp.test.ts +++ b/tests/boundary/mcp/serverUnknownsMcp.test.ts @@ -18,7 +18,7 @@ import { createServer } from "../../../src/server/createServer.js"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; import { parseEvidenceBundle } from "../../../src/domain/evidenceBundle.js"; import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../../../src/domain/process/processCaptureExample.js"; -import { jsonValueSchema } from "../../../src/domain/jsonValue.js"; + import { PROCESS_PROVIDER } from "../../../src/domain/process/processEvidenceProvider.js"; import { processCaptureSchema } from "../../../src/domain/process/processCapture.js"; @@ -153,7 +153,7 @@ it("records capture disagreement as a contradicted unknown", async () => { predicateType: "rea.process-capture", operation: "capture_process_scenario", parameters: {}, - result: jsonValueSchema.parse(capture), + result: capture, confidence: "observed", authority: "controlled-replay", }); diff --git a/tests/boundary/mcp/workflowUnknownIdentityMcp.test.ts b/tests/boundary/mcp/workflowUnknownIdentityMcp.test.ts index 92779dd525..f1653c4ff3 100644 --- a/tests/boundary/mcp/workflowUnknownIdentityMcp.test.ts +++ b/tests/boundary/mcp/workflowUnknownIdentityMcp.test.ts @@ -4,7 +4,7 @@ import { expect, it } from "vitest"; import { FUNCTION_COMPARISON_EXAMPLE } from "../../../src/contracts/functionComparisonExample.js"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; import { functionDossierSchema } from "../../../src/domain/hopperValues.js"; -import { jsonValueSchema } from "../../../src/domain/jsonValue.js"; + import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; @@ -44,7 +44,7 @@ const pathSource = () => { ...template.parameters, observation: "workflow-unknown-path", }, - result: jsonValueSchema.parse({ + result: { ...dossier, callees: [ { @@ -54,7 +54,7 @@ const pathSource = () => { body: { available: false, reason: "No body was observed." }, }, ], - }), + }, confidence: template.confidence, authority: template.authority, relationshipCoverage: { status: "complete" }, @@ -76,10 +76,8 @@ const incompleteRight = (suffix: string) => { { operation: "analyze_function", parameters: { ...template.parameters, observation: suffix }, - result: jsonValueSchema.parse( - functionDossierSchema.parse( - FUNCTION_COMPARISON_EXAMPLE.left.normalized_result, - ), + result: functionDossierSchema.parse( + FUNCTION_COMPARISON_EXAMPLE.left.normalized_result, ), confidence: template.confidence, authority: template.authority, diff --git a/tests/boundary/providers/managed/managedMemberComparison.test.ts b/tests/boundary/providers/managed/managedMemberComparison.test.ts index 2ddaa6018c..b579b83fe6 100644 --- a/tests/boundary/providers/managed/managedMemberComparison.test.ts +++ b/tests/boundary/providers/managed/managedMemberComparison.test.ts @@ -12,15 +12,17 @@ import { compareManagedMemberPaths, } from "../../../../src/application/managed/ManagedMemberComparisonService.js"; import { parseBinaryTarget } from "../../../../src/application/BinaryTargetResolver.js"; -import { managedMemberComparisonResultSchema } from "../../../../src/domain/managed/managedMemberComparison.js"; +import { + managedMemberComparisonResultSchema, + compareManagedMembersInputSchema, +} from "../../../../src/domain/managed/managedMemberComparison.js"; import { readManagedMemberEvidence } from "../../../../src/domain/managed/managedMemberComparisonMatch.js"; import { createEvidence } from "../../../../src/domain/evidence.js"; import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js"; -import { jsonValueSchema } from "../../../../src/domain/jsonValue.js"; -import { compareManagedMembersInputSchema } from "../../../../src/domain/managed/managedMemberComparison.js"; + import { inspectManagedMembersBytes } from "../../../../src/dotnet/ManagedMemberInspector.js"; -import { managedPeFixtureTarget } from "../../../../src/dotnet/ManagedPe.fixture.js"; import { + managedPeFixtureTarget, buildManagedNestedTypeFixture, buildManagedPeFixture, } from "../../../../src/dotnet/ManagedPe.fixture.js"; @@ -139,7 +141,7 @@ const compare = ( { operation: "inspect_managed_members", parameters: {}, - result: jsonValueSchema.parse(change?.(inspection) ?? inspection), + result: change?.(inspection) ?? inspection, confidence: "observed", authority: "shipped-artifact", }, @@ -475,14 +477,14 @@ describe("managed member comparison subject identity", () => { { operation: "inspect_managed_members", parameters: {}, - result: jsonValueSchema.parse({ + result: { ...inspection, artifact: { ...inspection.artifact, sha256: otherDigest }, identity_scope: { ...inspection.identity_scope, requires_artifact_sha256: otherDigest, }, - }), + }, }, ); const aligned = createEvidence( @@ -563,11 +565,11 @@ describe("managed call-target comparison", () => { { operation: "inspect_managed_members", parameters: {}, - result: jsonValueSchema.parse({ + result: { ...inspection, call_edges: alterEdges?.(inspection.call_edges) ?? inspection.call_edges, - }), + }, confidence: "observed", authority: "shipped-artifact", }, @@ -636,10 +638,10 @@ describe("managed member comparison path workflow", () => { { operation: "inspect_managed_members", parameters: {}, - result: jsonValueSchema.parse({ + result: { ...inspection, coverage: { ...inspection.coverage, state: "partial" }, - }), + }, confidence: "observed", authority: "shipped-artifact", }, diff --git a/tests/evaluation/evidenceJsonPreadmissionRule.test.ts b/tests/evaluation/evidenceJsonPreadmissionRule.test.ts new file mode 100644 index 0000000000..fc435b3713 --- /dev/null +++ b/tests/evaluation/evidenceJsonPreadmissionRule.test.ts @@ -0,0 +1,265 @@ +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { RuleTester } from "oxlint/plugins-dev"; +import { describe, it } from "vitest"; + +import rule from "../../scripts/lint/rules/no-evidence-json-preadmission.mjs"; + +const filename = resolve("src/application/RuleFixture.ts"); +const imports = ` +import { createEvidence, createImmutableEvidenceSteps } from "../domain/evidence.js"; +import { jsonValueSchema, jsonObjectSchema } from "../domain/jsonValue.js"; +`; +const evidenceSource = new URL("../../src/domain/evidence.js", import.meta.url); +const jsonSource = new URL("../../src/domain/jsonValue.js", import.meta.url); +const fileImports = ` +import { createEvidence } from ${JSON.stringify(evidenceSource.href)}; +import { jsonValueSchema } from ${JSON.stringify(jsonSource.href)}; +`; +const absoluteImports = ` +import { createEvidence } from ${JSON.stringify(fileURLToPath(evidenceSource))}; +import { jsonValueSchema } from ${JSON.stringify(fileURLToPath(jsonSource))}; +`; +const valid = (name: string, code: string) => ({ name, filename, code }); +const invalid = (name: string, code: string, field: string) => ({ + name, + filename, + code, + errors: [{ messageId: "redundantJsonAdmission", data: { field } }], + output: null, +}); + +RuleTester.describe = describe; +RuleTester.it = it; +new RuleTester({ + languageOptions: { sourceType: "module", parserOptions: { lang: "ts" } }, +}).run("no-evidence-json-preadmission", rule, { + valid: [ + valid( + "file URL binding still respects lexical shadows", + `${fileImports} + function build(createEvidence) { createEvidence(target, provider, {result: jsonValueSchema.parse(input)}); }`, + ), + valid( + "HTTP modules are not filesystem owners", + ` + import { createEvidence } from "https://example.test/src/domain/evidence.js"; + import { jsonValueSchema } from "https://example.test/src/domain/jsonValue.js"; + createEvidence(target, provider, {result: jsonValueSchema.parse(input)});`, + ), + valid( + "malformed file URLs do not crash owner lookup", + ` + import {createEvidence} from "file://%"; + import {jsonValueSchema} from ${JSON.stringify(jsonSource.href)}; + createEvidence(target, provider, {result: jsonValueSchema.parse(input)});`, + ), + valid( + "queried file URL does not establish the canonical owner", + ` + import {createEvidence} from ${JSON.stringify(`${evidenceSource.href}?instance=other`)}; + import {jsonValueSchema} from ${JSON.stringify(jsonSource.href)}; + createEvidence(target, provider, {result: jsonValueSchema.parse(input)});`, + ), + valid( + "fragment file URL does not establish the canonical schema", + ` + import {createEvidence} from ${JSON.stringify(evidenceSource.href)}; + import {jsonValueSchema} from ${JSON.stringify(`${jsonSource.href}#other`)}; + createEvidence(target, provider, {result: jsonValueSchema.parse(input)});`, + ), + valid( + "post-fix process capture keeps producer facts", + `${imports} + createEvidence(undefined, provider, { result: capture });`, + ), + valid( + "semantic format admission remains", + `${imports} + createEvidence(target, provider, { result: processCaptureSchema.parse(capture) });`, + ), + valid( + "standalone JSON admission remains", + `${imports} + const observation = { result: jsonValueSchema.parse(result) }; + executeAnalysis(observation);`, + ), + valid( + "strong immutable Evidence contract remains", + `${imports} + createImmutableEvidenceSteps(target, provider, { result: jsonValueSchema.parse(result) });`, + ), + valid( + "digest helper owns its typed JSON input", + `${imports} + const parameters = { ...jsonObjectSchema.parse(scenario), digest: scenarioDigest(scenario) }; + createEvidence(target, provider, { parameters });`, + ), + valid( + "multiuse parsed value retains its separate contract", + `${imports} + const result = jsonValueSchema.parse(input); + consumeJson(result); + createEvidence(target, provider, { result });`, + ), + valid( + "single-use typed local is outside the direct expression rule", + `${imports} + const result: JsonValue = jsonValueSchema.parse(input); + createEvidence(target, provider, { result });`, + ), + valid( + "local creator shadows imported creator", + `${imports} + function build(createEvidence) { + return createEvidence(target, provider, { result: jsonValueSchema.parse(input) }); + }`, + ), + valid( + "local schema shadows imported schema", + `${imports} + function build(jsonValueSchema) { + return createEvidence(target, provider, { result: jsonValueSchema.parse(input) }); + }`, + ), + valid( + "namespace shadowing is respected", + ` + import * as evidence from "../domain/evidence.js"; + import * as json from "../domain/jsonValue.js"; + function build(json) { + return evidence.createEvidence(target, provider, { result: json.jsonValueSchema.parse(input) }); + }`, + ), + valid( + "creator namespace shadowing is respected", + ` + import * as evidence from "../domain/evidence.js"; + import * as json from "../domain/jsonValue.js"; + function build(evidence) { + return evidence.createEvidence(target, provider, { result: json.jsonValueSchema.parse(input) }); + }`, + ), + valid( + "same-named exports from another module are unrelated", + ` + import { createEvidence } from "../other/evidence.js"; + import { jsonValueSchema } from "../domain/jsonValue.js"; + createEvidence(target, provider, { result: jsonValueSchema.parse(input) });`, + ), + valid( + "same-named schema from another module is unrelated", + ` + import { createEvidence } from "../domain/evidence.js"; + import { jsonValueSchema } from "../other/jsonValue.js"; + createEvidence(target, provider, { result: jsonValueSchema.parse(input) });`, + ), + valid( + "safeParse is a separate Result boundary", + `${imports} + createEvidence(target, provider, { result: jsonValueSchema.safeParse(input) });`, + ), + valid( + "nested parsing is not an immediate payload admission", + `${imports} + createEvidence(target, provider, { parameters: { trace_spec: jsonValueSchema.parse(specification) } });`, + ), + valid( + "later spread may replace the parsed field", + `${imports} + createEvidence(target, provider, { result: jsonValueSchema.parse(input), ...overrides });`, + ), + valid( + "later duplicate field replaces the parsed field", + `${imports} + createEvidence(target, provider, { result: jsonValueSchema.parse(input), result: replacement });`, + ), + valid( + "unknown computed field may replace the parsed field", + `${imports} + createEvidence(target, provider, { result: jsonValueSchema.parse(input), [field]: replacement });`, + ), + valid( + "explicit parse options are outside the direct admission rule", + `${imports} + createEvidence(target, provider, { result: jsonValueSchema.parse(input, options) });`, + ), + ], + invalid: [ + invalid( + "file URLs preserve canonical JSON ownership", + `${fileImports} + createEvidence(target, provider, {result: jsonValueSchema.parse(input)});`, + "result", + ), + invalid( + "absolute paths preserve canonical JSON ownership", + `${absoluteImports} + createEvidence(target, provider, {result: jsonValueSchema.parse(input)});`, + "result", + ), + invalid( + "pre-fix process capture repeats JSON admission", + `${imports} + createEvidence(undefined, PROCESS_PROVIDER, { result: jsonValueSchema.parse(capture) });`, + "result", + ), + invalid( + "object parameters repeat generic admission", + `${imports} + createEvidence(target, provider, { parameters: jsonObjectSchema.parse(parameters) });`, + "parameters", + ), + invalid( + "raw payload admission belongs to Evidence", + `${imports} + createEvidence(target, provider, { rawResult: jsonValueSchema.parse(raw) });`, + "rawResult", + ), + invalid( + "aliased imports retain their owner", + ` + import { createEvidence as record } from "../domain/evidence.js"; + import { jsonValueSchema as json } from "../domain/jsonValue.js"; + record(target, provider, { result: json.parse(input) });`, + "result", + ), + invalid( + "namespace members retain their owner", + ` + import * as evidence from "../domain/evidence.js"; + import * as json from "../domain/jsonValue.js"; + evidence.createEvidence(target, provider, { result: json.jsonValueSchema.parse(input) });`, + "result", + ), + invalid( + "static computed members retain their owner", + ` + import * as evidence from "../domain/evidence.js"; + import * as json from "../domain/jsonValue.js"; + evidence["createEvidence"](target, provider, { ["result"]: json["jsonValueSchema"]["parse"](input) });`, + "result", + ), + invalid( + "TypeScript source specifiers resolve to the same owner", + ` + import { createEvidence } from "../domain/evidence.ts"; + import { jsonObjectSchema } from "../domain/jsonValue.ts"; + createEvidence(target, provider, { parameters: jsonObjectSchema.parse(input) });`, + "parameters", + ), + invalid( + "an earlier spread does not replace the final payload", + `${imports} + createEvidence(target, provider, { ...base, result: jsonValueSchema.parse(input) });`, + "result", + ), + invalid( + "unrelated local scopes do not hide the import", + `${imports} + function unrelated(jsonValueSchema) { return jsonValueSchema.parse(input); } + createEvidence(target, provider, { result: jsonValueSchema.parse(input) });`, + "result", + ), + ], +}); diff --git a/tests/evaluation/fixtureRootCleanupRule.test.ts b/tests/evaluation/fixtureRootCleanupRule.test.ts new file mode 100644 index 0000000000..8d6292e84a --- /dev/null +++ b/tests/evaluation/fixtureRootCleanupRule.test.ts @@ -0,0 +1,58 @@ +import { describe, it } from "vitest"; +import { fileURLToPath } from "node:url"; +import { RuleTester } from "oxlint/plugins-dev"; +import rule from "../../scripts/lint/rules/no-fixture-root-cleanup.mjs"; + +RuleTester.describe = describe; +RuleTester.it = it; +const tester = new RuleTester({ + languageOptions: { parserOptions: { lang: "ts" } }, +}); +const filename = fileURLToPath( + new URL("../boundary/ruleControl.test.ts", import.meta.url), +); +const imports = ` +import { createTestTempDirectory as temp } from "../fixtures/temporaryDirectory.js"; +import { afterEach as cleanup, afterAll, onTestFinished } from "vitest"; +import { rm as remove } from "node:fs/promises"; +`; +const fixtureSource = new URL( + "../fixtures/temporaryDirectory.js", + import.meta.url, +); +const localCleanupImports = `import {afterEach} from "vitest"; import {rm} from "node:fs/promises";`; +tester.run("no-fixture-root-cleanup", rule, { + valid: [ + `import {createTestTempDirectory as temp} from ${JSON.stringify(fixtureSource.href)}; ${localCleanupImports} function work(temp: Function) { const root = temp(); afterEach(() => rm(root)); }`, + `import {createTestTempDirectory as temp} from "https://example.test/tests/fixtures/temporaryDirectory.js"; ${localCleanupImports} const root = await temp("owned-"); afterEach(() => rm(root));`, + `import {createTestTempDirectory as temp} from ${JSON.stringify(`${fixtureSource.href}?other`)}; ${localCleanupImports} const root = await temp("owned-"); afterEach(() => rm(root));`, + `${imports} const root = await temp("owned-"); await remove(root, { recursive: true });`, + `${imports} const root = await temp("owned-"); cleanup(() => remove(join(root, "child"), {recursive: true}));`, + `${imports} const root = await otherOwner(); cleanup(() => remove(root, { recursive: true }));`, + `${imports} const root = await temp("owned-"); cleanup(async () => { const root = await otherOwner(); await remove(root, {recursive: true}); });`, + `${imports} function work(temp: Function) { const root = temp(); cleanup(() => remove(root)); }`, + `${imports} const root = await temp("owned-"); function unrelated(cleanup: Function) { cleanup(() => remove(root)); }`, + `${imports} const root = await temp("owned-"); cleanup(() => { function remove(root: string) {} remove(root); });`, + `${imports} const roots = []; const root = await temp("owned-"); roots.push(root); roots.push(await createIdaTarget()); cleanup(async () => { for (const owned of roots) await remove(owned); });`, + `${imports} let root = await temp("owned-"); root = await otherOwner(); cleanup(() => remove(root));`, + `${imports} const root = await temp("owned-"); cleanup(() => connection.close());`, + `${imports} import * as fs from "node:fs/promises"; const root = await temp("owned-"); cleanup((fs: {rm: Function}) => fs.rm(root));`, + `${imports} const root = await temp("owned-"); { const remove = otherOwner; cleanup(() => remove(root)); }`, + 'import { createTestTempDirectory as temp } from "./unrelated.js"; import { afterEach } from "vitest"; import { rm } from "node:fs/promises"; const root = await temp(); afterEach(() => rm(root));', + `${imports} const root = await temp("owned-"); function callback(root: string) { return remove(root); } cleanup(callback);`, + ].map((code) => ({ code, filename })), + invalid: [ + `import {createTestTempDirectory as temp} from ${JSON.stringify(fixtureSource.href)}; ${localCleanupImports} const root = await temp("owned-"); afterEach(() => rm(root));`, + `import {createTestTempDirectory as temp} from ${JSON.stringify(fileURLToPath(fixtureSource))}; ${localCleanupImports} const root = await temp("owned-"); afterEach(() => rm(root));`, + `${imports} const root = await temp("owned-"); cleanup(() => remove(root, {recursive: true}));`, + `${imports} const root = await temp("owned-"); afterAll(async () => { await remove(root, {recursive: true}); });`, + `${imports} const root = await temp("owned-"); onTestFinished(() => remove(root, {recursive: true}));`, + `${imports} const root = await temp("owned-"); const alias = root; cleanup(() => remove(alias));`, + `${imports} const root = await temp("owned-"); async function release() { await remove(root); } cleanup(release);`, + `${imports} const root = await temp("owned-"); const release = () => remove(root); cleanup(release);`, + `${imports} import { rmSync } from "node:fs"; const root = await temp("owned-"); cleanup(() => rmSync(root, {recursive: true}));`, + `${imports} import { removeTestWorkspace as release } from "../support/workspace/workspaceFixture.js"; const root = await temp("owned-"); cleanup(() => release(root));`, + `${imports} const root = await temp("owned-"); cleanup(async () => { await connection.close(); await remove(root, {recursive: true}); });`, + 'import * as fixture from "../fixtures/temporaryDirectory.ts"; import * as vitest from "vitest"; import * as fs from "node:fs/promises"; const root = await fixture.createTestTempDirectory("owned-"); vitest.afterEach(() => fs.rm(root, {recursive: true}));', + ].map((code) => ({ code, filename, errors: [{ messageId: "duplicate" }] })), +}); diff --git a/tests/evaluation/positionalToolContractRule.test.ts b/tests/evaluation/positionalToolContractRule.test.ts new file mode 100644 index 0000000000..231be60622 --- /dev/null +++ b/tests/evaluation/positionalToolContractRule.test.ts @@ -0,0 +1,184 @@ +import { describe, expect, it } from "vitest"; +import { fileURLToPath } from "node:url"; +import { RuleTester } from "oxlint/plugins-dev"; +import rule from "../../scripts/lint/rules/no-positional-tool-contract.mjs"; +import { collectToolCatalogSources } from "../../scripts/lint/rules/tool-catalog-sources.mjs"; + +RuleTester.describe = describe; +RuleTester.it = it; +const tester = new RuleTester({ + languageOptions: { parserOptions: { lang: "ts" } }, +}); +const filename = fileURLToPath( + new URL("../../src/server/ruleControl.ts", import.meta.url), +); +const catalog = + 'import { GPU_TOOL_CONTRACTS as contracts } from "../contracts/gpu/gpuToolContracts.js";'; +const catalogSource = new URL( + "../../src/contracts/gpu/gpuToolContracts.js", + import.meta.url, +); +tester.run("no-positional-tool-contract", rule, { + valid: [ + `import {GPU_TOOL_CONTRACTS as contracts} from ${JSON.stringify(catalogSource.href)}; function read(contracts: {name: string}[]) { return contracts[0].name; }`, + 'import {GPU_TOOL_CONTRACTS as contracts} from "https://example.test/src/contracts/gpu/gpuToolContracts.js"; contracts[0].name;', + `import {GPU_TOOL_CONTRACTS as contracts} from ${JSON.stringify(`${catalogSource.href}#other`)}; contracts[0].name;`, + `${catalog} for (const contract of contracts) register(contract);`, + `${catalog} contracts.forEach(register);`, + `${catalog} const selected = contracts.find(contract => contract.name === "inspect_gpu_code_objects");`, + `${catalog} type Contract = (typeof contracts)[number];`, + `${catalog} const copies = [...contracts];`, + `${catalog} const [...copies] = contracts;`, + `${catalog} function read(contracts: {name: string}[]) { return contracts[0].name; }`, + `${catalog} function read() { const contracts = [{name: "local"}]; return contracts.at(0)?.name; }`, + 'import { GPU_TOOL_CONTRACTS as contracts } from "../unrelated.js"; contracts[0].name;', + 'import { unrelated as contracts } from "../contracts/gpu/gpuToolContracts.js"; contracts[0].name;', + 'const TOOL_CONTRACTS = [{name: "local"}]; TOOL_CONTRACTS[0].name;', + `${catalog} const mutable = [...contracts]; mutable[0].inputSchema;`, + `${catalog} contracts.length;`, + `${catalog} contracts[0]; contracts.at(1); const [unused] = contracts;`, + `${catalog} expect(contracts[0]?.description).toContain("documented behavior");`, + `${catalog} const [first] = contracts; const toggled = contracts.map(contract => contract === first ? {...contract, returnsTargetContent: !contract.returnsTargetContent} : contract); createCatalogDigests(toggled);`, + `${catalog} const selected = contracts[0]; const alias = selected; display(alias.description);`, + `${catalog} const selected = contracts[0]; selected.name = "catalog-control";`, + `${catalog} const [selected] = contracts; const { description } = selected; display(description);`, + `${catalog} function toolRegistrationOptions(contract: unknown) {} toolRegistrationOptions(contracts[0]);`, + `${catalog} { const contracts = [{name: "local"}]; contracts[0].name; }`, + 'import * as catalogs from "../contracts/gpu/gpuToolContracts.js"; function read(catalogs: {GPU_TOOL_CONTRACTS: {name: string}[]}) { return catalogs.GPU_TOOL_CONTRACTS[0].name; }', + 'import * as catalogs from "../contracts/gpu/gpuToolContracts.js"; catalogs.other[0].name;', + ].map((code) => ({ code, filename })), + invalid: [ + `import {GPU_TOOL_CONTRACTS as contracts} from ${JSON.stringify(catalogSource.href)}; contracts[0].name;`, + `import {GPU_TOOL_CONTRACTS as contracts} from ${JSON.stringify(fileURLToPath(catalogSource))}; contracts[0].inputSchema;`, + `${catalog} import {toolRegistrationOptions as options} from ${JSON.stringify(new URL("../../src/server/toolRegistrationOptions.js", import.meta.url).href)}; options(contracts[0]);`, + `${catalog} register(contracts[0].name);`, + `${catalog} contracts["0"].inputSchema.parse(input);`, + `${catalog} contracts.at(-1)?.outputSchema;`, + `${catalog} contracts["at"](1)?.name;`, + `${catalog} const alias = contracts; alias[2].name;`, + `${catalog} const first = contracts; const second = first; second[0].outputSchema;`, + `${catalog} type Selected = (typeof contracts)[0];`, + `${catalog} const [first] = contracts; register(first.name);`, + `${catalog} const [, second] = contracts; const selected = second; selected.inputSchema;`, + `${catalog} const selected = contracts[0]; const alias = selected; alias.inputSchema;`, + `${catalog} const { inputSchema } = contracts[0];`, + `${catalog} import {toolRegistrationOptions as options} from "./toolRegistrationOptions.js"; options(contracts[0]);`, + `${catalog} import {applicationToolRegistrationOptions as options} from "./toolRegistrationOptions.ts"; const selected = contracts.at(0); options(selected);`, + `${catalog} const selected = contracts[0]; selected.name; selected.outputSchema;`, + 'import * as catalogs from "../contracts/gpu/gpuToolContracts.js"; catalogs.GPU_TOOL_CONTRACTS[0].name;', + 'import * as catalogs from "../contracts/gpu/gpuToolContracts.js"; type Selected = (typeof catalogs.GPU_TOOL_CONTRACTS)[0];', + 'import { TOOL_CONTRACTS } from "../contracts/toolContracts.ts"; TOOL_CONTRACTS[0].name;', + 'import { MODEL_TOOL_CONTRACTS as selected } from "../contracts/model/modelToolContracts.js"; selected[0].inputSchema;', + 'import { IL2CPP_TOOL_CONTRACTS as selected } from "../contracts/il2cpp/il2cppToolContracts.js"; selected[0].outputSchema;', + 'import { BROWSER_SCENARIO_TOOL_CONTRACTS as nested } from "../contracts/browserScenarioToolContracts.js"; nested[0].name;', + 'import { FUNCTION_WORKFLOW_TOOL_CONTRACTS as nested } from "../contracts/functionWorkflowToolContracts.js"; nested.at(0)?.name;', + ].map((code) => ({ code, filename, errors: [{ messageId: "positional" }] })), +}); + +const sourcePath = (name: string) => + fileURLToPath(new URL(`./catalog-rule-fixture/${name}`, import.meta.url)); + +describe("canonical catalog source composition", () => { + it("discovers newly composed exports and nested groups without evaluating modules", () => { + const sources = new Map([ + [ + sourcePath("toolContracts.ts"), + ` + import { NEW_CATALOG as renamed } from "./newCatalog.js"; + import { GROUP } from "./group.js"; + import { UNUSED } from "./unused.js"; + const generated = buildContracts(); + const local = [...renamed, ...GROUP, ...generated, ...UNUSED.map(makeContract)]; + const alias = local; + export const TOOL_CONTRACTS = [...alias] as const; + `, + ], + [ + sourcePath("newCatalog.ts"), + ` + throw new Error("Must never execute the runtime catalog"); + export const NEW_CATALOG = [{name: "new_tool"}]; + `, + ], + [ + sourcePath("group.ts"), + ` + import { NESTED as alias } from "./nested.ts"; + export const GROUP = [...alias] satisfies readonly unknown[]; + `, + ], + [ + sourcePath("nested.ts"), + 'export const NESTED = [{name: "nested_tool"}];', + ], + ]); + const actual = collectToolCatalogSources( + sourcePath("toolContracts.ts"), + (path) => { + const source = sources.get(path); + if (source === undefined) + throw new Error(`Unexpected source read: ${path}`); + return source; + }, + ); + expect(actual).toEqual( + new Map([ + [sourcePath("toolContracts"), new Set(["TOOL_CONTRACTS"])], + [sourcePath("newCatalog"), new Set(["NEW_CATALOG"])], + [sourcePath("group"), new Set(["GROUP"])], + [sourcePath("nested"), new Set(["NESTED"])], + ]), + ); + }); + + it("accepts official computed definitions while discovering imported catalog groups", () => { + const actual = collectToolCatalogSources( + fileURLToPath( + new URL("../../src/contracts/toolContracts.ts", import.meta.url), + ), + ); + const official = fileURLToPath( + new URL("../../src/contracts/officialToolContracts", import.meta.url), + ); + const definitions = fileURLToPath( + new URL("../../src/contracts/hopperMemoryContracts", import.meta.url), + ); + const nested = fileURLToPath( + new URL( + "../../src/contracts/browserScenarioToolContracts", + import.meta.url, + ), + ); + expect(actual.get(official)).toEqual(new Set(["OFFICIAL_TOOL_CONTRACTS"])); + expect(actual.has(definitions)).toBe(false); + expect(actual.get(nested)).toEqual( + new Set(["BROWSER_SCENARIO_TOOL_CONTRACTS"]), + ); + }); + + it("propagates source-read and parse failures instead of disabling enforcement", () => { + const failure = new Error("catalog source unavailable"); + expect(() => + collectToolCatalogSources(sourcePath("toolContracts.ts"), () => { + throw failure; + }), + ).toThrow(failure); + expect(() => + collectToolCatalogSources( + sourcePath("toolContracts.ts"), + () => "export const TOOL_CONTRACTS = [", + ), + ).toThrow(); + }); + + it.each([ + "export const TOOL_CONTRACTS = buildCatalog();", + "export const OTHER = [];", + "const alias = alias; export const TOOL_CONTRACTS = [...alias];", + "export const TOOL_CONTRACTS = [...unresolved];", + ])("refuses unsupported or missing canonical composition: %s", (source) => { + expect(() => + collectToolCatalogSources(sourcePath("toolContracts.ts"), () => source), + ).toThrow(); + }); +}); diff --git a/tests/fixtures/artifactEvidence.ts b/tests/fixtures/artifactEvidence.ts index 57f50c9a58..49c748ed13 100644 --- a/tests/fixtures/artifactEvidence.ts +++ b/tests/fixtures/artifactEvidence.ts @@ -1,6 +1,5 @@ import { artifactInventoryResultSchema } from "../../src/domain/artifactGraph.js"; import { createEvidence, type Evidence } from "../../src/domain/evidence.js"; -import { jsonValueSchema } from "../../src/domain/jsonValue.js"; /** Partition a real scan into authenticated partial observations for merge tests. */ export const fragmentInventoryEvidence = ( @@ -26,7 +25,7 @@ export const fragmentInventoryEvidence = ( parameters: evidence.parameters, confidence: "derived", authority: evidence.authority, - result: jsonValueSchema.parse({ + result: { ...inventory, nodes: inventory.nodes.filter( (_, ordinal) => ordinal % count === index, @@ -37,7 +36,7 @@ export const fragmentInventoryEvidence = ( edges: inventory.edges.filter( (_, ordinal) => ordinal % count === index, ), - }), + }, }, ), ); diff --git a/tests/fixtures/nativeFunctionMatching.ts b/tests/fixtures/nativeFunctionMatching.ts index b5969f1fb6..bc0bf80b61 100644 --- a/tests/fixtures/nativeFunctionMatching.ts +++ b/tests/fixtures/nativeFunctionMatching.ts @@ -1,5 +1,5 @@ import { createEvidence, type Evidence } from "../../src/domain/evidence.js"; -import { jsonValueSchema } from "../../src/domain/jsonValue.js"; + import { FUNCTION_COMPARISON_EXAMPLE } from "../../src/contracts/functionComparisonExample.js"; import type { NativeSymbolMetadata } from "../../src/domain/native/nativeSymbol.js"; import { functionDossierSchema } from "../../src/domain/hopperValues.js"; @@ -24,7 +24,7 @@ export const nativeMatchingInventory = ( createEvidence(nativeMatchingTarget(side), nativeMatchingProvider, { operation: "list_procedures", parameters: {}, - result: jsonValueSchema.parse(entries), + result: entries, evidenceLinks: [...evidenceLinks], }); /** Explicit imported-origin fixture for tests that select exact-symbol correspondence. */ @@ -97,7 +97,7 @@ export const nativeMatchingDetails = ( observation = createEvidence(target, nativeMatchingProvider, { operation: "analyze_function", parameters: { procedure: address }, - result: jsonValueSchema.parse(value), + result: value, relationshipCoverage: { status: "complete" }, }); } else diff --git a/tests/fixtures/oxlintConfiguration.ts b/tests/fixtures/oxlintConfiguration.ts new file mode 100644 index 0000000000..0e9cabbce2 --- /dev/null +++ b/tests/fixtures/oxlintConfiguration.ts @@ -0,0 +1,39 @@ +import { readFile, writeFile } from "node:fs/promises"; +import { join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { z } from "zod"; + +const repository = fileURLToPath(new URL("../../", import.meta.url)); +const configurationSchema = z.looseObject({ + jsPlugins: z + .array(z.union([z.string(), z.looseObject({ specifier: z.string() })])) + .optional(), +}); + +/** Preserve repository lint policy while resolving local plugins at their owner. */ +export const writeRepositoryOxlintConfiguration = async ( + project: string, +): Promise => { + const configuration = configurationSchema.parse( + JSON.parse(await readFile(join(repository, ".oxlintrc.json"), "utf8")), + ); + const rebase = (specifier: string): string => + specifier.startsWith(".") ? resolve(repository, specifier) : specifier; + const path = join(project, ".oxlintrc.json"); + await writeFile( + path, + JSON.stringify({ + ...configuration, + ...(configuration.jsPlugins === undefined + ? {} + : { + jsPlugins: configuration.jsPlugins.map((plugin) => + typeof plugin === "string" + ? rebase(plugin) + : { ...plugin, specifier: rebase(plugin.specifier) }, + ), + }), + }), + ); + return path; +}; diff --git a/tests/fixtures/reconstructionProofApplicability.ts b/tests/fixtures/reconstructionProofApplicability.ts index 15d3882afc..c1f71e1761 100644 --- a/tests/fixtures/reconstructionProofApplicability.ts +++ b/tests/fixtures/reconstructionProofApplicability.ts @@ -1,7 +1,7 @@ import { readyReviewedLedgerRequest } from "../../src/application/ReconstructionObligationLedger.fixture.js"; import { createEvidence } from "../../src/domain/evidence.js"; import { createEvidenceBundle } from "../../src/domain/evidenceBundle.js"; -import { jsonValueSchema } from "../../src/domain/jsonValue.js"; + import { reconstructionObligationProofSchema } from "../../src/domain/reconstructionObligationProof.js"; import type { ReconstructionObligationLedgerInput } from "../../src/domain/reconstructionObligationLedgerSchemas.js"; @@ -16,7 +16,7 @@ const replaceProof = ( predicateType: original.predicate_type, operation: original.operation, parameters: original.parameters, - result: jsonValueSchema.parse(result), + result: result, confidence: original.confidence, authority: original.authority, }); diff --git a/tests/fixtures/webScriptCapture.ts b/tests/fixtures/webScriptCapture.ts index fef099676a..06682ef614 100644 --- a/tests/fixtures/webScriptCapture.ts +++ b/tests/fixtures/webScriptCapture.ts @@ -5,7 +5,6 @@ import { browserScenarioCaptureSchema } from "../../src/domain/browserScenarioCa import { DEFAULT_BROWSER_SCENARIO_CAPTURE_LIMITS } from "../../src/domain/browserScenarioCaptureLimits.js"; import { browserScenarioEventSchema } from "../../src/domain/browserScenarioCaptureValues.js"; import { createEvidence } from "../../src/domain/evidence.js"; -import { jsonValueSchema } from "../../src/domain/jsonValue.js"; /** Source-owned script capture with independently authored bytes. */ export const scriptScenarioFixture = ( @@ -134,7 +133,7 @@ export const scriptCaptureEvidenceFixture = () => { id: "source-fixture", name: "Source fixture", version: "1" }, { operation: "capture_browser_scenario", - result: jsonValueSchema.parse(scriptScenarioFixture()), + result: scriptScenarioFixture(), parameters: {}, confidence: "observed", authority: "controlled-replay",