diff --git a/.github/workflows/publish-one.yml b/.github/workflows/publish-one.yml index 1ef6b4b3c..5ca6c23d3 100644 --- a/.github/workflows/publish-one.yml +++ b/.github/workflows/publish-one.yml @@ -8,7 +8,7 @@ on: workflow_call: inputs: package: - description: "Workspace member directory (e.g. packages/core, packages/code-review-agent)" + description: "Workspace member directory (e.g. packages/core, packages/cli)" required: true type: string version: diff --git a/.github/workflows/publish-packages.yml b/.github/workflows/publish-packages.yml index e40486d91..2818c2e64 100644 --- a/.github/workflows/publish-packages.yml +++ b/.github/workflows/publish-packages.yml @@ -30,7 +30,7 @@ jobs: - name: Validate the manifests declare this version run: | VERSION="${{ steps.resolve.outputs.value }}" - for f in packages/durable-streams/deno.json packages/runtime/deno.json packages/core/deno.json packages/acp/deno.json packages/workflow/deno.json packages/git/deno.json packages/testing/deno.json packages/test-agent/deno.json packages/web/deno.json packages/cli/deno.json packages/code-review-agent/deno.json; do + for f in packages/durable-streams/deno.json packages/runtime/deno.json packages/core/deno.json packages/acp/deno.json packages/workflow/deno.json packages/git/deno.json packages/testing/deno.json packages/test-agent/deno.json packages/web/deno.json packages/cli/deno.json; do declared="$(jq -r .version "$f")" if [ "$declared" != "$VERSION" ]; then echo "::error::$f declares $declared, not $VERSION — the tag does not match the manifests" @@ -131,13 +131,6 @@ jobs: package: packages/cli version: ${{ needs.version.outputs.value }} - code-review-agent: - needs: [version, core, runtime] - uses: ./.github/workflows/publish-one.yml - with: - package: packages/code-review-agent - version: ${{ needs.version.outputs.value }} - jsr: needs: [version] runs-on: ubuntu-latest diff --git a/.github/workflows/repo-analysis.yml b/.github/workflows/repo-analysis.yml deleted file mode 100644 index 2ac0b5215..000000000 --- a/.github/workflows/repo-analysis.yml +++ /dev/null @@ -1,79 +0,0 @@ -name: Repo Analysis - -on: - workflow_dispatch: - inputs: - ref: - description: Git ref to analyze - required: true - default: main - report_name: - description: Optional report label prefix - required: false - default: repo-analysis - -concurrency: - group: repo-analysis-${{ inputs.ref }} - cancel-in-progress: true - -jobs: - analyze: - runs-on: ubuntu-latest - timeout-minutes: 25 - permissions: - contents: read - issues: write - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - with: - ref: ${{ inputs.ref }} - fetch-depth: 0 - - - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 - - - uses: denoland/setup-deno@e95548e56dfa95d4e1a28d6f422fafe75c4c26fb # v2.0.3 - with: - deno-version: v2.9.5 - - - name: Prepare the checked-out xmd binary - run: deno task setup - - - name: Build the checked-out xmd binary - run: deno task build - - - name: Run repo analysis - env: - DEEPINFRA_TOKEN: ${{ secrets.DEEPINFRA_TOKEN }} - GITHUB_TOKEN: ${{ github.token }} - GITHUB_REPOSITORY: ${{ github.repository }} - run: | - ./dist/xmd run .reviews/AnalyzeRepoCI.md \ - --plugin ./packages/code-review-agent/mod.ts \ - -j .reviews/journal.analyze.ci.jsonl > .reviews/analyze-report.md - - - name: Write run metadata - if: always() - run: | - mkdir -p .reviews - cat > .reviews/analyze-run.json <<'JSON' - { - "workflow": "repo-analysis.yml", - "run_id": "${{ github.run_id }}", - "run_number": "${{ github.run_number }}", - "run_attempt": "${{ github.run_attempt }}", - "sha": "${{ github.sha }}", - "ref": "${{ inputs.ref }}", - "report_name": "${{ inputs.report_name }}" - } - JSON - - - name: Upload analysis artifacts - if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: ${{ inputs.report_name }}-${{ github.sha }} - path: | - .reviews/analyze-report.md - .reviews/journal.analyze.ci.jsonl - .reviews/analyze-run.json - retention-days: 30 diff --git a/.github/workflows/review.yml b/.github/workflows/review.yml deleted file mode 100644 index 7c911bb7a..000000000 --- a/.github/workflows/review.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: PR Review - -on: - pull_request: - types: [opened, synchronize, reopened] - -jobs: - review: - runs-on: ubuntu-latest - permissions: - pull-requests: write - contents: read - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - with: - fetch-depth: 0 - - - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 - - - uses: denoland/setup-deno@e95548e56dfa95d4e1a28d6f422fafe75c4c26fb # v2.0.3 - with: - deno-version: v2.9.5 - - - name: Prepare the checked-out xmd binary - run: deno task setup - - - name: Build the checked-out xmd binary - run: deno task build - - - name: Run review - env: - PR_NUMBER: ${{ github.event.pull_request.number }} - PR_TITLE: ${{ github.event.pull_request.title }} - BASE_SHA: ${{ github.event.pull_request.base.sha }} - HEAD_SHA: ${{ github.event.pull_request.head.sha }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_REPOSITORY: ${{ github.repository }} - DEEPINFRA_TOKEN: ${{ secrets.DEEPINFRA_TOKEN }} - run: | - ./dist/xmd run .reviews/ReviewPR.md \ - --plugin ./packages/code-review-agent/mod.ts \ - -j .reviews/journal.jsonl \ - --verbose - - - name: Upload journal - if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: review-journal-${{ github.event.pull_request.head.sha }} - path: .reviews/journal.jsonl - retention-days: 30 diff --git a/.gitignore b/.gitignore index de5459fd4..15a9c80e6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,5 @@ node_modules/ .vscode/ -.reviews/journal*.jsonl -.reviews/.oxlint/ -.reviews/tsconfig.oxlint.json -.reviews/analyze-report.md -.reviews/analyze-run.json .xmd-eval/ # xmd test failure journals (specs/testing-spec.md) .xmd/test-artifacts/ diff --git a/.reviews/.oxlintrc.json b/.reviews/.oxlintrc.json deleted file mode 100644 index 6a9bc310a..000000000 --- a/.reviews/.oxlintrc.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "../node_modules/oxlint/configuration_schema.json", - - "extends": ["../oxlint.shared.json"], - - "plugins": ["typescript", "unicorn", "import"] -} diff --git a/.reviews/AnalyzeRepo.md b/.reviews/AnalyzeRepo.md deleted file mode 100644 index e10db3939..000000000 --- a/.reviews/AnalyzeRepo.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -title: Repository Analysis ---- - - - - - - -```ts eval -const fileList = inventory.fileList; -const fileCount = inventory.fileCount; -const lineCount = inventory.lineCount; -const pr = { - files: [], - added: [], - removed: [], - created: [], - modified: [], - deleted: [], - directories: new Set(), - addedSource: "", - diffPreview: "", - stats: { totalFiles: fileCount, additions: lineCount, deletions: 0, totalChanges: lineCount }, - meta: { title: "Repo Analysis", body: "", number: "" }, -}; -``` - - - - - -```ts eval -import { buildCleanupAnalysis, buildDiagnostics } from "@executablemd/code-review-agent"; - -const diagnostics = buildDiagnostics(rawDiagnostics, pr, doctor); -const cleanupAnalysis = buildCleanupAnalysis(diagnostics); -``` - - - - - - - - - - diff --git a/.reviews/AnalyzeRepoCI.md b/.reviews/AnalyzeRepoCI.md deleted file mode 100644 index 4ee99b258..000000000 --- a/.reviews/AnalyzeRepoCI.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -title: Repository Analysis (CI) ---- - - - - - - - - -```ts eval -const fileList = inventory.fileList; -const fileCount = inventory.fileCount; -const lineCount = inventory.lineCount; -const pr = { - files: [], - added: [], - removed: [], - created: [], - modified: [], - deleted: [], - directories: new Set(), - addedSource: "", - diffPreview: "", - stats: { totalFiles: fileCount, additions: lineCount, deletions: 0, totalChanges: lineCount }, - meta: { title: "Repo Analysis", body: "", number: "" }, -}; -``` - - - - - -```ts eval -import { buildCleanupAnalysis, buildDiagnostics } from "@executablemd/code-review-agent"; - -const diagnostics = buildDiagnostics(rawDiagnostics, pr, doctor); -const cleanupAnalysis = buildCleanupAnalysis(diagnostics); -``` - - - - - - - - - - - - diff --git a/.reviews/DispatchRepoAnalysis.md b/.reviews/DispatchRepoAnalysis.md deleted file mode 100644 index cac429f5e..000000000 --- a/.reviews/DispatchRepoAnalysis.md +++ /dev/null @@ -1,127 +0,0 @@ ---- -title: Dispatch Repo Analysis Workflow ---- - -```ts eval -const requestedRef = process.env.ANALYZE_REF ?? ""; -const reportPrefix = process.env.ANALYZE_REPORT_NAME ?? "repo-analysis"; -``` - - - -```bash exec -git rev-parse --abbrev-ref HEAD -``` - - - - - -```bash exec -gh repo view --json nameWithOwner -q .nameWithOwner -``` - - - - - -```bash exec -date -u +"%Y-%m-%dT%H:%M:%SZ" -``` - - - -```ts eval -const targetRef = requestedRef || currentBranch.trim(); -const repo = repoName.trim(); -const dispatchStartIso = dispatchStart.trim(); -``` - -```bash exec -gh workflow run repo-analysis.yml --repo {repo} --ref {targetRef} -f ref={targetRef} -f report_name={reportPrefix} -``` - - - -```bash exec -for i in $(seq 1 30); do - RUN_ID=$(gh run list --repo {repo} --workflow repo-analysis.yml --event workflow_dispatch --json databaseId,headBranch,createdAt -q 'map(select(.headBranch == "{targetRef}" and .createdAt >= "{dispatchStartIso}")) | .[0].databaseId // ""') - if [ -n "$RUN_ID" ]; then - printf '%s' "$RUN_ID" - exit 0 - fi - sleep 2 -done -echo "" -``` - - - -```ts eval -const runIdValue = runId.trim(); -``` - -```bash exec -if [ -z "{runIdValue}" ]; then - echo "Unable to locate workflow run id" >&2 - exit 1 -fi -gh run watch {runIdValue} --repo {repo} --exit-status -``` - - - -```bash exec -gh run view {runIdValue} --repo {repo} --json databaseId,url,status,conclusion,headSha,displayTitle,createdAt,updatedAt -``` - - - -```bash silent exec -mkdir -p .reviews/artifacts/{runIdValue} -gh run download {runIdValue} --repo {repo} --dir .reviews/artifacts/{runIdValue} -``` - -```ts eval -const run = JSON.parse(runJson); -const artifactDir = `.reviews/artifacts/${runIdValue}`; -const reportPath = `${artifactDir}/${reportPrefix}-${run.headSha}/analyze-report.md`; -const metadataPath = `${artifactDir}/${reportPrefix}-${run.headSha}/analyze-run.json`; -const runUrl = run.url ?? ""; -const runStatus = run.status ?? ""; -const runConclusion = run.conclusion ?? ""; - -let reportText = ""; -let metadataText = ""; - -try { - reportText = Deno.readTextFileSync(reportPath); -} catch { - reportText = "(analyze-report.md artifact not found)"; -} - -try { - metadataText = Deno.readTextFileSync(metadataPath); -} catch { - metadataText = "{}"; -} -``` - -## Repo Analysis Dispatch - -- Repository: `{repo}` -- Ref: `{targetRef}` -- Run ID: `{runIdValue}` -- Run URL: {runUrl} -- Status: `{runStatus}` -- Conclusion: `{runConclusion}` - -### Run Metadata - -```json -{metadataText} -``` - -### Report - -{reportText} diff --git a/.reviews/ReviewPR.local.md b/.reviews/ReviewPR.local.md deleted file mode 100644 index 053485895..000000000 --- a/.reviews/ReviewPR.local.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: PR Review (local) ---- - - - - - - - -```ts eval -const pr = context.pr; -const changedFilePaths = context.changedFilePaths; -const changedTsFiles = pr.files - .filter((file) => file.language === "typescript" && !file.isTest && !file.isTypeDeclaration) - .map((file) => file.path) - .slice(0, 200); -``` - - - - - - - - - - - - - - - - diff --git a/.reviews/ReviewPR.md b/.reviews/ReviewPR.md deleted file mode 100644 index 9b5dbe5fc..000000000 --- a/.reviews/ReviewPR.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: PR Review ---- - - - - - - - - - -```ts eval -const pr = context.pr; -const changedFilePaths = context.changedFilePaths; -const changedTsFiles = pr.files - .filter((file) => file.language === "typescript" && !file.isTest && !file.isTypeDeclaration) - .map((file) => file.path) - .slice(0, 200); -``` - - - - - - - - - - - - - - - - - - - - - diff --git a/.reviews/policies/README.md b/.reviews/policies/README.md deleted file mode 100644 index 5652c36e4..000000000 --- a/.reviews/policies/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# Review Policies - -The executable policy documents and the reusable review components they compose -with are installed from `@executablemd/code-review-agent`, not from this -directory. The package declares them to every `xmd` run, so a review resolves the -same components whatever checkout it is reviewing and whatever directory it runs -from — which is the point: a review must not be answerable by its subject. - -The policies live at `packages/code-review-agent/src/documents/policies/`: - -- `ScopePolicy.md` checks PR size and scoping hygiene. -- `BloatPolicy.md` surfaces structural bloat patterns. -- `SlopPolicy.md` surfaces verbosity/slop indicators. -- `ExtraneousCodePolicy.md` asks for semantic/correctness-focused review. -- `RepoCleanupPolicy.md` performs repo-wide cleanup policy analysis. - -Policies are composed by report components, which the same package declares: - -- `PrPolicyReport` composes PR policies. -- `RepoPolicyReport` composes repo policies. - -Reusable workflow steps and primitives are at -`packages/code-review-agent/src/documents/components/`, and the six TypeScript -components at `packages/code-review-agent/src/components/`. - -What stays here is this repository's own review programs — `ReviewPR.md`, -`ReviewPR.local.md`, `AnalyzeRepo.md`, `AnalyzeRepoCI.md` and -`DispatchRepoAnalysis.md` — which compose the installed components, together -with the sensor configuration and runtime state those runs read and write -(`.oxlintrc.json`, `.oxlint/`, the generated `tsconfig.oxlint.json`, and the -journals). None of the review and analysis commands passes a component -`--include`. diff --git a/.reviews/retired/agents-review-boundaries.md b/.reviews/retired/agents-review-boundaries.md deleted file mode 100644 index 889dbd94b..000000000 --- a/.reviews/retired/agents-review-boundaries.md +++ /dev/null @@ -1,73 +0,0 @@ -# Retired: AGENTS.md review boundaries (parked verbatim pending the review-workflow rewrite) - -## Review boundaries - -An ordinary implementation verdict reviews one exact feedback-commit SHA against -the settled contract, the patch, the implementation and the focused evidence -reported with it. It does not inspect, monitor or wait for CI, and CI status is -neither positive nor negative evidence for it. A passing verdict is `PASS` — -never `PASS pending CI`, `mark ready after CI`, or an equivalent condition. CI -is inspected only when the user explicitly assigns CI troubleshooting. - -### Structural consequences - -An architecture finding has a **structural consequence** only when it changes -one or more of: - -- what is authorized to execute; -- which durable identity or retained history is accepted; -- what durable state is committed, published, or journaled; -- whether replay can resume the intended run; -- ownership of a transaction, resource, invocation, or lifecycle; -- concurrency or cancellation behavior that violates that ownership; -- which authoritative outcome wins after a fatal failure; or -- a public persistence or compatibility boundary. - -The Architect returns `REQUEST CHANGES` only when all five of these hold: - -1. The finding is reproduced or directly traced against the exact reviewed - commit. -2. It violates a previously settled structural invariant. -3. It uses an in-scope supported surface. -4. It produces a structural consequence from the list above. -5. Its correction belongs within the current PR's purpose. - -The verdict names every one of them: the reviewed SHA, the settled invariant, -the reproducer or direct trace, the supported surface, the structural -consequence, and why the correction belongs in this PR. A hypothetical risk, a -plausible concern, or an adjacent invariant cannot fail architecture review. - -The structural checklist is frozen before implementation. A distinct structural -invariant added afterwards takes an explicit architecture amendment naming its -consequence, not an implicit review expansion. - -### Finite evidence - -The Planner owns the evidence sufficient to prove the settled acceptance -criteria: how much implementation detail each criterion needs, the -representative scenarios, the focused tests, and when one regression proves a -criterion. That acceptance and evidence matrix is frozen before implementation. - -The Implementor executes the frozen matrix. When implementation evidence shows -the matrix cannot prove a criterion, the Implementor returns that evidence -rather than expanding acceptance independently. The Architect may restore a -structural invariant the plan omits, but does not expand a sufficient matrix -with permutations that carry no distinct structural consequence. - -Once implementation begins, a newly imagined edge case blocks only when it -proves an existing criterion unmet, or carries a distinct structural consequence -requiring an explicit architecture amendment. - -### Follow-ups and closure - -A non-blocking observation does not automatically become an issue. The Planner -decides whether recurrence likelihood, user impact, or expected remediation -value makes it worth tracking; otherwise the behavior stays for reactive -maintenance. - -Architect review closes when the frozen structural checklist passes. Planner -review closes when the frozen acceptance criteria and the selected evidence -pass. Neither role reopens review for CI, diagnostic hardening, speculative -permutations, unrelated correctness polish, or an incidental non-structural -observation. A later correction returns to a role only when it materially -changes that role's reviewed contract. diff --git a/README.md b/README.md index b32222bf5..6d1f57496 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,8 @@ Locate the package that owns the behavior you are about to change: - `packages/testing/` and `packages/test-agent/` — `` and the deterministic agent; `packages/test-support/` is the one BDD surface all three runtimes share. -- `packages/acp/`, `packages/web/`, `packages/code-review-agent/` — the coding - agent bridge, the web form host, and the review agent. +- `packages/acp/` and `packages/web/` — the coding agent bridge and the web + form host. - `scripts/` — this repository's own tooling: setup, builds, release targets, and the checks CI runs. - `specs/` — the specification each package is held to. diff --git a/bun.lock b/bun.lock index 780fb584b..c8f67edbf 100644 --- a/bun.lock +++ b/bun.lock @@ -33,7 +33,6 @@ "@durable-streams/server": "^0.3.8", "@executablemd/acp": "workspace:*", "@executablemd/cli": "workspace:*", - "@executablemd/code-review-agent": "workspace:*", "@executablemd/core": "workspace:*", "@executablemd/durable-streams": "workspace:*", "@executablemd/git": "workspace:*", @@ -84,10 +83,6 @@ "zod": "^4.3.6", }, }, - "packages/code-review-agent": { - "name": "@executablemd/code-review-agent", - "version": "0.14.1", - }, "packages/core": { "name": "@executablemd/core", "version": "0.14.1", @@ -335,8 +330,6 @@ "@executablemd/cli": ["@executablemd/cli@workspace:packages/cli"], - "@executablemd/code-review-agent": ["@executablemd/code-review-agent@workspace:packages/code-review-agent"], - "@executablemd/core": ["@executablemd/core@workspace:packages/core"], "@executablemd/durable-streams": ["@executablemd/durable-streams@workspace:packages/durable-streams"], diff --git a/deno.json b/deno.json index f64bc1f68..9769f257c 100644 --- a/deno.json +++ b/deno.json @@ -74,11 +74,6 @@ "verify": "deno run --allow-all --node-modules-dir=none --cached-only --frozen scripts/preflight.ts scripts/verify.ts", "vendor:verify": "deno run --allow-read --allow-write=/tmp --allow-env --allow-run --cached-only --frozen scripts/verify-cloudflare-dofs.ts", "check": "deno check --frozen", - "check:jsr": "deno publish --dry-run --allow-dirty", - "review": "deno run --allow-all packages/cli/src/deno.ts run --plugin ./packages/code-review-agent/mod.ts .reviews/ReviewPR.md -j .reviews/journal.jsonl", - "review:local": "deno run --allow-all packages/cli/src/deno.ts run --plugin ./packages/code-review-agent/mod.ts .reviews/ReviewPR.local.md -j .reviews/journal.local.jsonl", - "analyze": "deno run --allow-all packages/cli/src/deno.ts run --plugin ./packages/code-review-agent/mod.ts .reviews/AnalyzeRepo.md -j .reviews/journal.analyze.jsonl", - "analyze:ci": "deno run --allow-all packages/cli/src/deno.ts run --plugin ./packages/code-review-agent/mod.ts .reviews/AnalyzeRepoCI.md -j .reviews/journal.analyze.ci.jsonl", - "analyze:dispatch": "deno run --allow-all packages/cli/src/deno.ts run .reviews/DispatchRepoAnalysis.md -j .reviews/journal.dispatch.jsonl" + "check:jsr": "deno publish --dry-run --allow-dirty" } } diff --git a/deno.lock b/deno.lock index 850356aa4..2ae9cd884 100644 --- a/deno.lock +++ b/deno.lock @@ -4911,15 +4911,6 @@ ] } }, - "packages/code-review-agent": { - "packageJson": { - "dependencies": [ - "npm:@effectionx/fetch@0.2.1", - "npm:@effectionx/fs@0.3.0", - "npm:effection@4.1.0" - ] - } - }, "packages/core": { "dependencies": [ "npm:@babel/standalone@8.0.7", diff --git a/knip.json b/knip.json index 1f3732255..af107ae42 100644 --- a/knip.json +++ b/knip.json @@ -27,10 +27,6 @@ "@executablemd/acp/embedded-adapters": ["packages/acp/embedded-adapters.ts"], "@executablemd/cli": ["packages/cli/src/deno.ts"], "@executablemd/cli/api": ["packages/cli/api.ts"], - "@executablemd/code-review-agent": ["packages/code-review-agent/mod.ts"], - "@executablemd/code-review-agent/review-components": [ - "packages/code-review-agent/src/review-components.ts" - ], "@executablemd/core": ["packages/core/mod.ts"], "@executablemd/core/api": ["packages/core/api.ts"], "@executablemd/core/host/deno": ["packages/core/component-modules/deno.ts"], @@ -72,10 +68,6 @@ "@executablemd/acp/embedded-adapters": ["../../packages/acp/embedded-adapters.ts"], "@executablemd/cli": ["../../packages/cli/src/deno.ts"], "@executablemd/cli/api": ["../../packages/cli/api.ts"], - "@executablemd/code-review-agent": ["../../packages/code-review-agent/mod.ts"], - "@executablemd/code-review-agent/review-components": [ - "../../packages/code-review-agent/src/review-components.ts" - ], "@executablemd/core": ["../../packages/core/mod.ts"], "@executablemd/core/api": ["../../packages/core/api.ts"], "@executablemd/core/host/deno": ["../../packages/core/component-modules/deno.ts"], diff --git a/package.json b/package.json index f2224aff2..aa327e29f 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,6 @@ "@durable-streams/server": "^0.3.8", "@executablemd/acp": "workspace:*", "@executablemd/cli": "workspace:*", - "@executablemd/code-review-agent": "workspace:*", "@executablemd/core": "workspace:*", "@executablemd/durable-streams": "workspace:*", "@executablemd/git": "workspace:*", diff --git a/packages/cli/package.json b/packages/cli/package.json index c2c5dc578..7cfc98353 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -31,8 +31,5 @@ "marked": "^17.0.4", "semver": "^7.8.5", "zod": "^4.3.6" - }, - "devDependencies": { - "@executablemd/code-review-agent": "workspace:*" } } diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 8c94340f6..aa2b2a9eb 100755 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -1389,10 +1389,10 @@ export function* installDocumentComponents(mode: DocumentMode, verbose: boolean) yield* useVerboseComponent(); yield* installTestingComponents({ verbose }); // No package-specific vocabulary is bootstrapped here. A Plugin registers - // what it claims in the command scope this assembly runs inside, which is - // what lets `xmd test` claim none of the review graph's names while the - // nested `host="run"` child it can launch installs the same Plugins again - // and claims all of them. + // its names in the command scope this assembly runs inside, which is what + // lets a Plugin contribute nothing to `xmd test` while the nested + // `host="run"` child it can launch installs the same Plugins again with + // command `run`. } // `` for both commands. Registered rather than reserved, so a diff --git a/packages/cli/tests/fixtures/plugins/declaring.mjs b/packages/cli/tests/fixtures/plugins/declaring.mjs index 27810290f..16d26e162 100644 --- a/packages/cli/tests/fixtures/plugins/declaring.mjs +++ b/packages/cli/tests/fixtures/plugins/declaring.mjs @@ -1,6 +1,8 @@ -// Declares one exact Markdown component. The bytes, their origin and their -// digest are what the host states about them, exactly as any selected Plugin -// states its own — a Plugin is trusted code, and this is the same boundary. +// Declares exact Markdown components and reserves one registration. The bytes, +// their origin and their digest are what the host states about them, exactly as +// any selected Plugin states its own — a Plugin is trusted code, and this is the +// same boundary. +import { registerComponents } from "@executablemd/core"; import { Plugin } from "@executablemd/core/api"; // A declaration is what a *host* states about bytes it ships, so its // constructors live on the host boundary rather than on the consumer API. @@ -11,8 +13,22 @@ const SHADOWED = "the selected declaration, not the checkout's\n"; export default Plugin({ name: "declaring", - // deno-lint-ignore require-yield *install() { + // The registered tier beside the declared one: a reserved name a + // checkout's same-named file cannot replace either. + yield* registerComponents([ + { + name: "Stamp", + origin: "fixture/declaring", + reserved: true, + props: { type: "object", properties: {}, additionalProperties: false }, + description: "Mark the output as the selected Plugin's.", + // deno-lint-ignore require-yield + *fn() { + return "stamped by the selected Plugin"; + }, + }, + ]); return { components: [ Markdown({ diff --git a/packages/cli/tests/plugin-cli.test.ts b/packages/cli/tests/plugin-cli.test.ts index d3931f52b..436162e62 100644 --- a/packages/cli/tests/plugin-cli.test.ts +++ b/packages/cli/tests/plugin-cli.test.ts @@ -138,10 +138,11 @@ describe("PC2 — a Plugin declares components, and a checkout cannot answer for it("resolves a declared component, and leaves the checkout's own components alone", function* () { yield* useWorkspace( { - "doc.md": "\n\n\n\n\n", - // A repository component of the same name as one the Plugin declares, - // and one of a name nothing declares. + "doc.md": "\n\n\n\n\n\n\n", + // Repository components of the names the Plugin declares and reserves, + // and one of a name nothing claims. "components/Shadowed.md": "the checkout's own component\n", + "components/Stamp.md": "the checkout's own stamp\n", "components/Local.md": "an unrelated checkout component\n", }, function* (dir) { @@ -151,8 +152,10 @@ describe("PC2 — a Plugin declares components, and a checkout cannot answer for expect(run.stdout).toContain("hello from the selected Plugin"); expect(run.stdout).toContain("the selected declaration, not the checkout's"); expect(run.stdout).not.toContain("the checkout's own component"); - // Declaring two names claims exactly those two: everything else in the - // checkout resolves as it always did. + expect(run.stdout).toContain("stamped by the selected Plugin"); + expect(run.stdout).not.toContain("the checkout's own stamp"); + // Three names belong to the Plugin and no others: everything else in + // the checkout resolves as it always did. expect(run.stdout).toContain("an unrelated checkout component"); }, ); @@ -365,6 +368,7 @@ describe("PC6 — every surface of one command sees one vocabulary", () => { cwd: dir, }).expect(); expect(run.stdout).toContain("Greeting"); + expect(run.stdout).toContain("Stamp"); }); }); @@ -373,6 +377,7 @@ describe("PC6 — every surface of one command sees one vocabulary", () => { const run = yield* runCli(["syntax"], { cwd: dir }).expect(); // Nothing an operator did not select. expect(run.stdout).not.toContain("Greeting"); + expect(run.stdout).not.toContain("Stamp"); // And everything the one bundled Plugin brings, because every syntax // catalog is built from the profile the command runs with. for (const name of ["Repository", "Worktree", "Dir", "PullRequest", "Issue"]) { @@ -409,47 +414,6 @@ describe("PC6 — every surface of one command sees one vocabulary", () => { }); }); -describe("PC9 — the review graph arrives only when it is selected", () => { - const REVIEW = "@executablemd/code-review-agent"; - const PACKAGE = fileURLToPath(new URL("../../code-review-agent/mod.ts", import.meta.url)); - const FINDING = '\n'; - - it("is absent from every command that would run it, until it is named", function* () { - yield* useWorkspace({ "doc.md": FINDING }, function* (dir) { - // `xmd syntax` describes the language a run has, so it is where an absent - // vocabulary shows without running anything. - const described = yield* runCli(["syntax"], { cwd: dir }).expect(); - expect(described.stdout).not.toContain("Finding"); - expect(described.stdout).not.toContain("ReviewContext"); - - // And a document that writes one of the forty-one names resolves nothing. - const run = yield* runCli(["run", "doc.md"], { cwd: dir }).join(); - expect(run.stdout).not.toContain("probe"); - }); - }); - - it("arrives for run and syntax when it is named", function* () { - yield* useWorkspace({ "doc.md": FINDING }, function* (dir) { - const described = yield* runCli(["syntax", `--plugin=${PACKAGE}`], { cwd: dir }).expect(); - expect(described.stdout).toContain("Finding"); - expect(described.stdout).toContain("ReviewContext"); - - const run = yield* runCli(["run", `--plugin=${PACKAGE}`, "doc.md"], { cwd: dir }).expect(); - expect(run.stdout).toContain("probe"); - }); - }); - - it("names itself by its own name, however it was reached", function* () { - yield* useWorkspace({ "doc.md": DOCUMENT }, function* (dir) { - const run = yield* runCli( - ["run", `--plugin=${PACKAGE}`, `--plugin=${fixture("active.mjs")}`, "doc.md"], - { cwd: dir }, - ).expect(); - expect(run.stdout).toContain(`active: ${GIT}, ${REVIEW}, active`); - }); - }); -}); - /** * A package installed where the command runs, as an operator would have it. * diff --git a/packages/cli/tests/plugin-host.test.ts b/packages/cli/tests/plugin-host.test.ts index 0fa516117..80ccc4919 100644 --- a/packages/cli/tests/plugin-host.test.ts +++ b/packages/cli/tests/plugin-host.test.ts @@ -7,11 +7,6 @@ * before the first `install()`, so a refused selection installs nothing and * reads no document. And the whole list has the command's lifetime, so a * failure part-way through unwinds what came before it. - * - * The review Plugin's own rows are here too, because which commands reserve its - * forty-one names is a property of this assembly rather than of the package — - * and because nothing installs it until a selection names it. The graph itself - * is `scripts/tests/review-infrastructure.test.ts`. */ import { describe, it } from "@executablemd/test-support/bdd"; import { expect } from "@executablemd/test-support/expect"; @@ -19,10 +14,8 @@ import { ensure, scoped, spawn, suspend, withResolvers } from "effection"; import type { Operation } from "effection"; import { activePlugins, Plugin } from "@executablemd/core/api"; import type { PluginInstallation } from "@executablemd/core/api"; -import { inspectComponent } from "@executablemd/core"; import { Markdown, sourceDigest, Structural } from "@executablemd/core/host"; import { Config, verbose } from "@executablemd/runtime/api"; -import reviewPlugin from "@executablemd/code-review-agent"; import { admitPlugins, installPlugins, NO_PLUGINS } from "../src/plugin-host.ts"; import { syntaxSymbols } from "../src/syntax.ts"; import { structuralValidation } from "../src/plan-component.ts"; @@ -407,79 +400,7 @@ describe("PH5 — a Plugin reads the command's typed configuration", () => { }); }); -describe("PH6 — the selected review Plugin claims the commands that run a review", () => { - /** The declared names this command contributes when the Plugin is selected. */ - function* declaredFor(command: string, args: readonly string[]): Operation { - return yield* scoped(function* () { - const assembly = yield* installPlugins([reviewPlugin], { command, args }); - return assembly.declarations.map((declaration) => declaration.name); - }); - } - - it("declares the thirty-five Markdown components for run, syntax and plan", function* () { - for (const command of ["run", "syntax", "plan"]) { - const declared = yield* declaredFor(command, [command]); - expect(`${command}: ${declared.length}`).toBe(`${command}: 35`); - expect(declared).toContain("Finding"); - } - }); - - it("declares them for a workflow action that executes a document", function* () { - for (const action of ["start", "resume", "fork"]) { - const declared = yield* declaredFor("workflow", ["workflow", action, "flow.md"]); - expect(`${action}: ${declared.length}`).toBe(`${action}: 35`); - } - }); - - it("declares none for the test root, upgrade, or a workflow management action", function* () { - expect(yield* declaredFor("test", ["test"])).toEqual([]); - expect(yield* declaredFor("upgrade", ["upgrade"])).toEqual([]); - for (const action of ["list", "status", "history", "cancel", "delete", "export", "answer"]) { - expect(`${action}: ${(yield* declaredFor("workflow", ["workflow", action])).length}`).toBe( - `${action}: 0`, - ); - } - }); - - it("reads an option's value as a value, never as the action", function* () { - // Every option the command defines that takes a separated value, each with - // an executing action's own name as that value and a management action - // after it. Reading the first recognized word would have installed the - // review graph for a command that executes no document. - const valued: readonly (readonly [string, string, readonly string[]])[] = [ - ["--plugin", "start", ["workflow", "--plugin", "start", "list"]], - ["--output", "start", ["workflow", "--output", "start", "export", "run-1"]], - ["--status", "resume", ["workflow", "--status", "resume", "list"]], - ["--id", "fork", ["workflow", "--id", "fork", "list"]], - ["--at", "start", ["workflow", "--at", "start", "history", "run-1"]], - ["--artifact", "start", ["workflow", "--artifact", "start", "status"]], - ["--props", "start", ["workflow", "--props", "start", "list"]], - ["--props-name", "start", ["workflow", "--props-name", "start", "list"]], - ]; - for (const [option, value, args] of valued) { - const declared = yield* declaredFor("workflow", args); - expect(`${option}=${value}: ${declared.length}`).toBe(`${option}=${value}: 0`); - } - - // The assigned spelling is one token and was never a hazard, but a scan - // that special-cased the separated form would have found this one. - expect( - yield* declaredFor("workflow", ["workflow", "--output=start.xmd", "export", "r"]), - ).toEqual([]); - - // And the same reading still finds a real action written after an option. - const executing: readonly (readonly string[])[] = [ - ["workflow", "--plugin", "list", "start", "flow.md"], - ["workflow", "--id", "release-1", "start", "flow.md"], - ["workflow", "--at", "event-4", "fork", "run-1", "flow.md"], - ["workflow", "--verbose", "resume", "run-1"], - ]; - for (const args of executing) { - const declared = yield* declaredFor("workflow", args); - expect(`${args.join(" ")}: ${declared.length}`).toBe(`${args.join(" ")}: 35`); - } - }); - +describe("PH6 — an empty selection contributes nothing", () => { it("contributes nothing at all when it is not in the list", function* () { // A claim about this primitive, not about the product. `installPlugins()` // installs the list it is handed and nothing else, so an empty list @@ -495,50 +416,6 @@ describe("PH6 — the selected review Plugin claims the commands that run a revi expect(`${command}: ${assembly.installations.length}`).toBe(`${command}: 0`); } }); - - it("registers the six reserved names where it claims the graph", function* () { - const reserved = [ - "CommentReviewData", - "CommentReviewState", - "Doctor", - "OxlintDiagnostics", - "RepositoryInventory", - "ReviewContext", - ]; - const described = yield* scoped(function* () { - yield* installPlugins([reviewPlugin], { command: "run", args: ["run"] }); - const found: string[] = []; - for (const name of reserved) { - const info = yield* inspectComponent({ name, includes: [] }); - if ( - info.kind === "registered" && - info.origin.kind === "registered" && - info.origin.origin === "@executablemd/code-review-agent" && - info.origin.reserved - ) { - found.push(name); - } - } - return found; - }); - expect(described).toEqual(reserved); - }); - - it("registers none of them where no Plugin was selected", function* () { - const kind = yield* scoped(function* () { - yield* installPlugins([], { command: "run", args: ["run"] }); - return (yield* inspectComponent({ name: "ReviewContext", includes: [] })).kind; - }); - expect(kind).not.toBe("registered"); - }); - - it("registers none of them for the test root", function* () { - const kind = yield* scoped(function* () { - yield* installPlugins([reviewPlugin], { command: "test", args: ["test"] }); - return (yield* inspectComponent({ name: "ReviewContext", includes: [] })).kind; - }); - expect(kind).not.toBe("registered"); - }); }); describe("PH7 — a structural-only Plugin reaches every consumer of the catalog", () => { diff --git a/packages/cli/tests/support/run-markdown-tier.ts b/packages/cli/tests/support/run-markdown-tier.ts index 7a9c6d07e..9b9ecf354 100644 --- a/packages/cli/tests/support/run-markdown-tier.ts +++ b/packages/cli/tests/support/run-markdown-tier.ts @@ -37,7 +37,6 @@ import { useDenoService } from "../../src/deno-service.ts"; import { useNodeService } from "../../src/node-service.ts"; import { BUNDLED_PLUGIN } from "../../src/run-profile.ts"; -import reviewPlugin from "@executablemd/code-review-agent"; /** The native service adapter the entrypoint for this runtime installs. */ const SERVICES = { bun: useBunService, @@ -79,12 +78,9 @@ export function runMarkdownTier(document: string): Operation { const installService = SERVICES[cliRuntime()]; const testingHost = testingExecutionHost({ includes: ["components", "."], - // The Plugin a run would have selected, because a `host="run"` child gets - // what `xmd run` gets: the child installs it again in its own scope, with - // command `run`. XMD bundles only Git, so this harness names the rest - // exactly as an - // operator does. - plugins: [reviewPlugin], + // What `xmd test ` selects with no `--plugin`: a `host="run"` + // child gets the bundled Git Plugin and nothing else. + plugins: [], pluginArgs: [], secretDetection: true, installService, diff --git a/packages/cli/tests/testing-execution-host.test.ts b/packages/cli/tests/testing-execution-host.test.ts index b02d7e47a..5371c9d1d 100644 --- a/packages/cli/tests/testing-execution-host.test.ts +++ b/packages/cli/tests/testing-execution-host.test.ts @@ -31,7 +31,6 @@ import { testingExecutionHost } from "../src/testing-host.ts"; import { planComponentDeclaration, planComponentDescription } from "../src/plan-component.ts"; import { BUNDLED_PLUGIN } from "../src/run-profile.ts"; -import reviewPlugin from "@executablemd/code-review-agent"; function doc(...lines: string[]): string { return `${lines.join("\n")}\n`; } @@ -775,11 +774,7 @@ describe("deterministic dependencies declared for a nested run", () => { const hold = withResolvers(); const host = testingExecutionHost({ includes: [], - // The Plugin a run would have selected, because a `host="run"` child gets - // what `xmd run` gets: the child installs it again in its own scope, with - // command `run`. XMD bundles none, so a harness that wants the review - // graph names it exactly as an operator does. - plugins: [reviewPlugin], + plugins: [], pluginArgs: [], secretDetection: true, // deno-lint-ignore require-yield @@ -998,11 +993,7 @@ describe("deterministic dependencies declared for a nested run", () => { // to say about it, and what it says is why. const host = testingExecutionHost({ includes: [], - // The Plugin a run would have selected, because a `host="run"` child gets - // what `xmd run` gets: the child installs it again in its own scope, with - // command `run`. XMD bundles none, so a harness that wants the review - // graph names it exactly as an operator does. - plugins: [reviewPlugin], + plugins: [], pluginArgs: [], secretDetection: true, // deno-lint-ignore require-yield diff --git a/packages/code-review-agent/deno.json b/packages/code-review-agent/deno.json deleted file mode 100644 index 4d16e0b98..000000000 --- a/packages/code-review-agent/deno.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "@executablemd/code-review-agent", - "version": "0.14.1", - "exports": { - ".": "./mod.ts", - "./review-components": "./src/review-components.ts" - } -} diff --git a/packages/code-review-agent/mod.ts b/packages/code-review-agent/mod.ts deleted file mode 100644 index fd0864297..000000000 --- a/packages/code-review-agent/mod.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * @executablemd/code-review-agent - * - * The review component graph, as the Plugin a host installs once an operator - * has selected this package, and the parsers behind the components in it. - * - * The default export is the Plugin. The named exports are the library they - * always were, and `./review-components` still publishes the graph's own - * assembly helpers. - */ - -/** - * The review graph as a Plugin — the package's default export, so a host that - * was pointed at this module by an explicit `--plugin` selection has one - * unambiguous value to install. - */ -export { default } from "./src/plugin.ts"; - -export { parseDiff } from "./src/parse-diff.ts"; -export { buildDiagnostics, parseDiagnostics } from "./src/parse-diagnostics.ts"; -export { parseDoctorResult } from "./src/parse-doctor.ts"; -export { buildDoctorResult, isOxlintCrash, summarizeDoctorProbe } from "./src/doctor.ts"; -export { normalizeDiagnostic, normalizeOxlintOutput } from "./src/parse-oxlint.ts"; -export type { DoctorEnvironment, DoctorProbeInput, DoctorProbeSummary } from "./src/doctor.ts"; -export { - buildCleanupAnalysis, - clusterByFile, - buildPolicyInput, - buildPolicyReport, - clusterDiagnostics, - extractEvidence, - scoreClusters, -} from "./src/policy.ts"; -export { - categorizeRule, - STRUCTURAL_RULES, - TYPE_AWARE_RULES, - VERBOSITY_RULES, -} from "./src/categories.ts"; -export type { - CleanupAnalysis, - CleanupEvidence, - Diagnostics, - DiagnosticGroup, - DiffFile, - DiffHunk, - DiffLine, - DoctorResult, - FileCluster, - FileKind, - OxlintDiagnostic, - PolicyCategory, - PolicyCluster, - PolicyInput, - PolicyMode, - PolicyReport, - PolicyScore, - PR, -} from "./src/types.ts"; diff --git a/packages/code-review-agent/package.json b/packages/code-review-agent/package.json deleted file mode 100644 index b578cf1a4..000000000 --- a/packages/code-review-agent/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "@executablemd/code-review-agent", - "version": "0.14.1", - "description": "The review component graph and the parsers behind it, for executable.md reviews.", - "type": "module", - "exports": { - ".": "./mod.ts", - "./review-components": "./src/review-components.ts" - }, - "dependencies": { - "@effectionx/fetch": "0.2.1", - "@effectionx/fs": "0.3.0", - "@executablemd/core": "workspace:*", - "@executablemd/runtime": "workspace:*", - "effection": "4.1.0" - } -} diff --git a/packages/code-review-agent/src/categories.ts b/packages/code-review-agent/src/categories.ts deleted file mode 100644 index 7c5d42041..000000000 --- a/packages/code-review-agent/src/categories.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Rule categorization for Oxlint diagnostics. - * - * Rules are grouped by what kind of code issue they detect. - * A rule may appear in multiple categories. - */ - -export const STRUCTURAL_RULES = [ - "no-unused-vars", - "no-empty-function", - "no-empty-object-type", - "no-static-only-class", - "no-useless-empty-export", - "no-unnecessary-type-constraint", - "no-unnecessary-parameter-property-assignment", - "no-unnecessary-type-arguments", - "no-unnecessary-type-assertion", - "no-redundant-type-constituents", - "no-unnecessary-boolean-literal-compare", -] as const; - -export const VERBOSITY_RULES = ["no-inferrable-types", "no-console", "no-debugger"] as const; - -export const TYPE_AWARE_RULES = [ - "no-unnecessary-type-assertion", - "no-redundant-type-constituents", - "no-unnecessary-type-arguments", - "no-unnecessary-boolean-literal-compare", -] as const; - -/** - * The curated signal catalog the Oxlint policy enables explicitly, in the order - * the category lists declare them. - */ -export const SENSOR_RULES: readonly string[] = [ - ...new Set([...STRUCTURAL_RULES, ...VERBOSITY_RULES, ...TYPE_AWARE_RULES]), -]; - -const structuralSet = new Set(STRUCTURAL_RULES); -const verbositySet = new Set(VERBOSITY_RULES); -const typeAwareSet = new Set(TYPE_AWARE_RULES); - -/** - * A rule may belong to several categories; unmatched rules belong to `other`. - */ -export function categorizeRule( - ruleId: string, -): ("structural" | "verbosity" | "typeAware" | "other")[] { - const bare = ruleId.includes("/") ? ruleId.split("/").pop()! : ruleId; - - const categories: ("structural" | "verbosity" | "typeAware" | "other")[] = []; - - if (structuralSet.has(bare)) { - categories.push("structural"); - } - if (verbositySet.has(bare)) { - categories.push("verbosity"); - } - if (typeAwareSet.has(bare)) { - categories.push("typeAware"); - } - if (categories.length === 0) { - categories.push("other"); - } - - return categories; -} diff --git a/packages/code-review-agent/src/comment-review-data.ts b/packages/code-review-agent/src/comment-review-data.ts deleted file mode 100644 index 9a801e086..000000000 --- a/packages/code-review-agent/src/comment-review-data.ts +++ /dev/null @@ -1,196 +0,0 @@ -import type { PR } from "./types.ts"; - -export interface Pair { - comment: string; - code: string; - file: string; - lineNumber: number; -} - -export interface Reply { - file: string; - lineNumber: number; - comment?: string; - botCommentId?: number; - replyText: string; - replyId?: number; - alreadyProcessed?: boolean; -} - -export interface Location { - file: string; - lineNumber: number; - comment: string; -} - -export interface ReviewData { - pairs: Pair[]; - hasPairs: boolean; - pairsText: string; - previousFindings: { file: string; lineNumber: number }[]; - dismissedReplies: Reply[]; - repliesForClassification: Reply[]; - hasRepliesToClassify: boolean; - repliesText: string; -} - -export function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null; -} - -export function stringValue(record: Record, key: string): string | undefined { - const value = record[key]; - if (typeof value === "string") { - return value; - } - return undefined; -} - -export function numberValue(record: Record, key: string): number | undefined { - const value = record[key]; - if (typeof value === "number") { - return value; - } - return undefined; -} - -export function records(value: unknown): Record[] { - if (!Array.isArray(value)) { - return []; - } - return value.filter((entry) => isRecord(entry)); -} - -export function nonEmpty(value: string | undefined): value is string { - return value !== undefined && value.length > 0; -} - -export function userLogin(record: Record): string | undefined { - const { user } = record; - if (isRecord(user)) { - return stringValue(user, "login"); - } - return undefined; -} - -export function userType(record: Record): string | undefined { - const { user } = record; - if (isRecord(user)) { - return stringValue(user, "type"); - } - return undefined; -} - -function isBotReview(record: Record): boolean { - return ( - userLogin(record) === "github-actions[bot]" && - (stringValue(record, "body")?.includes("Redundant comment") ?? false) - ); -} - -export function commentPayload(value: unknown): Record[] { - if (!Array.isArray(value) || !value.every((entry) => isRecord(entry))) { - throw new Error("GitHub comments response was not an array"); - } - return value; -} - -export function pairsFor(pr: PR): Pair[] { - const pairs: Pair[] = []; - const lines = pr.added.filter((line) => !line.isTest); - for (let index = 0; index < lines.length - 1; index++) { - const current = lines[index].content.trim(); - const next = lines[index + 1].content.trim(); - if (current.startsWith("//") && !next.startsWith("//") && next.length > 0) { - pairs.push({ - comment: current, - code: next, - file: lines[index].file, - lineNumber: lines[index].lineNumber, - }); - } - } - return pairs; -} - -export function replyLocation( - reply: Record, - botCommentMap: Map, -): Location | undefined { - const parentId = numberValue(reply, "in_reply_to_id"); - if (parentId === undefined) { - return undefined; - } - return botCommentMap.get(parentId); -} - -interface CommentLocationEntry { - id: number; - location: Location; -} - -function commentLine(diffHunk: string): string { - const addedLine = diffHunk - .split("\n") - .filter((line) => line.startsWith("+")) - .pop(); - return addedLine?.replace(/^\+\s*/u, "").trim() ?? ""; -} - -function commentLocation(comment: Record): CommentLocationEntry | undefined { - if (!isBotReview(comment)) { - return undefined; - } - const id = numberValue(comment, "id"); - const path = stringValue(comment, "path"); - const lineNumber = numberValue(comment, "original_line") ?? numberValue(comment, "line"); - if (id === undefined || !nonEmpty(path) || lineNumber === undefined) { - return undefined; - } - return { - id, - location: { - file: path, - lineNumber, - comment: commentLine(stringValue(comment, "diff_hunk") ?? ""), - }, - }; -} - -export function commentLocations(comments: Record[]): { - botCommentMap: Map; - previousFindings: { file: string; lineNumber: number }[]; -} { - const entries = comments - .map((comment) => commentLocation(comment)) - .filter((entry): entry is CommentLocationEntry => entry !== undefined); - const botCommentMap = new Map(); - for (const entry of entries) { - botCommentMap.set(entry.id, entry.location); - } - return { - botCommentMap, - previousFindings: entries.map(({ location }) => ({ - file: location.file, - lineNumber: location.lineNumber, - })), - }; -} - -export function formatPairs(pairs: Pair[], hasPairs: boolean): string { - if (!hasPairs) { - return ""; - } - return pairs - .map((pair, index) => `[${index}] COMMENT: ${pair.comment}\nCODE: ${pair.code}`) - .join("\n---\n"); -} - -export function formatReplies(replies: Reply[]): string { - return replies - .map( - (reply, index) => - `[${index}] FILE: ${reply.file}:${reply.lineNumber}\nREPLY: "${reply.replyText}"`, - ) - .join("\n---\n"); -} diff --git a/packages/code-review-agent/src/components.md b/packages/code-review-agent/src/components.md deleted file mode 100644 index 221e25355..000000000 --- a/packages/code-review-agent/src/components.md +++ /dev/null @@ -1,126 +0,0 @@ -Long-form documentation for the code-review agent's TypeScript components. - -Seven components gather or group the evidence a review reasons about: the pull -request and its diff, the checkout's readiness, the Oxlint sensor's diagnostics, the -repository's source inventory, and the review comments already on the pull -request. They read or transform evidence without writing a comment, pushing a -branch or changing a file. - -They are reserved: this package claims these seven names, so a repository under -review cannot supply its own `ReviewContext.ts` and have a review run it. The -Markdown components that compose them are declared by this package for the same -reason, and are documented by their own bodies rather than here. - -The evidence-gathering components run against the working directory. A review -of another checkout establishes that checkout first — with `` or -`` — and these report on whatever is current. - -## ReviewContext - -Builds the review's subject: which revisions are being compared, and what changed -between them. - -```mdx - -``` - -Reads the local Git checkout for the configured base and head revisions and binds -the pull-request metadata together with the changed file paths. This is normally -the first thing a review document does, because almost everything downstream is -scoped to the files it names. - -Where a pull request is available, the metadata comes from it; where one is not, -the revision range still describes the comparison, so a review of a local branch -works the same way. - -## Doctor - -Reports whether this checkout can actually be analyzed, before a review depends -on it. - -```mdx - -``` - -Probes for the pinned Oxlint sensor and its type-aware companion, decides whether -TypeScript analysis is available, and lists the files that would be analyzed. The -binding carries actionable setup guidance, so a document can show a person what -to install rather than failing with an absence. - -Use it to branch: a review that finds the sensor unavailable can report that -plainly instead of reporting a clean result it never measured. - -## OxlintDiagnostics - -Runs the pinned Oxlint sensor over selected files and returns what it found. - -```mdx - -``` - -The diagnostics are normalized, so a document reads one shape regardless of the -sensor's own output format. `typeAware` selects the type-aware pass, which needs -the TypeScript support `` reports on. - -It fails rather than returning nothing when the sensor crashes or emits output it -cannot use. A silent empty result would be indistinguishable from a clean run, -and a review that reported "no findings" because its sensor died is worse than -one that stopped. - -## ReviewDiagnostics - -Group normalized Oxlint diagnostics for review policies. - -```mdx - -``` - -Groups the sensor results by rule, applies the Doctor's import-noise filtering, -and computes categories, density and summary through the package's existing -`buildDiagnostics` helper. This transformation uses only its supplied values. - -## RepositoryInventory - -Counts what is in the repository, for a review that reasons about the whole tree -rather than a diff. - -```mdx - -``` - -Selects the repository's source paths and binds them with their file and line -totals. Repository analysis uses it for ratios and thresholds — how much of the -tree a signal covers, whether a count is large relative to what is there. - -## CommentReviewData - -Collects the review comments already on the pull request, and what was said back. - -```mdx - -``` - -Binds the review-comment pairs, the findings a previous run recorded, and the -replies grouped by the comment they answer. It is the input to deciding which of -a previous review's findings still stand — a review that could not see its own -earlier comments would repeat every one of them on each run. - -## CommentReviewState - -Decides what the pull request's review comments should become, given the -classified evidence. - -```mdx - -``` - -Takes the collected comment data together with the classification of it and binds -the checklist text, the findings still pending, and the replies that dismiss the -ones that are not. It decides; it does not post. What a document does with the -result — write a comment, render a report, do nothing — is the document's. diff --git a/packages/code-review-agent/src/components/CommentReviewData.ts b/packages/code-review-agent/src/components/CommentReviewData.ts deleted file mode 100644 index 738efc3bf..000000000 --- a/packages/code-review-agent/src/components/CommentReviewData.ts +++ /dev/null @@ -1,246 +0,0 @@ -import type { Operation } from "effection"; -import { fetch } from "@effectionx/fetch"; -import type { PR } from "../types.ts"; -import { env as runtimeEnv } from "@executablemd/runtime"; -import { - commentLocations, - commentPayload, - formatPairs, - formatReplies, - nonEmpty, - numberValue, - pairsFor, - records, - replyLocation, - stringValue, - type Location, - type Pair, - type Reply, - type ReviewData, - userLogin, - userType, -} from "../comment-review-data.ts"; - -export const props = { - type: "object", - properties: { pr: { type: "object" } }, - required: ["pr"], - additionalProperties: false, -}; - -const locationProperties = { - file: { type: "string" }, - lineNumber: { type: "number" }, -}; - -const replyProperties = { - ...locationProperties, - comment: { type: "string" }, - botCommentId: { type: "number" }, - replyText: { type: "string" }, - replyId: { type: "number" }, - alreadyProcessed: { type: "boolean" }, -}; - -export const returns = { - type: "object", - properties: { - pairs: { - type: "array", - items: { - type: "object", - properties: { - comment: { type: "string" }, - code: { type: "string" }, - ...locationProperties, - }, - required: ["comment", "code", "file", "lineNumber"], - additionalProperties: false, - }, - }, - hasPairs: { type: "boolean" }, - pairsText: { type: "string" }, - previousFindings: { - type: "array", - items: { - type: "object", - properties: locationProperties, - required: ["file", "lineNumber"], - additionalProperties: false, - }, - }, - dismissedReplies: { - type: "array", - items: { - type: "object", - properties: replyProperties, - required: ["file", "lineNumber", "replyText"], - additionalProperties: false, - }, - }, - repliesForClassification: { - type: "array", - items: { - type: "object", - properties: replyProperties, - required: ["file", "lineNumber", "replyText"], - additionalProperties: false, - }, - }, - hasRepliesToClassify: { type: "boolean" }, - repliesText: { type: "string" }, - }, - required: [ - "pairs", - "hasPairs", - "pairsText", - "previousFindings", - "dismissedReplies", - "repliesForClassification", - "hasRepliesToClassify", - "repliesText", - ], - additionalProperties: false, -}; - -interface CommentReviewProps { - pr: PR; -} - -function* githubApi(): Operation { - const token = yield* runtimeEnv("GITHUB_TOKEN"); - const repository = yield* runtimeEnv("GITHUB_REPOSITORY"); - const number = yield* runtimeEnv("PR_NUMBER"); - if (!nonEmpty(token) || !nonEmpty(repository) || !nonEmpty(number)) { - return undefined; - } - const [owner, name] = repository.split("/"); - if (nonEmpty(owner) && nonEmpty(name)) { - return `https://api.github.com/repos/${owner}/${name}`; - } - return undefined; -} - -function* fetchReplyReactions(api: string, reply: Reply): Operation { - try { - const reactions = records( - yield* fetch(`${api}/pulls/comments/${reply.replyId}/reactions`).expect().json(), - ); - return reactions.some( - (reaction) => userLogin(reaction) === "github-actions[bot]" && reaction.content === "+1", - ); - } catch { - return false; - } -} - -function* collectReplies( - api: string, - comments: Record[], - botCommentMap: Map, -): Operation<{ dismissed: Reply[]; pending: Reply[] }> { - const dismissed: Reply[] = []; - const pending: Reply[] = []; - for (const reply of comments) { - const entry = yield* replyForClassification(api, reply, botCommentMap); - if (entry !== undefined) { - if (entry.alreadyProcessed === true) { - dismissed.push(entry); - } else { - pending.push(entry); - } - } - } - return { dismissed, pending }; -} - -function* replyForClassification( - api: string, - reply: Record, - botCommentMap: Map, -): Operation { - const parentId = numberValue(reply, "in_reply_to_id"); - const location = replyLocation(reply, botCommentMap); - const replyText = stringValue(reply, "body"); - const replyId = numberValue(reply, "id"); - if ( - location === undefined || - userType(reply) === "Bot" || - replyText === undefined || - replyId === undefined - ) { - return undefined; - } - - const entry: Reply = { ...location, botCommentId: parentId, replyText, replyId }; - if (yield* fetchReplyReactions(api, entry)) { - return { ...entry, alreadyProcessed: true }; - } - return entry; -} - -interface ReviewDataParts { - pairs: Pair[]; - previousFindings: { file: string; lineNumber: number }[]; - dismissedReplies: Reply[]; - repliesForClassification: Reply[]; -} - -function reviewData({ - pairs, - previousFindings, - dismissedReplies, - repliesForClassification, -}: ReviewDataParts): ReviewData { - const hasPairs = pairs.length >= 3; - return { - pairs, - hasPairs, - pairsText: formatPairs(pairs, hasPairs), - previousFindings, - dismissedReplies, - repliesForClassification, - hasRepliesToClassify: repliesForClassification.length > 0, - repliesText: formatReplies(repliesForClassification), - }; -} - -function emptyReviewData(pairs: Pair[]): ReviewData { - return reviewData({ - pairs, - previousFindings: [], - dismissedReplies: [], - repliesForClassification: [], - }); -} - -function* fetchedReviewData(api: string, number: string, pairs: Pair[]): Operation { - const comments = commentPayload( - yield* fetch(`${api}/pulls/${number}/comments?per_page=100`).expect().json(), - ); - const locations = commentLocations(comments); - const replies = yield* collectReplies(api, comments, locations.botCommentMap); - return reviewData({ - pairs, - previousFindings: locations.previousFindings, - dismissedReplies: replies.dismissed, - repliesForClassification: replies.pending, - }); -} - -function* reviewDataFor(pr: PR): Operation { - const pairs = pairsFor(pr); - const api = yield* githubApi(); - if (api === undefined) { - return emptyReviewData(pairs); - } - const number = yield* runtimeEnv("PR_NUMBER"); - if (!nonEmpty(number)) { - return emptyReviewData(pairs); - } - return yield* fetchedReviewData(api, number, pairs); -} - -export default function* CommentReviewData({ pr }: CommentReviewProps): Operation { - return yield* reviewDataFor(pr); -} diff --git a/packages/code-review-agent/src/components/CommentReviewState.ts b/packages/code-review-agent/src/components/CommentReviewState.ts deleted file mode 100644 index 08f6a0769..000000000 --- a/packages/code-review-agent/src/components/CommentReviewState.ts +++ /dev/null @@ -1,251 +0,0 @@ -import type { Operation } from "effection"; -import type { PR } from "../types.ts"; - -const locationProperties = { - file: { type: "string" }, - lineNumber: { type: "number" }, -}; - -const findingProperties = { - ...locationProperties, - comment: { type: "string" }, - code: { type: "string" }, -}; - -const replyProperties = { - ...locationProperties, - comment: { type: "string" }, - botCommentId: { type: "number" }, - replyText: { type: "string" }, - replyId: { type: "number" }, - alreadyProcessed: { type: "boolean" }, -}; - -export const props = { - type: "object", - properties: { - pr: { type: "object" }, - data: { type: "object" }, - classificationResult: { type: "string", default: "" }, - sampleResult: { type: "string", default: "" }, - }, - required: ["pr", "data", "classificationResult", "sampleResult"], - additionalProperties: false, -}; - -export const returns = { - type: "object", - properties: { - hasChecklist: { type: "boolean" }, - checklistMd: { type: "string" }, - hasFindings: { type: "boolean" }, - pendingFindings: { - type: "array", - items: { - type: "object", - properties: findingProperties, - required: ["comment", "code", "file", "lineNumber"], - additionalProperties: false, - }, - }, - newDismissReplies: { - type: "array", - items: { - type: "object", - properties: replyProperties, - required: ["file", "lineNumber", "replyText"], - additionalProperties: false, - }, - }, - }, - required: ["hasChecklist", "checklistMd", "hasFindings", "pendingFindings", "newDismissReplies"], - additionalProperties: false, -}; - -interface Pair { - comment: string; - code: string; - file: string; - lineNumber: number; -} - -interface Reply { - file: string; - lineNumber: number; - comment?: string; - botCommentId?: number; - replyText: string; - replyId?: number; - alreadyProcessed?: boolean; -} - -interface ReviewData { - pairs: Pair[]; - previousFindings: { file: string; lineNumber: number }[]; - dismissedReplies: Reply[]; - repliesForClassification: Reply[]; -} - -interface CommentReviewStateProps { - pr: PR; - data: ReviewData; - classificationResult: string; - sampleResult: string; -} - -interface CommentReviewStateValue { - hasChecklist: boolean; - checklistMd: string; - hasFindings: boolean; - pendingFindings: Pair[]; - newDismissReplies: Reply[]; -} - -type ChecklistItem = - | { status: "applied"; file: string; lineNumber: number; label: string } - | { status: "dismissed"; file: string; lineNumber: number; comment: string; label: string } - | { status: "pending"; file: string; lineNumber: number; comment: string }; - -const CLASSIFICATION_PATTERN = /\[(?\d+)\]\s*(?DISMISS|ACCEPT)/giu; -const REDUNDANT_PATTERN = /REDUNDANT\[(?\d+)\]/gu; - -function key(file: string, lineNumber: number): string { - return `${file}:${lineNumber}`; -} - -function matchedIndices(input: string, pattern: RegExp): number[] { - const indices: number[] = []; - for (const match of input.matchAll(pattern)) { - const index = Number(match.groups?.index); - if (Number.isInteger(index)) { - indices.push(index); - } - } - return indices; -} - -function dismissedIndices(input: string): number[] { - const indices: number[] = []; - for (const match of input.matchAll(CLASSIFICATION_PATTERN)) { - if (match.groups?.action?.toUpperCase() === "DISMISS") { - const index = Number(match.groups?.index); - if (Number.isInteger(index)) { - indices.push(index); - } - } - } - return indices; -} - -function dismissedRepliesFor(data: ReviewData, classificationResult: string): Reply[] { - const dismissedReplies = [...data.dismissedReplies]; - for (const index of dismissedIndices(classificationResult)) { - const reply = data.repliesForClassification[index]; - if (reply !== undefined) { - dismissedReplies.push(reply); - } - } - return dismissedReplies; -} - -function dismissedSetFor(replies: Reply[]): Set { - return new Set(replies.map((reply) => key(reply.file, reply.lineNumber))); -} - -function appliedFindingsFor( - data: ReviewData, - pr: PR, - dismissedSet: Set, -): { file: string; lineNumber: number }[] { - const addedLineSet = new Set(pr.added.map((line) => key(line.file, line.lineNumber))); - return data.previousFindings.filter((finding) => { - const findingKey = key(finding.file, finding.lineNumber); - return !addedLineSet.has(findingKey) && !dismissedSet.has(findingKey); - }); -} - -function pendingFindingsFor( - data: ReviewData, - sampleResult: string, - dismissedSet: Set, -): Pair[] { - return matchedIndices(sampleResult, REDUNDANT_PATTERN) - .map((index) => data.pairs[index]) - .filter( - (finding): finding is Pair => - finding !== undefined && !dismissedSet.has(key(finding.file, finding.lineNumber)), - ); -} - -function checklistItemsFor( - appliedFindings: { file: string; lineNumber: number }[], - dismissedReplies: Reply[], - pendingFindings: Pair[], -): ChecklistItem[] { - return [ - ...appliedFindings.map( - (finding): ChecklistItem => ({ - status: "applied", - file: finding.file, - lineNumber: finding.lineNumber, - label: "removed", - }), - ), - ...dismissedReplies.map( - (reply): ChecklistItem => ({ - status: "dismissed", - file: reply.file, - lineNumber: reply.lineNumber, - comment: reply.comment ?? "", - label: reply.replyText, - }), - ), - ...pendingFindings.map( - (finding): ChecklistItem => ({ - status: "pending", - file: finding.file, - lineNumber: finding.lineNumber, - comment: finding.comment, - }), - ), - ]; -} - -function checklistItemText(item: ChecklistItem): string { - let checked = "x"; - if (item.status === "pending") { - checked = " "; - } - if (item.status === "applied") { - return `- [${checked}] \`${item.file}:${item.lineNumber}\` (removed)`; - } - if (item.status === "dismissed") { - return `- [${checked}] \`${item.file}:${item.lineNumber}\` — \`${item.comment}\` (kept: "${item.label}")`; - } - return `- [${checked}] \`${item.file}:${item.lineNumber}\` — \`${item.comment}\``; -} - -function checklistMarkdown(items: ChecklistItem[]): string { - return items.map((item) => checklistItemText(item)).join("\n"); -} - -export default function* CommentReviewState({ - pr, - data, - classificationResult, - sampleResult, -}: CommentReviewStateProps): Operation { - const dismissedReplies = dismissedRepliesFor(data, classificationResult); - const dismissedSet = dismissedSetFor(dismissedReplies); - const appliedFindings = appliedFindingsFor(data, pr, dismissedSet); - const pendingFindings = pendingFindingsFor(data, sampleResult, dismissedSet); - const checklistItems = checklistItemsFor(appliedFindings, dismissedReplies, pendingFindings); - - return { - hasChecklist: checklistItems.length > 0, - checklistMd: checklistMarkdown(checklistItems), - hasFindings: pendingFindings.length > 0, - pendingFindings, - newDismissReplies: dismissedReplies.filter((reply) => reply.alreadyProcessed !== true), - }; -} diff --git a/packages/code-review-agent/src/components/Doctor.ts b/packages/code-review-agent/src/components/Doctor.ts deleted file mode 100644 index e3e9b7427..000000000 --- a/packages/code-review-agent/src/components/Doctor.ts +++ /dev/null @@ -1,248 +0,0 @@ -import { type DoctorResult, type OxlintDiagnostic } from "../types.ts"; -import { - type DoctorEnvironment, - buildDoctorResult, - isOxlintCrash, - summarizeDoctorProbe, -} from "../doctor.ts"; -import { normalizeOxlintOutput } from "../parse-oxlint.ts"; -import { exec, glob, readTextFile, stat } from "@executablemd/runtime"; -import type { Operation } from "effection"; - -const OXLINT_PATH = ".reviews/.oxlint/oxlint"; -const TSGOLINT_PATH = ".reviews/.oxlint/tsgolint"; -const OXLINT_CONFIG = ".reviews/.oxlintrc.json"; -const NATIVE_SPECIFIER_PATTERN = /^\s*(?:import|export)\s.*?from\s+["'](?jsr:|npm:)/u; - -export const props = { - type: "object", - properties: { - pr: { type: "object" }, - tsconfigPath: { - type: "string", - default: ".reviews/tsconfig.oxlint.json", - }, - }, - required: ["pr"], - additionalProperties: false, -}; - -export const returns = { - type: "object", - properties: { - oxlintInstalled: { type: "boolean" }, - oxlintVersion: { type: "string" }, - tsgolintInstalled: { type: "boolean" }, - tsgolintVersion: { type: "string" }, - tsconfigExists: { type: "boolean" }, - nodeModulesExists: { type: "boolean" }, - typeAwareAvailable: { type: "boolean" }, - filesAnalyzed: { type: "number" }, - filesSkipped: { type: "number" }, - importErrors: { type: "number" }, - availableRuleIds: { type: "array", items: { type: "string" } }, - bloatRulesAvailable: { type: "array", items: { type: "string" } }, - bloatRulesMissing: { type: "array", items: { type: "string" } }, - recommendation: { type: "string" }, - nativeSpecifiers: { - type: "object", - properties: { - count: { type: "number" }, - files: { type: "array", items: { type: "string" } }, - jsr: { type: "number" }, - npm: { type: "number" }, - }, - required: ["count", "files", "jsr", "npm"], - additionalProperties: false, - }, - }, - required: [ - "oxlintInstalled", - "oxlintVersion", - "tsgolintInstalled", - "tsgolintVersion", - "tsconfigExists", - "nodeModulesExists", - "typeAwareAvailable", - "filesAnalyzed", - "filesSkipped", - "importErrors", - "availableRuleIds", - "bloatRulesAvailable", - "bloatRulesMissing", - "recommendation", - "nativeSpecifiers", - ], - additionalProperties: false, -}; - -interface DoctorProps { - pr: object; - tsconfigPath?: string; -} - -function* version(command: string[]): Operation { - const result = yield* exec({ command }); - if (result.exitCode === 0) { - return result.stdout.trim(); - } - return ""; -} - -interface NativeSpecifierCounts { - jsr: number; - npm: number; -} - -function nativeSpecifierCounts(source: string): NativeSpecifierCounts { - const counts = { jsr: 0, npm: 0 }; - for (const line of source.split(/\r?\n/u)) { - const scheme = NATIVE_SPECIFIER_PATTERN.exec(line)?.groups?.scheme; - if (scheme === "jsr:") { - counts.jsr++; - } - if (scheme === "npm:") { - counts.npm++; - } - } - return counts; -} - -interface NativeSpecifierFile { - path: string; - counts: NativeSpecifierCounts; -} - -function* nativeSpecifierFile(entry: { - isFile: boolean; - path: string; -}): Operation { - if (!entry.isFile) { - return undefined; - } - return { path: entry.path, counts: nativeSpecifierCounts(yield* readTextFile(entry.path)) }; -} - -function nativeSpecifierValue(files: NativeSpecifierFile[]): DoctorResult["nativeSpecifiers"] { - let jsr = 0; - let npm = 0; - const paths: string[] = []; - for (const file of files) { - jsr += file.counts.jsr; - npm += file.counts.npm; - if (file.counts.jsr + file.counts.npm > 0) { - paths.push(file.path); - } - } - return { count: jsr + npm, files: [...new Set(paths)], jsr, npm }; -} - -function* nativeSpecifierFiles( - entries: { isFile: boolean; path: string }[], -): Operation { - const files: NativeSpecifierFile[] = []; - for (const entry of entries) { - const file = yield* nativeSpecifierFile(entry); - if (file !== undefined) { - files.push(file); - } - } - return files; -} - -function* nativeSpecifierSummary(): Operation { - const entries = yield* glob({ - root: ".", - patterns: ["packages/**/*.ts", "durable-effects/**/*.ts"], - exclude: ["**/*.test.ts", "**/*.spec.ts", "**/node_modules/**"], - }); - return nativeSpecifierValue(yield* nativeSpecifierFiles(entries)); -} - -function probeCommand(tsconfigPath: string): string[] { - return [ - OXLINT_PATH, - "--config", - OXLINT_CONFIG, - "--type-aware", - "--tsconfig", - tsconfigPath, - "--format", - "json", - ]; -} - -function probeFailure(exitCode: number, stdout: string, stderr: string): Error | undefined { - if (exitCode > 1 && !isOxlintCrash(stderr)) { - return new Error(stderr || `Oxlint probe failed with exit code ${exitCode}`); - } - if (stdout.trim().length === 0 && exitCode !== 0 && !isOxlintCrash(stderr)) { - return new Error(stderr || "Oxlint probe returned no JSON output"); - } - return undefined; -} - -function probeDiagnostics(stdout: string): OxlintDiagnostic[] { - if (stdout.trim().length === 0) { - return []; - } - return normalizeOxlintOutput(stdout); -} - -function* probeTypeAware(canProbe: boolean, tsconfigPath: string) { - if (!canProbe) { - return summarizeDoctorProbe({ diagnostics: [], stderr: "", exitCode: 2 }); - } - - const result = yield* exec({ - command: probeCommand(tsconfigPath), - env: { OXLINT_TSGOLINT_PATH: TSGOLINT_PATH }, - }); - const failure = probeFailure(result.exitCode, result.stdout, result.stderr); - if (failure !== undefined) { - throw failure; - } - const diagnostics = probeDiagnostics(result.stdout); - return summarizeDoctorProbe({ - diagnostics, - stderr: result.stderr, - exitCode: result.exitCode, - }); -} - -function* toolVersion(path: string): Operation { - const result = yield* stat(path); - if (!result.isFile) { - return ""; - } - return yield* version([path, "--version"]); -} - -function* doctorEnvironment(tsconfigPath: string): Operation { - const oxlint = yield* stat(OXLINT_PATH); - const tsgolint = yield* stat(TSGOLINT_PATH); - const tsconfig = yield* stat(tsconfigPath); - const nodeModules = yield* stat("node_modules"); - return { - oxlintInstalled: oxlint.isFile, - oxlintVersion: yield* toolVersion(OXLINT_PATH), - tsgolintInstalled: tsgolint.isFile, - tsgolintVersion: yield* toolVersion(TSGOLINT_PATH), - tsconfigExists: tsconfig.isFile, - nodeModulesExists: nodeModules.isDirectory, - nativeSpecifiers: yield* nativeSpecifierSummary(), - }; -} - -export default function* Doctor({ - tsconfigPath = ".reviews/tsconfig.oxlint.json", -}: DoctorProps): Operation { - const environment = yield* doctorEnvironment(tsconfigPath); - const canProbe = - environment.oxlintInstalled && - environment.tsgolintInstalled && - environment.tsconfigExists && - environment.nodeModulesExists; - const probe = yield* probeTypeAware(canProbe, tsconfigPath); - return buildDoctorResult(environment, probe); -} diff --git a/packages/code-review-agent/src/components/OxlintDiagnostics.ts b/packages/code-review-agent/src/components/OxlintDiagnostics.ts deleted file mode 100644 index eb11abc64..000000000 --- a/packages/code-review-agent/src/components/OxlintDiagnostics.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { type OxlintDiagnostic } from "../types.ts"; -import { normalizeOxlintOutput } from "../parse-oxlint.ts"; -import { exec } from "@executablemd/runtime"; -import type { Operation } from "effection"; - -const OXLINT_PATH = ".reviews/.oxlint/oxlint"; -const OXLINT_CONFIG = ".reviews/.oxlintrc.json"; -const OXLINT_CRASH_PATTERN = /panic|oom|out of memory|fatal|segmentation fault/iu; - -export const props = { - type: "object", - properties: { - files: { type: "array", items: { type: "string" } }, - typeAware: { type: "boolean", default: false }, - tsconfigPath: { type: "string", default: ".reviews/tsconfig.oxlint.json" }, - }, - required: ["files"], - additionalProperties: false, -}; - -export const returns = { - type: "array", - items: { - type: "object", - properties: { - message: { type: "string" }, - ruleId: { type: "string" }, - severity: { type: "string" }, - file: { type: "string" }, - line: { type: "number" }, - column: { type: "number" }, - }, - required: ["message", "ruleId", "severity", "file", "line", "column"], - additionalProperties: false, - }, -}; - -interface OxlintDiagnosticsProps { - files: string[]; - typeAware?: boolean; - tsconfigPath?: string; -} - -interface OxlintCommand { - command: string[]; - environment: Record; -} - -function oxlintCommand(files: string[], typeAware: boolean, tsconfigPath: string): OxlintCommand { - const command = [OXLINT_PATH, "--config", OXLINT_CONFIG, "--format", "json"]; - const environment: Record = {}; - if (typeAware) { - command.push("--type-aware", "--tsconfig", tsconfigPath); - environment.OXLINT_TSGOLINT_PATH = ".reviews/.oxlint/tsgolint"; - } - command.push(...files); - return { command, environment }; -} - -function oxlintFailure(exitCode: number, stdout: string, stderr: string): Error | undefined { - if (exitCode > 1) { - return new Error(stderr || `Oxlint failed with exit code ${exitCode}`); - } - if (OXLINT_CRASH_PATTERN.test(stderr)) { - return new Error(stderr); - } - if (stdout.trim().length === 0) { - return new Error(stderr || "Oxlint returned no JSON output"); - } - return undefined; -} - -function* runOxlint( - files: string[], - typeAware: boolean, - tsconfigPath: string, -): Operation { - if (files.length === 0) { - return []; - } - - const { command, environment } = oxlintCommand(files, typeAware, tsconfigPath); - const result = yield* exec({ command, env: environment }); - const failure = oxlintFailure(result.exitCode, result.stdout, result.stderr); - if (failure !== undefined) { - throw failure; - } - return normalizeOxlintOutput(result.stdout, files); -} - -export default function* OxlintDiagnostics({ - files, - typeAware = false, - tsconfigPath = ".reviews/tsconfig.oxlint.json", -}: OxlintDiagnosticsProps): Operation { - return yield* runOxlint(files, typeAware, tsconfigPath); -} diff --git a/packages/code-review-agent/src/components/RepositoryInventory.ts b/packages/code-review-agent/src/components/RepositoryInventory.ts deleted file mode 100644 index c60d7448f..000000000 --- a/packages/code-review-agent/src/components/RepositoryInventory.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { glob, readTextFile } from "@executablemd/runtime"; -import type { Operation } from "effection"; - -export const props = { - type: "object", - properties: {}, - additionalProperties: false, -}; - -export const returns = { - type: "object", - properties: { - fileList: { type: "array", items: { type: "string" } }, - fileCount: { type: "number" }, - lineCount: { type: "number" }, - }, - required: ["fileList", "fileCount", "lineCount"], - additionalProperties: false, -}; - -interface RepositoryInventoryValue { - fileList: string[]; - fileCount: number; - lineCount: number; -} - -function lineCount(source: string): number { - if (source.length === 0) { - return 0; - } - const lines = source.split(/\r?\n/u); - if (source.endsWith("\n")) { - return lines.length - 1; - } - return lines.length; -} - -function filePaths(entries: { isFile: boolean; path: string }[]): string[] { - const paths = entries.filter((entry) => entry.isFile).map((entry) => entry.path); - const ordered: string[] = []; - for (const path of paths) { - const index = ordered.findIndex((candidate) => candidate > path); - if (index === -1) { - ordered.push(path); - } else { - ordered.splice(index, 0, path); - } - } - return ordered; -} - -function* totalLineCount(paths: string[]): Operation { - let total = 0; - for (const path of paths) { - total += lineCount(yield* readTextFile(path)); - } - return total; -} - -export default function* RepositoryInventory( - _props: Record, -): Operation { - const entries = yield* glob({ - root: ".", - patterns: ["durable-effects/**/*.ts", "packages/**/*.ts"], - exclude: ["**/*.test.ts", "**/*.spec.ts", "**/node_modules/**"], - }); - const fileList = filePaths(entries); - const totalLines = yield* totalLineCount(fileList); - return { fileList, fileCount: fileList.length, lineCount: totalLines }; -} diff --git a/packages/code-review-agent/src/components/ReviewContext.ts b/packages/code-review-agent/src/components/ReviewContext.ts deleted file mode 100644 index 3b9dcaabc..000000000 --- a/packages/code-review-agent/src/components/ReviewContext.ts +++ /dev/null @@ -1,98 +0,0 @@ -import type { PR } from "../types.ts"; -import { parseDiff } from "../parse-diff.ts"; -import { exec, env as runtimeEnv } from "@executablemd/runtime"; -import { fetch } from "@effectionx/fetch"; -import type { Operation } from "effection"; - -export const props = { - type: "object", - properties: {}, - additionalProperties: false, -}; - -export const returns = { - type: "object", - properties: { - pr: { type: "object" }, - changedFilePaths: { type: "array", items: { type: "string" } }, - }, - required: ["pr", "changedFilePaths"], - additionalProperties: false, -}; - -interface PullRequestResponse { - body?: unknown; -} - -interface ReviewContextValue { - pr: Omit & { directories: string[] }; - changedFilePaths: string[]; -} - -interface ReviewInputs { - base: string; - head: string; - title: string; - number: string; - repository: string; - localBody: string; -} - -function isPullRequestResponse(value: unknown): value is PullRequestResponse { - return typeof value === "object" && value !== null; -} - -function* reviewInputs(): Operation { - return { - base: (yield* runtimeEnv("BASE_SHA")) ?? "HEAD~1", - head: (yield* runtimeEnv("HEAD_SHA")) ?? "HEAD", - title: (yield* runtimeEnv("PR_TITLE")) ?? "", - number: (yield* runtimeEnv("PR_NUMBER")) ?? "", - repository: (yield* runtimeEnv("GITHUB_REPOSITORY")) ?? "", - localBody: (yield* runtimeEnv("PR_BODY")) ?? "", - }; -} - -function* gitOutput(command: string[], description: string): Operation { - const result = yield* exec({ command }); - if (result.exitCode !== 0) { - throw new Error(result.stderr || `${description} failed with exit code ${result.exitCode}`); - } - return result.stdout; -} - -function* pullBody(repository: string, number: string, fallback: string): Operation { - if (fallback || !repository || !number) { - return fallback; - } - - const payload: unknown = yield* fetch( - `https://api.github.com/repos/${repository}/pulls/${number}`, - ) - .expect() - .json(); - if (!isPullRequestResponse(payload)) { - return fallback; - } - if (typeof payload.body === "string") { - return payload.body; - } - return fallback; -} - -export default function* ReviewContext( - _props: Record, -): Operation { - const inputs = yield* reviewInputs(); - const range = `${inputs.base}...${inputs.head}`; - const diff = yield* gitOutput(["git", "diff", range], "git diff"); - const names = yield* gitOutput(["git", "diff", "--name-status", range], "git diff --name-status"); - const body = yield* pullBody(inputs.repository, inputs.number, inputs.localBody); - const parsed = parseDiff(diff, names, { - title: inputs.title, - body, - number: inputs.number, - }); - const pr = { ...parsed, directories: [...parsed.directories] }; - return { pr, changedFilePaths: pr.files.map((file) => file.path) }; -} diff --git a/packages/code-review-agent/src/components/ReviewDiagnostics.ts b/packages/code-review-agent/src/components/ReviewDiagnostics.ts deleted file mode 100644 index 84c33b425..000000000 --- a/packages/code-review-agent/src/components/ReviewDiagnostics.ts +++ /dev/null @@ -1,68 +0,0 @@ -import type { Operation } from "effection"; -import type { Diagnostics, DoctorResult, OxlintDiagnostic, PR } from "../types.ts"; -import { buildDiagnostics } from "../parse-diagnostics.ts"; -import { returns as rawDiagnosticsSchema } from "./OxlintDiagnostics.ts"; -import { returns as doctorSchema } from "./Doctor.ts"; - -export const props = { - type: "object", - properties: { - raw: rawDiagnosticsSchema, - pr: { type: "object" }, - doctor: doctorSchema, - }, - required: ["raw", "pr", "doctor"], - additionalProperties: false, -}; - -const groupSchema = { - type: "object", - properties: { - ruleId: { type: "string" }, - count: { type: "number" }, - files: { type: "array", items: { type: "string" } }, - instances: rawDiagnosticsSchema, - }, - required: ["ruleId", "count", "files", "instances"], - additionalProperties: false, -}; -const groupsSchema = { type: "array", items: groupSchema }; - -export const returns = { - type: "object", - properties: { - groups: groupsSchema, - total: { type: "number" }, - fileCount: { type: "number" }, - ruleCount: { type: "number" }, - byCategory: { - type: "object", - properties: { - structural: groupsSchema, - verbosity: groupsSchema, - typeAware: groupsSchema, - other: groupsSchema, - }, - required: ["structural", "verbosity", "typeAware", "other"], - additionalProperties: false, - }, - summary: { type: "string" }, - density: { type: "number" }, - }, - required: ["groups", "total", "fileCount", "ruleCount", "byCategory", "summary", "density"], - additionalProperties: false, -}; - -interface ReviewDiagnosticsProps { - raw: OxlintDiagnostic[]; - pr: PR; - doctor: DoctorResult; -} - -export default function* ReviewDiagnostics({ - raw, - pr, - doctor, -}: ReviewDiagnosticsProps): Operation { - return buildDiagnostics(raw, pr, doctor); -} diff --git a/packages/code-review-agent/src/doctor.ts b/packages/code-review-agent/src/doctor.ts deleted file mode 100644 index b9e351639..000000000 --- a/packages/code-review-agent/src/doctor.ts +++ /dev/null @@ -1,120 +0,0 @@ -import type { DoctorResult, OxlintDiagnostic } from "./types.ts"; -import { SENSOR_RULES, TYPE_AWARE_RULES } from "./categories.ts"; - -const TYPE_AWARE_RULE_SET = new Set(TYPE_AWARE_RULES); -const IMPORT_NOISE_RATIO = 0.3; -const OXLINT_CRASH_PATTERN = /panic|oom|out of memory|fatal|segmentation fault/iu; - -export interface DoctorProbeSummary { - typeAwareAvailable: boolean; - filesAnalyzed: number; - filesSkipped: number; - importErrors: number; - availableRuleIds: string[]; - bloatRulesAvailable: string[]; - bloatRulesMissing: string[]; - recommendation: DoctorResult["recommendation"]; -} - -export interface DoctorProbeInput { - diagnostics: readonly OxlintDiagnostic[]; - stderr: string; - exitCode: number; -} - -function bloatRulesFor(typeAwareAvailable: boolean): { - available: string[]; - missing: string[]; -} { - if (typeAwareAvailable) { - return { available: [...SENSOR_RULES], missing: [] }; - } - return { - available: SENSOR_RULES.filter((rule) => !TYPE_AWARE_RULE_SET.has(rule)), - missing: [...TYPE_AWARE_RULES], - }; -} - -function recommendationFor( - typeAwareAvailable: boolean, - ratio: number, -): DoctorResult["recommendation"] { - if (!typeAwareAvailable) { - return "syntax-only"; - } - if (ratio < IMPORT_NOISE_RATIO) { - return "type-aware"; - } - return "type-aware-filtered"; -} - -function noiseRatio(total: number, importNoise: number): number { - if (total === 0) { - return 0; - } - return importNoise / total; -} - -function availableRuleIds(diagnostics: readonly OxlintDiagnostic[]): string[] { - return [...new Set(diagnostics.map((diagnostic) => diagnostic.ruleId).filter(Boolean))].sort(); -} - -function isImportNoise(diagnostic: OxlintDiagnostic): boolean { - return ( - diagnostic.message.includes("Cannot find module") || - diagnostic.message.includes("cannot find") || - diagnostic.ruleId.includes("import") - ); -} - -export function isOxlintCrash(stderr: string): boolean { - return OXLINT_CRASH_PATTERN.test(stderr); -} - -/** Summarize a bounded type-aware probe without retaining its raw output. */ -export function summarizeDoctorProbe(input: DoctorProbeInput): DoctorProbeSummary { - const importNoise = input.diagnostics.filter(isImportNoise); - const files = new Set(input.diagnostics.map((diagnostic) => diagnostic.file).filter(Boolean)); - const skippedFiles = new Set(importNoise.map((diagnostic) => diagnostic.file).filter(Boolean)); - const available = input.exitCode <= 1 && !isOxlintCrash(input.stderr); - const ratio = noiseRatio(input.diagnostics.length, importNoise.length); - const rules = bloatRulesFor(available); - - return { - typeAwareAvailable: available, - filesAnalyzed: files.size, - filesSkipped: skippedFiles.size, - importErrors: importNoise.length, - availableRuleIds: availableRuleIds(input.diagnostics), - bloatRulesAvailable: rules.available, - bloatRulesMissing: rules.missing, - recommendation: recommendationFor(available, ratio), - }; -} - -export interface DoctorEnvironment { - oxlintInstalled: boolean; - oxlintVersion: string; - tsgolintInstalled: boolean; - tsgolintVersion: string; - tsconfigExists: boolean; - nodeModulesExists: boolean; - nativeSpecifiers: DoctorResult["nativeSpecifiers"]; -} - -/** Construct the durable Doctor value from bounded environment observations. */ -export function buildDoctorResult( - environment: DoctorEnvironment, - probe: DoctorProbeSummary, -): DoctorResult { - return { - ...environment, - ...probe, - nativeSpecifiers: { - count: environment.nativeSpecifiers.count, - files: [...environment.nativeSpecifiers.files], - jsr: environment.nativeSpecifiers.jsr, - npm: environment.nativeSpecifiers.npm, - }, - }; -} diff --git a/packages/code-review-agent/src/documents/components/AbstractionNames.md b/packages/code-review-agent/src/documents/components/AbstractionNames.md deleted file mode 100644 index ddd47703c..000000000 --- a/packages/code-review-agent/src/documents/components/AbstractionNames.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -props: - type: object - properties: - pr: - type: object - pattern: - type: string - default: "factory|abstract|base|provider|strategy|adapter|helper|util" - severity: - type: string - default: warning - message: - type: string - default: "New abstraction files: {names}. Verify 3+ consumers." - required: [pr] - additionalProperties: false -description: >- - Flag new files whose names suggest a speculative abstraction. `` reports created TypeScript files named like factories, adapters or - helpers, so a reviewer can ask what consumes them. ---- - -```ts eval -const re = new RegExp(props.pattern, "i"); -const suspicious = props.pr.created - .filter(f => f.path.endsWith(".ts") && !f.isTest && !f.isTypeDeclaration) - .filter(f => re.test(f.path)); -const triggered = suspicious.length > 0; -const resolvedMessage = props.message.replace( - "{names}", suspicious.map(f => f.path).join(", ") -); -``` - - diff --git a/packages/code-review-agent/src/documents/components/CleanupIssues.md b/packages/code-review-agent/src/documents/components/CleanupIssues.md deleted file mode 100644 index 8f0290820..000000000 --- a/packages/code-review-agent/src/documents/components/CleanupIssues.md +++ /dev/null @@ -1,159 +0,0 @@ ---- -props: - type: object - properties: - cleanupAnalysis: - type: object - diagnostics: - type: object - required: [cleanupAnalysis, diagnostics] - additionalProperties: false -description: >- - File and retire GitHub issues for the repository's worst cleanup clusters. - `` keeps one - labelled issue per file in the top five and closes the ones that dropped out. It - does nothing without `GITHUB_TOKEN` and `GITHUB_REPOSITORY`. ---- - -```ts persist eval -import { env as runtimeEnv } from "@executablemd/runtime"; - -function* githubConfiguration() { - const token = yield* runtimeEnv("GITHUB_TOKEN"); - const repo = yield* runtimeEnv("GITHUB_REPOSITORY"); - if (!token || !repo) { - return undefined; - } - const [owner, repoName] = repo.split("/"); - return { api: `https://api.github.com/repos/${owner}/${repoName}` }; -} - -const github = yield* githubConfiguration(); -if (!github) { - return ""; -} - -const { api } = github; - -const LABEL = "cleanup"; -const TOP_N = 5; - -// 1. Ensure label exists -const labelResponse = yield* fetch(`${api}/labels/${LABEL}`); -if (labelResponse.status === 404) { - yield* fetch(`${api}/labels`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - name: LABEL, - description: "Auto-generated cleanup finding from repo analysis", - color: "d4c5f9", - }), - }).expect(); -} - -// 2. Fetch all open issues with cleanup label -const existingIssues = []; -let page = 1; -while (true) { - const batch = yield* fetch( - `${api}/issues?labels=${LABEL}&state=open&per_page=100&page=${page}`, - ).expect().json(); - - if (!Array.isArray(batch) || batch.length === 0) break; - existingIssues.push(...batch); - if (batch.length < 100) break; - page++; -} - -// 3. Build marker → issue map -const markerRe = //; -const issuesByFile = new Map(); -for (const issue of existingIssues) { - const match = issue.body ? issue.body.match(markerRe) : null; - if (match) { - issuesByFile.set(match[1], issue); - } -} - -// 4. Process top 5 clusters -const topClusters = props.cleanupAnalysis.fileClusters.slice(0, TOP_N); -const topFiles = new Set(topClusters.map(c => c.file)); - -let created = 0; -let updated = 0; -let closed = 0; - -for (const cluster of topClusters) { - const marker = ``; - const title = `cleanup(${cluster.kind}): ${cluster.file} — ${cluster.totalViolations} violations, ${cluster.coOccurrence} rules`; - - const bodyLines = [ - marker, - "", - "## Cleanup: `" + cluster.file + "`", - "", - "| Metric | Value |", - "|---|---|", - "| Score | " + cluster.score + " |", - "| Kind | " + cluster.kind + " |", - "| Violations | " + cluster.totalViolations + " |", - "| Co-occurring rules | " + cluster.coOccurrence + " |", - "| Structural | " + cluster.categories.structural + " |", - "| Verbosity | " + cluster.categories.verbosity + " |", - "| Type-aware | " + cluster.categories.typeAware + " |", - "| Other | " + cluster.categories.other + " |", - "", - "**Top rules:** " + cluster.ruleIds.slice(0, 15).join(", ") + (cluster.ruleIds.length > 15 ? " (+" + (cluster.ruleIds.length - 15) + " more)" : ""), - "", - "---", - "Generated by executable.md repo analysis. Last updated: " + new Date().toISOString() + "", - ]; - const body = bodyLines.join("\n"); - - const existing = issuesByFile.get(cluster.file); - - if (existing) { - yield* fetch(api + "/issues/" + existing.number, { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ title, body }), - }).expect(); - updated++; - } else { - yield* fetch(api + "/issues", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - title, - body, - labels: [LABEL], - }), - }).expect(); - created++; - } -} - -// 5. Close issues whose files are no longer in top 5 -for (const [file, issue] of issuesByFile.entries()) { - if (!topFiles.has(file)) { - yield* fetch(api + "/issues/" + issue.number + "/comments", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - body: "Resolved — file no longer in top-5 cleanup targets. Closing automatically.", - }), - }).expect(); - - yield* fetch(api + "/issues/" + issue.number, { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ state: "closed" }), - }).expect(); - closed++; - } -} - -const summary = "Cleanup issues: created " + created + ", updated " + updated + ", closed " + closed; -return summary; -``` diff --git a/packages/code-review-agent/src/documents/components/CommentReview.md b/packages/code-review-agent/src/documents/components/CommentReview.md deleted file mode 100644 index 682146bb2..000000000 --- a/packages/code-review-agent/src/documents/components/CommentReview.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -props: - type: object - properties: - pr: - type: object - required: [pr] - additionalProperties: false -description: >- - Review the pull request's code comments for ones that only restate the code. - `` asks the model in scope which are redundant, reads - replies to earlier suggestions, and renders what remains outstanding. When findings - remain and the required GitHub environment is present, it also reconciles inline - removal suggestions on the pull request. ---- - - - - - - - - -For each reply to an automated code review suggestion, classify the -user's intent. They are replying to a suggestion to remove a redundant -code comment. - -DISMISS — the user wants to keep the comment (any reason) -ACCEPT — the user agrees the comment should be removed - -Format: [index] DISMISS or [index] ACCEPT - -{reviewData.repliesText} - - - - - - - - - - - -Review these comment/code pairs. List ONLY obvious/redundant ones -where the comment restates what the code does. - -Format each finding as: REDUNDANT[index]: comment text - -If none are obvious: "No obvious comments found." - -{reviewData.pairsText} - - - - - - - - - - - - - - - - -{state.checklistMd} - - diff --git a/packages/code-review-agent/src/documents/components/ConfigSourceMix.md b/packages/code-review-agent/src/documents/components/ConfigSourceMix.md deleted file mode 100644 index d043afb69..000000000 --- a/packages/code-review-agent/src/documents/components/ConfigSourceMix.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -props: - type: object - properties: - pr: - type: object - minFiles: - type: number - default: 5 - severity: - type: string - default: warning - message: - type: string - default: "PR mixes config and source changes." - required: [pr] - additionalProperties: false -description: >- - Flag a change that mixes configuration and source edits. `` reports when both appear in a pull request larger than `minFiles`. ---- - -```ts eval -const hasConfig = props.pr.files.some(f => f.isConfig); -const hasSource = props.pr.files.some(f => - !f.isConfig && !f.isTest && !f.isTypeDeclaration -); -const triggered = hasConfig && hasSource && props.pr.stats.totalFiles > props.minFiles; -``` - - diff --git a/packages/code-review-agent/src/documents/components/DeepInfraProvider.md b/packages/code-review-agent/src/documents/components/DeepInfraProvider.md deleted file mode 100644 index 870855799..000000000 --- a/packages/code-review-agent/src/documents/components/DeepInfraProvider.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -props: - type: object - properties: - model: - type: string - required: [model] - additionalProperties: false -description: >- - Answer this document's `` calls from DeepInfra. `…` handles samples that name that model - and samples that name none, and passes the rest outward. It requires - `DEEPINFRA_TOKEN`. -context: >- - The Markdown whose samples this provider answers. ---- - -```ts persist eval -import { env as runtimeEnv } from "@executablemd/runtime"; - -yield* Sample.around({ - *sample([context], next) { - if (context.model !== undefined && context.model !== props.model) { - return yield* next(context); - } - - const messages = []; - if (context.system) { - messages.push({ role: "system", content: context.system }); - } - messages.push({ role: "user", content: context.content }); - const token = yield* runtimeEnv("DEEPINFRA_TOKEN"); - if (!token) { - throw new Error("DeepInfraProvider requires DEEPINFRA_TOKEN"); - } - - const result = yield* fetch("https://api.deepinfra.com/v1/openai/chat/completions", { - method: "POST", - headers: { - "Content-Type": "application/json", - "Authorization": `Bearer ${token}`, - }, - body: JSON.stringify({ model: props.model, messages, temperature: 0, max_tokens: 4096 }), - }) - .expect() - .json(); - - const content = result.choices?.[0]?.message?.content; - if (typeof content !== "string") { - throw new Error("DeepInfra response did not contain model content"); - } - return content; - }, -}, { at: 'min' }); -``` - - diff --git a/packages/code-review-agent/src/documents/components/DescriptionCheck.md b/packages/code-review-agent/src/documents/components/DescriptionCheck.md deleted file mode 100644 index 0398a8867..000000000 --- a/packages/code-review-agent/src/documents/components/DescriptionCheck.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -props: - type: object - properties: - pr: - type: object - minLength: - type: number - default: 50 - severity: - type: string - default: error - message: - type: string - default: "PR description must explain what and why." - required: [pr] - additionalProperties: false -description: >- - Require a pull-request description that explains the change. `` reports a body shorter than `minLength`. ---- - - diff --git a/packages/code-review-agent/src/documents/components/EnsureOxlint.md b/packages/code-review-agent/src/documents/components/EnsureOxlint.md deleted file mode 100644 index 84cef645f..000000000 --- a/packages/code-review-agent/src/documents/components/EnsureOxlint.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -props: - type: object - properties: - dir: - type: string - default: ".reviews/.oxlint" - additionalProperties: false -description: >- - Install the review's pinned Oxlint and tsgolint binaries. `` - downloads them into the sensor directory and verifies each against a known checksum, - so a review measures the version it expects. It fails on a platform it ships no - build for. ---- - -```ts eval -import { platform } from "@executablemd/runtime"; - -const OXLINT_TAG = "apps_v1.74.0"; -const TSGOLINT_VERSION = "0.25.0"; -const host = yield* platform(); -const target = { - oxlintOs: host.os === "darwin" ? "apple-darwin" : host.os === "linux" ? "unknown-linux-gnu" : "", - tsgolintOs: host.os === "darwin" ? "darwin" : host.os === "linux" ? "linux" : "", - oxlintArch: host.arch === "arm64" || host.arch === "aarch64" - ? "aarch64" - : host.arch === "x86_64" || host.arch === "amd64" || host.arch === "x64" - ? "x86_64" - : "", - tsgolintArch: host.arch === "arm64" || host.arch === "aarch64" - ? "arm64" - : host.arch === "x86_64" || host.arch === "amd64" || host.arch === "x64" - ? "x64" - : "", -}; -if (!target.oxlintOs || !target.oxlintArch) { - throw new Error(`EnsureOxlint: unsupported platform ${host.os}/${host.arch}`); -} - -const oxlintHashes = { - "aarch64-apple-darwin": "768a2d00e7e0a95cbf89837086f475d25dc1a1ba605b8831fb5a1db6d590a643", - "x86_64-apple-darwin": "04ae38d56ae4990ac96320c03f05f38cf1103b5fe64b08cd7203b87e767b45b4", - "aarch64-unknown-linux-gnu": "ced0d2433bda2b4295e1ab93b40c3f24224713c32a44e13abcda656590dba1cb", - "x86_64-unknown-linux-gnu": "fd3ed5d2dc55ab6f7a243583c69dd5da4ac97cd1f6e10321225ca6343c9451a9", -}; -const tsgolintHashes = { - "darwin-arm64": "3ad51d1b88070b491b81a4f5c6169148914127b9da65f8087823b25568431e1e", - "darwin-x64": "6b78caa20db383c055cead96724aeafab3cd277b00b1f95c6f56b4bfcf22fd60", - "linux-arm64": "20bcbab4bb37dd396102566740ee39e67d1e3d16d06096802180426c022bc414", - "linux-x64": "f6ea083842395d7439eadbbbf380f23793a1fa890fbda92013ee0f6033e75630", -}; -const oxlintKey = `${target.oxlintArch}-${target.oxlintOs}`; -const tsgolintKey = `${target.tsgolintOs}-${target.tsgolintArch}`; -const oxlintUrl = `https://github.com/oxc-project/oxc/releases/download/${OXLINT_TAG}/oxlint-${target.oxlintArch}-${target.oxlintOs}.tar.gz`; -const tsgolintUrl = `https://registry.npmjs.org/@oxlint-tsgolint/${target.tsgolintOs}-${target.tsgolintArch}/-/${target.tsgolintOs}-${target.tsgolintArch}-${TSGOLINT_VERSION}.tgz`; -const oxlintSha = oxlintHashes[oxlintKey]; -const tsgolintSha = tsgolintHashes[tsgolintKey]; -if (!oxlintSha || !tsgolintSha) { - throw new Error(`EnsureOxlint: no checksum for ${oxlintKey}/${tsgolintKey}`); -} -``` - -```bash silent exec -set -euo pipefail -DIR="{props.dir}" -mkdir -p "$DIR" - -verify() { - expected="$2" - actual="$(sha256sum "$1" 2>/dev/null || shasum -a 256 "$1")" - actual="${actual%% *}" - if [ "$actual" != "$expected" ]; then - echo "EnsureOxlint: checksum mismatch for $1" >&2 - exit 1 - fi -} - -if [ ! -x "$DIR/oxlint" ]; then - tmp="$(mktemp -d)" - trap 'rm -rf "$tmp"' EXIT - curl -fsSL -o "$tmp/oxlint.tar.gz" "{oxlintUrl}" - verify "$tmp/oxlint.tar.gz" "{oxlintSha}" - tar xz -C "$tmp" -f "$tmp/oxlint.tar.gz" - mv "$tmp/oxlint-{target.oxlintArch}-{target.oxlintOs}" "$DIR/oxlint" - chmod +x "$DIR/oxlint" - rm -rf "$tmp" -fi - -if [ ! -x "$DIR/tsgolint" ]; then - tmp="$(mktemp -d)" - trap 'rm -rf "$tmp"' EXIT - curl -fsSL -o "$tmp/tsgolint.tgz" "{tsgolintUrl}" - verify "$tmp/tsgolint.tgz" "{tsgolintSha}" - tar xz -C "$tmp" -f "$tmp/tsgolint.tgz" - mv "$tmp/package/tsgolint" "$DIR/tsgolint" - chmod +x "$DIR/tsgolint" - rm -rf "$tmp" -fi -``` diff --git a/packages/code-review-agent/src/documents/components/Finding.md b/packages/code-review-agent/src/documents/components/Finding.md deleted file mode 100644 index bf32165b7..000000000 --- a/packages/code-review-agent/src/documents/components/Finding.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -props: - type: object - properties: - when: - type: boolean - severity: - type: string - default: warning - message: - type: string - required: [when, message] - additionalProperties: false -description: >- - Report one review finding. `` renders a severity icon and the message when `when` holds, and nothing - when it does not — so a rule can be written unconditionally and stay quiet. ---- - -```ts eval -const icon = props.severity === "error" ? "\ud83d\udd34" : "\ud83d\udfe1"; -``` - - - -{icon} {props.message} - - diff --git a/packages/code-review-agent/src/documents/components/Format.md b/packages/code-review-agent/src/documents/components/Format.md deleted file mode 100644 index 79b22ee09..000000000 --- a/packages/code-review-agent/src/documents/components/Format.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -props: - type: object - properties: {} - additionalProperties: false -description: >- - Reformat its content as Markdown. `…` renders the content, - normalizes it through remark and trims the result. -context: >- - The Markdown to reformat. ---- - -```ts eval -import { remark } from "npm:remark@15"; - -const content = yield* renderChildren(); -const file = yield* call(() => remark().process(content)); -return String(file).trim(); -``` diff --git a/packages/code-review-agent/src/documents/components/GitHubAuth.md b/packages/code-review-agent/src/documents/components/GitHubAuth.md deleted file mode 100644 index 4acd147aa..000000000 --- a/packages/code-review-agent/src/documents/components/GitHubAuth.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -props: - type: object - properties: {} - additionalProperties: false -description: >- - Authorize the GitHub requests made inside it. `…` adds a - bearer token to requests to `api.github.com` over HTTPS and leaves every other host - untouched. Without `GITHUB_TOKEN` it adds nothing and the requests still run. -context: >- - The Markdown whose GitHub requests are authorized. ---- - -```ts persist eval -import { FetchApi } from "@effectionx/fetch"; -import { env as runtimeEnv } from "@executablemd/runtime"; - -function* installGitHubAuth() { - const token = yield* runtimeEnv("GITHUB_TOKEN"); - if (!token) { - return; - } - yield* FetchApi.around({ - *fetch([input, init, shouldExpect], next) { - let url; - try { - url = input instanceof Request ? new URL(input.url) : new URL(input); - } catch { - return yield* next(input, init, shouldExpect); - } - - if (url.protocol !== "https:" || url.hostname !== "api.github.com") { - return yield* next(input, init, shouldExpect); - } - - const headers = new Headers(input instanceof Request ? input.headers : undefined); - const requestHeaders = new Headers(init?.headers); - requestHeaders.forEach((value, key) => headers.set(key, value)); - headers.set("Authorization", `Bearer ${token}`); - if (!headers.has("Accept")) { - headers.set("Accept", "application/vnd.github+json"); - } - - return yield* next(input, { ...init, headers }, shouldExpect); - }, - }); -} - -yield* installGitHubAuth(); -``` - - diff --git a/packages/code-review-agent/src/documents/components/GitHubComment.md b/packages/code-review-agent/src/documents/components/GitHubComment.md deleted file mode 100644 index a78015f82..000000000 --- a/packages/code-review-agent/src/documents/components/GitHubComment.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -props: - type: object - properties: - marker: - type: string - default: "" - additionalProperties: false -description: >- - Publish the review as one comment on the pull request. - `…` posts its content the first time and edits that - same comment afterwards, so repeated runs leave one comment rather than a thread. It - requires `GITHUB_REPOSITORY` and `PR_NUMBER`. -context: >- - The comment body. ---- - -```ts eval -import { env as runtimeEnv } from "@executablemd/runtime"; - -function* reviewConfiguration() { - const repository = yield* runtimeEnv("GITHUB_REPOSITORY"); - const number = yield* runtimeEnv("PR_NUMBER"); - if (!repository || !number || !repository.includes("/")) { - throw new Error("GitHubComment requires GITHUB_REPOSITORY and PR_NUMBER"); - } - const [owner, name] = repository.split("/"); - return { api: `https://api.github.com/repos/${owner}/${name}`, number }; -} - -const content = yield* renderChildren(); -const body = props.marker + "\n" + content.trim(); -const { api, number } = yield* reviewConfiguration(); -const comments = yield* fetch(`${api}/issues/${number}/comments`).expect().json(); -if (!Array.isArray(comments)) { - throw new Error("GitHub comments response was not an array"); -} - -const existing = comments.find((comment) => - comment.user?.type === "Bot" && typeof comment.body === "string" && comment.body.includes(props.marker) -); -if (existing) { - yield* fetch(`${api}/issues/comments/${existing.id}`, { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ body }), - }).expect(); -} else { - yield* fetch(`${api}/issues/${number}/comments`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ body }), - }).expect(); -} - -return content; -``` diff --git a/packages/code-review-agent/src/documents/components/Instruction.md b/packages/code-review-agent/src/documents/components/Instruction.md deleted file mode 100644 index c1bee0a3a..000000000 --- a/packages/code-review-agent/src/documents/components/Instruction.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -meta: - componentName: Instruction - description: >- - Add a system prompt to the `` calls inside it. `…` appends to whatever - instruction is already in scope, so an enclosing instruction is read first and - nesting narrows rather than replaces. - context: >- - The Markdown whose samples carry this instruction. - -props: - type: object - properties: - system: - type: string - description: > - System prompt text. When Sample calls are made within this - component's children, the text is included in the - SampleContext.system field, which providers use as the system - prompt. Multiple Instruction components accumulate — instructions - from enclosing scopes appear first, inner instructions are appended. - required: [system] - additionalProperties: false ---- - -```js persist eval -yield * - Sample.around( - { - *sample([context], next) { - const existing = context.system || ""; - return yield* next({ - ...context, - system: existing ? existing + "\n" + props.system : props.system, - }); - }, - }, - { at: "min" }, - ); -``` - - diff --git a/packages/code-review-agent/src/documents/components/LinkedIssue.md b/packages/code-review-agent/src/documents/components/LinkedIssue.md deleted file mode 100644 index 6ea5c3649..000000000 --- a/packages/code-review-agent/src/documents/components/LinkedIssue.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -props: - type: object - properties: - pr: - type: object - whenLinesExceed: - type: number - default: 0 - severity: - type: string - default: warning - message: - type: string - default: "Large PR with no linked issue." - required: [pr] - additionalProperties: false -description: >- - Ask a large change to link an issue. `` - reports when the description names no issue and the change is bigger than that. ---- - -```ts eval -const hasIssue = /(?:#\d+|https:\/\/github\.com\/.*\/issues\/\d+)/.test(props.pr.meta.body); -``` - - props.whenLinesExceed} - severity={props.severity} message={props.message} /> diff --git a/packages/code-review-agent/src/documents/components/NewDependencies.md b/packages/code-review-agent/src/documents/components/NewDependencies.md deleted file mode 100644 index 591e5f4ff..000000000 --- a/packages/code-review-agent/src/documents/components/NewDependencies.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -props: - type: object - properties: - pr: - type: object - severity: - type: string - default: warning - message: - type: string - default: "package.json changed without dependency justification." - required: [pr] - additionalProperties: false -description: >- - Ask a change that edits `package.json` to account for its dependencies. - `` reports when the manifest moved and the description - never mentions dependencies. ---- - -```ts eval -const touchesPkg = props.pr.files.some(f => - f.path === "package.json" || f.path.endsWith("/package.json") -); -const mentionsDeps = props.pr.meta.body.toLowerCase().includes("dependenc"); -const triggered = touchesPkg && !mentionsDeps; -``` - - diff --git a/packages/code-review-agent/src/documents/components/OllamaProvider.md b/packages/code-review-agent/src/documents/components/OllamaProvider.md deleted file mode 100644 index b62eba5a2..000000000 --- a/packages/code-review-agent/src/documents/components/OllamaProvider.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -props: - type: object - properties: - model: - type: string - baseUrl: - type: string - default: "http://localhost:11434" - required: [model] - additionalProperties: false -description: >- - Answer this document's `` calls from a local Ollama server. `…` handles samples that name that model and - samples that name none, and passes the rest outward. -context: >- - The Markdown whose samples this provider answers. ---- - -```ts persist eval -yield* Sample.around({ - *sample([context], next) { - if (context.model !== undefined && context.model !== props.model) { - return yield* next(context); - } - - const messages = []; - if (context.system) { - messages.push({ role: "system", content: context.system }); - } - messages.push({ role: "user", content: context.content }); - - const result = yield* fetch(`${props.baseUrl}/v1/chat/completions`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ model: props.model, messages, temperature: 0 }), - }) - .expect() - .json(); - - const content = result.choices?.[0]?.message?.content; - if (typeof content !== "string") { - throw new Error("Ollama response did not contain model content"); - } - return content; - }, -}, { at: 'min' }); -``` - - diff --git a/packages/code-review-agent/src/documents/components/OxlintConfig.md b/packages/code-review-agent/src/documents/components/OxlintConfig.md deleted file mode 100644 index 97a7113cc..000000000 --- a/packages/code-review-agent/src/documents/components/OxlintConfig.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -props: - type: object - properties: - path: - type: string - default: ".reviews/tsconfig.oxlint.json" - additionalProperties: false -description: >- - Write the TypeScript configuration the Oxlint sensor's type-aware pass reads. - `` generates it in the checkout, so a type-aware run has a project - to resolve against. ---- - -```ts eval -import { ensureDir, writeTextFile } from "@executablemd/runtime"; - -yield* ensureDir(".reviews"); -yield* writeTextFile(props.path, JSON.stringify({ - compilerOptions: { - target: "ESNext", - module: "ESNext", - moduleResolution: "bundler", - strict: true, - noEmit: true, - skipLibCheck: true, - resolveJsonModule: true, - lib: ["ESNext", "DOM"], - types: [], - }, - include: [ - "packages/*/src/**/*.ts", - "packages/*/*.ts", - "durable-effects/**/*.ts", - ], - exclude: ["node_modules", "dist", ".vendor", "**/*.test.ts"], -}, null, 2)); -``` diff --git a/packages/code-review-agent/src/documents/components/OxlintSignals.md b/packages/code-review-agent/src/documents/components/OxlintSignals.md deleted file mode 100644 index 46a420d9f..000000000 --- a/packages/code-review-agent/src/documents/components/OxlintSignals.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -props: - type: object - properties: - groups: - type: array - label: - type: string - required: [groups, label] - additionalProperties: false -description: >- - List one category of Oxlint diagnostics. `` names each rule - with how often it fired and the first few files, and renders nothing when the - category is empty. ---- - -```ts eval -if (props.groups.length === 0) return; - -const lines = props.groups.map(g => - `- \`${g.ruleId}\` ×${g.count}: ${g.files.slice(0, 3).join(", ")}${g.files.length > 3 ? ` (+${g.files.length - 3})` : ""}` -); - -return `**Oxlint ${props.label}:**\n${lines.join("\n")}`; -``` diff --git a/packages/code-review-agent/src/documents/components/OxlintSummary.md b/packages/code-review-agent/src/documents/components/OxlintSummary.md deleted file mode 100644 index 334b918fd..000000000 --- a/packages/code-review-agent/src/documents/components/OxlintSummary.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -props: - type: object - properties: - diagnostics: - type: object - doctor: - type: object - required: [diagnostics, doctor] - additionalProperties: false -description: >- - Report what the Oxlint sensor found. `` renders a Static Analysis section that says the sensor was - unavailable rather than reporting a clean run it never measured, and notes type- - aware rules that could not run. ---- - - - - - -🟡 Oxlint not installed. Static analysis skipped. - - - - 0}> - -{props.diagnostics.summary} - - - - 0 - && props.doctor.oxlintInstalled}> - -*{props.doctor.bloatRulesMissing.length} type-aware rules unavailable -— install `oxlint-tsgolint` for full coverage.* - - - - diff --git a/packages/code-review-agent/src/documents/components/Pattern.md b/packages/code-review-agent/src/documents/components/Pattern.md deleted file mode 100644 index aeeb021ca..000000000 --- a/packages/code-review-agent/src/documents/components/Pattern.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -props: - type: object - properties: - pr: - type: object - pattern: - type: string - min: - type: number - default: 1 - excludeTests: - type: boolean - default: true - severity: - type: string - default: warning - message: - type: string - required: [pr, pattern, message] - additionalProperties: false -description: >- - Report added lines matching a regular expression. `` renders once at - least `min` lines match, and substitutes the count into `{count}`. Test files are - excluded unless `excludeTests` says otherwise. ---- - -```ts eval -const re = new RegExp(props.pattern, "g"); -const lines = props.excludeTests - ? props.pr.added.filter(l => !l.isTest) - : props.pr.added; -const matches = lines.filter(l => re.test(l.content)); -re.lastIndex = 0; - -if (matches.length >= props.min) { - const icon = props.severity === "error" ? "\ud83d\udd34" : "\ud83d\udfe1"; - return icon + " " + props.message - .replace("{count}", String(matches.length)); -} -``` diff --git a/packages/code-review-agent/src/documents/components/PrPolicyReport.md b/packages/code-review-agent/src/documents/components/PrPolicyReport.md deleted file mode 100644 index b136120fb..000000000 --- a/packages/code-review-agent/src/documents/components/PrPolicyReport.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -props: - type: object - properties: - pr: - type: object - diagnostics: - type: object - doctor: - type: object - required: [pr, diagnostics, doctor] - additionalProperties: false -description: >- - Compose the whole pull-request review. `` renders the scope, structural, slop, - static-analysis and correctness sections under one heading. When redundant comments - remain and the required GitHub environment is present, it also reconciles inline - removal suggestions on the pull request. ---- - -## PR #{props.pr.meta.number}: {props.pr.meta.title} - -**{props.pr.stats.totalFiles}** files, **+{props.pr.stats.additions}** / **-{props.pr.stats.deletions}** - - - - - - - - - - diff --git a/packages/code-review-agent/src/documents/components/Ratio.md b/packages/code-review-agent/src/documents/components/Ratio.md deleted file mode 100644 index cc96cd441..000000000 --- a/packages/code-review-agent/src/documents/components/Ratio.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -props: - type: object - properties: - pr: - type: object - numerator: - type: string - denominator: - type: string - threshold: - type: number - minDenominator: - type: number - default: 10 - excludeTests: - type: boolean - default: true - severity: - type: string - default: warning - message: - type: string - required: [pr, numerator, denominator, threshold, message] - additionalProperties: false -description: >- - Report when one pattern is too common relative to another. `` stays quiet until the denominator has at least `minDenominator` matches, - so a small diff cannot trip it on one line. ---- - -```ts eval -const numRe = new RegExp(props.numerator, "g"); -const denRe = new RegExp(props.denominator, "g"); -const lines = props.excludeTests - ? props.pr.added.filter(l => !l.isTest) - : props.pr.added; -const source = lines.map(l => l.content).join("\n"); - -const numCount = (source.match(numRe) ?? []).length; -const denCount = (source.match(denRe) ?? []).length; - -if (denCount >= props.minDenominator && numCount / denCount > props.threshold) { - const ratio = (numCount / denCount * 100).toFixed(1); - const icon = props.severity === "error" ? "\ud83d\udd34" : "\ud83d\udfe1"; - return icon + " " + props.message - .replace("{ratio}", ratio) - .replace("{numeratorCount}", String(numCount)) - .replace("{denominatorCount}", String(denCount)); -} -``` diff --git a/packages/code-review-agent/src/documents/components/ReleaseSpecWarning.md b/packages/code-review-agent/src/documents/components/ReleaseSpecWarning.md deleted file mode 100644 index b2a5b3bf1..000000000 --- a/packages/code-review-agent/src/documents/components/ReleaseSpecWarning.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -props: - type: object - properties: - files: - type: array - items: - type: string - required: [files] - additionalProperties: false -description: >- - Warn when release configuration changes without its specification. - `` renders a warning if any release file - changed and `specs/release-process-spec.md` did not. ---- - -# Release Config Files - -The following is the list of files used in the release process. Update this -list when a file is added or removed. - - - -- .github/workflows/draft-release.yml -- .github/workflows/release.yml -- .github/workflows/publish-one.yml -- .github/workflows/publish-packages.yml -- .github/release-drafter.yml -- scripts/gen-publish-workflow.md -- scripts/build-npm.ts -- scripts/bump-version.ts - - - -```ts eval -const releaseChanged = props.files.filter((path) => releaseConfigFiles.includes(`- ${path}`)); -const changedList = releaseChanged.join(", "); - -// TODO: evaluate whether the diff's changes are actually reflected in the -// spec document, not just whether the spec file was touched. -``` - - - - 0 && !props.files.includes("specs/release-process-spec.md")}> - -> [!WARNING] -> This PR changes release configuration ({changedList}) without touching -> `specs/release-process-spec.md`. Review the spec and update it to match, or -> state `spec-reviewed: no changes needed` in the PR description (AGENTS.md -> rule 8). - - - - diff --git a/packages/code-review-agent/src/documents/components/RepoPolicyReport.md b/packages/code-review-agent/src/documents/components/RepoPolicyReport.md deleted file mode 100644 index 7a596309b..000000000 --- a/packages/code-review-agent/src/documents/components/RepoPolicyReport.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -props: - type: object - properties: - diagnostics: - type: object - doctor: - type: object - fileList: - type: string - fileCount: - type: number - lineCount: - type: number - cleanupAnalysis: - type: object - required: [diagnostics, doctor, fileList, fileCount, lineCount] - additionalProperties: false -description: >- - Compose the whole repository analysis. `` renders the - static-analysis and cleanup sections. With the configured GitHub environment, it - also creates, updates and closes cleanup issues for the five highest-ranked file - clusters. ---- - -## Repository Analysis - -**{props.fileCount}** TypeScript files, **{props.lineCount}** total lines - - - - - - diff --git a/packages/code-review-agent/src/documents/components/ReviewSection.md b/packages/code-review-agent/src/documents/components/ReviewSection.md deleted file mode 100644 index a14e2bbd1..000000000 --- a/packages/code-review-agent/src/documents/components/ReviewSection.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -props: - type: object - properties: - heading: - type: string - clean: - type: string - default: "\u2705 No issues found." - required: [heading] - additionalProperties: false -description: >- - Render one section of a review, or say it found nothing. `…` prints the heading with whatever its content - rendered, and prints the `clean` message when the content rendered nothing — so a - section of quiet rules reads as a pass rather than as a blank. -context: >- - The findings that belong to this section. ---- - -```ts eval -const content = yield* renderChildren(); -return content.trim().length > 0 - ? `### ${props.heading}\n\n${content}` - : `### ${props.heading}\n\n${props.clean}`; -``` diff --git a/packages/code-review-agent/src/documents/components/ReviewSetup.md b/packages/code-review-agent/src/documents/components/ReviewSetup.md deleted file mode 100644 index c82358708..000000000 --- a/packages/code-review-agent/src/documents/components/ReviewSetup.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -props: - type: object - properties: - pullModel: - type: boolean - default: false - model: - type: string - default: "qwen3:30b-a3b" - additionalProperties: false -description: >- - Prepare the checkout for a review run. `` installs the pinned Oxlint - sensor and writes its configuration; with `pullModel` it also provisions the local - Ollama model first. ---- - - - - - -```ts eval -import { exec } from "@executablemd/runtime"; - -if (props.pullModel) { - const installed = yield* exec({ command: ["ollama", "show", props.model] }); - if (installed.exitCode !== 0) { - const pulled = yield* exec({ command: ["ollama", "pull", props.model] }); - if (pulled.exitCode !== 0) { - throw new Error(pulled.stderr || `Unable to provision Ollama model ${props.model}`); - } - } -} -``` diff --git a/packages/code-review-agent/src/documents/components/Sample.md b/packages/code-review-agent/src/documents/components/Sample.md deleted file mode 100644 index 525ca0d37..000000000 --- a/packages/code-review-agent/src/documents/components/Sample.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -meta: - componentName: Sample - description: >- - Ask the model in scope for text. `Summarize what changed.` - sends its content, or the `prompt` when written self-closing, to whichever - provider is installed — and `model` picks between providers when more than one - is. - as: >- - The model's reply. - context: >- - The prompt. - -props: - type: object - properties: - prompt: - type: string - default: "" - description: > - Text prompt to send to the Sample Api. Used in self-closing mode - when no children are provided. If both children and prompt are - present, children output takes precedence. - model: - type: string - default: "" - description: > - Model routing key. When set, the SampleContext.model field is - populated so the Sample Api middleware can route to a specific - provider. When unset, the innermost active provider handles the call. - params: - type: string - default: "" - description: > - Additional instruction params passed to the Sample Api middleware - as SampleContext.params. Providers can use this for custom behavior - (e.g., "brief", "json", "classify"). - additionalProperties: false ---- - -```js persist eval -const childrenOutput = yield * renderChildren(); -const content = childrenOutput || props.prompt || ""; - -const sampleResult = - yield * - Sample.operations.sample({ - content, - params: props.params || undefined, - componentName: "Sample", - model: props.model || undefined, - }); - -return sampleResult; -``` diff --git a/packages/code-review-agent/src/documents/components/SuggestRemoval.md b/packages/code-review-agent/src/documents/components/SuggestRemoval.md deleted file mode 100644 index b04f62357..000000000 --- a/packages/code-review-agent/src/documents/components/SuggestRemoval.md +++ /dev/null @@ -1,165 +0,0 @@ ---- -props: - type: object - properties: - findings: - type: array - dismissedReplies: - type: array - default: [] - required: [findings] - additionalProperties: false -description: >- - Reconcile the review's removal suggestions on the pull request. `` - deletes previous bot reviews when GitHub permits it, acknowledges declined - suggestions and resolves their threads, then posts pending findings as a new review. - It does nothing without the required GitHub environment. ---- - -```ts eval -import { env as runtimeEnv } from "@executablemd/runtime"; - -function* githubConfiguration() { - const token = yield* runtimeEnv("GITHUB_TOKEN"); - const repo = yield* runtimeEnv("GITHUB_REPOSITORY"); - const prNumber = yield* runtimeEnv("PR_NUMBER"); - const headSha = yield* runtimeEnv("HEAD_SHA"); - if (!token || !repo || !prNumber || !headSha) { - return undefined; - } - const [owner, name] = repo.split("/"); - return { - api: `https://api.github.com/repos/${owner}/${name}`, - graphql: "https://api.github.com/graphql", - prNumber, - headSha, - owner, - name, - }; -} - -const github = yield* githubConfiguration(); -if (!github) { - return ""; -} - -const { api, graphql, prNumber, headSha, owner, name } = github; - -const existingReviews = yield* fetch( - `${api}/pulls/${prNumber}/reviews` -).expect().json(); - -const botReviews = existingReviews.filter(r => - r.user.login === "github-actions[bot]" && - r.body && r.body.includes("redundant comment") -); - -for (const review of botReviews) { - try { - yield* fetch(`${api}/pulls/${prNumber}/reviews/${review.id}`, { - method: "DELETE", - }).expect(); - } catch { - // Review may already be submitted (can't delete submitted reviews). - } -} - -// 2. React 👍 on dismiss replies and resolve their threads -if (props.dismissedReplies.length > 0) { - // Fetch review threads via GraphQL to get thread node IDs - const threadsQuery = `query($owner: String!, $name: String!, $pr: Int!) { - repository(owner: $owner, name: $name) { - pullRequest(number: $pr) { - reviewThreads(first: 100) { - nodes { - id - isResolved - comments(first: 1) { - nodes { databaseId } - } - } - } - } - } - }`; - - let threadMap = new Map(); - try { - const threadsResult = yield* fetch(graphql, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - query: threadsQuery, - variables: { owner, name, pr: parseInt(prNumber, 10) }, - }), - }).expect().json(); - - const threads = threadsResult.data?.repository?.pullRequest?.reviewThreads?.nodes ?? []; - for (const thread of threads) { - const commentId = thread.comments?.nodes?.[0]?.databaseId; - if (commentId) { - threadMap.set(commentId, { threadId: thread.id, isResolved: thread.isResolved }); - } - } - } catch { - // If GraphQL fails, skip thread resolution — 👍 reaction still works - } - - for (const reply of props.dismissedReplies) { - // React 👍 - if (reply.replyId) { - try { - yield* fetch(`${api}/pulls/comments/${reply.replyId}/reactions`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ content: "+1" }), - }).expect(); - } catch {} - } - - // Resolve the thread - if (reply.botCommentId && threadMap.has(reply.botCommentId)) { - const { threadId, isResolved } = threadMap.get(reply.botCommentId); - if (!isResolved) { - try { - yield* fetch(graphql, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - query: `mutation($threadId: ID!) { - resolveReviewThread(input: { threadId: $threadId }) { - thread { isResolved } - } - }`, - variables: { threadId }, - }), - }).expect(); - } catch {} - } - } - } -} - -// 3. Post new review with pending findings -if (props.findings.length > 0) { - const comments = props.findings.map(f => ({ - path: f.file, - line: f.lineNumber, - body: `Redundant comment — restates what the code does.\n\`\`\`suggestion\n\`\`\``, - })); - - yield* fetch(`${api}/pulls/${prNumber}/reviews`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - commit_id: headSha, - event: "COMMENT", - body: `Found ${props.findings.length} redundant comment${props.findings.length === 1 ? "" : "s"}. Inline suggestions to remove them below.`, - comments, - }), - }).expect(); -} - -return ""; -``` diff --git a/packages/code-review-agent/src/documents/components/ThinkFilter.md b/packages/code-review-agent/src/documents/components/ThinkFilter.md deleted file mode 100644 index 6ffd55d98..000000000 --- a/packages/code-review-agent/src/documents/components/ThinkFilter.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -props: - type: object - properties: {} - additionalProperties: false -description: >- - Drop a model's reasoning from its answers. `…` removes - `` blocks from every `` reply inside it, including an unterminated - one. -context: >- - The Markdown whose samples are filtered. ---- - -```ts persist eval -yield* Sample.around({ - *sample([context], next) { - const result = yield* next(context); - return result.replace(/[\s\S]*?<\/think>/gi, "").replace(/[\s\S]*$/gi, "").trim(); - }, -}); -``` - - diff --git a/packages/code-review-agent/src/documents/components/Threshold.md b/packages/code-review-agent/src/documents/components/Threshold.md deleted file mode 100644 index 02dc9b5dc..000000000 --- a/packages/code-review-agent/src/documents/components/Threshold.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -props: - type: object - properties: - pr: - type: object - metric: - type: string - op: - type: string - value: - type: number - severity: - type: string - default: warning - message: - type: string - required: [pr, metric, op, value, message] - additionalProperties: false -description: >- - Report when a pull-request metric crosses a limit. `` - compares one of `totalChanges`, `totalFiles`, `additions`, `deletions` or - `directories`, and substitutes `{actual}` and `{value}` into the message. ---- - -```ts eval -const metrics = { - totalChanges: props.pr.stats.totalChanges, - totalFiles: props.pr.stats.totalFiles, - additions: props.pr.stats.additions, - deletions: props.pr.stats.deletions, - directories: Array.isArray(props.pr.directories) ? props.pr.directories.length : props.pr.directories.size, -}; - -const actual = metrics[props.metric]; -const ops = { - ">": (a, b) => a > b, - ">=": (a, b) => a >= b, - "<": (a, b) => a < b, - "<=": (a, b) => a <= b, - "==": (a, b) => a == b, -}; - -if (ops[props.op](actual, props.value)) { - const icon = props.severity === "error" ? "\ud83d\udd34" : "\ud83d\udfe1"; - return icon + " " + props.message - .replace("{actual}", String(actual)) - .replace("{value}", String(props.value)); -} -``` diff --git a/packages/code-review-agent/src/documents/components/UnusedInDiff.md b/packages/code-review-agent/src/documents/components/UnusedInDiff.md deleted file mode 100644 index b6b904914..000000000 --- a/packages/code-review-agent/src/documents/components/UnusedInDiff.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -props: - type: object - properties: - pr: - type: object - construct: - type: string - severity: - type: string - default: warning - message: - type: string - required: [pr, construct, message] - additionalProperties: false -description: >- - Report declarations the change adds and nothing references. `` scans added TypeScript for that - keyword where a declaration starts, so an import specifier naming the same keyword - is not mistaken for one. ---- - -```ts eval -const lines = props.pr.added.filter(l => - l.file.endsWith(".ts") || l.file.endsWith(".tsx") -); -const source = lines.map(l => l.content).join("\n"); - -// Anchoring the keyword to statement position (line start, after an optional -// export/default/declare prefix) excludes `import { type X }` specifiers, -// whose `type` keyword sits inside braces rather than at the start of a -// declaration. -const declPattern = new RegExp( - `^\\s*(?:export\\s+)?(?:default\\s+|declare\\s+)?${props.construct}\\s+(\\w+)` -); - -const decls = []; -for (const line of lines) { - const match = declPattern.exec(line.content); - if (match) { - decls.push({ name: match[1], file: line.file, lineNumber: line.lineNumber }); - } -} - -const unused = decls - .map(d => ({ - ...d, - refs: (source.match(new RegExp(`\\b${d.name}\\b`, "g")) ?? []).length, - })) - .filter(d => d.refs <= 1); - -const hasUnused = unused.length > 0; -const icon = props.severity === "error" ? "🔴" : "🟡"; -const summary = icon + " " + props.message - .replace("{names}", unused.map(u => u.name).join(", ")) - .replace("{count}", String(unused.length)); -``` - - - -
-{summary} - -| Symbol | Declared at | Refs in diff | Why flagged | -| --- | --- | --- | --- | -| `{u.name}` | `{u.file}:{u.lineNumber}` | {u.refs} | referenced ≤1× within the added diff (pre-existing usages not counted) | - - -
- -
diff --git a/packages/code-review-agent/src/documents/policies/BloatPolicy.md b/packages/code-review-agent/src/documents/policies/BloatPolicy.md deleted file mode 100644 index 84b2a5c4c..000000000 --- a/packages/code-review-agent/src/documents/policies/BloatPolicy.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -props: - type: object - properties: - pr: - type: object - diagnostics: - type: object - required: [pr, diagnostics] - additionalProperties: false -description: >- - Report structural bloat in a pull request. `` renders a Structural section covering unreferenced - type and interface declarations, `any` density, empty function bodies, console - statements and the sensor's structural signals. ---- - - - - - - - - - - - - - - - - diff --git a/packages/code-review-agent/src/documents/policies/ExtraneousCodePolicy.md b/packages/code-review-agent/src/documents/policies/ExtraneousCodePolicy.md deleted file mode 100644 index 3877c9028..000000000 --- a/packages/code-review-agent/src/documents/policies/ExtraneousCodePolicy.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -props: - type: object - properties: - pr: - type: object - diagnostics: - type: object - doctor: - type: object - required: [pr, diagnostics, doctor] - additionalProperties: false -description: >- - Review a pull request for code that should not be there. `` asks the model in scope to - weigh the sensor's violation density against the specific patterns behind it, and - skips changes of twenty lines or fewer. ---- - - - - 20}> - - - -You are reviewing a TypeScript PR for EXTRANEOUS code only. - -PR: {props.pr.meta.title} -Description: {props.pr.meta.body} - -STATIC ANALYSIS SIGNALS: -{props.diagnostics.summary} -Violation density: {props.diagnostics.density} per added line. - -DENSITY CALIBRATION: -- Below 0.020: clean — experienced contributor, reviewed code -- 0.020–0.080: normal — minor issues, typical development -- Above 0.100: elevated — likely unreviewed generated code -Adjust judgment based on specific patterns, not number alone. - -INTERPRETATION RULES: -- A few inferrable-type warnings in a large PR are noise. -- Clusters of unused-vars + empty-functions + unnecessary-type- - assertions in the SAME FILES suggest unreviewed generated code. -- Files with both high comment density AND multiple Oxlint - structural violations are the strongest slop signal. - -Report ONLY: -1. Scope creep — changes unrelated to stated purpose -2. Speculative abstractions — fewer than 3 consumers (Rule of - Three: don't abstract until third use) -3. Dead constructs — declarations never referenced in diff -4. Wrapper indirection — functions that only forward calls -5. Signal clusters — files where multiple Oxlint rules fire -6. Object literal assertions — `{} as Type` hiding missing props - -Do NOT flag test helpers, exported types, or style preferences. - -For each finding: FILE, PATTERN, CONCERN, QUESTION for author. - -If clean: "No extraneous code patterns detected." - -DIFF: -{props.pr.diffPreview} - - - - - - diff --git a/packages/code-review-agent/src/documents/policies/RepoCleanupPolicy.md b/packages/code-review-agent/src/documents/policies/RepoCleanupPolicy.md deleted file mode 100644 index b2e91d4d4..000000000 --- a/packages/code-review-agent/src/documents/policies/RepoCleanupPolicy.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -props: - type: object - properties: - diagnostics: - type: object - doctor: - type: object - fileList: - type: string - cleanupAnalysis: - type: object - required: [diagnostics, doctor, fileList] - additionalProperties: false -description: >- - Explain the repository's top cleanup clusters. `` asks the model in scope why each already-ranked - cluster matters and what to do about it, rather than re-ranking them. ---- - - - - 0 && !!props.cleanupAnalysis}> - - - -You are reviewing pre-scored cleanup clusters from a TypeScript monorepo. -Oxlint provided the raw signals. The system ranked files by co-occurrence -of distinct rule violations, weighted toward production code. - -Your job is NOT to re-analyze diagnostics. The ranking is already done. -Your job IS to explain why each top cluster matters and what specific -cleanup action to take. - -PRINCIPLES: -- Rule of Three: flag any abstraction with fewer than 3 consumers. - Single-consumer abstractions should be inlined. -- YAGNI: flag code that exists "just in case" with no current caller. - -{props.cleanupAnalysis.promptContext} - -For each of the top 5 clusters above, produce exactly this format: - -### [rank]. [file path] -- **Why**: [1-2 sentences explaining what the co-occurring signals mean together] -- **Action**: [specific verb: remove, inline, delete, extract, add try/finally, etc.] -- **Scope**: [mechanical | review-required] -- **Confidence**: [high | medium | low] - -Rules: -- "mechanical" means a maintainer can fix it without design decisions. -- "review-required" means the fix depends on intent not obvious from signals. -- Do NOT restate raw counts only; focus on what to DO. -- Do NOT add items outside ranked clusters. If fewer than 5 clusters, report fewer. -- If a file is test/demo, call it lower priority unless the same pattern appears in production. - -Example of a good item: - -### 1. packages/durable-streams/operations.ts -- **Why**: 4 distinct rules fire here (type assertions, empty functions, unused vars, floating promises), indicating mechanical scaffolding that was not cleaned up. -- **Action**: Remove unnecessary type assertions first, then audit empty function stubs for dead code. -- **Scope**: mechanical -- **Confidence**: high - - - - - - 0 && !props.cleanupAnalysis}> - -{props.diagnostics.summary} - - - - diff --git a/packages/code-review-agent/src/documents/policies/ScopePolicy.md b/packages/code-review-agent/src/documents/policies/ScopePolicy.md deleted file mode 100644 index e55ae57ca..000000000 --- a/packages/code-review-agent/src/documents/policies/ScopePolicy.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -props: - type: object - properties: - pr: - type: object - required: [pr] - additionalProperties: false -description: >- - Report the scope and hygiene of a pull request. `` renders a - Scope section covering size, file and directory counts, description length, issue - linkage and config/source mixing. ---- - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/code-review-agent/src/documents/policies/SlopPolicy.md b/packages/code-review-agent/src/documents/policies/SlopPolicy.md deleted file mode 100644 index a47d89aff..000000000 --- a/packages/code-review-agent/src/documents/policies/SlopPolicy.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -props: - type: object - properties: - pr: - type: object - diagnostics: - type: object - required: [pr, diagnostics] - additionalProperties: false -description: >- - Report verbosity indicators in a pull request. `` renders a Slop section covering comment ratio, - comments that restate the code, and the sensor's verbosity signals. When redundant - comments remain and the required GitHub environment is present, it also reconciles - inline removal suggestions on the pull request. ---- - - - - - - - - - - diff --git a/packages/code-review-agent/src/parse-diagnostics.ts b/packages/code-review-agent/src/parse-diagnostics.ts deleted file mode 100644 index 6f23dc438..000000000 --- a/packages/code-review-agent/src/parse-diagnostics.ts +++ /dev/null @@ -1,212 +0,0 @@ -import type { DiagnosticGroup, Diagnostics, DoctorResult, OxlintDiagnostic, PR } from "./types.ts"; -import { categorizeRule } from "./categories.ts"; -import { normalizeOxlintOutput } from "./parse-oxlint.ts"; - -const IMPORT_NOISE_MARKERS = ["Cannot find module", "cannot find"]; -const SUMMARY_FILE_LIMIT = 3; -const DENSITY_PRECISION = 1000; - -function emptyDiagnostics(): Diagnostics { - return { - groups: [], - total: 0, - fileCount: 0, - ruleCount: 0, - byCategory: { - structural: [], - verbosity: [], - typeAware: [], - other: [], - }, - summary: "", - density: 0, - }; -} - -function isImportNoise(diagnostic: OxlintDiagnostic): boolean { - return ( - IMPORT_NOISE_MARKERS.some((marker) => diagnostic.message.includes(marker)) || - diagnostic.ruleId.includes("import") - ); -} - -function filteredDiagnostics( - raw: readonly OxlintDiagnostic[], - doctor: DoctorResult, -): OxlintDiagnostic[] { - if (doctor.recommendation !== "type-aware-filtered") { - return [...raw]; - } - return raw.filter((diagnostic) => !isImportNoise(diagnostic)); -} - -function groupedDiagnostics(raw: readonly OxlintDiagnostic[]): Map { - const groups = new Map(); - for (const diagnostic of raw) { - const { ruleId } = diagnostic; - const group = groups.get(ruleId); - let next = [diagnostic]; - if (group !== undefined) { - next = [...group, diagnostic]; - } - groups.set(ruleId, next); - } - return groups; -} - -function filesForGroup(instances: readonly OxlintDiagnostic[]): string[] { - const files = new Set(); - for (const diagnostic of instances) { - if (diagnostic.file.length > 0) { - files.add(diagnostic.file); - } - } - return [...files]; -} - -function fileCountFor(raw: readonly OxlintDiagnostic[]): number { - const files = new Set(); - for (const diagnostic of raw) { - if (diagnostic.file.length > 0) { - files.add(diagnostic.file); - } - } - return files.size; -} - -function diagnosticGroups(raw: readonly OxlintDiagnostic[]): DiagnosticGroup[] { - const groups = [...groupedDiagnostics(raw).entries()].map(([ruleId, instances]) => ({ - ruleId, - count: instances.length, - files: filesForGroup(instances), - instances, - })); - const ordered: DiagnosticGroup[] = []; - for (const group of groups) { - const index = ordered.findIndex((candidate) => candidate.count < group.count); - if (index === -1) { - ordered.push(group); - } else { - ordered.splice(index, 0, group); - } - } - return ordered; -} - -function categorizedGroups(groups: readonly DiagnosticGroup[]): Diagnostics["byCategory"] { - const byCategory: Diagnostics["byCategory"] = { - structural: [], - verbosity: [], - typeAware: [], - other: [], - }; - for (const group of groups) { - for (const category of categorizeRule(group.ruleId)) { - byCategory[category].push(group); - } - } - return byCategory; -} - -function densityFor(pr: PR, total: number): number { - if (pr.stats.additions === 0) { - return 0; - } - return Math.round((total / pr.stats.additions) * DENSITY_PRECISION) / DENSITY_PRECISION; -} - -function pluralized(count: number, singular: string, plural: string): string { - if (count === 1) { - return singular; - } - return plural; -} - -interface SummaryInput { - total: number; - fileCount: number; - ruleCount: number; - density: number; - groups: readonly DiagnosticGroup[]; - doctor: DoctorResult; -} - -function summaryFor({ - total, - fileCount, - ruleCount, - density, - groups, - doctor, -}: SummaryInput): string { - const lines = [ - `Oxlint: ${total} ${pluralized(total, "diagnostic", "diagnostics")} across ${fileCount} ${pluralized(fileCount, "file", "files")} (${ruleCount} ${pluralized(ruleCount, "rule", "rules")})`, - `Density: ${density.toFixed(3)} violations/added-line`, - "", - ]; - for (const group of groups) { - const fileList = group.files.slice(0, SUMMARY_FILE_LIMIT).join(", "); - const moreFiles = group.files.length - SUMMARY_FILE_LIMIT; - let more = ""; - if (moreFiles > 0) { - more = ` (+${moreFiles})`; - } - lines.push(` ${group.ruleId} (${group.count}): ${fileList}${more}`); - } - appendDoctorNotes(lines, doctor); - return lines.join("\n"); -} - -function appendDoctorNotes(lines: string[], doctor: DoctorResult): void { - if (doctor.bloatRulesMissing.length > 0) { - lines.push( - "", - `Note: ${doctor.bloatRulesMissing.length} type-aware rules unavailable (${doctor.bloatRulesMissing.join(", ")}). Density may be understated.`, - ); - } - if (doctor.nativeSpecifiers.count > 0) { - lines.push( - "", - `Note: ${doctor.nativeSpecifiers.count} source files use scheme specifiers (jsr:, npm:).`, - "Run `deno lint --fix` with no-scheme-specifiers plugin to migrate.", - ); - } -} - -/** Group bounded Oxlint diagnostics for review policies. */ -export function buildDiagnostics( - raw: readonly OxlintDiagnostic[], - pr: PR, - doctor: DoctorResult, -): Diagnostics { - const filtered = filteredDiagnostics(raw, doctor); - const groups = diagnosticGroups(filtered); - const total = filtered.length; - const fileCount = fileCountFor(filtered); - const density = densityFor(pr, total); - return { - groups, - total, - fileCount, - ruleCount: groups.length, - byCategory: categorizedGroups(groups), - summary: summaryFor({ - total, - fileCount, - ruleCount: groups.length, - density, - groups, - doctor, - }), - density, - }; -} - -/** Parse raw Oxlint JSON into structured diagnostics. */ -export function parseDiagnostics(rawJson: string, pr: PR, doctor: DoctorResult): Diagnostics { - try { - return buildDiagnostics(normalizeOxlintOutput(rawJson), pr, doctor); - } catch { - return emptyDiagnostics(); - } -} diff --git a/packages/code-review-agent/src/parse-diff.ts b/packages/code-review-agent/src/parse-diff.ts deleted file mode 100644 index 38d156d69..000000000 --- a/packages/code-review-agent/src/parse-diff.ts +++ /dev/null @@ -1,277 +0,0 @@ -/** - * Parse Git unified diffs and name-status output into review data. - * Binary files are omitted; previews are bounded independently of the full diff. - */ - -import type { PR, DiffFile, DiffHunk, DiffLine } from "./types.ts"; - -const DIFF_PREVIEW_MAX = 40_000; - -const EXTENSION_LANGUAGE: Record = { - ".ts": "typescript", - ".tsx": "typescript", - ".js": "javascript", - ".jsx": "javascript", - ".mjs": "javascript", - ".cjs": "javascript", - ".json": "json", - ".md": "markdown", - ".mdx": "markdown", - ".yml": "yaml", - ".yaml": "yaml", - ".toml": "toml", - ".css": "css", - ".scss": "scss", - ".html": "html", - ".py": "python", - ".rs": "rust", - ".go": "go", - ".sh": "shell", - ".bash": "shell", - ".zsh": "shell", - ".sql": "sql", - ".graphql": "graphql", - ".gql": "graphql", - ".xml": "xml", - ".svg": "xml", - ".dockerfile": "docker", - ".rb": "ruby", - ".java": "java", - ".kt": "kotlin", - ".swift": "swift", - ".c": "c", - ".cpp": "c++", - ".h": "c", - ".hpp": "c++", -}; - -function inferLanguage(filePath: string): string { - const lower = filePath.toLowerCase(); - if (lower === "dockerfile" || lower.endsWith("/dockerfile")) { - return "docker"; - } - - const lastDot = filePath.lastIndexOf("."); - if (lastDot === -1) { - return "unknown"; - } - const ext = filePath.slice(lastDot).toLowerCase(); - return EXTENSION_LANGUAGE[ext] ?? "unknown"; -} - -function isTestFile(filePath: string): boolean { - const lower = filePath.toLowerCase(); - return ( - lower.includes(".test.") || - lower.includes(".spec.") || - lower.includes("__tests__/") || - lower.includes("/test/") || - lower.startsWith("test/") - ); -} - -function isConfigFile(filePath: string): boolean { - const lower = filePath.toLowerCase(); - const base = lower.split("/").pop() ?? ""; - return ( - base.includes(".config.") || - (base.startsWith(".") && - (base.endsWith("rc") || - base.endsWith("rc.json") || - base.endsWith("rc.js") || - base.endsWith("rc.yml") || - base.endsWith("rc.yaml"))) || - base.startsWith("tsconfig") || - base === "package.json" || - base === "package-lock.json" || - base === "deno.json" || - base === "deno.lock" || - base === ".gitignore" || - base === ".eslintignore" || - base === ".prettierignore" - ); -} - -function isTypeDeclaration(filePath: string): boolean { - return filePath.endsWith(".d.ts"); -} - -/** - * Parse the `git diff --name-status` output into a map of path → status. - * Each line is: `STATUS\tFILE` or `STATUS\tOLD\tNEW` (for renames). - */ -function parseNameStatus(raw: string): Map { - const result = new Map(); - for (const line of raw.split("\n")) { - const trimmed = line.trim(); - if (!trimmed) { - continue; - } - const parts = trimmed.split("\t"); - const statusChar = (parts[0] ?? "")[0] as "A" | "M" | "D" | "R" | "C"; - if (!["A", "M", "D", "R", "C"].includes(statusChar)) { - continue; - } - - if (statusChar === "R" || statusChar === "C") { - const newPath = parts[2] ?? parts[1] ?? ""; - result.set(newPath, statusChar); - } else { - const filePath = parts[1] ?? ""; - result.set(filePath, statusChar); - } - } - return result; -} - -/** - * Parse a unified diff (`git diff`) into DiffFile objects. - */ -function parseDiffContent( - rawDiff: string, - statusMap: Map, -): DiffFile[] { - const files: DiffFile[] = []; - - const fileSections = rawDiff.split(/^diff --git /m).filter(Boolean); - - for (const section of fileSections) { - const lines = section.split("\n"); - - const headerMatch = lines[0]?.match(/a\/(.+?) b\/(.+)/); - if (!headerMatch) { - continue; - } - const filePath = headerMatch[2]; - - if (section.includes("Binary files ")) { - continue; - } - - const status = statusMap.get(filePath) ?? "M"; - const hunks: DiffHunk[] = []; - let currentHunk: DiffHunk | null = null; - let addLineNo = 0; - let removeLineNo = 0; - - for (let i = 1; i < lines.length; i++) { - const line = lines[i]; - - // Hunk header: @@ -old,count +new,count @@ - const hunkMatch = line.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@(.*)/); - if (hunkMatch) { - currentHunk = { header: line, lines: [] }; - hunks.push(currentHunk); - removeLineNo = parseInt(hunkMatch[1], 10); - addLineNo = parseInt(hunkMatch[2], 10); - continue; - } - - if (!currentHunk) { - continue; - } - - if (line.startsWith("+")) { - currentHunk.lines.push({ - type: "add", - content: line.slice(1), - file: filePath, - lineNumber: addLineNo, - isTest: isTestFile(filePath), - }); - addLineNo++; - } else if (line.startsWith("-")) { - currentHunk.lines.push({ - type: "remove", - content: line.slice(1), - file: filePath, - lineNumber: removeLineNo, - isTest: isTestFile(filePath), - }); - removeLineNo++; - } else if (line.startsWith(" ")) { - currentHunk.lines.push({ - type: "context", - content: line.slice(1), - file: filePath, - lineNumber: addLineNo, - isTest: isTestFile(filePath), - }); - addLineNo++; - removeLineNo++; - } - } - - files.push({ - path: filePath, - status, - hunks, - language: inferLanguage(filePath), - isTest: isTestFile(filePath), - isConfig: isConfigFile(filePath), - isTypeDeclaration: isTypeDeclaration(filePath), - }); - } - - return files; -} - -/** - * Compute unique directories at depth 2 (e.g., "src/components"). - */ -function computeDirectories(files: DiffFile[]): Set { - const dirs = new Set(); - for (const file of files) { - const parts = file.path.split("/"); - if (parts.length >= 2) { - dirs.add(parts.slice(0, 2).join("/")); - } else if (parts.length === 1) { - dirs.add("."); - } - } - return dirs; -} - -/** - * Parse raw `git diff` and `git diff --name-status` output into a typed PR object. - * - * @param rawDiff - Output of `git diff BASE...HEAD` - * @param rawFiles - Output of `git diff --name-status BASE...HEAD` - * @param meta - PR metadata (title, body, number) - * @returns Typed PR object for use by review components - */ -export function parseDiff( - rawDiff: string, - rawFiles: string, - meta: { title: string; body: string; number: string }, -): PR { - const statusMap = parseNameStatus(rawFiles); - const files = parseDiffContent(rawDiff, statusMap); - - const allLines = files.flatMap((f) => f.hunks.flatMap((h) => h.lines)); - const added = allLines.filter((l) => l.type === "add"); - const removed = allLines.filter((l) => l.type === "remove"); - - const addedSource = added.map((l) => l.content).join("\n"); - const diffPreview = - addedSource.length > DIFF_PREVIEW_MAX ? addedSource.slice(0, DIFF_PREVIEW_MAX) : addedSource; - - return { - files, - added, - removed, - created: files.filter((f) => f.status === "A"), - modified: files.filter((f) => f.status === "M"), - deleted: files.filter((f) => f.status === "D"), - directories: computeDirectories(files), - addedSource, - diffPreview, - stats: { - totalFiles: files.length, - additions: added.length, - deletions: removed.length, - totalChanges: added.length + removed.length, - }, - meta, - }; -} diff --git a/packages/code-review-agent/src/parse-doctor.ts b/packages/code-review-agent/src/parse-doctor.ts deleted file mode 100644 index 449006484..000000000 --- a/packages/code-review-agent/src/parse-doctor.ts +++ /dev/null @@ -1,140 +0,0 @@ -import type { DoctorResult } from "./types.ts"; - -const DEFAULTS: DoctorResult = { - oxlintInstalled: false, - oxlintVersion: "", - tsgolintInstalled: false, - tsgolintVersion: "", - tsconfigExists: false, - nodeModulesExists: false, - typeAwareAvailable: false, - filesAnalyzed: 0, - filesSkipped: 0, - importErrors: 0, - availableRuleIds: [], - bloatRulesAvailable: [], - bloatRulesMissing: [], - recommendation: "syntax-only", - nativeSpecifiers: { - count: 0, - files: [], - jsr: 0, - npm: 0, - }, -}; - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -function booleanValue(record: Record, key: string): boolean | undefined { - const value = record[key]; - if (typeof value === "boolean") { - return value; - } - return undefined; -} - -function numberValue(record: Record, key: string): number | undefined { - const value = record[key]; - if (typeof value === "number" && Number.isFinite(value)) { - return value; - } - return undefined; -} - -function stringValue(record: Record, key: string): string | undefined { - const value = record[key]; - if (typeof value === "string") { - return value; - } - return undefined; -} - -function isStringArray(value: unknown): value is string[] { - return Array.isArray(value) && value.every((item): item is string => typeof item === "string"); -} - -function stringArrayValue(record: Record, key: string): string[] | undefined { - const value = record[key]; - if (isStringArray(value)) { - return value; - } - return undefined; -} - -function recommendationValue( - record: Record, -): DoctorResult["recommendation"] | undefined { - const value = record.recommendation; - if (value === "type-aware" || value === "type-aware-filtered" || value === "syntax-only") { - return value; - } - return undefined; -} - -function nativeSpecifierValue(value: unknown): DoctorResult["nativeSpecifiers"] { - if (!isRecord(value)) { - return cloneNativeSpecifiers(DEFAULTS.nativeSpecifiers); - } - return { - count: numberValue(value, "count") ?? DEFAULTS.nativeSpecifiers.count, - files: stringArrayValue(value, "files") ?? [...DEFAULTS.nativeSpecifiers.files], - jsr: numberValue(value, "jsr") ?? DEFAULTS.nativeSpecifiers.jsr, - npm: numberValue(value, "npm") ?? DEFAULTS.nativeSpecifiers.npm, - }; -} - -function cloneNativeSpecifiers( - value: DoctorResult["nativeSpecifiers"], -): DoctorResult["nativeSpecifiers"] { - return { ...value, files: [...value.files] }; -} - -function defaultDoctorResult(): DoctorResult { - return { - ...DEFAULTS, - bloatRulesAvailable: [...DEFAULTS.bloatRulesAvailable], - bloatRulesMissing: [...DEFAULTS.bloatRulesMissing], - nativeSpecifiers: cloneNativeSpecifiers(DEFAULTS.nativeSpecifiers), - }; -} - -function parseDoctorObject(record: Record): DoctorResult { - return { - oxlintInstalled: booleanValue(record, "oxlintInstalled") ?? DEFAULTS.oxlintInstalled, - oxlintVersion: stringValue(record, "oxlintVersion") ?? DEFAULTS.oxlintVersion, - tsgolintInstalled: booleanValue(record, "tsgolintInstalled") ?? DEFAULTS.tsgolintInstalled, - tsgolintVersion: stringValue(record, "tsgolintVersion") ?? DEFAULTS.tsgolintVersion, - tsconfigExists: booleanValue(record, "tsconfigExists") ?? DEFAULTS.tsconfigExists, - nodeModulesExists: booleanValue(record, "nodeModulesExists") ?? DEFAULTS.nodeModulesExists, - typeAwareAvailable: booleanValue(record, "typeAwareAvailable") ?? DEFAULTS.typeAwareAvailable, - filesAnalyzed: numberValue(record, "filesAnalyzed") ?? DEFAULTS.filesAnalyzed, - filesSkipped: numberValue(record, "filesSkipped") ?? DEFAULTS.filesSkipped, - importErrors: numberValue(record, "importErrors") ?? DEFAULTS.importErrors, - availableRuleIds: stringArrayValue(record, "availableRuleIds") ?? [ - ...DEFAULTS.availableRuleIds, - ], - bloatRulesAvailable: stringArrayValue(record, "bloatRulesAvailable") ?? [ - ...DEFAULTS.bloatRulesAvailable, - ], - bloatRulesMissing: stringArrayValue(record, "bloatRulesMissing") ?? [ - ...DEFAULTS.bloatRulesMissing, - ], - recommendation: recommendationValue(record) ?? DEFAULTS.recommendation, - nativeSpecifiers: nativeSpecifierValue(record.nativeSpecifiers), - }; -} - -/** Parse the published Doctor JSON contract while applying safe defaults. */ -export function parseDoctorResult(json: string): DoctorResult { - try { - const parsed: unknown = JSON.parse(json); - if (isRecord(parsed)) { - return parseDoctorObject(parsed); - } - return defaultDoctorResult(); - } catch { - return defaultDoctorResult(); - } -} diff --git a/packages/code-review-agent/src/parse-oxlint.ts b/packages/code-review-agent/src/parse-oxlint.ts deleted file mode 100644 index a4b4192be..000000000 --- a/packages/code-review-agent/src/parse-oxlint.ts +++ /dev/null @@ -1,155 +0,0 @@ -import type { OxlintDiagnostic } from "./types.ts"; - -const RULE_CODE_PATTERN = /\((?[^)]+)\)/u; - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null; -} - -function stringValue(record: Record, key: string): string | undefined { - const value = record[key]; - if (typeof value === "string") { - return value; - } - return undefined; -} - -function numberValue(record: Record, key: string): number | undefined { - const value = record[key]; - if (typeof value === "number" && Number.isFinite(value)) { - return value; - } - return undefined; -} - -function firstSpan(record: Record): Record | undefined { - const { labels } = record; - if (!Array.isArray(labels)) { - return undefined; - } - const [label] = Array.from(labels, (entry: unknown) => entry); - if (!isRecord(label) || !isRecord(label.span)) { - return undefined; - } - return label.span; -} - -function spanNumber(span: Record | undefined, key: string): number | undefined { - if (span === undefined) { - return undefined; - } - return numberValue(span, key); -} - -function ruleIdFromCode(code: string): string { - const match = RULE_CODE_PATTERN.exec(code); - return match?.groups?.ruleId ?? code; -} - -function ruleIdFor(value: Record, code: string | undefined): string { - const explicitRuleId = stringValue(value, "ruleId"); - if (explicitRuleId !== undefined) { - return explicitRuleId; - } - if (code !== undefined) { - return ruleIdFromCode(code); - } - return "unknown"; -} - -function severityFor(value: string | undefined): "error" | "warning" { - if (value === "error") { - return "error"; - } - return "warning"; -} - -function positionFor( - value: Record, - span: Record | undefined, - key: string, -): number { - return numberValue(value, key) ?? spanNumber(span, key) ?? 0; -} - -/** Convert one Oxlint object to the bounded representation used by reviews. */ -export function normalizeDiagnostic(value: unknown): OxlintDiagnostic | undefined { - if (!isRecord(value)) { - return undefined; - } - - const code = stringValue(value, "code"); - const span = firstSpan(value); - const severity = stringValue(value, "severity"); - const file = stringValue(value, "file") ?? stringValue(value, "filename") ?? ""; - return { - message: stringValue(value, "message") ?? "", - ruleId: ruleIdFor(value, code), - severity: severityFor(severity), - file, - line: positionFor(value, span, "line"), - column: positionFor(value, span, "column"), - }; -} - -function diagnosticEntries(value: unknown): unknown[] | undefined { - if (Array.isArray(value)) { - return Array.from(value, (entry: unknown) => entry); - } - if (!isRecord(value)) { - return undefined; - } - - const { diagnostics } = value; - if (Array.isArray(diagnostics)) { - return Array.from(diagnostics, (entry: unknown) => entry); - } - if (isRecord(diagnostics) && Array.isArray(diagnostics.diagnostics)) { - return Array.from(diagnostics.diagnostics, (entry: unknown) => entry); - } - return undefined; -} - -function parseOxlintJson(stdout: string): unknown { - try { - const parsed: unknown = JSON.parse(stdout); - return parsed; - } catch (error) { - let message = String(error); - if (error instanceof Error) { - const { message: errorMessage } = error; - message = errorMessage; - } - throw new Error(`Oxlint returned malformed JSON: ${message}`, { cause: error }); - } -} - -function changedFiles(files: readonly string[] | undefined): Set | undefined { - if (files === undefined) { - return undefined; - } - return new Set(files); -} - -/** - * Parse and bound Oxlint JSON before it enters an executable document value. - * Unknown fields never cross this boundary. - */ -export function normalizeOxlintOutput( - stdout: string, - files?: readonly string[], -): OxlintDiagnostic[] { - const entries = diagnosticEntries(parseOxlintJson(stdout)); - if (entries === undefined) { - throw new Error("Oxlint JSON did not contain a diagnostics array"); - } - - const changed = changedFiles(files); - return entries.flatMap((entry) => { - const diagnostic = normalizeDiagnostic(entry); - if (diagnostic === undefined || (changed !== undefined && !changed.has(diagnostic.file))) { - return []; - } - return [diagnostic]; - }); -} diff --git a/packages/code-review-agent/src/plugin.ts b/packages/code-review-agent/src/plugin.ts deleted file mode 100644 index 14865a704..000000000 --- a/packages/code-review-agent/src/plugin.ts +++ /dev/null @@ -1,113 +0,0 @@ -/** - * Install the packaged review graph when an operator selects this Plugin. - * - * Run, syntax, plan and executing workflow actions reserve its thirty-five - * Markdown declarations and seven reserved registrations, so the repository being - * reviewed cannot replace the review program. Root `xmd test` reserves none; - * its isolated `` child reinstalls this Plugin with - * command `run`. - */ - -import type { Operation } from "effection"; - -import { Plugin } from "@executablemd/core/api"; -import type { PluginInstallRequest, PluginInstallation } from "@executablemd/core/api"; - -import { reviewComponentDeclarations, useReviewComponents } from "./review-components.ts"; - -/** The `xmd workflow` actions that execute a document, and so run a profile. */ -const EXECUTING_ACTIONS: ReadonlySet = new Set(["start", "resume", "fork"]); - -/** - * Workflow options whose separated values must not be mistaken for actions. - * Keep this aligned with `workflowConfig` in `packages/cli/src/workflow.ts` and - * argv-level Plugin/root-property options; the CLI dependency prevents - * importing its grammar here. For example, `workflow --output start export - * run-1` exports and must not install the review graph because the output value - * reads `start`. - */ -const VALUED_OPTIONS: ReadonlySet = new Set([ - "--plugin", - "--id", - "--at", - "--status", - "--artifact", - "--output", -]); - -/** The generated root-property options, which take a separated value too. */ -const PROPERTY_OPTION = "--props"; - -/** Whether this token is an option that takes the token after it. */ -function takesValue(token: string): boolean { - return ( - VALUED_OPTIONS.has(token) || - token === PROPERTY_OPTION || - token.startsWith(`${PROPERTY_OPTION}-`) - ); -} - -/** - * Read the first positional after `workflow`, skipping options and their - * values. Stop at `--`; unknown actions remain the command's own grammar error - * and install no review graph. - */ -function executesDocument(args: readonly string[]): boolean { - const start = args.indexOf("workflow"); - if (start === -1) { - return false; - } - const tokens = args.slice(start + 1); - for (let index = 0; index < tokens.length; index++) { - const token = tokens[index]; - if (token === undefined || token === "--") { - return false; - } - if (takesValue(token)) { - index += 1; - continue; - } - if (token.startsWith("-")) { - continue; - } - return EXECUTING_ACTIONS.has(token); - } - return false; -} - -/** Whether the review graph belongs to this command. */ -function claimsGraph(request: PluginInstallRequest): boolean { - switch (request.command) { - case "run": - case "plan": - case "syntax": - return true; - case "workflow": - return executesDocument(request.args); - default: - return false; - } -} - -/** - * Written as a named, explicitly typed value rather than as a default export - * expression, because a package publishing to JSR states the type of what it - * exports rather than leaving it to be inferred. - */ -const reviewPlugin: Plugin = Plugin({ - name: "@executablemd/code-review-agent", - *install(request: PluginInstallRequest): Operation { - if (!claimsGraph(request)) { - return undefined; - } - // The seven reserved registrations and the documentation describing them, in - // the scope the command runs in. Declarative only: nothing here installs a - // provider, spawns a process, reads a credential or reaches the network. - yield* useReviewComponents(); - // The thirty-five Markdown declarations travel by value, read from this - // build's own assets, exactly as they always have. - return { components: yield* reviewComponentDeclarations() }; - }, -}); - -export default reviewPlugin; diff --git a/packages/code-review-agent/src/policy.ts b/packages/code-review-agent/src/policy.ts deleted file mode 100644 index 5c64be9d2..000000000 --- a/packages/code-review-agent/src/policy.ts +++ /dev/null @@ -1,225 +0,0 @@ -import type { - CleanupAnalysis, - CleanupEvidence, - Diagnostics, - DoctorResult, - FileCluster, - FileKind, - OxlintDiagnostic, - PolicyCluster, - PolicyInput, - PolicyMode, - PolicyReport, - PolicyScore, - PR, -} from "./types.ts"; -import { categorizeRule } from "./categories.ts"; - -export function buildPolicyInput( - mode: PolicyMode, - diagnostics: Diagnostics, - options: { - pr?: PR; - doctor?: DoctorResult; - source?: string; - } = {}, -): PolicyInput { - return { - mode, - diagnostics, - pr: options.pr, - doctor: options.doctor, - metadata: { - source: options.source, - generatedAt: new Date().toISOString(), - }, - }; -} - -export function clusterDiagnostics(input: PolicyInput): PolicyCluster[] { - const diagnostics = input.diagnostics; - const clusters: PolicyCluster[] = []; - - const push = ( - category: PolicyCluster["category"], - groups: Diagnostics["byCategory"][keyof Diagnostics["byCategory"]], - ) => { - for (const group of groups) { - clusters.push({ - id: `${category}:${group.ruleId}`, - category, - ruleIds: [group.ruleId], - files: group.files, - count: group.count, - summary: `${group.ruleId} appears ${group.count} time${group.count === 1 ? "" : "s"}`, - }); - } - }; - - push("bloat", diagnostics.byCategory.structural); - push("slop", diagnostics.byCategory.verbosity); - push("correctness", diagnostics.byCategory.typeAware); - push("correctness", diagnostics.byCategory.other); - - return clusters.sort((a, b) => b.count - a.count); -} - -export function scoreClusters(clusters: PolicyCluster[]): PolicyScore[] { - return clusters.map((cluster) => { - const score = Math.min(100, cluster.count * 2); - const confidence = score >= 30 ? "high" : score >= 12 ? "medium" : "low"; - return { - clusterId: cluster.id, - score, - confidence, - }; - }); -} - -export function buildPolicyReport(input: PolicyInput): PolicyReport { - const clusters = clusterDiagnostics(input); - const scores = scoreClusters(clusters); - const summary = `Policy report: ${clusters.length} cluster${clusters.length === 1 ? "" : "s"} from ${input.diagnostics.total} diagnostics.`; - - return { - summary, - clusters, - scores, - }; -} - -function classifyFile(path: string): FileKind { - if (/\.test\.|\.spec\.|(^|\/)tests\//.test(path)) { - return "test"; - } - if (/(^|\/)demo\//.test(path)) { - return "demo"; - } - if (/\.json$|\.ya?ml$|deno\.json|tsconfig/.test(path)) { - return "config"; - } - return "production"; -} - -export function clusterByFile(diagnostics: Diagnostics): FileCluster[] { - const fileMap = new Map(); - - for (const group of diagnostics.groups) { - for (const instance of group.instances) { - if (!instance.file) { - continue; - } - - const existing = fileMap.get(instance.file); - if (existing) { - existing.push(instance); - } else { - fileMap.set(instance.file, [instance]); - } - } - } - - const clusters: FileCluster[] = []; - - for (const [file, instances] of fileMap.entries()) { - const kind = classifyFile(file); - const ruleIds = [...new Set(instances.map((d) => d.ruleId))]; - const categories = { - structural: 0, - verbosity: 0, - typeAware: 0, - other: 0, - }; - - for (const instance of instances) { - const cats = categorizeRule(instance.ruleId); - for (const cat of cats) { - categories[cat]++; - } - } - - const coOccurrence = ruleIds.length; - const kindWeight = - kind === "production" ? 1 : kind === "test" ? 0.3 : kind === "demo" ? 0.2 : 0.1; - const score = Math.round(coOccurrence * instances.length * kindWeight); - - clusters.push({ - file, - kind, - totalViolations: instances.length, - ruleIds, - categories, - coOccurrence, - score, - }); - } - - return clusters.sort((a, b) => b.score - a.score); -} - -export function extractEvidence( - diagnostics: Diagnostics, - fileClusters: FileCluster[], - topN = 10, -): CleanupEvidence[] { - const topFiles = new Set(fileClusters.slice(0, topN).map((cluster) => cluster.file)); - const evidence: CleanupEvidence[] = []; - - for (const group of diagnostics.groups) { - for (const instance of group.instances) { - if (!topFiles.has(instance.file)) { - continue; - } - - if (evidence.length >= 40) { - return evidence; - } - - evidence.push({ - file: instance.file, - ruleId: instance.ruleId, - line: instance.line, - message: instance.message, - }); - } - } - - return evidence; -} - -export function buildCleanupAnalysis(diagnostics: Diagnostics, topN = 10): CleanupAnalysis { - const fileClusters = clusterByFile(diagnostics); - const evidence = extractEvidence(diagnostics, fileClusters, topN); - const top = fileClusters.slice(0, topN); - - const lines: string[] = []; - lines.push(`RANKED FILE CLUSTERS (top ${top.length} of ${fileClusters.length}):`); - lines.push(""); - - for (let i = 0; i < top.length; i++) { - const cluster = top[i]; - lines.push(`${i + 1}. ${cluster.file} [${cluster.kind}]`); - lines.push( - ` Score: ${cluster.score} | Violations: ${cluster.totalViolations} | Co-occurring rules: ${cluster.coOccurrence}`, - ); - lines.push(` Rules: ${cluster.ruleIds.join(", ")}`); - lines.push( - ` Breakdown: structural=${cluster.categories.structural} slop=${cluster.categories.verbosity} type-aware=${cluster.categories.typeAware} other=${cluster.categories.other}`, - ); - } - - if (evidence.length > 0) { - lines.push(""); - lines.push("EVIDENCE (representative diagnostics from top files):"); - lines.push(""); - for (const item of evidence.slice(0, 20)) { - lines.push(` ${item.file}:${item.line} [${item.ruleId}] ${item.message}`); - } - } - - return { - fileClusters, - evidence, - promptContext: lines.join("\n"), - }; -} diff --git a/packages/code-review-agent/src/review-components.ts b/packages/code-review-agent/src/review-components.ts deleted file mode 100644 index 446d22eed..000000000 --- a/packages/code-review-agent/src/review-components.ts +++ /dev/null @@ -1,342 +0,0 @@ -/** - * Declare the packaged review graph before a root document is imported. - * - * Thirty-five Markdown components cross as {@link MarkdownComponent} values - * with packaged bytes, stable asset origins and digests. Seven TypeScript - * components use reserved registrations. Both reserve names so the repository - * being reviewed cannot replace its reviewers; duplicate host reservations - * refuse at admission. Ordinary includes and component discovery remain - * available for unreserved names. - * - * Assets resolve from this module's own URL in source, npm and compiled builds. - * Read them through the Effection filesystem directly: document-facing API.Fs - * and Files middleware must not substitute the review program. Missing assets - * fail assembly rather than leaving an empty component reservation. - */ - -import { readTextFile } from "@effectionx/fs"; -import type { Operation } from "effection"; -import { - contributeDocumentation, - documented, - packageDocumentation, - registerComponents, -} from "@executablemd/core"; -import type { - ComponentRegistration, - DocumentationContribution, - DocumentationReader, - FunctionComponent, -} from "@executablemd/core"; -// The declaration constructor and its digest are a *host* concern: they are what -// a trusted installation hands to an execution, rather than anything a document -// or a registered component reaches. -import { Markdown, sourceDigest } from "@executablemd/core/host"; -import type { MarkdownComponent } from "@executablemd/core/host"; - -import CommentReviewData, { - props as commentReviewDataProps, - returns as commentReviewDataReturns, -} from "./components/CommentReviewData.ts"; -import CommentReviewState, { - props as commentReviewStateProps, - returns as commentReviewStateReturns, -} from "./components/CommentReviewState.ts"; -import Doctor, { props as doctorProps, returns as doctorReturns } from "./components/Doctor.ts"; -import OxlintDiagnostics, { - props as oxlintDiagnosticsProps, - returns as oxlintDiagnosticsReturns, -} from "./components/OxlintDiagnostics.ts"; -import RepositoryInventory, { - props as repositoryInventoryProps, - returns as repositoryInventoryReturns, -} from "./components/RepositoryInventory.ts"; -import ReviewDiagnostics, { - props as reviewDiagnosticsProps, - returns as reviewDiagnosticsReturns, -} from "./components/ReviewDiagnostics.ts"; -import ReviewContext, { - props as reviewContextProps, - returns as reviewContextReturns, -} from "./components/ReviewContext.ts"; - -/** The package every component in this graph reports as its source. */ -export const REVIEW_ORIGIN = "@executablemd/code-review-agent"; - -/** Which of the two packaged document trees a component's bytes sit in. */ -export type ReviewDocumentGroup = "components" | "policies"; - -/** One packaged Markdown component: the name a document writes, and where it lives. */ -export interface ReviewDocument { - readonly name: string; - readonly group: ReviewDocumentGroup; -} - -/** - * Declare the packaged Markdown manifest in stable order. - * Compiled assets cannot rely on directory enumeration. Groups also determine - * origins, so adding or relocating a component is an explicit identity edit. - */ -export const REVIEW_DOCUMENTS: readonly ReviewDocument[] = [ - { name: "AbstractionNames", group: "components" }, - { name: "CleanupIssues", group: "components" }, - { name: "CommentReview", group: "components" }, - { name: "ConfigSourceMix", group: "components" }, - { name: "DeepInfraProvider", group: "components" }, - { name: "DescriptionCheck", group: "components" }, - { name: "EnsureOxlint", group: "components" }, - { name: "Finding", group: "components" }, - { name: "Format", group: "components" }, - { name: "GitHubAuth", group: "components" }, - { name: "GitHubComment", group: "components" }, - { name: "Instruction", group: "components" }, - { name: "LinkedIssue", group: "components" }, - { name: "NewDependencies", group: "components" }, - { name: "OllamaProvider", group: "components" }, - { name: "OxlintConfig", group: "components" }, - { name: "OxlintSignals", group: "components" }, - { name: "OxlintSummary", group: "components" }, - { name: "Pattern", group: "components" }, - { name: "PrPolicyReport", group: "components" }, - { name: "Ratio", group: "components" }, - { name: "ReleaseSpecWarning", group: "components" }, - { name: "RepoPolicyReport", group: "components" }, - { name: "ReviewSection", group: "components" }, - { name: "ReviewSetup", group: "components" }, - { name: "Sample", group: "components" }, - { name: "SuggestRemoval", group: "components" }, - { name: "ThinkFilter", group: "components" }, - { name: "Threshold", group: "components" }, - { name: "UnusedInDiff", group: "components" }, - { name: "BloatPolicy", group: "policies" }, - { name: "ExtraneousCodePolicy", group: "policies" }, - { name: "RepoCleanupPolicy", group: "policies" }, - { name: "ScopePolicy", group: "policies" }, - { name: "SlopPolicy", group: "policies" }, -]; - -/** Where one packaged component's bytes live, as a URL beside this module. */ -export function reviewDocumentUrl(document: ReviewDocument): URL { - return new URL(`./documents/${document.group}/${document.name}.md`, import.meta.url); -} - -/** - * Identify a component by package and asset, never its host filesystem path. - * Source, npm and compiled distributions therefore report the same origin. - */ -export function reviewDocumentOrigin(document: ReviewDocument): string { - return `${REVIEW_ORIGIN}/${document.group}/${document.name}.md`; -} - -/** - * Read this build's packaged Markdown lazily during trusted host assembly. - * Each assembly creates a fresh array; declaration identity is name, origin, bytes - * and digest rather than object identity. Missing assets fail before root import. - */ -export function* reviewComponentDeclarations(): Operation { - const declarations: MarkdownComponent[] = []; - for (const document of REVIEW_DOCUMENTS) { - const url = reviewDocumentUrl(document); - let source: string; - try { - source = yield* readTextFile(url); - } catch (error) { - throw new Error( - `the packaged review component ${document.group}/${document.name}.md is missing from ` + - `this build (looked in ${url.href})`, - { cause: error }, - ); - } - declarations.push( - Markdown({ - name: document.name, - origin: reviewDocumentOrigin(document), - source, - // Stated about the bytes this build actually read, and checked against - // them at admission. A build that shipped different bytes is refused - // where it is installed rather than wherever a document happens to - // write the name. - digest: sourceDigest(source), - // `forms` is deliberately unstated. These components accept both - // spellings today, and a declaration that narrowed them would change - // what the existing review roots may write. - }), - ); - } - return declarations; -} - -/** - * Adapt typed implementations after the engine validates their declared schemas. - * TypeScript cannot infer a prop type from its JSON Schema value. Keep each - * implementation's prop type aligned with that schema; the schema remains the - * runtime check. - */ -// deno-lint-ignore no-explicit-any -function reviewComponent(fn: (props: any) => Operation): FunctionComponent { - return fn; -} - -/** - * Reserve the seven TypeScript component names alongside the Markdown declarations. - * These implementations run processes, read credentials and reach the network; - * leaving them shadowable would let the review subject replace its own reviewers. - * Their existing public names and contracts are retained. - */ -export const REVIEW_REGISTRATIONS: readonly ComponentRegistration[] = [ - { - name: "CommentReviewData", - origin: REVIEW_ORIGIN, - reserved: true, - props: commentReviewDataProps, - returns: commentReviewDataReturns, - fn: reviewComponent(CommentReviewData), - ...documented({ - description: - `Collect the pull request's existing review comments and replies. ` + - `\`\` prepares the pairs and prior finding ` + - `state used by review classification.`, - as: "The normalized review-comment pairs, previous findings, and reply groups.", - context: null, - }), - }, - { - name: "CommentReviewState", - origin: REVIEW_ORIGIN, - reserved: true, - props: commentReviewStateProps, - returns: commentReviewStateReturns, - fn: reviewComponent(CommentReviewState), - ...documented({ - description: - `Build the pending review-comment update from classified review data. ` + - `\`\` decides which findings and dismissals remain.`, - as: "The checklist text, pending findings, and new dismissal replies.", - context: null, - }), - }, - { - name: "Doctor", - origin: REVIEW_ORIGIN, - reserved: true, - props: doctorProps, - returns: doctorReturns, - fn: reviewComponent(Doctor), - ...documented({ - description: - `Inspect whether the checkout is ready for review analysis. ` + - `\`\` reports pinned tool availability, TypeScript ` + - `support, analyzed files, and actionable setup guidance.`, - as: "The checkout readiness report used by review and repository analysis.", - context: null, - }), - }, - { - name: "OxlintDiagnostics", - origin: REVIEW_ORIGIN, - reserved: true, - props: oxlintDiagnosticsProps, - returns: oxlintDiagnosticsReturns, - fn: reviewComponent(OxlintDiagnostics), - ...documented({ - description: - `Run the review's pinned Oxlint sensor over selected files. ` + - `\`\` returns normalized ` + - `diagnostics and fails on a sensor crash or unusable output.`, - as: "The normalized Oxlint diagnostics for the selected files.", - context: null, - }), - }, - { - name: "ReviewDiagnostics", - origin: REVIEW_ORIGIN, - reserved: true, - props: reviewDiagnosticsProps, - returns: reviewDiagnosticsReturns, - fn: reviewComponent(ReviewDiagnostics), - ...documented({ - description: - `Group normalized Oxlint diagnostics for review policies. ` + - `\`\`.`, - as: "The grouped diagnostics, categories, density, and review summary.", - context: null, - }), - }, - { - name: "RepositoryInventory", - origin: REVIEW_ORIGIN, - reserved: true, - props: repositoryInventoryProps, - returns: repositoryInventoryReturns, - fn: reviewComponent(RepositoryInventory), - ...documented({ - description: - `Inventory the repository source used by repository analysis. ` + - `\`\` returns the selected paths and their file ` + - `and line totals.`, - as: "The selected source paths and their file and line totals.", - context: null, - }), - }, - { - name: "ReviewContext", - origin: REVIEW_ORIGIN, - reserved: true, - props: reviewContextProps, - returns: reviewContextReturns, - fn: reviewComponent(ReviewContext), - ...documented({ - description: - `Build review context for the configured base and head revisions. ` + - `\`\` returns pull-request metadata and changed file paths ` + - `from the local Git checkout.`, - as: "The pull-request data and changed file paths for the configured revision range.", - context: null, - }), - }, -]; - -/** - * This boundary's long-form documentation, and the components it must cover. - * - * The covered set is derived from {@link REVIEW_REGISTRATIONS} rather than - * written out again, so a registration added here demands its documentation - * without anyone remembering to update a second list. - * - * The thirty-five Markdown declarations are deliberately absent from it. A - * declared component's documentation is its own frontmatter and body — the - * documentation index joins on a package origin, and declared Markdown names - * none — so listing them here would demand headings for components this file - * could not be the documentation of. - */ -export function* reviewDocumentation( - read?: DocumentationReader, -): Operation { - return yield* packageDocumentation( - new URL("./components.md", import.meta.url), - { owner: REVIEW_ORIGIN, asset: "packages/code-review-agent/src/components.md" }, - REVIEW_REGISTRATIONS.map((registration) => registration.name), - read, - ); -} - -/** - * Install the seven reserved registrations and the documentation describing them. - * - * One call, both halves, for the reason every other package bootstrap is one - * call: a scope that has the components has the words that describe them, and - * two installers kept in step by hand eventually are not. - * - * Declarative only. Nothing here installs a provider, spawns a process, reads a - * credential or reaches the network — which is what lets `xmd syntax` and - * document validation enter it to describe the profile a run would have. - * - * The thirty-five Markdown components are not installed here: a declaration - * crosses on an `ExecutionInstallation`, by value, rather than being registered - * into a scope. {@link reviewComponentDeclarations} is that half. - */ -export function* useReviewComponents(): Operation { - yield* registerComponents(REVIEW_REGISTRATIONS); - yield* contributeDocumentation(reviewDocumentation); -} diff --git a/packages/code-review-agent/src/types.ts b/packages/code-review-agent/src/types.ts deleted file mode 100644 index c1a777b2e..000000000 --- a/packages/code-review-agent/src/types.ts +++ /dev/null @@ -1,174 +0,0 @@ -/** - * Types for the code review agent's parsed PR representation - * and Oxlint diagnostic structures. - */ - -export interface PR { - files: DiffFile[]; - added: DiffLine[]; - removed: DiffLine[]; - created: DiffFile[]; - modified: DiffFile[]; - deleted: DiffFile[]; - directories: Set; - /** Concatenated content of all added lines. */ - addedSource: string; - /** addedSource truncated to 80,000 characters. */ - diffPreview: string; - stats: { - totalFiles: number; - additions: number; - deletions: number; - totalChanges: number; - }; - meta: { - title: string; - body: string; - number: string; - }; -} - -export interface DiffFile { - path: string; - status: "A" | "M" | "D" | "R" | "C"; - hunks: DiffHunk[]; - language: string; - isTest: boolean; - isConfig: boolean; - isTypeDeclaration: boolean; -} - -export interface DiffHunk { - header: string; - lines: DiffLine[]; -} - -export interface DiffLine { - type: "add" | "remove" | "context"; - content: string; - file: string; - lineNumber: number; - isTest: boolean; -} - -export interface OxlintDiagnostic { - ruleId: string; - severity: "error" | "warning"; - message: string; - file: string; - line: number; - column: number; - endLine?: number; - endColumn?: number; -} - -export interface DiagnosticGroup { - ruleId: string; - count: number; - files: string[]; - instances: OxlintDiagnostic[]; -} - -export interface Diagnostics { - groups: DiagnosticGroup[]; - total: number; - fileCount: number; - ruleCount: number; - byCategory: { - structural: DiagnosticGroup[]; - verbosity: DiagnosticGroup[]; - typeAware: DiagnosticGroup[]; - other: DiagnosticGroup[]; - }; - summary: string; - density: number; -} - -export interface DoctorResult { - oxlintInstalled: boolean; - oxlintVersion: string; - tsgolintInstalled: boolean; - tsgolintVersion: string; - tsconfigExists: boolean; - nodeModulesExists: boolean; - typeAwareAvailable: boolean; - filesAnalyzed: number; - filesSkipped: number; - importErrors: number; - availableRuleIds: string[]; - bloatRulesAvailable: string[]; - bloatRulesMissing: string[]; - recommendation: "type-aware" | "type-aware-filtered" | "syntax-only"; - nativeSpecifiers: { - count: number; - files: string[]; - jsr: number; - npm: number; - }; -} - -export type PolicyMode = "pr" | "repo"; - -export interface PolicyInput { - mode: PolicyMode; - diagnostics: Diagnostics; - pr?: PR; - doctor?: DoctorResult; - metadata?: { - source?: string; - generatedAt?: string; - }; -} - -export type PolicyCategory = "bloat" | "slop" | "correctness" | "scope"; - -export interface PolicyCluster { - id: string; - category: PolicyCategory; - ruleIds: string[]; - files: string[]; - count: number; - summary: string; -} - -export interface PolicyScore { - clusterId: string; - score: number; - confidence: "low" | "medium" | "high"; -} - -export interface PolicyReport { - summary: string; - clusters: PolicyCluster[]; - scores: PolicyScore[]; -} - -export type FileKind = "production" | "test" | "demo" | "config"; - -export interface FileCluster { - file: string; - kind: FileKind; - totalViolations: number; - ruleIds: string[]; - categories: { - structural: number; - verbosity: number; - typeAware: number; - other: number; - }; - coOccurrence: number; - score: number; -} - -export interface CleanupEvidence { - file: string; - ruleId: string; - line: number; - message: string; -} - -export interface CleanupAnalysis { - fileClusters: FileCluster[]; - evidence: CleanupEvidence[]; - promptContext: string; -} diff --git a/packages/code-review-agent/tests/doctor.test.ts b/packages/code-review-agent/tests/doctor.test.ts deleted file mode 100644 index ca43e1a27..000000000 --- a/packages/code-review-agent/tests/doctor.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { describe, it } from "@executablemd/test-support/bdd"; -import { expect } from "@executablemd/test-support/expect"; -import { buildDoctorResult, summarizeDoctorProbe } from "../src/doctor.ts"; - -describe("Doctor analysis", () => { - it("classifies import noise and chooses filtered type-aware mode", function* () { - const summary = summarizeDoctorProbe({ - exitCode: 1, - stderr: "", - diagnostics: [ - { - message: "Cannot find module x", - ruleId: "import/no-unresolved", - severity: "error", - file: "a.ts", - line: 1, - column: 1, - }, - { - message: "unused", - ruleId: "no-unused-vars", - severity: "warning", - file: "b.ts", - line: 2, - column: 1, - }, - ], - }); - - expect(summary.typeAwareAvailable).toBe(true); - expect(summary.recommendation).toBe("type-aware-filtered"); - expect(summary.importErrors).toBe(1); - expect(summary.filesAnalyzed).toBe(2); - expect(summary.filesSkipped).toBe(1); - expect(summary.availableRuleIds).toEqual(["import/no-unresolved", "no-unused-vars"]); - }); - - it("classifies a type-aware crash without exporting raw process output", function* () { - const summary = summarizeDoctorProbe({ - exitCode: 1, - stderr: "tsgolint panic: OOM", - diagnostics: [], - }); - const result = buildDoctorResult( - { - oxlintInstalled: true, - oxlintVersion: "oxlint 1", - tsgolintInstalled: true, - tsgolintVersion: "tsgolint 1", - tsconfigExists: true, - nodeModulesExists: true, - nativeSpecifiers: { count: 0, files: [], jsr: 0, npm: 0 }, - }, - summary, - ); - - expect(result.typeAwareAvailable).toBe(false); - expect(result.recommendation).toBe("syntax-only"); - expect(JSON.stringify(result)).not.toContain("panic"); - }); -}); diff --git a/packages/code-review-agent/tests/parse-diagnostics.test.ts b/packages/code-review-agent/tests/parse-diagnostics.test.ts deleted file mode 100644 index 7e4209aad..000000000 --- a/packages/code-review-agent/tests/parse-diagnostics.test.ts +++ /dev/null @@ -1,314 +0,0 @@ -/** - * Tests for parseDiagnostics — Oxlint JSON output parser. - * Covers PD1-PD8 from the oxlint-sensor-spec. - */ -import { describe, it } from "@executablemd/test-support/bdd"; -import { expect } from "@executablemd/test-support/expect"; -import { buildDiagnostics, parseDiagnostics } from "../src/parse-diagnostics.ts"; -import type { DoctorResult, OxlintDiagnostic, PR } from "../src/types.ts"; - -function makePR(additions = 100): PR { - return { - files: [], - added: [], - removed: [], - created: [], - modified: [], - deleted: [], - directories: new Set(), - addedSource: "", - diffPreview: "", - stats: { totalFiles: 1, additions, deletions: 0, totalChanges: additions }, - meta: { title: "Test", body: "", number: "1" }, - }; -} - -function makeDoctor(overrides: Partial = {}): DoctorResult { - return { - oxlintInstalled: true, - oxlintVersion: "0.16.0", - tsgolintInstalled: true, - tsgolintVersion: "0.16.0", - tsconfigExists: true, - nodeModulesExists: true, - typeAwareAvailable: true, - filesAnalyzed: 10, - filesSkipped: 0, - importErrors: 0, - availableRuleIds: [], - bloatRulesAvailable: [], - bloatRulesMissing: [], - recommendation: "type-aware", - nativeSpecifiers: { count: 0, files: [], jsr: 0, npm: 0 }, - ...overrides, - }; -} - -function makeDiag(ruleId: string, file: string, line = 1, message = ""): OxlintDiagnostic { - return { - ruleId, - severity: "warning", - message: message || `${ruleId} violation`, - file, - line, - column: 1, - }; -} - -describe("parseDiagnostics", () => { - it("builds directly from normalized diagnostics", function* () { - const result = buildDiagnostics([makeDiag("no-console", "src/a.ts")], makePR(), makeDoctor()); - - expect(result.total).toBe(1); - expect(result.groups[0].ruleId).toBe("no-console"); - }); - - it("PD1: groups diagnostics by ruleId", function* () { - const raw = JSON.stringify([ - makeDiag("no-unused-vars", "src/a.ts"), - makeDiag("no-unused-vars", "src/b.ts"), - makeDiag("no-console", "src/a.ts"), - ]); - - const result = parseDiagnostics(raw, makePR(), makeDoctor()); - - expect(result.groups).toHaveLength(2); - const unused = result.groups.find((g) => g.ruleId === "no-unused-vars"); - expect(unused).toBeDefined(); - expect(unused!.count).toBe(2); - expect(unused!.files).toEqual(["src/a.ts", "src/b.ts"]); - - const console = result.groups.find((g) => g.ruleId === "no-console"); - expect(console).toBeDefined(); - expect(console!.count).toBe(1); - }); - - it("PD2: computes density as total / additions", function* () { - const raw = JSON.stringify([ - makeDiag("no-unused-vars", "src/a.ts"), - makeDiag("no-unused-vars", "src/b.ts"), - makeDiag("no-console", "src/a.ts"), - makeDiag("no-console", "src/b.ts"), - makeDiag("no-console", "src/c.ts"), - ]); - - const result = parseDiagnostics(raw, makePR(50), makeDoctor()); - - expect(result.total).toBe(5); - expect(result.density).toBe(0.1); - }); - - it("PD2b: density is 0 when additions is 0", function* () { - const raw = JSON.stringify([makeDiag("no-unused-vars", "src/a.ts")]); - - const result = parseDiagnostics(raw, makePR(0), makeDoctor()); - - expect(result.density).toBe(0); - }); - - it("PD3: categorizes rules into structural, verbosity, typeAware", function* () { - const raw = JSON.stringify([ - makeDiag("no-unused-vars", "src/a.ts"), - makeDiag("no-console", "src/a.ts"), - makeDiag("no-unnecessary-type-assertion", "src/a.ts"), - makeDiag("no-inferrable-types", "src/b.ts"), - ]); - - const result = parseDiagnostics(raw, makePR(), makeDoctor()); - - expect(result.byCategory.structural.map((g) => g.ruleId)).toContain("no-unused-vars"); - expect(result.byCategory.structural.map((g) => g.ruleId)).toContain( - "no-unnecessary-type-assertion", - ); - - expect(result.byCategory.verbosity.map((g) => g.ruleId)).toContain("no-console"); - expect(result.byCategory.verbosity.map((g) => g.ruleId)).toContain("no-inferrable-types"); - - expect(result.byCategory.typeAware.map((g) => g.ruleId)).toContain( - "no-unnecessary-type-assertion", - ); - }); - - it("PD4: filters import noise in type-aware-filtered mode", function* () { - const raw = JSON.stringify([ - makeDiag("no-unused-vars", "src/a.ts"), - makeDiag("import/no-unresolved", "src/b.ts", 1, 'Cannot find module "jsr:@std/assert"'), - makeDiag("import/no-unresolved", "src/c.ts", 1, "cannot find module foo"), - ]); - - const filtered = parseDiagnostics( - raw, - makePR(), - makeDoctor({ recommendation: "type-aware-filtered" }), - ); - - expect(filtered.total).toBe(1); - expect(filtered.groups).toHaveLength(1); - expect(filtered.groups[0].ruleId).toBe("no-unused-vars"); - }); - - it("PD4b: does NOT filter noise in type-aware mode", function* () { - const raw = JSON.stringify([ - makeDiag("no-unused-vars", "src/a.ts"), - makeDiag("import/no-unresolved", "src/b.ts", 1, 'Cannot find module "jsr:@std/assert"'), - ]); - - const unfiltered = parseDiagnostics( - raw, - makePR(), - makeDoctor({ recommendation: "type-aware" }), - ); - - expect(unfiltered.total).toBe(2); - }); - - it("PD5: annotates missing rules in summary", function* () { - const raw = JSON.stringify([makeDiag("no-unused-vars", "src/a.ts")]); - - const result = parseDiagnostics( - raw, - makePR(), - makeDoctor({ - bloatRulesMissing: ["no-unnecessary-type-assertion", "no-redundant-type-constituents"], - }), - ); - - expect(result.summary).toContain("2 type-aware rules unavailable"); - expect(result.summary).toContain("Density may be understated"); - }); - - it("PD6: annotates scheme specifiers in summary", function* () { - const raw = JSON.stringify([makeDiag("no-unused-vars", "src/a.ts")]); - - const result = parseDiagnostics( - raw, - makePR(), - makeDoctor({ - nativeSpecifiers: { - count: 5, - files: ["src/a.ts", "src/b.ts"], - jsr: 3, - npm: 2, - }, - }), - ); - - expect(result.summary).toContain("5 source files use scheme specifiers"); - expect(result.summary).toContain("no-scheme-specifiers plugin"); - }); - - it("PD7: empty input returns zero diagnostics", function* () { - const result = parseDiagnostics("[]", makePR(), makeDoctor()); - - expect(result.total).toBe(0); - expect(result.density).toBe(0); - expect(result.groups).toHaveLength(0); - expect(result.fileCount).toBe(0); - expect(result.ruleCount).toBe(0); - }); - - it("accepts oxlint object shape with diagnostics array", function* () { - const raw = JSON.stringify({ - diagnostics: [makeDiag("no-unused-vars", "src/a.ts"), makeDiag("no-console", "src/b.ts")], - }); - - const result = parseDiagnostics(raw, makePR(), makeDoctor()); - - expect(result.total).toBe(2); - expect(result.groups.map((g) => g.ruleId)).toEqual(["no-unused-vars", "no-console"]); - }); - - it("accepts nested diagnostics wrapper shape", function* () { - const raw = JSON.stringify({ - diagnostics: { - diagnostics: [makeDiag("no-unused-vars", "src/a.ts")], - }, - }); - - const result = parseDiagnostics(raw, makePR(), makeDoctor()); - - expect(result.total).toBe(1); - expect(result.groups[0].ruleId).toBe("no-unused-vars"); - }); - - it("normalizes native oxlint fields", function* () { - const raw = JSON.stringify({ - diagnostics: [ - { - message: "Type 'X' is imported but never used.", - code: "eslint(no-unused-vars)", - severity: "warning", - filename: "src/example.ts", - labels: [{ span: { line: 12, column: 7 } }], - }, - ], - }); - - const result = parseDiagnostics(raw, makePR(), makeDoctor()); - - expect(result.total).toBe(1); - expect(result.groups).toHaveLength(1); - expect(result.groups[0].ruleId).toBe("no-unused-vars"); - expect(result.groups[0].files).toEqual(["src/example.ts"]); - expect(result.byCategory.structural.map((g) => g.ruleId)).toContain("no-unused-vars"); - }); - - it("preserves tolerant parsing for malformed JSON", function* () { - const result = parseDiagnostics("not valid json {{{", makePR(), makeDoctor()); - - expect(result.total).toBe(0); - expect(result.groups).toHaveLength(0); - }); - - it("preserves tolerant parsing for unsupported JSON shapes", function* () { - const result = parseDiagnostics(JSON.stringify({ output: [] }), makePR(), makeDoctor()); - - expect(result.total).toBe(0); - expect(result.groups).toHaveLength(0); - }); - - it("sorts groups by count descending", function* () { - const raw = JSON.stringify([ - makeDiag("no-console", "src/a.ts"), - makeDiag("no-unused-vars", "src/a.ts"), - makeDiag("no-unused-vars", "src/b.ts"), - makeDiag("no-unused-vars", "src/c.ts"), - ]); - - const result = parseDiagnostics(raw, makePR(), makeDoctor()); - - expect(result.groups[0].ruleId).toBe("no-unused-vars"); - expect(result.groups[0].count).toBe(3); - expect(result.groups[1].ruleId).toBe("no-console"); - expect(result.groups[1].count).toBe(1); - }); - - it("summary includes density and rule breakdown", function* () { - const raw = JSON.stringify([ - makeDiag("no-unused-vars", "src/a.ts"), - makeDiag("no-unused-vars", "src/b.ts"), - makeDiag("no-console", "src/a.ts"), - ]); - - const result = parseDiagnostics(raw, makePR(100), makeDoctor()); - - expect(result.summary).toContain("3 diagnostics across 2 files (2 rules)"); - expect(result.summary).toContain("Density: 0.030 violations/added-line"); - expect(result.summary).toContain("no-unused-vars (2)"); - expect(result.summary).toContain("no-console (1)"); - }); - - it("handles plugin-prefixed rule IDs for categorization", function* () { - const raw = JSON.stringify([ - makeDiag("eslint/no-unused-vars", "src/a.ts"), - makeDiag("typescript/no-inferrable-types", "src/b.ts"), - ]); - - const result = parseDiagnostics(raw, makePR(), makeDoctor()); - - expect(result.byCategory.structural.map((g) => g.ruleId)).toContain("eslint/no-unused-vars"); - expect(result.byCategory.verbosity.map((g) => g.ruleId)).toContain( - "typescript/no-inferrable-types", - ); - }); -}); diff --git a/packages/code-review-agent/tests/parse-diff.test.ts b/packages/code-review-agent/tests/parse-diff.test.ts deleted file mode 100644 index 6d703c185..000000000 --- a/packages/code-review-agent/tests/parse-diff.test.ts +++ /dev/null @@ -1,254 +0,0 @@ -/** - * Tests for parseDiff — unified diff parser. - */ -import { describe, it } from "@executablemd/test-support/bdd"; -import { expect } from "@executablemd/test-support/expect"; -import { parseDiff } from "../src/parse-diff.ts"; - -const META = { title: "Test PR", body: "Test body", number: "42" }; - -describe("parseDiff", () => { - it("parses a simple added file", function* () { - const rawDiff = [ - "diff --git a/src/hello.ts b/src/hello.ts", - "new file mode 100644", - "--- /dev/null", - "+++ b/src/hello.ts", - "@@ -0,0 +1,3 @@", - "+export function hello() {", - '+ return "hello";', - "+}", - ].join("\n"); - const rawFiles = "A\tsrc/hello.ts"; - - const pr = parseDiff(rawDiff, rawFiles, META); - - expect(pr.files).toHaveLength(1); - expect(pr.files[0].path).toBe("src/hello.ts"); - expect(pr.files[0].status).toBe("A"); - expect(pr.files[0].language).toBe("typescript"); - expect(pr.files[0].isTest).toBe(false); - expect(pr.files[0].isConfig).toBe(false); - expect(pr.created).toHaveLength(1); - expect(pr.stats.additions).toBe(3); - expect(pr.stats.deletions).toBe(0); - expect(pr.stats.totalFiles).toBe(1); - }); - - it("parses modified and deleted files", function* () { - const rawDiff = [ - "diff --git a/src/a.ts b/src/a.ts", - "--- a/src/a.ts", - "+++ b/src/a.ts", - "@@ -1,2 +1,3 @@", - " const x = 1;", - "+const y = 2;", - " export { x };", - "diff --git a/src/b.ts b/src/b.ts", - "deleted file mode 100644", - "--- a/src/b.ts", - "+++ /dev/null", - "@@ -1,2 +0,0 @@", - "-const old = true;", - "-export { old };", - ].join("\n"); - const rawFiles = "M\tsrc/a.ts\nD\tsrc/b.ts"; - - const pr = parseDiff(rawDiff, rawFiles, META); - - expect(pr.files).toHaveLength(2); - expect(pr.modified).toHaveLength(1); - expect(pr.deleted).toHaveLength(1); - expect(pr.stats.additions).toBe(1); - expect(pr.stats.deletions).toBe(2); - expect(pr.stats.totalChanges).toBe(3); - }); - - it("handles rename detection", function* () { - const rawDiff = [ - "diff --git a/old-name.ts b/new-name.ts", - "similarity index 100%", - "rename from old-name.ts", - "rename to new-name.ts", - ].join("\n"); - const rawFiles = "R100\told-name.ts\tnew-name.ts"; - - const pr = parseDiff(rawDiff, rawFiles, META); - - expect(pr.files).toHaveLength(1); - expect(pr.files[0].path).toBe("new-name.ts"); - expect(pr.files[0].status).toBe("R"); - expect(pr.files[0].hunks).toHaveLength(0); - expect(pr.stats.totalChanges).toBe(0); - }); - - it("skips binary files", function* () { - const rawDiff = [ - "diff --git a/image.png b/image.png", - "new file mode 100644", - "Binary files /dev/null and b/image.png differ", - ].join("\n"); - const rawFiles = "A\timage.png"; - - const pr = parseDiff(rawDiff, rawFiles, META); - - expect(pr.files).toHaveLength(0); - }); - - it("classifies test files", function* () { - const rawDiff = [ - "diff --git a/src/hello.test.ts b/src/hello.test.ts", - "new file mode 100644", - "--- /dev/null", - "+++ b/src/hello.test.ts", - "@@ -0,0 +1,1 @@", - '+it("works", () => {});', - ].join("\n"); - const rawFiles = "A\tsrc/hello.test.ts"; - - const pr = parseDiff(rawDiff, rawFiles, META); - - expect(pr.files[0].isTest).toBe(true); - expect(pr.added[0].isTest).toBe(true); - }); - - it("classifies config files", function* () { - const rawDiff = [ - "diff --git a/package.json b/package.json", - "--- a/package.json", - "+++ b/package.json", - "@@ -1,1 +1,2 @@", - ' { "name": "test"', - '+, "version": "1.0.0" }', - ].join("\n"); - const rawFiles = "M\tpackage.json"; - - const pr = parseDiff(rawDiff, rawFiles, META); - - expect(pr.files[0].isConfig).toBe(true); - }); - - it("classifies type declarations", function* () { - const rawDiff = [ - "diff --git a/types/global.d.ts b/types/global.d.ts", - "new file mode 100644", - "--- /dev/null", - "+++ b/types/global.d.ts", - "@@ -0,0 +1,1 @@", - "+declare const foo: string;", - ].join("\n"); - const rawFiles = "A\ttypes/global.d.ts"; - - const pr = parseDiff(rawDiff, rawFiles, META); - - expect(pr.files[0].isTypeDeclaration).toBe(true); - }); - - it("truncates diffPreview at 40K chars", function* () { - const longLine = "x".repeat(100_000); - const rawDiff = [ - "diff --git a/big.ts b/big.ts", - "new file mode 100644", - "--- /dev/null", - "+++ b/big.ts", - "@@ -0,0 +1,1 @@", - "+" + longLine, - ].join("\n"); - const rawFiles = "A\tbig.ts"; - - const pr = parseDiff(rawDiff, rawFiles, META); - - expect(pr.addedSource.length).toBe(100_000); - expect(pr.diffPreview.length).toBe(40_000); - }); - - it("computes directories at depth 2", function* () { - const rawDiff = [ - "diff --git a/src/components/a.ts b/src/components/a.ts", - "--- /dev/null", - "+++ b/src/components/a.ts", - "@@ -0,0 +1,1 @@", - "+const a = 1;", - "diff --git a/src/utils/b.ts b/src/utils/b.ts", - "--- /dev/null", - "+++ b/src/utils/b.ts", - "@@ -0,0 +1,1 @@", - "+const b = 2;", - "diff --git a/root.ts b/root.ts", - "--- /dev/null", - "+++ b/root.ts", - "@@ -0,0 +1,1 @@", - "+const c = 3;", - ].join("\n"); - const rawFiles = "A\tsrc/components/a.ts\nA\tsrc/utils/b.ts\nA\troot.ts"; - - const pr = parseDiff(rawDiff, rawFiles, META); - - expect(pr.directories.size).toBe(3); - expect(pr.directories.has("src/components")).toBe(true); - expect(pr.directories.has("src/utils")).toBe(true); - expect(pr.directories.has(".")).toBe(true); - }); - - it("handles empty diff", function* () { - const pr = parseDiff("", "", META); - - expect(pr.files).toHaveLength(0); - expect(pr.added).toHaveLength(0); - expect(pr.removed).toHaveLength(0); - expect(pr.stats.totalChanges).toBe(0); - expect(pr.meta.title).toBe("Test PR"); - expect(pr.meta.number).toBe("42"); - }); - - it("preserves meta passthrough", function* () { - const pr = parseDiff("", "", { - title: "feat: add feature", - body: "This PR adds a feature.\n\nFixes #123", - number: "456", - }); - - expect(pr.meta.title).toBe("feat: add feature"); - expect(pr.meta.body).toContain("Fixes #123"); - expect(pr.meta.number).toBe("456"); - }); - - it("infers languages correctly", function* () { - const files = [ - "a.ts", - "b.js", - "c.py", - "d.go", - "e.md", - "f.json", - "g.yaml", - "Dockerfile", - "h.unknown", - ]; - const rawDiff = files - .map((f) => - [ - `diff --git a/${f} b/${f}`, - `--- /dev/null`, - `+++ b/${f}`, - `@@ -0,0 +1,1 @@`, - `+content`, - ].join("\n"), - ) - .join("\n"); - const rawFiles = files.map((f) => `A\t${f}`).join("\n"); - - const pr = parseDiff(rawDiff, rawFiles, META); - - const langs = new Map(pr.files.map((f) => [f.path, f.language])); - expect(langs.get("a.ts")).toBe("typescript"); - expect(langs.get("b.js")).toBe("javascript"); - expect(langs.get("c.py")).toBe("python"); - expect(langs.get("d.go")).toBe("go"); - expect(langs.get("e.md")).toBe("markdown"); - expect(langs.get("f.json")).toBe("json"); - expect(langs.get("g.yaml")).toBe("yaml"); - expect(langs.get("Dockerfile")).toBe("docker"); - expect(langs.get("h.unknown")).toBe("unknown"); - }); -}); diff --git a/packages/code-review-agent/tests/parse-doctor.test.ts b/packages/code-review-agent/tests/parse-doctor.test.ts deleted file mode 100644 index 7000b20fd..000000000 --- a/packages/code-review-agent/tests/parse-doctor.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { describe, it } from "@executablemd/test-support/bdd"; -import { expect } from "@executablemd/test-support/expect"; -import { parseDoctorResult } from "../mod.ts"; - -describe("parseDoctorResult", () => { - it("parses a complete Doctor value", function* () { - const result = parseDoctorResult( - JSON.stringify({ - oxlintInstalled: true, - recommendation: "type-aware", - nativeSpecifiers: { count: 2, files: ["a.ts"], jsr: 1, npm: 1 }, - }), - ); - - expect(result.oxlintInstalled).toBe(true); - expect(result.recommendation).toBe("type-aware"); - expect(result.nativeSpecifiers.count).toBe(2); - }); - - it("returns defaults for malformed input", function* () { - const result = parseDoctorResult("not json"); - - expect(result.oxlintInstalled).toBe(false); - expect(result.recommendation).toBe("syntax-only"); - expect(result.nativeSpecifiers.files).toEqual([]); - }); -}); diff --git a/packages/code-review-agent/tests/parse-oxlint.test.ts b/packages/code-review-agent/tests/parse-oxlint.test.ts deleted file mode 100644 index d3e0336f5..000000000 --- a/packages/code-review-agent/tests/parse-oxlint.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { describe, it } from "@executablemd/test-support/bdd"; -import { expect } from "@executablemd/test-support/expect"; -import { normalizeOxlintOutput } from "../src/parse-oxlint.ts"; - -describe("Oxlint normalization", () => { - it("keeps only the bounded diagnostic fields and filters files", function* () { - const result = normalizeOxlintOutput( - JSON.stringify({ - diagnostics: [ - { - message: "unused", - code: "eslint(no-unused-vars)", - severity: "warning", - filename: "src/a.ts", - labels: [{ span: { line: 4, column: 7 } }], - source: "SECRET_SOURCE_EXCERPT", - cause: "ARBITRARY_CAUSE", - url: "https://example.invalid/diagnostic", - }, - { message: "other", file: "src/b.ts" }, - ], - stderr: "UNBOUNDED_STDERR", - }), - ["src/a.ts"], - ); - - expect(result).toEqual([ - { - message: "unused", - ruleId: "no-unused-vars", - severity: "warning", - file: "src/a.ts", - line: 4, - column: 7, - }, - ]); - }); - - it("rejects malformed or unsupported output", function* () { - let caught: unknown; - try { - normalizeOxlintOutput("not json"); - } catch (error) { - caught = error; - } - expect(caught).toBeInstanceOf(Error); - if (caught instanceof Error) { - expect(caught.message).toContain("malformed JSON"); - expect(caught.cause).toBeInstanceOf(SyntaxError); - } - expect(() => normalizeOxlintOutput(JSON.stringify({ output: [] }))).toThrow( - "diagnostics array", - ); - }); -}); diff --git a/packages/code-review-agent/tests/policy.test.ts b/packages/code-review-agent/tests/policy.test.ts deleted file mode 100644 index f8a14c0d1..000000000 --- a/packages/code-review-agent/tests/policy.test.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { describe, it } from "@executablemd/test-support/bdd"; -import { expect } from "@executablemd/test-support/expect"; -import { - buildCleanupAnalysis, - buildPolicyInput, - buildPolicyReport, - clusterByFile, - clusterDiagnostics, - extractEvidence, - scoreClusters, -} from "../src/policy.ts"; -import type { Diagnostics } from "../src/types.ts"; - -function diagnosticsFixture(): Diagnostics { - const unusedA = { - ruleId: "no-unused-vars", - severity: "warning" as const, - message: "'x' is defined but never used.", - file: "src/a.ts", - line: 10, - column: 1, - }; - const unusedATwo = { - ...unusedA, - line: 14, - }; - const emptyA = { - ruleId: "no-empty-function", - severity: "warning" as const, - message: "Unexpected empty function.", - file: "src/a.ts", - line: 20, - column: 1, - }; - const consoleDemo = { - ruleId: "no-console", - severity: "warning" as const, - message: "Unexpected console statement.", - file: "demo/b.ts", - line: 3, - column: 1, - }; - - return { - groups: [ - { - ruleId: "no-unused-vars", - count: 3, - files: ["src/a.ts", "demo/b.ts"], - instances: [ - unusedA, - unusedATwo, - { - ...unusedA, - file: "demo/b.ts", - line: 9, - }, - ], - }, - { - ruleId: "no-console", - count: 2, - files: ["demo/b.ts"], - instances: [ - consoleDemo, - { - ...consoleDemo, - line: 7, - }, - ], - }, - { - ruleId: "no-empty-function", - count: 1, - files: ["src/a.ts"], - instances: [emptyA], - }, - ], - total: 6, - fileCount: 2, - ruleCount: 3, - byCategory: { - structural: [ - { - ruleId: "no-unused-vars", - count: 3, - files: ["src/a.ts", "demo/b.ts"], - instances: [ - unusedA, - unusedATwo, - { - ...unusedA, - file: "demo/b.ts", - line: 9, - }, - ], - }, - { - ruleId: "no-empty-function", - count: 1, - files: ["src/a.ts"], - instances: [emptyA], - }, - ], - verbosity: [ - { - ruleId: "no-console", - count: 2, - files: ["demo/b.ts"], - instances: [ - consoleDemo, - { - ...consoleDemo, - line: 7, - }, - ], - }, - ], - typeAware: [], - other: [], - }, - summary: "fixture", - density: 0.6, - }; -} - -describe("policy helpers", () => { - it("builds policy input", function* () { - const diagnostics = diagnosticsFixture(); - const input = buildPolicyInput("repo", diagnostics, { - source: "tests", - }); - - expect(input.mode).toBe("repo"); - expect(input.diagnostics.total).toBe(6); - expect(input.metadata?.source).toBe("tests"); - expect(typeof input.metadata?.generatedAt).toBe("string"); - }); - - it("clusters diagnostics by category", function* () { - const diagnostics = diagnosticsFixture(); - const clusters = clusterDiagnostics(buildPolicyInput("pr", diagnostics)); - - expect(clusters).toHaveLength(3); - expect(clusters[0].category).toBe("bloat"); - expect(clusters[1].category).toBe("slop"); - expect(clusters[2].category).toBe("bloat"); - }); - - it("scores clusters and produces report", function* () { - const diagnostics = diagnosticsFixture(); - const input = buildPolicyInput("pr", diagnostics); - const clusters = clusterDiagnostics(input); - const scores = scoreClusters(clusters); - const report = buildPolicyReport(input); - - expect(scores).toHaveLength(3); - expect(report.clusters).toHaveLength(3); - expect(report.scores).toHaveLength(3); - expect(report.summary).toContain("3 clusters"); - }); - - it("builds ranked file clusters", function* () { - const diagnostics = diagnosticsFixture(); - const fileClusters = clusterByFile(diagnostics); - - expect(fileClusters).toHaveLength(2); - expect(fileClusters[0].file).toBe("src/a.ts"); - expect(fileClusters[0].kind).toBe("production"); - expect(fileClusters[0].coOccurrence).toBe(2); - expect(fileClusters[0].score).toBeGreaterThan(fileClusters[1].score); - }); - - it("extracts evidence from top ranked files", function* () { - const diagnostics = diagnosticsFixture(); - const fileClusters = clusterByFile(diagnostics); - const evidence = extractEvidence(diagnostics, fileClusters, 1); - - expect(evidence.length).toBeGreaterThan(0); - expect(evidence.every((entry) => entry.file === "src/a.ts")).toBe(true); - }); - - it("builds cleanup analysis prompt context", function* () { - const diagnostics = diagnosticsFixture(); - const cleanup = buildCleanupAnalysis(diagnostics, 2); - - expect(cleanup.fileClusters).toHaveLength(2); - expect(cleanup.evidence.length).toBeGreaterThan(0); - expect(cleanup.promptContext).toContain("RANKED FILE CLUSTERS"); - expect(cleanup.promptContext).toContain("EVIDENCE"); - }); -}); diff --git a/packages/core/src/sample-api.ts b/packages/core/src/sample-api.ts index 57be0f45f..776cc2971 100644 --- a/packages/core/src/sample-api.ts +++ b/packages/core/src/sample-api.ts @@ -1,7 +1,7 @@ /** * The Sample Api — Effection Api for LLM inference. * - * Provider components (e.g., DeepInfraProvider, OllamaProvider) install + * Provider components (e.g., OllamaProvider) install * middleware via `yield* Sample.around(...)` to route sample calls to * their inference server. * @@ -47,7 +47,7 @@ export const Sample: Api = createApi("Sample", { *sample(_context: SampleContext): Operation { throw new Error( "Sample Api requires provider middleware — " + - "install a provider (e.g., DeepInfraProvider, OllamaProvider) or " + + "install a provider (e.g., OllamaProvider) or " + "yield* Sample.around({ *sample([ctx], next) { ... } }) " + "before using components", ); diff --git a/packages/core/tests/component-schema-conformance.test.ts b/packages/core/tests/component-schema-conformance.test.ts index 9fe91b735..11066359d 100644 --- a/packages/core/tests/component-schema-conformance.test.ts +++ b/packages/core/tests/component-schema-conformance.test.ts @@ -7,12 +7,7 @@ import { parseFrontmatter } from "../src/frontmatter.ts"; import { compilePropsSchema } from "../src/validate.ts"; const ROOT = new URL("../../../", import.meta.url); -const COMPONENT_DIRS = [ - "packages/core/components", - "smoke-test", - "packages/code-review-agent/src/documents/components", - "packages/code-review-agent/src/documents/policies", -]; +const COMPONENT_DIRS = ["packages/core/components", "smoke-test"]; describe("component schema conformance", () => { it("every markdown component's props schema parses and compiles", function* () { diff --git a/packages/core/tests/eval-transform.test.ts b/packages/core/tests/eval-transform.test.ts index becbbd11f..84ec177d1 100644 --- a/packages/core/tests/eval-transform.test.ts +++ b/packages/core/tests/eval-transform.test.ts @@ -136,14 +136,12 @@ describe("User import extraction", () => { // T17: Single-line import → extracted in userImports, removed from code it("T17: single-line import extracted", function* () { const result = transformBlock( - 'import { parseDiff } from "@executablemd/code-review-agent";\n\nconst pr = parseDiff();', + 'import { parseDiff } from "@scope/diff";\n\nconst pr = parseDiff();', "block-17", [], ); expect(result.userImports).toHaveLength(1); - expect(result.userImports[0]).toBe( - 'import { parseDiff } from "@executablemd/code-review-agent";', - ); + expect(result.userImports[0]).toBe('import { parseDiff } from "@scope/diff";'); expect(result.code).not.toContain("import"); expect(result.code).toContain("const pr = parseDiff();"); expect(result.exports).toContain("pr"); @@ -152,14 +150,12 @@ describe("User import extraction", () => { // T18: import type → extracted correctly (TypeScript syntax preserved) it("T18: import type preserved in userImports", function* () { const result = transformBlock( - 'import type { PR } from "@executablemd/code-review-agent";\n\nconst x = 1;', + 'import type { PR } from "@scope/diff";\n\nconst x = 1;', "block-18", [], ); expect(result.userImports).toHaveLength(1); - expect(result.userImports[0]).toBe( - 'import type { PR } from "@executablemd/code-review-agent";', - ); + expect(result.userImports[0]).toBe('import type { PR } from "@scope/diff";'); expect(result.code).not.toContain("import"); expect(result.code).toContain("const x = 1;"); }); diff --git a/packages/core/tests/unused-in-diff.test.ts b/packages/core/tests/unused-in-diff.test.ts deleted file mode 100644 index 5a272c4a5..000000000 --- a/packages/core/tests/unused-in-diff.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { describe, it, beforeAll } from "@executablemd/test-support/bdd"; -import { useTempFileCompiler } from "../src/temp-file-compiler.ts"; -import { expect } from "@executablemd/test-support/expect"; -import { readTextFile } from "@effectionx/fs"; -import type { Operation } from "effection"; -import { InMemoryStream } from "@executablemd/durable-streams"; -import { useStubFs } from "@executablemd/runtime/test"; -import { execute } from "../src/execute.ts"; -import { collect } from "../src/collect.ts"; -import { asText } from "./helpers.ts"; - -interface AddedLine { - file: string; - lineNumber: number; - content: string; -} - -function added(file: string, lineNumber: number, content: string): AddedLine { - return { file, lineNumber, content }; -} - -const WHY = "referenced ≤1× within the added diff"; - -function doc(lines: AddedLine[], construct: string): string { - return [ - "```js eval", - `const pr = ${JSON.stringify({ added: lines })};`, - "```", - "", - ``, - ].join("\n"); -} - -// The real component is read before useStubFs replaces the filesystem, so the -// tests exercise the packaged review source rather than a test-owned copy. Its -// `` is a native directive, so the stub filesystem supplies no component for -// it. -function* render(lines: AddedLine[], construct: string): Operation { - const unusedInDiff = yield* readTextFile( - "packages/code-review-agent/src/documents/components/UnusedInDiff.md", - ); - - yield* useStubFs({ - "components/UnusedInDiff.md": unusedInDiff, - "doc.md": doc(lines, construct), - }); - - return asText(yield* collect(yield* execute({ path: "doc.md", stream: new InMemoryStream() }))); -} - -describe("UnusedInDiff", () => { - beforeAll(() => useTempFileCompiler()); - it("renders the disclosure with symbol, location, count and reason", function* () { - const output = yield* render([added("core/src/a.ts", 24, "type Orphan = string;")], "type"); - - // Exact so the selected branch's summary line, table header and row - // whitespace are locked, not merely present somewhere in the output. - expect(output.trim()).toBe( - [ - "
", - "🟡 1 with no consumers: Orphan.", - "", - "| Symbol | Declared at | Refs in diff | Why flagged |", - "| --- | --- | --- | --- |", - `| \`Orphan\` | \`core/src/a.ts:24\` | 1 | ${WHY} (pre-existing usages not counted) |`, - "", - "", - "
", - ].join("\n"), - ); - }); - - it("does not flag a type named only by an import specifier", function* () { - const output = yield* render( - [added("core/src/a.ts", 1, 'import { type Mods } from "./mods.ts";')], - "type", - ); - - expect(output).not.toContain("Mods"); - expect(output).not.toContain("
"); - expect(output.trim()).toBe(""); - }); - - it("excludes a declaration referenced again within the added diff", function* () { - const output = yield* render( - [ - added("core/src/a.ts", 30, "export type Used = string;"), - added("core/src/a.ts", 31, 'const value: Used = "x";'), - ], - "type", - ); - - expect(output).not.toContain("
"); - expect(output.trim()).toBe(""); - }); - - it("renders nothing when there are no findings", function* () { - const output = yield* render([added("core/src/a.ts", 5, "const plain = 1;")], "type"); - - expect(output.trim()).toBe(""); - }); - - it("detects exported, declared and default interface declarations", function* () { - const output = yield* render( - [ - added("core/src/a.ts", 1, "export interface Alpha {"), - added("core/src/a.ts", 5, "export declare interface Beta {"), - added("core/src/a.ts", 9, "export default interface Gamma {"), - ], - "interface", - ); - - expect(output).toContain("| `Alpha` | `core/src/a.ts:1` | 1 |"); - expect(output).toContain("| `Beta` | `core/src/a.ts:5` | 1 |"); - expect(output).toContain("| `Gamma` | `core/src/a.ts:9` | 1 |"); - expect(output).not.toContain("ERROR"); - }); - - it("detects an exported declared type alias", function* () { - const output = yield* render( - [added("core/src/a.ts", 12, "export declare type Delta = string;")], - "type", - ); - - expect(output).toContain("| `Delta` | `core/src/a.ts:12` | 1 |"); - expect(output).not.toContain("ERROR"); - }); -}); diff --git a/packages/git/src/plugin.ts b/packages/git/src/plugin.ts index 0db203fd4..e0a1828a1 100644 --- a/packages/git/src/plugin.ts +++ b/packages/git/src/plugin.ts @@ -63,11 +63,9 @@ const EXECUTING_ACTIONS: ReadonlySet = new Set(["start", "resume", "fork /** * The workflow options whose value is the token after them. * - * The same list the review Plugin's own scan carries, for the same reason and - * written out for the same reason: it is every non-boolean field of the - * command's grammar (`workflowConfig` in `packages/cli/src/workflow.ts`) plus - * the aggregate and generated root properties, which are read out of argv - * before that grammar exists. The boolean switches take no value and are + * It is every non-boolean field of the command's grammar (`workflowConfig` in + * `packages/cli/src/workflow.ts`) plus the aggregate and generated root + * properties, which are read out of argv before that grammar exists. The boolean switches take no value and are * skipped as the options they are. `--plugin` is not here: the host has already * taken it out of the argv this scans. * diff --git a/packages/runtime/test/README.md b/packages/runtime/test/README.md index eb8eab8cc..ad08f242f 100644 --- a/packages/runtime/test/README.md +++ b/packages/runtime/test/README.md @@ -205,9 +205,8 @@ yield * ``` `init.expect` is part of the request, not a check a caller applies afterwards: -it says the caller treats a non-2xx as a failure, and -`packages/code-review-agent/src/documents/components/GitHubAuth.md` -is a real consumer that reads it. A substituted provider that ignores it makes +it says the caller treats a non-2xx as a failure, and the runtime's own +provider in `packages/runtime/apis.ts` honors it. A substituted provider that ignores it makes an authored `fetch(url).expect()` succeed on a 404. Count what the provider was asked to perform separately from what a document diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c6b4e2664..f5a09a21b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -105,9 +105,6 @@ importers: '@executablemd/cli': specifier: workspace:* version: link:packages/cli - '@executablemd/code-review-agent': - specifier: workspace:* - version: link:packages/code-review-agent '@executablemd/core': specifier: workspace:* version: link:packages/core @@ -243,28 +240,6 @@ importers: zod: specifier: ^4.3.6 version: 4.3.6 - devDependencies: - '@executablemd/code-review-agent': - specifier: workspace:* - version: link:../code-review-agent - - packages/code-review-agent: - dependencies: - '@effectionx/fetch': - specifier: 0.2.1 - version: 0.2.1(effection@4.1.0) - '@effectionx/fs': - specifier: 0.3.0 - version: 0.3.0(effection@4.1.0) - '@executablemd/core': - specifier: workspace:* - version: link:../core - '@executablemd/runtime': - specifier: workspace:* - version: link:../runtime - effection: - specifier: 4.1.0 - version: 4.1.0 packages/core: dependencies: diff --git a/scripts/build-npm.ts b/scripts/build-npm.ts index aafbc9d76..554e89e77 100644 --- a/scripts/build-npm.ts +++ b/scripts/build-npm.ts @@ -5,7 +5,7 @@ * deno run -A scripts/build-npm.ts [version] * * is a workspace member directory (e.g. "packages/core" or - * "packages/code-review-agent"). [version] defaults to 0.0.0-dev. Output lands + * "packages/cli"). [version] defaults to 0.0.0-dev. Output lands * in /npm. * * Everything published is derived from the member's own deno.json (name, @@ -96,10 +96,8 @@ function* packagedDocuments(pkgDir: URL): Operation { const found: string[] = []; for (const name of names) { // A recursive listing names the directories as well as what is in them, and - // each entry here becomes a `copyFile`. Packages whose documents sit flat in - // `src/documents/` never noticed; the review graph keeps its documents under - // `components/` and `policies/`, and copying a directory as a file fails the - // whole npm build with `EINVAL`. + // each entry here becomes a `copyFile`. Copying a nested directory as a file + // fails the whole npm build with `EINVAL`. const info = yield* stat(join(root, name)); if (info.isFile()) { found.push(`src/documents/${name.split(sep).join("/")}`); diff --git a/scripts/lib/compile.ts b/scripts/lib/compile.ts index f51191806..28c7d6875 100644 --- a/scripts/lib/compile.ts +++ b/scripts/lib/compile.ts @@ -39,18 +39,6 @@ export const COMPILE_FLAGS = [ */ export const EMBEDDED_PACKAGES: readonly string[] = []; -/** - * Packages whose assets the binary deliberately does not carry. - * - * `scripts/tests/packaged-document.test.ts` walks the repository for packages - * that ship documents and holds the lists above to what it finds, which is what - * keeps a new asset from being forgotten. A package outside the binary would - * fail that walk for the right reason and the wrong subject, so it is named - * here — and the same test checks that exclusion rather than trusting it, by proving - * no CLI production module imports one. - */ -export const UNEMBEDDED_PACKAGES: readonly string[] = ["packages/code-review-agent"]; - /** * Each package's `src/documents/`, embedded whole. * diff --git a/scripts/tests/bump-version.test.ts b/scripts/tests/bump-version.test.ts index 43e5c0d32..d6ece50e7 100644 --- a/scripts/tests/bump-version.test.ts +++ b/scripts/tests/bump-version.test.ts @@ -8,11 +8,11 @@ import { pathToFileURL } from "node:url"; import { bumpManifests } from "../lib/bump-version.ts"; /** - * The install step of the review and analysis workflows, which resolve the - * latest published release. A bump must leave it alone: pinning it to the - * version being cut points CI at a release that does not exist yet (#160). + * A workflow step that resolves the latest published release. A bump must + * leave it alone: pinning it to the version being cut points CI at a release + * that does not exist yet (#160). */ -const REVIEW_WORKFLOW = ` - name: Install xmd release binary +const WORKFLOW = ` - name: Install xmd release binary run: | curl -fsSL https://executable.md/install.sh | sh `; @@ -41,7 +41,7 @@ function* workspace(): Operation { } yield* ensureDir(new URL(".github/workflows/", root)); - yield* writeTextFile(new URL(".github/workflows/review.yml", root), REVIEW_WORKFLOW); + yield* writeTextFile(new URL(".github/workflows/install.yml", root), WORKFLOW); return root; } @@ -103,8 +103,6 @@ describe("bumpManifests", () => { yield* bumpManifests("2.0.0", root); - expect(yield* readTextFile(new URL(".github/workflows/review.yml", root))).toEqual( - REVIEW_WORKFLOW, - ); + expect(yield* readTextFile(new URL(".github/workflows/install.yml", root))).toEqual(WORKFLOW); }); }); diff --git a/scripts/tests/cli-npm-bin.test.ts b/scripts/tests/cli-npm-bin.test.ts index b1eaeb84e..c1ac2d49e 100644 --- a/scripts/tests/cli-npm-bin.test.ts +++ b/scripts/tests/cli-npm-bin.test.ts @@ -321,15 +321,6 @@ describe("npm CLI package", { sanitizeOps: false, sanitizeResources: false }, () yield* readTextFile(path.join(root, PKG_DIR, "src/documents", asset)), ); } - - // The review graph is not part of this package any more. It is a Plugin an - // operator selects, so its assets ship with *it* — and the emitted CLI - // neither depends on it nor carries a byte of it. A build that quietly put - // it back would be a distribution deciding what a run installs. - const manifest = yield* readEmittedManifest(root); - expect(Object.keys(manifest.dependencies ?? {})).not.toContain( - "@executablemd/code-review-agent", - ); }); /** diff --git a/scripts/tests/no-sync-filesystem.test.ts b/scripts/tests/no-sync-filesystem.test.ts index 08baa9e80..0330ee949 100644 --- a/scripts/tests/no-sync-filesystem.test.ts +++ b/scripts/tests/no-sync-filesystem.test.ts @@ -157,7 +157,7 @@ describe("local/no-sync-filesystem", () => { }); it("finds no configuration entry turning the rule off for a path", function* () { - for (const config of [".oxlintrc.json", "oxlint.shared.json", ".reviews/.oxlintrc.json"]) { + for (const config of [".oxlintrc.json", "oxlint.shared.json"]) { const source = yield* readTextFile(path.join(ROOT, config)); const off = /"local\/no-sync-filesystem":\s*(?:"off"|\["off")/u.test(source); expect([config, off]).toEqual([config, false]); diff --git a/scripts/tests/oxlint-policy.test.ts b/scripts/tests/oxlint-policy.test.ts index 17628c9e4..039d9fb61 100644 --- a/scripts/tests/oxlint-policy.test.ts +++ b/scripts/tests/oxlint-policy.test.ts @@ -10,23 +10,12 @@ import { useTempDirectory } from "@executablemd/test-support/temp"; import { ROOT, runOxlint } from "./oxlint.ts"; import { BANNED_WORDS } from "../banned-words.js"; -import { - categorizeRule, - SENSOR_RULES, - TYPE_AWARE_RULES, -} from "../../packages/code-review-agent/src/categories.ts"; -import { summarizeDoctorProbe } from "../../packages/code-review-agent/src/doctor.ts"; const SHARED = "oxlint.shared.json"; const GATE = ".oxlintrc.json"; -const SENSOR = ".reviews/.oxlintrc.json"; -/** A production module, a test module, and a review component. */ -const SUBJECTS = [ - "packages/code-review-agent/src/doctor.ts", - "packages/code-review-agent/tests/doctor.test.ts", - "packages/code-review-agent/src/components/Doctor.ts", -]; +/** A production module and a test module. */ +const SUBJECTS = ["packages/core/mod.ts", "packages/core/tests/expand.test.ts"]; const PLUGINS = ["typescript", "unicorn", "import"]; @@ -167,13 +156,6 @@ function severities(rules: Record): Map { return new Map(Object.entries(rules).map(([id, value]) => [bare(id), severity(value)])); } -function enabled(rules: Record): string[] { - return Object.entries(rules) - .filter(([, value]) => severity(value) !== "off") - .map(([id]) => id) - .sort(); -} - /** A directory of this test's own. */ function scratch(prefix: string): Operation { return useTempDirectory(`${prefix}-`); @@ -213,23 +195,16 @@ describe("Oxlint policy", () => { expect((yield* runOxlint(".oxlint-prose.json", [file])).code).not.toBe(0); }); - it("extends one canonical shared policy from both profiles", function* () { + it("extends the one shared policy from the gate", function* () { const gate = yield* config(GATE); - const sensor = yield* config(SENSOR); expect(gate.extends).toEqual(["./oxlint.shared.json"]); - expect(sensor.extends).toEqual(["../oxlint.shared.json"]); - - const fromGate = path.resolve(ROOT, gate.extends[0]); - const fromSensor = path.resolve(ROOT, ".reviews", sensor.extends[0]); - expect(fromGate).toBe(path.join(ROOT, SHARED)); - expect(fromSensor).toBe(fromGate); + expect(path.resolve(ROOT, gate.extends[0])).toBe(path.join(ROOT, SHARED)); }); - it("keeps the built-in catalog in the shared source and out of both profiles", function* () { + it("keeps the built-in catalog in the shared source and out of the gate", function* () { const shared = yield* config(SHARED); const gate = yield* config(GATE); - const sensor = yield* config(SENSOR); expect(shared.plugins).toEqual(PLUGINS); expect(shared.categories).toEqual({ @@ -239,9 +214,6 @@ describe("Oxlint policy", () => { style: "off", }); - const signals = Object.entries(shared.rules).filter(([, value]) => severity(value) === "warn"); - expect(signals.map(([id]) => bare(id)).sort()).toEqual([...SENSOR_RULES].sort()); - const denials = Object.entries(shared.rules).filter(([, value]) => severity(value) === "off"); expect(denials.map(([id]) => id).sort()).toEqual([...DENIED].sort()); @@ -253,90 +225,24 @@ describe("Oxlint policy", () => { ]); expect(gate.categories).toEqual({}); - expect(sensor.categories).toEqual({}); - expect( - Object.keys(gate.rules) - .map(bare) - .filter((id) => SENSOR_RULES.includes(id)), - ).toEqual([]); - expect(Object.keys(sensor.rules)).toEqual([]); - }); - - it("keeps the curated signal options the sensor's density model reads", function* () { - const shared = yield* config(SHARED); - - expect(shared.rules["eslint/no-unused-vars"]).toEqual([ - "warn", - { - args: "all", - argsIgnorePattern: "^_", - varsIgnorePattern: "^_", - caughtErrorsIgnorePattern: "^_", - destructuredArrayIgnorePattern: "^_", - ignoreRestSiblings: true, - }, - ]); - expect(shared.rules["eslint/no-console"]).toEqual(["warn", { allow: ["warn", "error"] }]); + const builtIn = new Set(Object.keys(shared.rules)); + expect(Object.keys(gate.rules).filter((id) => builtIn.has(id))).toEqual([]); }); - it("resolves both inheritance chains for production, test and review-component paths", function* () { + it("resolves the inheritance chain for production and test paths", function* () { for (const subject of SUBJECTS) { const gate = yield* profile(GATE, subject); - const sensor = yield* profile(SENSOR, subject); expect(gate.plugins.sort()).toEqual([...PLUGINS].sort()); - expect(sensor.plugins.sort()).toEqual([...PLUGINS].sort()); expect(Object.keys(gate.rules).length).toBeGreaterThan(0); - expect(Object.keys(sensor.rules).length).toBeGreaterThan(0); - } - }); - - it("keeps the sensor's built-in rules a subset of the gate's", function* () { - for (const subject of SUBJECTS) { - const gate = new Set(enabled((yield* profile(GATE, subject)).rules)); - const sensor = enabled((yield* profile(SENSOR, subject)).rules); - - expect(sensor.filter((rule) => !gate.has(rule))).toEqual([]); } }); - it("keeps every curated signal warning-enabled for the sensor", function* () { - for (const subject of SUBJECTS) { - const applied = severities((yield* profile(SENSOR, subject)).rules); + it("keeps the named conflicts and undecided candidates off", function* () { + const applied = severities((yield* profile(GATE, SUBJECTS[0])).rules); - for (const rule of SENSOR_RULES) { - expect([rule, applied.get(rule)]).toEqual([rule, "warn"]); - expect([rule, categorizeRule(rule)]).not.toEqual([rule, ["other"]]); - } - } - }); - - it("keeps the named conflicts and undecided candidates off in both profiles", function* () { - for (const file of [GATE, SENSOR]) { - const applied = severities((yield* profile(file, SUBJECTS[0])).rules); - - for (const rule of DENIED) { - expect([file, rule, applied.get(bare(rule))]).toEqual([file, rule, "off"]); - } - } - }); - - it("keeps the sensor advisory, with no local plugin, local rule or error severity", function* () { - const sensor = yield* config(SENSOR); - expect(sensor.jsPlugins).toEqual([]); - expect(Object.keys(sensor.rules)).toEqual([]); - expect(sensor.overrides).toEqual([]); - - for (const subject of SUBJECTS) { - const applied = yield* profile(SENSOR, subject); - const rules = [ - ...Object.entries(applied.rules), - ...applied.overrides.flatMap((entry) => Object.entries(entry.rules)), - ]; - - expect(rules.filter(([, value]) => severity(value) === "error")).toEqual([]); - expect(rules.map(([id]) => id).filter((id) => id.startsWith("local/"))).toEqual([]); - expect(applied.jsPlugins).toEqual([]); + for (const rule of DENIED) { + expect([rule, applied.get(bare(rule))]).toEqual([rule, "off"]); } }); @@ -359,7 +265,6 @@ describe("Oxlint policy", () => { it("carries both the shared and the gate's test override through inheritance", function* () { for (const subject of SUBJECTS) { const gate = yield* profile(GATE, subject); - const sensor = yield* profile(SENSOR, subject); expect(gate.overrides.map((entry) => entry.files)).toEqual([TEST_FILES, TEST_FILES]); expect(gate.overrides.map((entry) => severities(entry.rules).get("no-console"))).toEqual([ @@ -369,70 +274,37 @@ describe("Oxlint policy", () => { expect( gate.overrides.map((entry) => severities(entry.rules).get("no-redundant-test-scope")), ).toEqual([undefined, "error"]); - - expect(sensor.overrides.map((entry) => entry.files)).toEqual([TEST_FILES]); - expect(severities(sensor.overrides[0].rules).get("no-console")).toBe("off"); - expect(severities(sensor.overrides[0].rules).get("no-empty-function")).toBe("off"); } }); - it("applies the shared test override to the files the sensor lints", function* () { + it("applies the shared test override to the files the gate lints", function* () { const directory = yield* scratch("oxlint-policy"); const body = 'export function probe() {\n console.log("probe");\n}\n'; yield* ensureDir(path.join(directory, "tests")); yield* writeTextFile(path.join(directory, "tests", "probe.ts"), body); yield* writeTextFile(path.join(directory, "probe.ts"), body); - for (const file of [GATE, SENSOR]) { - const run = yield* runOxlint(file, [ - "--format=json", - path.join(directory, "tests", "probe.ts"), - path.join(directory, "probe.ts"), - ]); - const report = parseReport(run.stdout, `oxlint -c ${file}`); - const consoles = report.diagnostics.filter((entry) => entry.code === "eslint(no-console)"); - - expect(consoles.map((entry) => path.relative(directory, entry.filename))).toEqual([ - "probe.ts", - ]); - } - }); - - it("reports Doctor's catalog as the curated signals", function* () { - const available = summarizeDoctorProbe({ exitCode: 0, stderr: "", diagnostics: [] }); - expect(available.bloatRulesAvailable).toEqual([...SENSOR_RULES]); - expect(available.bloatRulesMissing).toEqual([]); + const run = yield* runOxlint(GATE, [ + "--format=json", + path.join(directory, "tests", "probe.ts"), + path.join(directory, "probe.ts"), + ]); + const report = parseReport(run.stdout, `oxlint -c ${GATE}`); + const consoles = report.diagnostics.filter((entry) => entry.code === "eslint(no-console)"); - const syntaxOnly = summarizeDoctorProbe({ - exitCode: 2, - stderr: "tsgolint panic", - diagnostics: [], - }); - const typeAware = new Set(TYPE_AWARE_RULES); - expect(syntaxOnly.bloatRulesMissing).toEqual([...TYPE_AWARE_RULES]); - expect(syntaxOnly.bloatRulesAvailable.sort()).toEqual( - SENSOR_RULES.filter((rule) => !typeAware.has(rule)).sort(), - ); + expect(consoles.map((entry) => path.relative(directory, entry.filename))).toEqual(["probe.ts"]); }); - it("pins one Oxlint and one tsgolint version across the toolchain", function* () { - const provisioning = yield* readTextFile( - path.join(ROOT, "packages/code-review-agent/src/documents/components/EnsureOxlint.md"), - ); - const oxlint = /const OXLINT_TAG = "apps_v([\d.]+)";/u.exec(provisioning); - const tsgolint = /const TSGOLINT_VERSION = "([\d.]+)";/u.exec(provisioning); - if (!oxlint || !tsgolint) { - throw new Error("EnsureOxlint.md no longer declares its Oxlint and tsgolint versions"); - } + it("pins one exact Oxlint version across the toolchain", function* () { + const nodeManifest = JSON.parse(yield* readTextFile(path.join(ROOT, "package.json"))); + const version = nodeManifest.devDependencies.oxlint; + expect(version).toMatch(/^\d+\.\d+\.\d+$/u); const denoConfig = yield* readTextFile(path.join(ROOT, "deno.json")); - const nodeManifest = yield* readTextFile(path.join(ROOT, "package.json")); - expect(denoConfig).toContain(`"oxlint": "npm:oxlint@${oxlint[1]}"`); - expect(denoConfig).toContain(`"oxlint-tsgolint": "npm:oxlint-tsgolint@${tsgolint[1]}"`); - expect(nodeManifest).toContain(`"oxlint": "${oxlint[1]}"`); + expect(denoConfig).toContain(`"oxlint": "npm:oxlint@${version}"`); const run = yield* runOxlint(GATE, ["--version"]); - expect(run.stdout.trim()).toContain(oxlint[1]); + expect(run.stdout.trim()).toContain(version); }); it("fails rather than reporting an empty policy when inheritance is broken", function* () { diff --git a/scripts/tests/packaged-document.test.ts b/scripts/tests/packaged-document.test.ts index 6fa5ea0e5..f891d3fa1 100644 --- a/scripts/tests/packaged-document.test.ts +++ b/scripts/tests/packaged-document.test.ts @@ -22,10 +22,9 @@ * * The third kind still decides the other two. An embedded package carries * everything inside it, so a `src/documents/` or a `components.md` beneath one - * is already in the binary and must *not* be named again: the code-review - * package ships both, and naming either individually would compile the same - * bytes twice while reading as the coverage the whole-package entry already - * provides. Each discovered asset is therefore required in exactly one place — + * is already in the binary and must *not* be named again: naming either + * individually would compile the same bytes twice while reading as the + * coverage the whole-package entry already provides. Each discovered asset is therefore required in exactly one place — * the individual list when nothing covers it, the whole-package entry when * something does. */ @@ -44,7 +43,6 @@ import { EMBEDDED_PACKAGES, PACKAGED_DOCUMENTATION, PACKAGED_DOCUMENTS, - UNEMBEDDED_PACKAGES, } from "../lib/compile.ts"; const ROOT = fileURLToPath(new URL("../../", import.meta.url)); @@ -58,12 +56,7 @@ interface PackagedDocuments { } function* members(): Operation { - const all = (yield* until(readdir(path.join(ROOT, "packages")))).sort(); - // A package the binary does not contain ships its assets for whoever - // installed *it*, and embedding them would put bytes in the binary no code - // in it can reach. The claim that it is outside the binary is checked below - // rather than taken on trust. - return all.filter((member) => !UNEMBEDDED_PACKAGES.includes(`packages/${member}`)); + return (yield* until(readdir(path.join(ROOT, "packages")))).sort(); } /** Repository-relative paths beneath `directory`, or none when it does not exist. */ @@ -161,26 +154,6 @@ describe("the canonical compile inputs", () => { coverage(documentation, PACKAGED_DOCUMENTATION); }); - it("leave a package out of the binary only when nothing in the binary imports it", function* () { - // The filter above hides a package from the sweep, so it is exactly the - // kind of claim that rots. Every entry has to still be true: a CLI - // production module importing one would put it in the binary's module - // graph, and its assets would then be missing rather than deliberately - // absent. - expect(UNEMBEDDED_PACKAGES.length).toBeGreaterThan(0); - const sources = (yield* beneath("packages/cli/src")).filter((entry) => entry.endsWith(".ts")); - expect(sources.length).toBeGreaterThan(0); - const manifest = JSON.parse(yield* readTextFile(path.join(ROOT, "packages/cli/package.json"))); - for (const excluded of UNEMBEDDED_PACKAGES) { - const name = JSON.parse(yield* readTextFile(path.join(ROOT, excluded, "package.json"))).name; - for (const source of sources) { - const text = yield* readTextFile(path.join(ROOT, source)); - expect([source, text.includes(name)]).toEqual([source, false]); - } - expect(Object.keys(manifest.dependencies ?? {})).not.toContain(name); - } - }); - it("embed exactly those, and nothing else", function* () { const argv = compileArguments({ output: "dist/xmd" }); const included = argv.flatMap((token, index) => diff --git a/scripts/tests/plan-component-compiled.test.ts b/scripts/tests/plan-component-compiled.test.ts index 80221f129..88e7a068e 100644 --- a/scripts/tests/plan-component-compiled.test.ts +++ b/scripts/tests/plan-component-compiled.test.ts @@ -347,86 +347,52 @@ describe("compiled xmd", { sanitizeOps: false, sanitizeResources: false }, () => * asked for by name. */ describe( - "the trusted review graph, as both installations load it", + "a selected Plugin's own names, as both installations load it", { sanitizeOps: false, sanitizeResources: false }, () => { /** - * The review graph belongs to the Plugin an operator selected, in both - * installations, and a checkout cannot take a name back from it. - * - * A review is the one program that must not be answerable by the thing it is - * reviewing. While its components lived in `.reviews/` and every entrypoint - * reached them with `--include`, a pull request could add its own - * `Finding.md` and the review would run the branch's copy while reporting on - * it. So the graph moved into the package, and this is the proof that it - * actually reaches a person's installation when they select it. XMD bundles - * no Plugin: both installations name the package by path, which is also the - * route an installation with no checkout has. + * The names a selected Plugin declares or reserves belong to it, in both + * installations, and a checkout cannot take one back. * * Everything here is deliberately hostile to the mechanism. The working * directory is not the checkout, so a lookup that resolved relative to the - * process or to a subject tree finds nothing. No review include is passed, so - * a build that still depended on one renders nothing. And the one include - * that *is* passed contains a same-named `Finding.md` that accepts exactly the - * props the real one accepts — it would succeed and quietly emit its own - * marker if selection preferred it, rather than failing in a way that would - * pass this case for the wrong reason. + * process or to a subject tree finds nothing. The one include that is + * passed contains same-named `Shadowed.md` and `Stamp.md` files — one for + * the declared Markdown tier, one for the reserved registration — that + * would succeed and quietly emit their own markers if selection preferred + * them, rather than failing in a way that would pass this case for the + * wrong reason. * * The unrelated `CustomProbe.md` in the same directory is the other half: - * reserving forty-one names is not the same as turning discovery off, and a - * change that secured the review by disabling `--include` would be a - * regression this would catch. - * - * No agent, no network, no credential and no browser: the components chosen - * here render from their own bytes, which is what makes this cheap enough to - * be an ordinary case. + * reserving names is not the same as turning discovery off, and a change + * that secured them by disabling `--include` would be a regression this + * would catch. */ - it("supplies the trusted review graph to both installations that select it", function* () { + it("supplies the selected Plugin's components to both installations", function* () { if (!(yield* exists(BINARY))) { throw new Error(`${BINARY} is missing — run \`deno task build\` before this case`); } - const FINDING = path.join( - ROOT, - "packages/code-review-agent/src/documents/components/Finding.md", - ); - const trusted = yield* readTextFile(FINDING); - const digest = createHash("sha256").update(trusted, "utf8").digest("hex"); + // The bytes `declaring.mjs` declares for ``. + const shadowed = "the selected declaration, not the checkout's\n"; + const digest = createHash("sha256").update(shadowed, "utf8").digest("hex"); - const elsewhere = yield* until(mkdtemp(path.join(tmpdir(), "xmd-trusted-review-"))); + const elsewhere = yield* until(mkdtemp(path.join(tmpdir(), "xmd-selected-plugin-"))); yield* ensure(() => rm(elsewhere, { recursive: true, force: true })); const custom = path.join(elsewhere, "custom"); yield* until(mkdir(custom)); - yield* writeTextFile( - path.join(custom, "Finding.md"), - [ - "---", - "props:", - " type: object", - " properties:", - " when: { type: boolean }", - " severity: { type: string }", - " message: { type: string }", - " required: [when, message]", - " additionalProperties: false", - "---", - "", - "HOSTILE-FINDING {props.message}", - ].join("\n"), - ); + yield* writeTextFile(path.join(custom, "Shadowed.md"), "HOSTILE-SHADOWED"); + yield* writeTextFile(path.join(custom, "Stamp.md"), "HOSTILE-STAMP"); yield* writeTextFile(path.join(custom, "CustomProbe.md"), "the caller's own component ran"); yield* writeTextFile( path.join(elsewhere, "doc.md"), - ['', "", ""].join( - "\n", - ), + ["", "", "", "", ""].join("\n"), ); - // The graph is a Plugin an operator selects, so both installations name - // it the same way: an explicit path to the package that ships it. XMD - // bundles none, and neither of these carries it until it is named. - const review = `--plugin=${path.join(ROOT, "packages/code-review-agent/mod.ts")}`; + // Both installations name the Plugin the same way: an explicit path, which + // is also the route an installation with no checkout has. + const plugin = `--plugin=${path.join(ROOT, "packages/cli/tests/fixtures/plugins/declaring.mjs")}`; const installations = [ ["compiled", BINARY, [] as string[]], ["source", Deno.execPath(), ["run", "--allow-all", path.join(ROOT, SOURCE_ENTRY)]], @@ -438,28 +404,27 @@ describe( for (const [label, command, args] of installations) { const attempt = yield* timebox(TIMEOUT, function* () { return yield* exec(command, { - // One include, and it is the caller's. The Plugin is named, and - // nothing names the review components — which is the entire claim: - // a selected declaration beats a same-named file in the include. - arguments: [...args, "run", review, "doc.md", "--include", "custom"], + // One include, and it is the caller's: a selected declaration or + // reservation beats a same-named file in it. + arguments: [...args, "run", plugin, "doc.md", "--include", "custom"], cwd: elsewhere, }).join(); }); if (attempt.timeout) { - throw new Error(`the ${label} installation timed out running the review components`); + throw new Error(`the ${label} installation timed out running the Plugin's components`); } const run = attempt.value; expect(`${label}: ${run.code}\n${run.stderr}`).toBe(`${label}: 0\n`); rendered[label] = run.stdout; - // The declared component rendered: its own `ts eval` chose the icon from - // the severity it was given, which the caller's plain-prose copy could - // not have produced. - expect(`${label}: ${run.stdout.includes("🔴 probe")}`).toBe(`${label}: true`); - // And the checkout's same-named component never ran. - expect(`${label}: ${run.stdout.includes("HOSTILE-FINDING")}`).toBe(`${label}: false`); + expect(`${label}: ${run.stdout.includes(shadowed.trim())}`).toBe(`${label}: true`); + expect(`${label}: ${run.stdout.includes("stamped by the selected Plugin")}`).toBe( + `${label}: true`, + ); + // And the checkout's same-named components never ran. + expect(`${label}: ${run.stdout.includes("HOSTILE")}`).toBe(`${label}: false`); // While the caller's unrelated component resolved exactly as it always - // did. A change that secured the review by disabling inclusion fails here. + // did. A change that secured the names by disabling inclusion fails here. expect(`${label}: ${run.stdout.includes("the caller's own component ran")}`).toBe( `${label}: true`, ); @@ -467,9 +432,8 @@ describe( const described = yield* timebox(TIMEOUT, function* () { return yield* exec(command, { // The same selection: what an installation describes is what it - // would run, and describing it without the Plugin would describe a - // vocabulary the run above did not have. - arguments: [...args, "syntax", review, "--json", "--include", "custom"], + // would run. + arguments: [...args, "syntax", plugin, "--json", "--include", "custom"], cwd: elsewhere, }).join(); }); @@ -482,48 +446,40 @@ describe( (category: { entries: unknown[] }) => category.entries, ); - const finding = entries.filter((entry: { name?: string }) => entry?.name === "Finding"); + const declared = entries.filter((entry: { name?: string }) => entry?.name === "Shadowed"); // Once. A build listing it twice would be describing the caller's copy // beside the declared one, which is a vocabulary no run has. - expect(`${label}: ${finding.length}`).toBe(`${label}: 1`); - // The identity, whole: the package and the asset rather than a path, and - // the digest of the bytes this build actually ships. A build carrying - // different bytes under this name answers differently here. - expect(finding[0].origin).toEqual({ + expect(`${label}: ${declared.length}`).toBe(`${label}: 1`); + // The identity, whole: the declared origin rather than a path, and the + // digest of the bytes the Plugin states. + expect(declared[0].origin).toEqual({ kind: "declared-markdown", - origin: "@executablemd/code-review-agent/components/Finding.md", + origin: "fixture/declaring/Shadowed.md", digest, }); - expect(finding[0].sourceKind).toBe("declared-markdown"); - reported[label] = finding[0]; - - // The TypeScript half of the same graph is equally claimed. Moving only - // the Markdown would have left shadowable exactly the six components that - // run processes, read credentials and reach the network. - const context = entries.filter( - (entry: { name?: string }) => entry?.name === "ReviewContext", - ); - expect(`${label}: ${context.length}`).toBe(`${label}: 1`); - expect(context[0].origin).toEqual({ + expect(declared[0].sourceKind).toBe("declared-markdown"); + reported[label] = declared[0]; + + // The registered tier is reserved as well. + const stamp = entries.filter((entry: { name?: string }) => entry?.name === "Stamp"); + expect(`${label}: ${stamp.length}`).toBe(`${label}: 1`); + expect(stamp[0].origin).toEqual({ kind: "registered", - origin: "@executablemd/code-review-agent", + origin: "fixture/declaring", reserved: true, }); - // And the whole graph is present, not just the two probed above. A build - // that shipped a partial `src/documents/` would resolve some names and - // fail on the rest at a reviewer's first run. - const declared = entries.filter( + // And every declaration is present, not just the one probed above. + const all = entries.filter( (entry: { sourceKind?: string; origin?: { origin?: string } }) => entry?.sourceKind === "declared-markdown" && - entry.origin?.origin?.startsWith("@executablemd/code-review-agent/"), + entry.origin?.origin?.startsWith("fixture/declaring/"), ); - expect(`${label}: ${declared.length}`).toBe(`${label}: 35`); + expect(`${label}: ${all.length}`).toBe(`${label}: 2`); } - // The two installations are one product. A compiled binary has no checkout - // to read from and a source run has nothing else, so an asset that reached - // one and not the other would show up here rather than at a reviewer's. + // The two installations are one product, so a declaration that reached + // one and not the other shows up here. expect(rendered.compiled).toBe(rendered.source); expect(reported.compiled).toEqual(reported.source); }); diff --git a/scripts/tests/plugin-compiled.test.ts b/scripts/tests/plugin-compiled.test.ts index 58e418b6d..217baae83 100644 --- a/scripts/tests/plugin-compiled.test.ts +++ b/scripts/tests/plugin-compiled.test.ts @@ -18,7 +18,7 @@ import { describe, it } from "@executablemd/test-support/bdd"; import { expect } from "@executablemd/test-support/expect"; import { ensure, scoped, until } from "effection"; -import { ensureDir, exists, readTextFile, rm, writeTextFile } from "@effectionx/fs"; +import { ensureDir, exists, rm, writeTextFile } from "@effectionx/fs"; import { exec } from "@effectionx/process"; import { timebox } from "@effectionx/timebox"; import type { ProcessResult } from "@effectionx/process"; @@ -127,130 +127,32 @@ describe("compiled xmd", { sanitizeOps: false, sanitizeResources: false }, () => expect(dirEntry?.origin?.origin).toBe("@executablemd/core"); expect(dirEntry?.description ?? "").toContain("directory"); // And nothing from a Plugin nobody named. - expect(names).not.toContain("Finding"); - expect(names).not.toContain("ReviewContext"); + expect(names).not.toContain("Greeting"); + expect(names).not.toContain("Stamp"); }); }); - it("gains the review graph from a Plugin the operator named by path", function* () { + it("gains a Plugin's vocabulary from a module the operator named by path", function* () { if (!(yield* exists(BINARY))) { throw new Error(`${BINARY} is missing — run \`deno task build\` before this case`); } - // No `node_modules` here and nothing embedded, so the package is reached + // No `node_modules` here and nothing embedded, so the module is reached // the way the accepted contract says it is: an explicit filesystem path. - // The binary carries its own copy of core and the package resolves the - // checkout's, which is the loaded-copy case under real conditions. + // The binary carries its own copy of core and the fixture resolves the + // checkout's, which is the loaded-copy case under real conditions — for a + // declared Markdown component and a reserved registration alike. yield* useElsewhere(function* (dir) { - const review = path.join(ROOT, "packages/code-review-agent/mod.ts"); + const declaring = path.join(ROOT, "packages/cli/tests/fixtures/plugins/declaring.mjs"); const run = yield* runBinary( - ["syntax", "--json", `--plugin=${review}`, "--include", dir], + ["syntax", "--json", `--plugin=${declaring}`, "--include", dir], dir, ); if (run.code !== 0) { throw new Error(`the compiled binary exited ${run.code}\n${run.stderr}`); } const names = describedNames(run.stdout); - expect(names).toContain("Finding"); - expect(names).toContain("ReviewContext"); - }); - }); - - it("groups diagnostics through the checked-in review roots in the compiled host", function* () { - const rawDiagnostics = [ - { - ruleId: "no-console", - message: "console", - severity: "warning", - file: "a.ts", - line: 2, - column: 3, - }, - { - ruleId: "no-console", - message: "console", - severity: "warning", - file: "b.ts", - line: 4, - column: 1, - }, - { - ruleId: "import/no-unresolved", - message: "Cannot find module", - severity: "warning", - file: "a.ts", - line: 1, - column: 1, - }, - ]; - const pr = { stats: { additions: 100 } }; - const doctor = { - oxlintInstalled: true, - oxlintVersion: "1.74.0", - tsgolintInstalled: true, - tsgolintVersion: "0.25.0", - tsconfigExists: true, - nodeModulesExists: true, - typeAwareAvailable: true, - filesAnalyzed: 2, - filesSkipped: 0, - importErrors: 1, - availableRuleIds: [], - bloatRulesAvailable: [], - bloatRulesMissing: [], - recommendation: "type-aware-filtered", - nativeSpecifiers: { count: 0, files: [], jsr: 0, npm: 0 }, - }; - const header = `--- -props: - type: object - properties: - rawDiagnostics: { type: array } - pr: { type: object } - doctor: { type: object } - required: [rawDiagnostics, pr, doctor] - additionalProperties: false ---- -`; - const review = path.join(ROOT, "packages/code-review-agent/mod.ts"); - yield* useElsewhere(function* (dir) { - for (const root of ["ReviewPR.md", "ReviewPR.local.md"]) { - const source = yield* readTextFile(path.join(ROOT, ".reviews", root)); - const sensorEnd = source.indexOf("/>", source.indexOf('as="rawDiagnostics"')) + 2; - const stageEnd = source.indexOf("<", sensorEnd + 1); - // The stage is taken from the actual root, so restoring its old import - // fails this compiled path even if the adapter itself still works. - const remainder = source.slice(sensorEnd); - const stage = remainder.startsWith("\n\n```ts eval") - ? remainder.slice(0, remainder.indexOf("```", remainder.indexOf("```") + 3) + 3) - : source.slice(sensorEnd, source.indexOf("/>", stageEnd) + 2); - const document = path.join(dir, "diagnostics.md"); - yield* writeTextFile( - document, - `${header}\n\n\n${stage}\n\n`, - ); - const run = yield* runBinary( - [ - "run", - `--plugin=${review}`, - document, - "--props", - JSON.stringify({ rawDiagnostics, pr, doctor }), - ], - dir, - ); - if (run.code !== 0) { - throw new Error(`${root}: compiled diagnostics exited ${run.code}\n${run.stderr}`); - } - const grouped = JSON.parse(run.stdout); - expect(grouped.total).toBe(2); - expect(grouped.fileCount).toBe(2); - expect(grouped.ruleCount).toBe(1); - expect(grouped.density).toBe(0.02); - expect(grouped.groups[0].files).toEqual(["a.ts", "b.ts"]); - expect(grouped.groups[0].instances).toEqual(rawDiagnostics.slice(0, 2)); - expect(grouped.byCategory.verbosity[0].ruleId).toBe("no-console"); - expect(grouped.summary).toContain("2 diagnostics across 2 files (1 rule)"); - } + expect(names).toContain("Greeting"); + expect(names).toContain("Stamp"); }); }); diff --git a/scripts/tests/review-infrastructure.test.ts b/scripts/tests/review-infrastructure.test.ts deleted file mode 100644 index 5d39194ae..000000000 --- a/scripts/tests/review-infrastructure.test.ts +++ /dev/null @@ -1,1261 +0,0 @@ -import { useComponentModuleLoader } from "@executablemd/test-support/host"; -import { describe, it } from "@executablemd/test-support/bdd"; -import { expect } from "@executablemd/test-support/expect"; -import { FetchApi, type FetchResponse } from "@effectionx/fetch"; -import { expandGlob, readTextFile } from "@effectionx/fs"; -import { InMemoryStream } from "@executablemd/durable-streams"; -import { API } from "@executablemd/runtime"; -import { useStubFs } from "@executablemd/runtime/test"; -import { execute, executeInstalled } from "../../packages/core/src/execute.ts"; -import { - REVIEW_DOCUMENTS, - REVIEW_REGISTRATIONS, - reviewComponentDeclarations, -} from "../../packages/code-review-agent/src/review-components.ts"; -import type { ReviewDocument } from "../../packages/code-review-agent/src/review-components.ts"; -import { createHash } from "node:crypto"; -import { Sample } from "../../packages/core/src/sample-api.ts"; -import { useTempFileCompiler } from "../../packages/core/src/temp-file-compiler.ts"; -import { forEach } from "@effectionx/stream-helpers"; -import { each, scoped, until } from "effection"; -import type { Operation } from "effection"; - -interface RequestRecord { - input: string; - init?: RequestInit; - shouldExpect: boolean; -} - -interface ProcessResult { - exitCode: number; - stdout: string; - stderr: string; -} - -interface DocumentRunOptions { - includes?: string[]; - env?: Record; - glob?: Array<{ path: string; isFile: boolean; isDirectory: boolean }>; - stat?: (path: string) => { exists: boolean; isFile: boolean; isDirectory: boolean } | undefined; - process?: ProcessResult | ((command: readonly string[]) => ProcessResult); -} - -function response(body: string): FetchResponse { - const raw = new Response(body, { status: 200 }); - return { - raw, - get bodyUsed() { - return raw.bodyUsed; - }, - get ok() { - return raw.ok; - }, - get status() { - return raw.status; - }, - get statusText() { - return raw.statusText; - }, - get headers() { - return raw.headers; - }, - get url() { - return raw.url; - }, - get redirected() { - return raw.redirected; - }, - get type() { - return raw.type; - }, - *json(parse?: (value: unknown) => T): Operation { - const value: unknown = JSON.parse(body); - return parse ? parse(value) : (value as T); - }, - *text(): Operation { - return body; - }, - *arrayBuffer(): Operation { - return yield* until(raw.arrayBuffer()); - }, - *blob(): Operation { - return yield* until(raw.blob()); - }, - *formData(): Operation { - return yield* until(raw.formData()); - }, - body() { - throw new Error("body streaming is not used by this test"); - }, - *expect(): Operation { - return this; - }, - }; -} - -function* run( - provider: string, - token?: string, -): Operation<{ requests: RequestRecord[]; journal: string }> { - const requests: RequestRecord[] = []; - const files: Record = { - "doc.md": [ - "", - "", - '', - '', - '', - "", - "", - '', - ].join("\n"), - "components/GitHubAuth.md": provider, - "components/Probe.md": [ - "---", - "props:", - " type: object", - " properties:", - " label: { type: string }", - " url: { type: string }", - " required: [label, url]", - " additionalProperties: false", - "---", - "", - "```ts eval", - "const value = yield* fetch(props.url, { method: 'POST', headers: { 'X-Caller': 'kept', 'Content-Type': 'application/custom' }, body: 'request-body' }).expect().json();", - "```", - "", - "{props.label}={value.message}", - ].join("\n"), - "components/Wrapper.md": '', - "components/NoExpect.md": [ - "```ts eval", - 'yield* fetch("https://api.github.com/repos/no-expect");', - "```", - ].join("\n"), - }; - - yield* useStubFs(files); - yield* API.Env.around({ - *env([name], next) { - if (name === "GITHUB_TOKEN") { - return token; - } - return yield* next(name); - }, - }); - yield* FetchApi.around( - { - *fetch([input, init, shouldExpect]) { - const request = - input instanceof Request ? input.url : input instanceof URL ? input.href : input; - requests.push({ input: request, init, shouldExpect }); - return response(JSON.stringify({ message: "ok" })); - }, - }, - { at: "min" }, - ); - - const stream = new InMemoryStream(); - const execution = yield* execute({ path: "doc.md", stream, includes: ["components"] }); - yield* forEach(function* () {}, execution.output); - const result = yield* execution; - expect(result.ok).toBe(true); - return { requests, journal: JSON.stringify(stream.snapshot()) }; -} - -function* runDocumentResult( - document: string, - components: Record, - options: DocumentRunOptions = {}, -): Operation<{ ok: boolean; error?: Error; journal: string; text: string }> { - return yield* scoped(function* () { - const environment = options.env; - if (environment) { - yield* API.Env.around({ - *env([name], next) { - return name in environment ? environment[name] : yield* next(name); - }, - }); - } - const stat = options.stat; - const entries = options.glob; - if (stat || entries) { - yield* API.Fs.around({ - *stat([path], next) { - const value = stat?.(path); - return value ?? (yield* next(path)); - }, - *glob([parameters], next) { - return entries ?? (yield* next(parameters)); - }, - }); - } - yield* useComponentModuleLoader(); - yield* useStubFs({ "doc.md": document, ...components }); - const process = options.process; - if (process) { - yield* API.Process.around({ - *exec([parameters]) { - return typeof process === "function" ? process(parameters.command) : process; - }, - }); - } - const stream = new InMemoryStream(); - const execution = yield* execute({ - path: "doc.md", - stream, - includes: options.includes ?? ["components"], - }); - yield* forEach(function* () {}, execution.output); - const result = yield* execution; - return { - ok: result.ok, - error: result.ok ? undefined : result.error, - journal: JSON.stringify(stream.snapshot()), - text: result.ok && typeof result.value === "string" ? result.value.trim() : "", - }; - }); -} - -/** - * Where the review components now live, as explicit fixture includes. - * - * The package's own directories rather than a checkout path: these are the - * shipped definitions, and reading them here is what makes the behavioral cases - * below about the real components rather than about copies of them. - * - * This is *not* how a run finds them. Production selection is the host's - * declaration, asserted in "the trusted review graph" below and end to end in - * `scripts/tests/plan-component-compiled.test.ts`. These cases supply the files - * explicitly because their subject is what one component does with its inputs, - * which needs no profile at all. - */ -const OPERATIONAL_DIRS = [ - "packages/code-review-agent/src/documents/components", - "packages/code-review-agent/src/documents/policies", - "packages/code-review-agent/src/components", -]; - -/** - * Read operational components from the repository before `useStubFs` replaces - * the filesystem, so a run expands the shipped review documents rather than a - * copy. TypeScript selections retain these exact bytes while their dependencies - * resolve from their physical origin. - */ -function* operationalComponents(paths: string[]): Operation> { - const components: Record = {}; - for (const path of paths) { - components[path] = yield* readTextFile(path); - } - return components; -} - -function props(values: Record): string[] { - return [ - "```js eval", - ...Object.entries(values).map(([name, value]) => `const ${name} = ${JSON.stringify(value)};`), - "```", - "", - ]; -} - -function* runDocument( - document: string, - components: Record, - options: DocumentRunOptions = {}, -): Operation { - const result = yield* runDocumentResult(document, components, options); - return result.ok; -} - -describe("review infrastructure", () => { - it("uses the real GitHubAuth component for exact-host scoped middleware", function* () { - yield* useTempFileCompiler(); - const provider = yield* readTextFile( - "packages/code-review-agent/src/documents/components/GitHubAuth.md", - ); - const result = yield* run(provider, "secret-token"); - - expect(result.requests).toHaveLength(6); - expect(result.requests[0].input).toBe("https://api.github.com/repos/nested"); - expect(result.requests[0].init?.headers).toBeInstanceOf(Headers); - const nestedHeaders = new Headers(result.requests[0].init?.headers); - expect(nestedHeaders.get("Authorization")).toBe("Bearer secret-token"); - expect(nestedHeaders.get("Accept")).toBe("application/vnd.github+json"); - expect(nestedHeaders.get("X-Caller")).toBe("kept"); - expect(result.requests[0].init?.body).toBe("request-body"); - expect(result.requests[0].shouldExpect).toBe(true); - expect(new Headers(result.requests[1].init?.headers).get("Authorization")).toBe(null); - expect(new Headers(result.requests[2].init?.headers).get("Authorization")).toBe(null); - expect(new Headers(result.requests[3].init?.headers).get("Authorization")).toBe(null); - expect(new Headers(result.requests[4].init?.headers).get("Authorization")).toBe( - "Bearer secret-token", - ); - expect(result.requests[4].shouldExpect).toBe(false); - expect(new Headers(result.requests[5].init?.headers).get("Authorization")).toBe(null); - expect(result.journal).not.toContain("secret-token"); - }); - - it("delegates unchanged when the token is unavailable and restores scope", function* () { - yield* useTempFileCompiler(); - const provider = yield* readTextFile( - "packages/code-review-agent/src/documents/components/GitHubAuth.md", - ); - const result = yield* run(provider); - - for (const request of result.requests) { - expect(new Headers(request.init?.headers).get("Authorization")).toBe(null); - if (request.init?.body !== undefined) { - expect(request.init.body).toBe("request-body"); - } - } - expect(result.journal).not.toContain("secret-token"); - }); - - it("accepts exit 1 Oxlint diagnostics and journals only bounded fields", function* () { - yield* useTempFileCompiler(); - - const valid = yield* runDocumentResult( - '\n```ts eval\nconst summary = `${diagnostics.length}:${diagnostics[0].ruleId}`;\n```\n{summary}', - yield* operationalComponents([ - "packages/code-review-agent/src/components/OxlintDiagnostics.ts", - ]), - { - includes: [ - "packages/code-review-agent/src/documents/components", - "packages/code-review-agent/src/components", - ], - process: { - exitCode: 1, - stdout: JSON.stringify([ - { - message: "unused", - code: "eslint(no-unused-vars)", - severity: "warning", - filename: "a.ts", - labels: [{ span: { line: 2, column: 3 } }], - source: "raw source must not cross the boundary", - cause: { secret: "unbounded payload" }, - }, - ]), - stderr: "", - }, - }, - ); - expect(valid.ok).toBe(true); - expect(valid.journal).toContain("1:no-unused-vars"); - expect(valid.journal).not.toContain("raw source must not cross the boundary"); - expect(valid.journal).not.toContain("unbounded payload"); - }); - - for (const scenario of [ - { - name: "malformed JSON", - process: { exitCode: 0, stdout: "not json", stderr: "" }, - diagnostic: "Oxlint returned malformed JSON", - }, - { - name: "invocation failure", - process: { exitCode: 2, stdout: "[]", stderr: "invocation failed" }, - diagnostic: "invocation failed", - }, - ]) { - it(`rejects Oxlint ${scenario.name} with its diagnostic`, function* () { - yield* useTempFileCompiler(); - const result = yield* runDocumentResult( - '', - yield* operationalComponents([ - "packages/code-review-agent/src/components/OxlintDiagnostics.ts", - ]), - { - includes: OPERATIONAL_DIRS, - process: scenario.process, - }, - ); - expect(result.ok).toBe(false); - expect(result.error?.message).toContain(scenario.diagnostic); - }); - } - - it("skips Oxlint without starting a process for empty input", function* () { - yield* useTempFileCompiler(); - let calls = 0; - yield* API.Process.around({ - *exec() { - calls++; - return { exitCode: 2, stdout: "", stderr: "must not run" }; - }, - }); - expect( - yield* runDocument( - '', - yield* operationalComponents([ - "packages/code-review-agent/src/components/OxlintDiagnostics.ts", - ]), - { - includes: [ - "packages/code-review-agent/src/documents/components", - "packages/code-review-agent/src/components", - ], - }, - ), - ).toBe(true); - expect(calls).toBe(0); - }); - - it("extracts human comment replies and rejects malformed GitHub comments", function* () { - yield* useTempFileCompiler(); - const comments = [ - { - id: 100, - user: { login: "github-actions[bot]", type: "Bot" }, - body: "Redundant comment: this repeats the code", - path: "src/example.ts", - original_line: 4, - diff_hunk: "@@\n+const value = 1;", - }, - { - id: 200, - in_reply_to_id: 100, - user: { login: "human", type: "User" }, - body: "Please keep this comment", - }, - { - id: 201, - in_reply_to_id: 100, - user: { login: "acknowledged", type: "User" }, - body: "Remove it", - }, - { - id: 202, - in_reply_to_id: 100, - user: { login: "automation", type: "Bot" }, - body: "Bot reply must not be classified", - }, - ]; - let malformed = false; - yield* FetchApi.around({ - *fetch([input]) { - const url = - input instanceof Request ? input.url : input instanceof URL ? input.href : input; - if (url.includes("/pulls/1/comments?")) { - return response(malformed ? JSON.stringify({ comments }) : JSON.stringify(comments)); - } - if (url.includes("/comments/201/reactions")) { - return response( - JSON.stringify([ - { user: { login: "github-actions[bot]", type: "Bot" }, content: "+1" }, - ]), - ); - } - return response("[]"); - }, - }); - - const document = [ - "", - "```ts eval", - "const pr = { added: [{ content: '// note', file: 'src/example.ts', lineNumber: 4, isTest: false }] };", - "```", - '', - "```ts eval", - "const summary = `${data.previousFindings.length}:${data.repliesForClassification.length}:${data.dismissedReplies.length}`;", - "```", - "{summary}", - "{data.repliesText}", - "", - ].join("\n"); - const options = { - includes: [ - "packages/code-review-agent/src/documents/components", - "packages/code-review-agent/src/components", - ], - env: { - GITHUB_TOKEN: "test-token", - GITHUB_REPOSITORY: "taras/executable.md", - PR_NUMBER: "1", - }, - }; - const result = yield* runDocumentResult( - document, - yield* operationalComponents([ - "packages/code-review-agent/src/components/CommentReviewData.ts", - ]), - options, - ); - expect(result.ok).toBe(true); - expect(result.journal).toContain("1:1:1"); - expect(result.journal).toContain("Please keep this comment"); - expect(result.journal).not.toContain("Bot reply must not be classified"); - - malformed = true; - const malformedResult = yield* runDocumentResult( - document, - yield* operationalComponents([ - "packages/code-review-agent/src/components/CommentReviewData.ts", - ]), - options, - ); - expect(malformedResult.ok).toBe(false); - }); - - it("parses CommentReviewState classifications and durable checklist state", function* () { - yield* useTempFileCompiler(); - const document = [ - "", - "```ts eval", - "const pr = { added: [{ file: 'new.ts', lineNumber: 3 }] };", - "const data = {", - " pairs: [{ comment: 'redundant', code: 'const value = 1', file: 'new.ts', lineNumber: 3 }],", - " previousFindings: [{ file: 'old.ts', lineNumber: 1 }],", - " dismissedReplies: [],", - " repliesForClassification: [{ file: 'new.ts', lineNumber: 2, replyText: 'keep it', replyId: 7 }],", - "};", - "```", - '', - "```ts eval", - "const summary = `${state.hasChecklist}:${state.hasFindings}:${state.newDismissReplies.length}`;", - "```", - "{summary}", - "{state.checklistMd}", - "", - ].join("\n"); - const result = yield* runDocumentResult( - document, - yield* operationalComponents([ - "packages/code-review-agent/src/components/CommentReviewState.ts", - ]), - { - includes: [ - "packages/code-review-agent/src/documents/components", - "packages/code-review-agent/src/components", - ], - }, - ); - expect(result.ok).toBe(true); - expect(result.journal).toContain("true:true:1"); - expect(result.journal).toContain("old.ts:1"); - expect(result.journal).toContain("keep it"); - }); - - it("covers Doctor availability, probes, crashes, and failures", function* () { - yield* useTempFileCompiler(); - const document = [ - "", - "```ts eval", - "const pr = {};", - "```", - '', - "```ts eval", - "const summary = `${doctor.typeAwareAvailable}:${doctor.recommendation}`;", - "```", - "{summary}", - "", - ].join("\n"); - const component = yield* operationalComponents([ - "packages/code-review-agent/src/components/Doctor.ts", - ]); - const stats = (available: boolean) => (path: string) => { - if (path === ".reviews/.oxlint/oxlint" || path === ".reviews/.oxlint/tsgolint") { - return { exists: available, isFile: available, isDirectory: false }; - } - if (path === ".reviews/tsconfig.oxlint.json") { - return { exists: available, isFile: available, isDirectory: false }; - } - if (path === "node_modules") { - return { exists: available, isFile: false, isDirectory: available }; - } - return undefined; - }; - const baseOptions = { - includes: [ - "packages/code-review-agent/src/documents/components", - "packages/code-review-agent/src/components", - ], - glob: [], - }; - - const unavailable = yield* runDocumentResult(document, component, { - ...baseOptions, - stat: stats(false), - }); - expect(unavailable.ok).toBe(true); - expect(unavailable.journal).toContain("false:syntax-only"); - - const successful = yield* runDocumentResult(document, component, { - ...baseOptions, - stat: stats(true), - process: (command) => { - if (command.at(-1) === "--version") { - return { exitCode: 0, stdout: `${command[0]} 1.0\n`, stderr: "" }; - } - return { - exitCode: 1, - stdout: JSON.stringify([ - { - message: "unused", - code: "no-unused-vars", - severity: "warning", - filename: "a.ts", - labels: [{ span: { line: 1, column: 1 } }], - }, - ]), - stderr: "", - }; - }, - }); - expect(successful.ok).toBe(true); - expect(successful.journal).toContain("true:type-aware"); - - const crash = yield* runDocumentResult(document, component, { - ...baseOptions, - stat: stats(true), - process: (command) => - command.at(-1) === "--version" - ? { exitCode: 0, stdout: "tool 1.0\n", stderr: "" } - : { exitCode: 1, stdout: "", stderr: "tsgolint panic: OOM" }, - }); - expect(crash.ok).toBe(true); - expect(crash.journal).toContain("false:syntax-only"); - - const malformed = yield* runDocumentResult(document, component, { - ...baseOptions, - stat: stats(true), - process: (command) => - command.at(-1) === "--version" - ? { exitCode: 0, stdout: "tool 1.0\n", stderr: "" } - : { exitCode: 0, stdout: "not json", stderr: "" }, - }); - expect(malformed.ok).toBe(false); - expect(malformed.error?.message).toContain("Oxlint returned malformed JSON"); - - const failed = yield* runDocumentResult(document, component, { - ...baseOptions, - stat: stats(true), - process: { exitCode: 2, stdout: "[]", stderr: "invocation failed" }, - }); - expect(failed.ok).toBe(false); - expect(failed.error?.message).toContain("invocation failed"); - }); - - it("constructs ReviewContext from git and fails on git errors", function* () { - yield* useTempFileCompiler(); - const document = [ - "", - '', - "```ts eval", - "const summary = `${context.changedFilePaths.length}:${context.pr.files[0].path}`;", - "```", - "{summary}", - "", - ].join("\n"); - const diff = [ - "diff --git a/src/example.ts b/src/example.ts", - "new file mode 100644", - "--- /dev/null", - "+++ b/src/example.ts", - "@@ -0,0 +1,1 @@", - "+const value = 1;", - ].join("\n"); - const result = yield* runDocumentResult( - document, - yield* operationalComponents(["packages/code-review-agent/src/components/ReviewContext.ts"]), - { - includes: [ - "packages/code-review-agent/src/documents/components", - "packages/code-review-agent/src/components", - ], - env: { BASE_SHA: "base", HEAD_SHA: "head", PR_BODY: "local body" }, - process: (command) => - command.includes("--name-status") - ? { exitCode: 0, stdout: "A\tsrc/example.ts\n", stderr: "" } - : { exitCode: 0, stdout: diff, stderr: "" }, - }, - ); - expect(result.ok).toBe(true); - expect(result.journal).toContain("1:src/example.ts"); - - const failed = yield* runDocumentResult( - document, - yield* operationalComponents(["packages/code-review-agent/src/components/ReviewContext.ts"]), - { - includes: [ - "packages/code-review-agent/src/documents/components", - "packages/code-review-agent/src/components", - ], - process: { exitCode: 1, stdout: "", stderr: "git diff failed" }, - }, - ); - expect(failed.ok).toBe(false); - }); - - it("fails when a provider returns 2xx without model content", function* () { - yield* useTempFileCompiler(); - const deepInfra = yield* readTextFile( - "packages/code-review-agent/src/documents/components/DeepInfraProvider.md", - ); - const ollama = yield* readTextFile( - "packages/code-review-agent/src/documents/components/OllamaProvider.md", - ); - const sample = yield* readTextFile( - "packages/code-review-agent/src/documents/components/Sample.md", - ); - const requests: string[] = []; - yield* FetchApi.around({ - *fetch([input]) { - requests.push( - input instanceof Request ? input.url : input instanceof URL ? input.href : input, - ); - return response(JSON.stringify({ choices: [] })); - }, - }); - - expect( - yield* runDocument( - '', - { - "components/DeepInfraProvider.md": deepInfra, - "components/Sample.md": sample, - }, - { env: { DEEPINFRA_TOKEN: "test-token" } }, - ), - ).toBe(false); - expect(requests).toHaveLength(1); - expect(requests[0]).toBe("https://api.deepinfra.com/v1/openai/chat/completions"); - expect( - yield* runDocument( - '', - { - "components/OllamaProvider.md": ollama, - "components/Sample.md": sample, - }, - ), - ).toBe(false); - expect(requests).toHaveLength(2); - expect(requests[1]).toBe("http://localhost:11434/v1/chat/completions"); - }); - - it("requires GitHubComment metadata", function* () { - yield* useTempFileCompiler(); - const comment = yield* readTextFile( - "packages/code-review-agent/src/documents/components/GitHubComment.md", - ); - expect( - yield* runDocument("finding", { - "components/GitHubComment.md": comment, - }), - ).toBe(false); - }); - - it("executes the real RepositoryInventory function component", function* () { - yield* useTempFileCompiler(); - const inventoryResult = yield* runDocumentResult( - '\n{inventory.fileCount}:{inventory.lineCount}', - { - ...(yield* operationalComponents([ - "packages/code-review-agent/src/components/RepositoryInventory.ts", - ])), - "packages/example.ts": "first\nsecond\n", - }, - { - includes: [ - "packages/code-review-agent/src/documents/components", - "packages/code-review-agent/src/components", - ], - glob: [{ path: "packages/example.ts", isFile: true, isDirectory: false }], - }, - ); - expect(inventoryResult.ok).toBe(true); - }); - - it("retires the Show component from the review library", function* () { - const tag = /<\/?Show[\s/>]/; - const documents: string[] = []; - for (const entry of yield* each( - expandGlob("packages/code-review-agent/src/documents/**/*.md"), - )) { - documents.push(entry.path); - const source = yield* readTextFile(entry.path); - expect([entry.path, tag.test(source)]).toEqual([entry.path, false]); - yield* each.next(); - } - // The walk found the review library, so the scan above is not vacuous. - expect( - documents.some((path) => - path.endsWith("packages/code-review-agent/src/documents/components/Finding.md"), - ), - ).toBe(true); - expect(documents.some((path) => path.endsWith("Show.md"))).toBe(false); - - const focused = yield* readTextFile("packages/core/tests/unused-in-diff.test.ts"); - expect(focused).toContain("components/UnusedInDiff.md"); - expect(focused).not.toContain("Show.md"); - - const spec = yield* readTextFile("specs/code-review-agent-spec.md"); - expect(spec).toContain("### 5.10 `CommentReview.md`"); - expect(tag.test(spec)).toBe(false); - expect(spec).not.toContain("Show.md"); - expect(spec).not.toContain("`Show`"); - }); - - it("renders Finding's selected icon and message and suppresses its false case", function* () { - yield* useTempFileCompiler(); - const components = yield* operationalComponents([ - "packages/code-review-agent/src/documents/components/Finding.md", - ]); - - const selected = yield* runDocumentResult( - '', - components, - { includes: OPERATIONAL_DIRS }, - ); - expect(selected.ok).toBe(true); - expect(selected.text).toBe("🔴 Broken contract."); - - const suppressed = yield* runDocumentResult( - '', - components, - { includes: OPERATIONAL_DIRS }, - ); - expect(suppressed.ok).toBe(true); - expect(suppressed.text).toBe(""); - }); - - it("renders OxlintSummary's clean section and its unavailable warning", function* () { - yield* useTempFileCompiler(); - const components = yield* operationalComponents([ - "packages/code-review-agent/src/documents/components/OxlintSummary.md", - "packages/code-review-agent/src/documents/components/ReviewSection.md", - ]); - const summary = (oxlintInstalled: boolean) => - [ - ...props({ - diagnostics: { total: 0, summary: "1 warning" }, - doctor: { oxlintInstalled, bloatRulesMissing: [] }, - }), - "", - ].join("\n"); - - const clean = yield* runDocumentResult(summary(true), components, { - includes: OPERATIONAL_DIRS, - }); - expect(clean.ok).toBe(true); - expect(clean.text).toBe("### Static Analysis\n\n✅ Oxlint found no issues."); - - const unavailable = yield* runDocumentResult(summary(false), components, { - includes: OPERATIONAL_DIRS, - }); - expect(unavailable.ok).toBe(true); - // The selected branch keeps the blank lines the two suppressed blocks left - // behind, so this is exact rather than a containment check. - expect(unavailable.text).toBe( - [ - "### Static Analysis", - "", - "", - "", - "", - "", - "🟡 Oxlint not installed. Static analysis skipped.", - ].join("\n"), - ); - }); - - it("suppresses ReleaseSpecWarning for ordinary files and warns on release changes", function* () { - yield* useTempFileCompiler(); - const components = yield* operationalComponents([ - "packages/code-review-agent/src/documents/components/ReleaseSpecWarning.md", - ]); - const warning = (files: string[]) => - [...props({ files }), ""].join("\n"); - - const ordinary = yield* runDocumentResult( - warning(["packages/core/src/expand.ts"]), - components, - { - includes: OPERATIONAL_DIRS, - }, - ); - expect(ordinary.ok).toBe(true); - expect(ordinary.text).toBe(""); - - const changed = yield* runDocumentResult( - warning([".github/workflows/release.yml"]), - components, - { includes: OPERATIONAL_DIRS }, - ); - expect(changed.ok).toBe(true); - expect(changed.text).toContain("> [!WARNING]"); - expect(changed.text).toContain( - "> This PR changes release configuration (.github/workflows/release.yml) without touching", - ); - expect(changed.text).not.toContain("ERROR"); - }); - - it("expands UnusedInDiff and CommentReview to nothing without an If component", function* () { - yield* useTempFileCompiler(); - const unusedComponents = yield* operationalComponents([ - "packages/code-review-agent/src/documents/components/UnusedInDiff.md", - ]); - const unused = yield* runDocumentResult( - [ - ...props({ pr: { added: [{ file: "a.ts", lineNumber: 1, content: "const plain = 1;" }] } }), - '', - ].join("\n"), - unusedComponents, - { includes: OPERATIONAL_DIRS }, - ); - expect(unused.ok).toBe(true); - expect(unused.text).toBe(""); - - const reviewComponents = yield* operationalComponents([ - "packages/code-review-agent/src/documents/components/CommentReview.md", - "packages/code-review-agent/src/components/CommentReviewData.ts", - "packages/code-review-agent/src/components/CommentReviewState.ts", - "packages/code-review-agent/src/documents/components/SuggestRemoval.md", - "packages/code-review-agent/src/documents/components/Sample.md", - ]); - const requests: string[] = []; - yield* FetchApi.around({ - *fetch([input]) { - requests.push( - input instanceof Request ? input.url : input instanceof URL ? input.href : input, - ); - return response("[]"); - }, - }); - const sampled: string[] = []; - yield* Sample.around({ - // deno-lint-ignore require-yield - *sample([context]) { - sampled.push(context.content); - return "[sampled]"; - }, - }); - - const review = yield* runDocumentResult( - [...props({ pr: { added: [] } }), ""].join("\n"), - reviewComponents, - { - includes: OPERATIONAL_DIRS, - env: { - GITHUB_TOKEN: "test-token", - GITHUB_REPOSITORY: "taras/executable.md", - PR_NUMBER: "1", - }, - }, - ); - expect(review.ok).toBe(true); - // The typed data component ran — it fetched the pull request's comments — - // while both captures and both trailing branches stayed empty. - expect(requests.some((url) => url.includes("/pulls/1/comments"))).toBe(true); - expect(review.text).toBe(""); - expect(sampled).toEqual([]); - }); - - it("skips the ExtraneousCodePolicy sample below the review threshold", function* () { - yield* useTempFileCompiler(); - const components = yield* operationalComponents([ - "packages/code-review-agent/src/documents/policies/ExtraneousCodePolicy.md", - "packages/code-review-agent/src/documents/components/ReviewSection.md", - "packages/code-review-agent/src/documents/components/Sample.md", - ]); - function runPolicy(totalChanges: number): Operation<{ calls: string[]; text: string }> { - return scoped(function* () { - const calls: string[] = []; - yield* Sample.around({ - // deno-lint-ignore require-yield - *sample([context]) { - calls.push(context.content); - return "[sampled]"; - }, - }); - const result = yield* runDocumentResult( - [ - ...props({ - pr: { - stats: { totalChanges }, - meta: { title: "Title", body: "Body" }, - diffPreview: "+const value = 1;", - }, - diagnostics: { summary: "no diagnostics", density: 0 }, - doctor: { oxlintInstalled: true }, - }), - "", - ].join("\n"), - components, - { includes: OPERATIONAL_DIRS }, - ); - expect(result.ok).toBe(true); - return { calls, text: result.text }; - }); - } - - const small = yield* runPolicy(20); - expect(small.calls).toEqual([]); - expect(small.text).toBe("### Correctness\n\n✅ Small PR — correctness review skipped."); - - // The same probe records a call when the branch is selected, so the empty - // result above is non-execution rather than a probe that never wired up. - const large = yield* runPolicy(21); - expect(large.calls).toHaveLength(1); - expect(large.calls[0]).toContain("You are reviewing a TypeScript PR for EXTRANEOUS code only."); - expect(large.text).toContain("[sampled]"); - expect(large.text).not.toContain("ERROR"); - }); - - it("renders RepoCleanupPolicy's clean section without running either branch", function* () { - yield* useTempFileCompiler(); - const components = yield* operationalComponents([ - "packages/code-review-agent/src/documents/policies/RepoCleanupPolicy.md", - "packages/code-review-agent/src/documents/components/ReviewSection.md", - "packages/code-review-agent/src/documents/components/Sample.md", - ]); - const calls: string[] = []; - yield* Sample.around({ - // deno-lint-ignore require-yield - *sample([context]) { - calls.push(context.content); - return "[sampled]"; - }, - }); - - const clean = yield* runDocumentResult( - [ - ...props({ - diagnostics: { total: 0, summary: "MUST NOT RENDER" }, - doctor: { oxlintInstalled: true }, - fileList: "packages/core/src/expand.ts", - }), - "", - ].join("\n"), - components, - { includes: OPERATIONAL_DIRS }, - ); - expect(clean.ok).toBe(true); - expect(clean.text).toBe("### Cleanup Policy\n\n✅ No code health issues detected."); - expect(calls).toEqual([]); - }); -}); - -/** - * The review graph as the installed `run` profile supplies it. - * - * The cases above supply the package's component files as explicit fixtures, - * because what they are measuring is what one component does with its inputs. - * These ask a different question: not whether a component works, but whether the - * *host* is the one supplying it — so they pass no include at all and let the - * declaration answer. - * - * A review is the one program that must not be answerable by the thing it is - * reviewing. While the graph lived in `.reviews/`, a pull request could add its - * own `Finding.md` and the review would run the branch's copy — so what is - * asserted here is not that the components work, but that the *host* is the one - * supplying them and a checkout cannot take a name back. - */ -/** The packaged assets, read the way anything but the package would read them. */ -function* packagedSource(document: ReviewDocument): Operation { - return yield* readTextFile( - `packages/code-review-agent/src/documents/${document.group}/${document.name}.md`, - ); -} - -describe("the trusted review graph", () => { - it("declares exactly the packaged Markdown, each identified by origin and digest", function* () { - const declared = yield* reviewComponentDeclarations(); - - // The count is stated rather than derived from the manifest the - // implementation also derives from: a manifest that lost an entry would - // otherwise agree with itself. - expect(declared).toHaveLength(35); - expect(new Set(declared.map((one) => one.name)).size).toBe(35); - - for (const document of REVIEW_DOCUMENTS) { - const one = declared.find((candidate) => candidate.name === document.name); - expect(one).toBeDefined(); - const source = yield* packagedSource(document); - // The bytes are the packaged asset's, and the digest is of those bytes. - // A declaration stating a digest of something else is refused at - // admission, so this is the half admission cannot check for itself: - // whether what was read is what the package ships. - expect(one?.source).toBe(source); - expect(one?.digest).toBe(createHash("sha256").update(source, "utf8").digest("hex")); - // The package and the asset, never a filesystem path — the same component - // sits at three different absolute paths across a checkout, a - // `node_modules` tree and a binary, and all three are one component. - expect(one?.origin).toBe( - `@executablemd/code-review-agent/${document.group}/${document.name}.md`, - ); - // Unstated, so both spellings keep working. A declaration that narrowed - // `forms` would change what the existing review roots may write. - expect(one?.forms).toBeUndefined(); - } - }); - - it("reserves the seven TypeScript components rather than offering them as defaults", function* () { - expect(REVIEW_REGISTRATIONS.map((one) => one.name)).toEqual([ - "CommentReviewData", - "CommentReviewState", - "Doctor", - "OxlintDiagnostics", - "ReviewDiagnostics", - "RepositoryInventory", - "ReviewContext", - ]); - for (const registration of REVIEW_REGISTRATIONS) { - // Reserved is the registration tier's way of saying what a declaration - // says. Ordinary would have moved thirty-five components out of a - // subject's reach and left shadowable the implementations that run processes, - // read credentials and reach the network. - expect(registration.reserved).toBe(true); - expect(registration.origin).toBe("@executablemd/code-review-agent"); - expect(registration.props).toBeDefined(); - expect(registration.returns).toBeDefined(); - // Each is documented by this package, which the documentation index - // checks against this same list. - expect(typeof registration.description).toBe("string"); - } - }); - - it("claims each name once across both tiers", function* () { - const declared = (yield* reviewComponentDeclarations()).map((one) => one.name); - const reserved = REVIEW_REGISTRATIONS.map((one) => one.name); - const all = [...declared, ...reserved]; - // A declaration colliding with a reserved registration is refused before the - // root document is imported, so an overlap here is a build that cannot run - // a review at all rather than a precedence question. - expect(new Set(all).size).toBe(all.length); - expect(all).toHaveLength(42); - }); - - /** - * Every supported entrypoint takes the graph from the installation. - * - * The declaration tier winning is only half the outcome. While a command still - * passed `--include .reviews/components`, the retired directory was a live - * dependency of that command: deleting it broke the entrypoint, and restoring - * a file under it put a checkout-supplied component back in front of a review - * — which is the thing this issue exists to stop. - * - * So this reads the commands themselves rather than trusting that the - * migration reached all seven. Import-graph selection cannot see a task string - * or a workflow step, so nothing else in the suite would notice one being put - * back. - * - * Scoped to these seven by name. `ci.yml` passes `--include - * packages/core/components` for unrelated smoke fixtures and is none of this - * assertion's business — a sweep over every workflow would fail on it and - * teach the next person to weaken the check. - */ - it("runs every supported review entrypoint with no component include", function* () { - const RETIRED = [".reviews/components", ".reviews/policies", "packages/core/components"]; - - const tasks: Record = JSON.parse(yield* readTextFile("deno.json")).tasks; - const ROOTS: Record = { - review: ".reviews/ReviewPR.md", - "review:local": ".reviews/ReviewPR.local.md", - analyze: ".reviews/AnalyzeRepo.md", - "analyze:ci": ".reviews/AnalyzeRepoCI.md", - "analyze:dispatch": ".reviews/DispatchRepoAnalysis.md", - }; - - for (const [name, root] of Object.entries(ROOTS)) { - const command = tasks[name]; - // The task still exists and still runs its own root document. A - // migration that removed an entrypoint would otherwise satisfy every - // negative assertion below. - expect(`${name}: ${typeof command}`).toBe(`${name}: string`); - expect(`${name}: ${command.includes(root)}`).toBe(`${name}: true`); - // No component search path at all, rather than no retired path: an - // include pointing somewhere new would be a second way to answer for a - // review's components. - expect(`${name}: ${command.includes("--include")}`).toBe(`${name}: false`); - for (const retired of RETIRED) { - expect(`${name}: ${retired}: ${command.includes(retired)}`).toBe( - `${name}: ${retired}: false`, - ); - } - } - - const WORKFLOWS: Record = { - ".github/workflows/review.yml": ".reviews/ReviewPR.md", - ".github/workflows/repo-analysis.yml": ".reviews/AnalyzeRepoCI.md", - }; - - for (const [file, root] of Object.entries(WORKFLOWS)) { - const source = yield* readTextFile(file); - expect(`${file}: ${source.includes(`run ${root}`)}`).toBe(`${file}: true`); - for (const retired of RETIRED) { - expect(`${file}: ${retired}: ${source.includes(retired)}`).toBe( - `${file}: ${retired}: false`, - ); - } - } - }); - - it("ships review copies of Sample and Instruction byte-identical to core's", function* () { - // Deliberate duplication: the review graph owns its own copies so that the - // set a review runs is complete in one package. This asserts the copies have - // not drifted, which makes a future divergence an explicit decision in this - // package rather than something that happens to one of them. - for (const name of ["Sample", "Instruction"]) { - const review = yield* readTextFile( - `packages/code-review-agent/src/documents/components/${name}.md`, - ); - expect(review).toBe(yield* readTextFile(`packages/core/components/${name}.md`)); - } - }); - - it("runs the declared Finding even when the checkout supplies its own", function* () { - yield* useTempFileCompiler(); - const declarations = yield* reviewComponentDeclarations(); - expect(declarations.some((one) => one.name === "Finding")).toBe(true); - - // A checkout that supplies both a same-named component and an unrelated one. - // The first must lose; the second must still resolve, because claiming names - // is not the same as turning discovery off. - yield* useStubFs({ - "doc.md": '\n\n', - // Not a broken file: it accepts exactly the props the real one accepts, - // so a run that selected it would *succeed* and quietly emit this marker - // instead of the finding. A hostile component that merely failed would - // let this case pass for the wrong reason. - "components/Finding.md": [ - "---", - "props:", - " type: object", - " properties:", - " when: { type: boolean }", - " severity: { type: string }", - " message: { type: string }", - " required: [when, message]", - " additionalProperties: false", - "---", - "", - "HOSTILE-FINDING {props.message}", - ].join("\n"), - "components/CustomProbe.md": "custom probe ran", - }); - - const stream = new InMemoryStream(); - const execution = yield* executeInstalled( - { path: "doc.md", stream, includes: ["components"] }, - [{ declarations }], - ); - yield* forEach(function* () {}, execution.output); - const result = yield* execution; - - expect(result.ok).toBe(true); - const text = result.ok && typeof result.value === "string" ? result.value : ""; - // The hostile marker never reaches the output, and the trusted component's - // own rendering does. - expect(text).not.toContain("HOSTILE-FINDING"); - // The declared component's own rendering: its `ts eval` chose the icon from - // the severity it was given, which a body of plain prose could not produce. - expect(text).toContain("🔴 probe"); - // Ordinary inclusion still works: a caller's own component, of a name the - // host does not claim, resolves exactly as it always did. - expect(text).toContain("custom probe ran"); - }); -}); diff --git a/scripts/tests/scope-bound-event-registration.test.ts b/scripts/tests/scope-bound-event-registration.test.ts index 309082e03..81e05886f 100644 --- a/scripts/tests/scope-bound-event-registration.test.ts +++ b/scripts/tests/scope-bound-event-registration.test.ts @@ -442,7 +442,7 @@ describe("local/require-scope-bound-event-registration", () => { }); it("finds no configuration entry turning the rule off for a path", function* () { - for (const config of [".oxlintrc.json", "oxlint.shared.json", ".reviews/.oxlintrc.json"]) { + for (const config of [".oxlintrc.json", "oxlint.shared.json"]) { const source = yield* readTextFile(path.join(ROOT, config)); const off = new RegExp(`"local/${RULE}":\\s*(?:"off"|\\["off")`, "u").test(source); expect([config, off]).toEqual([config, false]); diff --git a/scripts/validate-documentation.ts b/scripts/validate-documentation.ts index bf2e32c8f..65f237ace 100644 --- a/scripts/validate-documentation.ts +++ b/scripts/validate-documentation.ts @@ -14,11 +14,8 @@ * exactly the reason they would fail a run. * * XMD bundles one Plugin — `@executablemd/git` — so this assembles the run - * profile: the bundled value, then the review graph named explicitly. A - * distribution carries the first, which makes its documentation this gate's - * business; the second is an operator's selection, and this script is a build - * tool rather than the product, which is why it may name the package that - * ships it. Nothing in the CLI does. + * profile a command gets with no `--plugin`: the bundled value. A distribution + * carries it, which makes its documentation this gate's business. */ import { main, scoped } from "effection"; @@ -28,7 +25,6 @@ import type { ComponentOrigin, DocumentationIndex } from "@executablemd/core"; import { useCommandComponents } from "../packages/cli/src/syntax.ts"; import { BUNDLED_PLUGIN } from "../packages/cli/src/run-profile.ts"; import { installPlugins } from "../packages/cli/src/plugin-host.ts"; -import reviewPlugin from "../packages/code-review-agent/mod.ts"; /** Assemble the complete index, throwing whatever it refuses with. */ export function* validateDocumentation(): Operation { @@ -36,13 +32,11 @@ export function* validateDocumentation(): Operation { // that installed it: collecting outside would find core's terminal alone and // pass every rule vacuously. const index = yield* scoped(function* () { - // The run profile as a command assembles it: the bundled Git Plugin first, - // then the first-party review Plugin exactly as `--plugin` installs one. A - // Plugin's registrations and the documentation describing them arrive - // together, so a build that shipped one without the other fails here — and - // the bundled one is in this list because a distribution carries it, which - // makes its documentation this gate's business too. - yield* installPlugins([BUNDLED_PLUGIN, reviewPlugin], { command: "run", args: ["run"] }); + // The run profile as a command assembles it, with the bundled Git Plugin + // installed exactly as a Plugin is. Its registrations and the + // documentation describing them arrive together, so a build that shipped + // one without the other fails here. + yield* installPlugins([BUNDLED_PLUGIN], { command: "run", args: ["run"] }); yield* useCommandComponents(); return documentationIndexFor(yield* capturedDocumentation()); }); @@ -57,12 +51,12 @@ export function* validateDocumentation(): Operation { origin: "@executablemd/web", reserved: false, }); - // And one from the selected Plugin, so an index assembled without it cannot + // And one from the bundled Plugin, so an index assembled without it cannot // pass this check either. - read(index, "ReviewContext", { + read(index, "Repository", { kind: "registered", - origin: "@executablemd/code-review-agent", - reserved: true, + origin: "@executablemd/workflow/composition", + reserved: false, }); return 1; } diff --git a/specs/code-review-agent-spec.md b/specs/code-review-agent-spec.md deleted file mode 100644 index 4b5f33347..000000000 --- a/specs/code-review-agent-spec.md +++ /dev/null @@ -1,1362 +0,0 @@ -# Specification: `@executablemd/code-review-agent` - -**Status:** Draft -**Scope:** An Executable Markdown Agent that reviews pull requests for extraneous code, posts findings as GitHub comments, and runs locally via Ollama or in CI via DeepInfra. - ---- - -## 1. Architecture - -A PR review is an executable Markdown document. Typed function components -gather the diff, environment observations, and bounded diagnostics. Markdown -then composes the policy components, model provider, and optional GitHub -delivery. - -``` -ReviewPR.md - ├─ → execution failures fail the document - ├─ → scoped exact-host GitHub authentication - ├─ ReviewContext → git diff and PR metadata → pr - ├─ Doctor → bounded environment recommendation - ├─ OxlintDiagnostics → normalized diagnostics - │ - └─ DeepInfraProvider (or OllamaProvider) - └─ Instructions (system prompt) - └─ GitHubComment (or stdout) - └─ ReviewBody - ├─ ScopeCheck - │ ├─ Threshold (×4) - │ ├─ DescriptionCheck - │ ├─ LinkedIssue - │ ├─ ConfigSourceMix - │ ├─ AbstractionNames - │ └─ NewDependencies - ├─ StructuralBloat - │ ├─ UnusedInDiff (×2) - │ ├─ Ratio - │ └─ Pattern (×2) - ├─ VerbosityCheck - │ ├─ Ratio - │ └─ CommentReview → Sample - └─ SemanticReview → Sample -``` - -The review composition has three layers of concern: - -| Layer | Component | Responsibility | -|---|---|---| -| Transport | `DeepInfraProvider` / `OllamaProvider` | Send HTTP request, return response | -| Policy | `Instructions` | Set system prompt | -| Delivery | `GitHubComment` | Post rendered output as PR comment | - -The review logic (`ReviewBody` and its children) knows nothing about -which model runs, what system prompt is set, or where the output goes. - ---- - -## 2. executable.md Changes (Implemented) - -All executable.md core changes and the full agent implementation are complete: - -- **Eval block `return` as rendered output** (PR #35) -- **Eval binding interpolation in text segments** (PR #34) -- **Simplified `SampleContext`** to `{content, model?, params?, system?, componentName?}` (PR #35) -- **Removed `sample` modifier** — all LLM calls via `` component (PR #35) -- **Renamed `Instruction.md` input** `text` → `system` for clarity -- **Fixed broken providers** — `OllamaProvider`, - `AnthropicProvider` updated to use direct `fetch()` calls -- **Component resolution** — the review component graph is owned and declared by - the installed `@executablemd/code-review-agent` package (§3.5), not resolved - through a checkout-relative `--include` -- **AST-based user import extraction** (DEC-93) — eval blocks can use - standard `import` declarations; extracted via acorn's - `allowImportExportEverywhere` and hoisted to module level -- **Projected children expression prop scoping** (DEC-91, DEC-92) — - children substituted via `` carry the caller's eval env. - Expression props on projected children resolve against merged env - (all ancestor bindings propagated through multi-level nesting) - ---- - -## 3. Package: `@executablemd/code-review-agent` - -The package exports the published diff, diagnostics, Oxlint normalization, and -Doctor parsing helpers. `parseDiff` takes raw `git diff` and -`git diff --name-status` output and returns a typed `PR` object. Existing -`parseDoctorResult` and tolerant `parseDiagnostics` behavior remains stable; -strict Oxlint validation belongs to `normalizeOxlintOutput` at the review -component boundary. - -### 3.1 `PR` type - -```typescript -interface PR { - files: DiffFile[]; - added: DiffLine[]; - removed: DiffLine[]; - created: DiffFile[]; - modified: DiffFile[]; - deleted: DiffFile[]; - directories: Set; - addedSource: string; - diffPreview: string; // addedSource truncated to 80K chars - stats: { - totalFiles: number; - additions: number; - deletions: number; - totalChanges: number; - }; - meta: { - title: string; - body: string; - number: string; - }; -} - -interface DiffFile { - path: string; - status: "A" | "M" | "D" | "R" | "C"; - hunks: DiffHunk[]; - language: string; - isTest: boolean; - isConfig: boolean; - isTypeDeclaration: boolean; -} - -interface DiffHunk { - header: string; - lines: DiffLine[]; -} - -interface DiffLine { - type: "add" | "remove" | "context"; - content: string; - file: string; - lineNumber: number; - isTest: boolean; -} -``` - -### 3.2 `parseDiff` signature - -```typescript -function parseDiff( - rawDiff: string, - rawFiles: string, - meta: { title: string; body: string; number: string }, -): PR; -``` - -### 3.3 What `parseDiff` handles - -- Standard unified diff format -- Rename detection (R status) -- Binary file detection (skipped) -- Language inference from file extension -- Test file detection: `*.test.ts`, `*.spec.ts`, `__tests__/`, `test/` -- Config file detection: `*.config.*`, `.*rc`, `tsconfig*`, `package.json` -- Type declaration detection: `*.d.ts` -- `diffPreview`: `addedSource` truncated to 80,000 characters -- `directories`: unique top-level dirs at depth 2 - -### 3.4 Package structure - -``` -packages/code-review-agent/ - src/ - components/ # the seven reserved TypeScript components - documents/ - components/ # 28 review components + Sample.md + Instruction.md - policies/ # the five policy documents - components.md # long-form documentation for the seven registrations - review-components.ts # the one assembly boundary - plugin.ts # the Plugin built from that boundary - parse-diff.ts - types.ts - mod.ts # the named library exports, and the Plugin as default -``` - -The package depends on `@executablemd/core`, `@executablemd/runtime`, -`@effectionx/fs`, `@effectionx/fetch` and `effection`. - -### 3.5 The declared component graph - -`src/review-components.ts` is the only assembly boundary, exported as -`@executablemd/code-review-agent/review-components`. It supplies forty-two -components in two tiers: - -- **Thirty-five declared Markdown components.** The twenty-eight review - components, the five policies, and the package's own copies of `Sample.md` and - `Instruction.md`. Each is read from its packaged asset and declared with - `Markdown({…})`, carrying the exact bytes, their SHA-256, and the origin - `@executablemd/code-review-agent/components/.md` or - `…/policies/.md`. `forms` is deliberately unstated, so both invocation - spellings keep working. -- **Seven reserved registrations.** `CommentReviewData`, `CommentReviewState`, - `Doctor`, `OxlintDiagnostics`, `ReviewDiagnostics`, `RepositoryInventory` and - `ReviewContext`, with origin `@executablemd/code-review-agent`. `useReviewComponents()` installs them - together with the documentation in `src/components.md`, which the - documentation index validates against this same registration list. - -Both tiers claim their names: a checkout under review cannot supply a component -of any of these forty-two names and have a review run it, and a second claim on -one name is refused at admission rather than ordered. Names outside the forty-two -resolve through ordinary inclusion and repository discovery, unchanged. - -The assets are located from the module's own URL, never from the working -directory and never through the component search path, so a source checkout, an -npm install and a compiled binary select the same bytes and report the same -digests. The reads use the Effection filesystem directly rather than `API.Fs` or -the document-facing `Files` provider, neither of which may decide what a -review's own components are. - -### 3.6 The Plugin - -`src/plugin.ts` builds the package's Plugin from that same boundary and exports -it as the package's default export, so `mod.ts` publishes the Plugin as -`default` while keeping every named library export and the -`./review-components` subpath. - -```ts -export default Plugin({ - name: "@executablemd/code-review-agent", - *install(request) { /* the assembly above, for the commands that run reviews */ }, -}); -``` - -The Plugin claims the forty-two names for the commands that can run a review: -`run`, `syntax`, `plan`, and a `workflow` action that executes a document -(`start`, `resume`, `fork`). It returns `undefined` for `xmd test` at its root, -for `xmd upgrade`, for the workflow management actions and for every other -command — those never had the graph. A nested `` is the -run profile whatever command hosts it, and installs this Plugin again in its own -scope with command `run`, so a run child of `xmd test` gains the graph while the -test root does not. - -The workflow action is found by name among the tokens after `workflow`, so an -option written before it — `--plugin` included — is never read as one. - -Nothing in the CLI names this package. XMD bundles only `@executablemd/git`, -so this graph arrives only when an operator selects it — `--plugin @executablemd/code-review-agent` -where the package is installed, or `--plugin ./packages/code-review-agent/mod.ts` -by path — and a command that names none has none of the forty-two names. - -One installation per command then serves every surface that describes or -validates the vocabulary — ordinary execution, `xmd syntax`, structural -validation, ``'s admission validator and `xmd plan`'s candidate validation -— so the three checks inside one Plan command consume one retained assembly -rather than re-reading every packaged asset. - ---- - -## 4. Standard Library Components - -Conditional rendering is not among them. `` and `` are expansion-engine -directives (executable-mdx-spec §6.5), so the components below branch with them -directly instead of wrapping a conditional of their own. - -### 4.1 `ReviewSection.md` - -````markdown ---- -props: - type: object - properties: - heading: - type: string - clean: - type: string - default: "✅ No issues found." - required: [heading] - additionalProperties: false ---- - -```ts eval -const content = yield* renderChildren(); -return content.trim().length > 0 - ? `### ${heading}\n\n${content}` - : `### ${heading}\n\n${clean}`; -``` -```` - -### 4.2 `Finding.md` - -````markdown ---- -props: - type: object - properties: - when: - type: boolean - severity: - type: string - default: warning - message: - type: string - required: [when, message] - additionalProperties: false ---- - -```ts eval -const icon = severity === "error" ? "🔴" : "🟡"; -``` - - - -{icon} {message} - - -```` - -### 4.3 `Instructions.md` - -````markdown ---- -props: - type: object - properties: - system: - type: string - required: [system] - additionalProperties: false ---- - -```ts persist eval -const scope = yield* useScope(); -scope.around(Sample, function* ([context], next) { - return yield* next({ - ...context, - system, - }); -}); -``` - - -```` - -### 4.4 `GitHubComment.md` - -CI roots place this component inside `` and ``. `GitHubAuth` -reads `GITHUB_TOKEN` once in a private generator closure and installs -`@effectionx/fetch`'s `FetchApi` middleware around its projected content. It -adds authorization only to HTTPS requests whose exact hostname is -`api.github.com`, preserves request headers and bodies, and forwards the -`shouldExpect` argument unchanged. Missing credentials and non-GitHub requests -delegate unchanged. `GitHubComment` therefore keeps only request-specific -headers such as `Content-Type`; it requires repository and PR metadata instead -of silently returning an empty report. - -`GitHubComment.md` renders the report, reads the configured repository and PR -metadata through contextual environment access, and uses the fluent -`fetch().expect()` operations to create or update the marked comment. It -requires its metadata and validates the comments payload. Authentication is -inherited from the surrounding `GitHubAuth` provider; the component supplies -only request-specific headers such as `Content-Type`. - -### 4.5 `DeepInfraProvider.md` - -`DeepInfraProvider` is a Markdown provider for the `Sample` API. It keeps -request construction and response validation in its scoped middleware. The -token is read through contextual environment access and stays in the private -request operation; a successful 2xx response without model content fails the -provider. - -### 4.6 `OllamaProvider.md` - -`OllamaProvider` uses the same `Sample` provider contract with its configured -local base URL. It validates that a successful response contains model -content and otherwise fails the enclosing document. - ---- - -## 5. Rule Components - -### 5.1 `Threshold.md` - -````markdown ---- -props: - type: object - properties: - pr: - type: object - metric: - type: string - op: - type: string - value: - type: number - severity: - type: string - default: warning - message: - type: string - required: [pr, metric, op, value, message] - additionalProperties: false ---- - -```ts eval -const metrics = { - totalChanges: pr.stats.totalChanges, - totalFiles: pr.stats.totalFiles, - additions: pr.stats.additions, - deletions: pr.stats.deletions, - directories: pr.directories.size, -}; - -const actual = metrics[metric]; -const ops = { - ">": (a, b) => a > b, - ">=": (a, b) => a >= b, - "<": (a, b) => a < b, - "<=": (a, b) => a <= b, - "==": (a, b) => a == b, -}; - -if (ops[op](actual, value)) { - const icon = severity === "error" ? "🔴" : "🟡"; - return icon + " " + message - .replace("{actual}", String(actual)) - .replace("{value}", String(value)); -} -``` -```` - -### 5.2 `Pattern.md` - -````markdown ---- -props: - type: object - properties: - pr: - type: object - pattern: - type: string - min: - type: number - default: 1 - excludeTests: - type: boolean - default: true - severity: - type: string - default: warning - message: - type: string - required: [pr, pattern, message] - additionalProperties: false ---- - -```ts eval -const re = new RegExp(pattern, "g"); -const lines = excludeTests - ? pr.added.filter(l => !l.isTest) - : pr.added; -const matches = lines.filter(l => re.test(l.content)); -re.lastIndex = 0; - -if (matches.length >= min) { - const icon = severity === "error" ? "🔴" : "🟡"; - return icon + " " + message - .replace("{count}", String(matches.length)); -} -``` -```` - -### 5.3 `Ratio.md` - -````markdown ---- -props: - type: object - properties: - pr: - type: object - numerator: - type: string - denominator: - type: string - threshold: - type: number - minDenominator: - type: number - default: 10 - excludeTests: - type: boolean - default: true - severity: - type: string - default: warning - message: - type: string - required: [pr, numerator, denominator, threshold, message] - additionalProperties: false ---- - -```ts eval -const numRe = new RegExp(numerator, "g"); -const denRe = new RegExp(denominator, "g"); -const lines = excludeTests - ? pr.added.filter(l => !l.isTest) - : pr.added; -const source = lines.map(l => l.content).join("\n"); - -const numCount = (source.match(numRe) ?? []).length; -const denCount = (source.match(denRe) ?? []).length; - -if (denCount >= minDenominator && numCount / denCount > threshold) { - const ratio = (numCount / denCount * 100).toFixed(1); - const icon = severity === "error" ? "🔴" : "🟡"; - return icon + " " + message - .replace("{ratio}", ratio) - .replace("{numeratorCount}", String(numCount)) - .replace("{denominatorCount}", String(denCount)); -} -``` -```` - -### 5.4 `UnusedInDiff.md` - -````markdown ---- -props: - type: object - properties: - pr: - type: object - construct: - type: string - severity: - type: string - default: warning - message: - type: string - required: [pr, construct, message] - additionalProperties: false ---- - -```ts eval -const lines = pr.added.filter(l => - l.file.endsWith(".ts") || l.file.endsWith(".tsx") -); -const source = lines.map(l => l.content).join("\n"); - -// Anchoring the keyword to statement position (line start, after an optional -// export/default/declare prefix) excludes `import { type X }` specifiers, -// whose `type` keyword sits inside braces rather than at the start of a -// declaration. -const declPattern = new RegExp( - `^\\s*(?:export\\s+)?(?:default\\s+|declare\\s+)?${construct}\\s+(\\w+)` -); - -const decls = []; -for (const line of lines) { - const match = declPattern.exec(line.content); - if (match) { - decls.push({ name: match[1], file: line.file, lineNumber: line.lineNumber }); - } -} - -const unused = decls - .map(d => ({ - ...d, - refs: (source.match(new RegExp(`\\b${d.name}\\b`, "g")) ?? []).length, - })) - .filter(d => d.refs <= 1); - -const hasUnused = unused.length > 0; -const icon = severity === "error" ? "🔴" : "🟡"; -const summary = icon + " " + message - .replace("{names}", unused.map(u => u.name).join(", ")) - .replace("{count}", String(unused.length)); -``` - - - -
-{summary} - -| Symbol | Declared at | Refs in diff | Why flagged | -| --- | --- | --- | --- | -| `{u.name}` | `{u.file}:{u.lineNumber}` | {u.refs} | referenced ≤1× within the added diff (pre-existing usages not counted) | - - -
- -
-```` - -Findings render as a `
` disclosure: the summary carries the terse -message, and the body is a per-symbol table giving the declaration -`file:lineNumber`, the added-diff reference count, and why the symbol was -flagged. The declaration matcher is anchored to statement position, so -`import { type X }` specifiers are not mistaken for declarations. Reference -counts cover added diff lines only — a symbol used elsewhere in the file -still reports a low count, which the "why flagged" column states. - -### 5.5 `DescriptionCheck.md` - -```markdown ---- -props: - type: object - properties: - pr: - type: object - minLength: - type: number - default: 50 - severity: - type: string - default: error - message: - type: string - default: "PR description must explain what and why." - required: [pr] - additionalProperties: false ---- - - -``` - -### 5.6 `LinkedIssue.md` - -````markdown ---- -props: - type: object - properties: - pr: - type: object - whenLinesExceed: - type: number - default: 0 - severity: - type: string - default: warning - message: - type: string - default: "Large PR with no linked issue." - required: [pr] - additionalProperties: false ---- - -```ts eval -const hasIssue = /(?:#\d+|https:\/\/github\.com\/.*\/issues\/\d+)/.test(pr.meta.body); -``` - - whenLinesExceed} - severity={severity} message={message} /> -```` - -### 5.7 `ConfigSourceMix.md` - -````markdown ---- -props: - type: object - properties: - pr: - type: object - minFiles: - type: number - default: 5 - severity: - type: string - default: warning - message: - type: string - default: "PR mixes config and source changes." - required: [pr] - additionalProperties: false ---- - -```ts eval -const hasConfig = pr.files.some(f => f.isConfig); -const hasSource = pr.files.some(f => - !f.isConfig && !f.isTest && !f.isTypeDeclaration -); -const triggered = hasConfig && hasSource && pr.stats.totalFiles > minFiles; -``` - - -```` - -### 5.8 `AbstractionNames.md` - -````markdown ---- -props: - type: object - properties: - pr: - type: object - pattern: - type: string - default: "factory|abstract|base|provider|strategy|adapter|helper|util" - severity: - type: string - default: warning - message: - type: string - default: "New abstraction files: {names}. Verify 3+ consumers." - required: [pr] - additionalProperties: false ---- - -```ts eval -const re = new RegExp(pattern, "i"); -const suspicious = pr.created - .filter(f => f.path.endsWith(".ts") && !f.isTest && !f.isTypeDeclaration) - .filter(f => re.test(f.path)); -const triggered = suspicious.length > 0; -const resolvedMessage = message.replace( - "{names}", suspicious.map(f => f.path).join(", ") -); -``` - - -```` - -### 5.9 `NewDependencies.md` - -````markdown ---- -props: - type: object - properties: - pr: - type: object - severity: - type: string - default: warning - message: - type: string - default: "package.json changed without dependency justification." - required: [pr] - additionalProperties: false ---- - -```ts eval -const touchesPkg = pr.files.some(f => - f.path === "package.json" || f.path.endsWith("/package.json") -); -const mentionsDeps = pr.meta.body.toLowerCase().includes("dependenc"); -const triggered = touchesPkg && !mentionsDeps; -``` - - -```` - -### 5.10 `CommentReview.md` - -`CommentReview.md` keeps the authored ``, ``, and structural -`` composition. `CommentReviewData.ts` performs pair extraction and bounded -GitHub response parsing, while `CommentReviewState.ts` parses model responses -and constructs the checklist and pending findings. Both are typed function -components with explicit schemas; no procedural eval block crosses the -Markdown boundary. - -```markdown - - - - {reviewData.pairsText} - - - -``` - ---- - -## 6. Policy Documents (zero JavaScript) - -### 6.1 `ScopeCheck.md` - -```markdown ---- -props: - type: object - properties: - pr: - type: object - required: [pr] - additionalProperties: false ---- - - - - - - - - - - - - - - - - - - - - - - -``` - -### 6.2 `StructuralBloat.md` - -```markdown ---- -props: - type: object - properties: - pr: - type: object - required: [pr] - additionalProperties: false ---- - - - - - - - - - - - - - - -``` - -### 6.3 `VerbosityCheck.md` - -```markdown ---- -props: - type: object - properties: - pr: - type: object - required: [pr] - additionalProperties: false ---- - - - - - - - - -``` - -### 6.4 `SemanticReview.md` - -```markdown ---- -props: - type: object - properties: - pr: - type: object - required: [pr] - additionalProperties: false ---- - - 20}> - - - -You are reviewing a TypeScript PR for EXTRANEOUS code only. - -PR: {pr.meta.title} -Description: {pr.meta.body} - -Report ONLY: -1. Scope creep — changes unrelated to stated purpose -2. Speculative abstractions — new constructs with one consumer -3. Dead constructs — declarations never referenced in diff -4. Wrapper indirection — functions that only forward calls - -Do NOT flag test helpers, exported types, or style preferences. - -For each finding: FILE, PATTERN, CONCERN, QUESTION for the author. - -If clean: "No extraneous code patterns detected." - -DIFF: -{pr.diffPreview} - - - - - -✅ Small PR — semantic review skipped. - - - -``` - -Zero eval blocks. - -### 6.5 `ReviewBody.md` - -```markdown ---- -props: - type: object - properties: - pr: - type: object - required: [pr] - additionalProperties: false ---- - -## PR #{pr.meta.number}: {pr.meta.title} - -**{pr.stats.totalFiles}** files, **+{pr.stats.additions}** / **-{pr.stats.deletions}** - - - - - - - - -``` - -Zero eval blocks. - ---- - -## 7. Entry Points - -The CI and local roots keep workflow composition in Markdown. Both use -`ReviewSetup`, `ReviewContext`, `Doctor`, and `OxlintDiagnostics`; the CI root -places the complete review inside `` and wraps it in `GitHubAuth`. -The local root uses the same composition without `GitHubComment`. - -```markdown - - - - - - - - - - - - - - -``` - -The remaining eval block in each checked-in PR root only selects bounded values. -`ReviewDiagnostics` groups the normalized values through the existing -`buildDiagnostics` helper inside the selected Plugin, so compiled execution -needs no review-package import from an eval module. Git, GitHub, Oxlint -execution, configuration, and normalization live in typed function components or package modules. - -## 8. CI Workflow - -The review workflow checks out the requested revision, installs the pinned -Deno toolchain, runs `deno task setup`, and executes that checkout's -`./dist/xmd` binary with `--plugin ./packages/code-review-agent/mod.ts`. The -binary embeds none of this package: the only Plugin it ships is -`@executablemd/git`, so this graph arrives because the workflow selected it, and the package reads its own assets from the -checkout it was named in. - -It still passes no component include, which is the anti-shadowing claim and is -unaffected by how the Plugin is reached: a declared component answers ahead of -any same-named file in a search path, so a pull request cannot add a `Finding.md` -and have the review run the branch's own copy while reporting on it. -Credentials stay in the workflow environment and are consumed by the scoped -`GitHubAuth` provider. The CI root uses `` so execution errors fail the CLI while -ordinary review findings remain successful report text. The journal is -uploaded under `if: always()`; Actions does not parse journal records or -rendered error markers. - -## 9. Deterministic Analysis (separate CI jobs, unchanged) - -executable.md replaces the advisory/AI review layer and process enforcement. -Deterministic static analysis continues as separate CI jobs: - -| Job | Tool | What it catches | -|---|---|---| -| `lint` | Oxlint `--type-aware` | Unused vars, inferrable types, empty functions, type bloat, console/debugger | -| `dead-code` | Knip | Unused exports, files, types, dependencies (cross-file) | - -These block merges. The executable.md review is advisory. - ---- - -## 10. File Tree - -The component graph is package-owned and declared to every run (§3.5). The -`.reviews/` tree holds this repository's own review programs and the sensor -configuration and runtime state they read and write — no component definitions, -and no command passes a component `--include`. - -``` -packages/code-review-agent/ - src/ - review-components.ts The one assembly boundary - components.md Long-form docs for the seven registrations - - components/ Reserved TypeScript registrations - CommentReviewData.ts Pair extraction + GitHub response parsing - CommentReviewState.ts Model-response and checklist state - Doctor.ts Checkout readiness for review analysis - OxlintDiagnostics.ts Pinned Oxlint sensor run + normalization - ReviewDiagnostics.ts Diagnostic grouping and policy summary - RepositoryInventory.ts Repository source paths and totals - ReviewContext.ts Revision range, PR metadata, changed files - - documents/ - components/ Declared Markdown components - # Standard library - Finding.md Severity icon + message - ReviewSection.md Heading + children or clean message - Instruction.md System prompt middleware - Sample.md Sample invocation (review-owned copy) - Format.md Shared formatting helper - GitHubAuth.md Host-scoped credential middleware - GitHubComment.md Post/update PR comment - DeepInfraProvider.md DeepInfra Sample Api provider - OllamaProvider.md Ollama Sample Api provider - ReviewSetup.md Review preamble composition - ThinkFilter.md Strips model reasoning from output - - # Rule primitives (one eval block each, written once) - Threshold.md Numeric comparison - Pattern.md Regex match on added lines - Ratio.md Ratio of two regex counts - UnusedInDiff.md Declarations with no references - DescriptionCheck.md PR body length - LinkedIssue.md Issue linkage - ConfigSourceMix.md Config + source mixing - AbstractionNames.md Suspicious file names - NewDependencies.md Dependency justification - ReleaseSpecWarning.md Release-spec change warning - SuggestRemoval.md Removal suggestion rendering - CommentReview.md Prompt composition for comment review - - # Oxlint sensor surface - EnsureOxlint.md Provisions the pinned sensor toolchain - OxlintConfig.md Sensor profile selection - OxlintSignals.md Diagnostic signal extraction - OxlintSummary.md Sensor summary or unavailable warning - CleanupIssues.md Idempotent GitHub issue lifecycle - - # Policy report composition - PrPolicyReport.md Composes PR policies - RepoPolicyReport.md Composes repo policies - - policies/ Declared policy documents - ScopePolicy.md PR size and scoping hygiene - BloatPolicy.md Structural bloat patterns - SlopPolicy.md Verbosity/slop indicators - ExtraneousCodePolicy.md Semantic/correctness-focused review - RepoCleanupPolicy.md Repo-wide cleanup policy analysis - -.reviews/ - ReviewPR.md CI entry point (DeepInfra + GitHubComment) - ReviewPR.local.md Local entry point (Ollama + stdout) - AnalyzeRepo.md Local repository analysis - AnalyzeRepoCI.md CI repository analysis - DispatchRepoAnalysis.md Repository-analysis dispatch - policies/README.md Points at the package-owned policies - .oxlintrc.json Sensor profile (committed) - .oxlint/ Provisioned sensor binaries (runtime state) - tsconfig.oxlint.json Generated type-aware config (runtime state) -``` - ---- - -## 11. Eval Block Census - -| Document | Eval blocks | Why | -|---|---|---| -| `Finding.md` | 1 | Icon selection | -| `ReviewSection.md` | 1 | renderChildren + heading | -| `Instructions.md` | 1 persist | Middleware install | -| `GitHubComment.md` | 1 | renderChildren + GitHub API | -| `DeepInfraProvider.md` | 1 persist | Provider middleware | -| `OllamaProvider.md` | 1 persist | Provider middleware | -| `Threshold.md` | 1 | Comparison logic | -| `Pattern.md` | 1 | Regex matching | -| `Ratio.md` | 1 | Ratio computation | -| `UnusedInDiff.md` | 1 | Declaration scanning | -| `DescriptionCheck.md` | 0 | Uses `` | -| `LinkedIssue.md` | 1 | Regex test for `` | -| `ConfigSourceMix.md` | 1 | File classification for `` | -| `AbstractionNames.md` | 1 | Name pattern for `` | -| `NewDependencies.md` | 1 | Dependency check for `` | -| `CommentReview.md` | 0 | Prompt and capture composition | -| `CommentReviewData.ts` | 0 | Pair extraction and GitHub response parsing | -| `CommentReviewState.ts` | 0 | Model-response and checklist state | -| **`ScopeCheck.md`** | **0** | | -| **`StructuralBloat.md`** | **0** | | -| **`VerbosityCheck.md`** | **0** | | -| **`SemanticReview.md`** | **0** | | -| **`ReviewBody.md`** | **0** | | -| `ReviewPR.md` | 1 | Changed-file selection | -| `ReviewPR.local.md` | 1 | Changed-file selection | - -The authored Markdown keeps the review prompts and policy composition. The -procedural components are typed TypeScript modules, while only short binding -adapters remain in the two root documents. - ---- - -## 12. Implementation Order (All Complete) - -All phases have been implemented across PRs #34, #35, and the -code-review-agent PR: - -- Phase 1: executable.md core (text interpolation, eval return) — PR #34, #35 -- Phase 2: Infrastructure components — code-review-agent PR -- Phase 3: `@executablemd/code-review-agent` package — code-review-agent PR -- Phase 4: Rule components — code-review-agent PR -- Phase 5: Policy documents + entry points + CI — code-review-agent PR -- Phase 6: Sample modifier removal — PR #35 - ---- - -## 13. Extension: Oxlint Sensor (v2) - -**Full spec:** `oxlint-sensor-spec-v2.md` - -Oxlint runs as a structured signal source inside the review pipeline. -Its JSON output becomes a density metric the LLM uses alongside the -diff to detect quality deficits that correlate with unreviewed AI -output. All rules at `"warn"` — Oxlint collects signals, not verdicts. - -### 13.1 Sensor configuration - -`.reviews/.oxlintrc.json` — the committed review-sensor profile. All oxlint -invocations reference it via `--config .reviews/.oxlintrc.json`. - -The profile holds no catalog of its own. It extends `oxlint.shared.json`, the -one project-owned Oxlint policy the repository lint gate also extends, and adds -nothing but the plugin list Oxlint requires in an entry configuration. That -shared policy enables `correctness` and `suspicious`, leaves `pedantic` and -`style` off, names the 14 curated bloat signals one at a time, and turns off the -rule families that conflict with repository conventions: component filename -casing, named-export and export-order rules, generator/function style rules, -mechanical import/key-order and magic-number rules, and readonly-parameter -types. - -Inheritance does not make divergence impossible — Oxlint lets a later -configuration override an inherited rule. The guarantee is that the committed -sensor profile declares no built-in rule of its own, and that its active -built-in set stays a subset of the gate's. `scripts/tests/oxlint-policy.test.ts` -enforces both (OP2 and OP5) and fails verification if either stops holding. - -The gate profile, `.oxlintrc.json`, adds the local JavaScript plugin and the -repository's blocking rules at error severity. Those stay out of the sensor: it -remains advisory and carries no error severity at all. Details of the shared -policy, its catalog ownership, and its conformance suite are in -`specs/oxlint-sensor-spec.md` §4 and §14. - -### 13.2 Environment detection (`Doctor.ts`) - -The Doctor component probes the environment before oxlint runs: -oxlint binary, tsgolint binary, `node_modules/`, tsconfig, scheme -specifier scan (`jsr:`, `npm:`), and a type-aware test run. Outputs -a recommendation: `type-aware`, `type-aware-filtered`, or -`syntax-only`. The typed function component returns the bounded Doctor -object directly; raw process output never becomes a document binding. - -### 13.3 PR-scoped analysis - -PR entry points (`ReviewPR.md`, `ReviewPR.local.md`) pass the parsed changed -`.ts`/`.tsx` paths to the typed `OxlintDiagnostics` component. Density against -`pr.stats.additions` is only meaningful when diagnostics come from the same -files the additions are in. Repo analysis entry points run on everything. - -### 13.4 Density calibration - -| Density | Interpretation | -|---|---| -| < 0.020 | Clean — experienced contributor, reviewed code | -| 0.020–0.080 | Normal — minor issues, typical development | -| > 0.100 | Elevated — likely unreviewed generated code | - -3 decimal places to preserve signal in the narrow normal band. - -### 13.5 Policy updates - -- `ExtraneousCodePolicy.md` — density calibration thresholds, - interpretation rules, Rule of Three, and object literal assertion - detection added to LLM prompt -- `BloatPolicy.md`, `SlopPolicy.md` — `diagnostics` changed from - optional to required; defensive guards removed -- `RepoCleanupPolicy.md` — Rule of Three and YAGNI principles - added to LLM prompt - -### 13.6 Import specifier enforcement - -`lint-plugins/no-scheme-specifiers.ts` — Deno lint plugin that -flags `jsr:` and `npm:` scheme specifiers in source files and -auto-fixes them to bare specifiers. Required for tsgo/Oxlint -compatibility since tsgo uses Node module resolution. - -### 13.7 Process enforcement - -`.github/pull_request_template.md` — scope confirmation, Rule of -Three checklist for new abstractions, dependency justification. - -### 13.8 Cleanup issues (`CleanupIssues.md`) - -The repo analysis pipeline creates idempotent GitHub issues for -the top 5 file clusters ranked by `buildCleanupAnalysis()`. - -**Identity:** Each issue body contains a marker comment -``. The component searches open issues -with the `cleanup` label for matching markers before creating. - -**Lifecycle:** - -| Existing issue? | File in top 5? | Action | -|---|---|---| -| No | Yes | Create new issue | -| Yes | Yes | Update title + body with latest stats | -| Yes | No | Close with resolution comment | - -Issues are driven entirely by deterministic cluster data — file -path, score, violation count, co-occurring rule count, category -breakdown. No LLM output parsing. - -Local runs skip issue creation (no `GITHUB_TOKEN` → return empty). - -### 13.9 Additional files - -``` -oxlint.shared.json Canonical Oxlint policy both profiles extend - -.reviews/ - .oxlintrc.json Sensor profile (committed) - -packages/code-review-agent/src/documents/components/ - CleanupIssues.md Idempotent GitHub issue lifecycle - -.github/ - pull_request_template.md Process enforcement - -lint-plugins/ - no-scheme-specifiers.ts Deno lint plugin -``` - -## 14. Current review-infrastructure boundary - -The CI entrypoints are declarative compositions. Their executable shape is: - -```markdown - - - - - - - - - -``` - -`Doctor`, `ReviewContext`, `RepositoryInventory`, and `OxlintDiagnostics` are -typed function components. They use contextual runtime operations directly; -their props and return schemas stay explicit at the module boundary. Git and -GitHub calls use argument-array `exec` and the fluent -`fetch().expect().json()` operations. The code-review-agent package owns diff, -Oxlint normalization, Doctor classification, and structured result -construction. Raw process output, response objects, and credentials remain -inside generator-local variables; only bounded values become document -bindings. Markdown remains responsible for the visible composition and -provider hierarchy. - -`SENSOR_RULES` in that package's `categories.ts` is the one TypeScript catalog -of curated signals; Doctor derives its available and missing rules from it -rather than keeping a second list. The catalog is internal policy structure and -is not a published export. - -`packages/code-review-agent/src/documents/components/EnsureOxlint.md` provisions -the sensor's Oxlint 1.74.0 and -tsgolint 0.25.0 by checksum, and the repository dependency inputs name the same -two exact versions, so the policy the gate applies and the policy the sensor -applies are evaluated by the same linter. - -The review workflow runs `deno task setup` and then `./dist/xmd`, so the binary -and executable Markdown are from the same checkout. It selects this package -explicitly — `--plugin ./packages/code-review-agent/mod.ts` — because the binary -carries no Plugin and embeds none of this package's assets; the same selection -is written into the `review`, `review:local`, `analyze` and `analyze:ci` tasks -so a local run and a CI run install the same graph. The two CI roots use -`` error mode: execution failures return a failed document result and a -nonzero CLI exit, while ordinary finding text remains successful output. The -journal is uploaded with `if: always()` and is not parsed by Actions. diff --git a/specs/executable-mdx-spec.md b/specs/executable-mdx-spec.md index 7bdc44c7d..fe1650d50 100644 --- a/specs/executable-mdx-spec.md +++ b/specs/executable-mdx-spec.md @@ -1486,16 +1486,16 @@ acorn parse, then extracting the original source text. ```typescript // Eval block source: -import { parseDiff } from "@executablemd/code-review-agent"; +import { parseDiff } from "@scope/diff"; const pr = parseDiff(rawDiff, rawFiles, meta); // transformBlock produces: -// userImports: ['import { parseDiff } from "@executablemd/code-review-agent";'] +// userImports: ['import { parseDiff } from "@scope/diff";'] // code: 'const { rawDiff, rawFiles, meta } = env;\nconst pr = parseDiff(rawDiff, rawFiles, meta); env.pr = pr;' // compileBlock generates: import { sleep, spawn, ... } from "effection"; // STANDARD_IMPORTS -import { parseDiff } from "@executablemd/code-review-agent"; // userImports +import { parseDiff } from "@scope/diff"; // userImports export default function*(env) { const { rawDiff, rawFiles, meta } = env; const pr = parseDiff(rawDiff, rawFiles, meta); env.pr = pr; @@ -3259,52 +3259,6 @@ collisions are refused before the root document is imported. Ordinary `execute()`, `xmd run` and every inspection resolve without the tier, so nothing outside a workflow run learns that a bundle exists. -#### The run profile's review declarations - -Forty-one names are the code-review agent's, and an explicitly selected review -Plugin installs all of them into the `run` profile: thirty-five declared Markdown components — twenty-eight review -components, five policies and review-owned copies of `Sample` and `Instruction` — -and six reserved registrations, `CommentReviewData`, `CommentReviewState`, -`Doctor`, `OxlintDiagnostics`, `RepositoryInventory` and `ReviewContext`. - -Regression coverage keeps the review-owned `Sample.md` and `Instruction.md` -copies byte-identical to core's until an explicit divergence decision in the -code-review package. - -Both tiers mean the same thing here: the host claims the name. A review is the -one program that must not be answerable by the thing it is reviewing, so a -checkout that supplies its own `Finding.md` or `ReviewContext.ts` does not have -it chosen, and a second claim on one of these names is refused at admission -rather than ordered. - -Each Markdown component reports the package and the asset — -`@executablemd/code-review-agent/components/.md`, or `/policies/` for a -policy — with the SHA-256 of the bytes that build ships. The origin names no -filesystem path, because one component sits at three different absolute paths -across a source checkout, an installed `node_modules` tree and a compiled binary -that has no file for it at all, and all three are the same component. The six -registrations report the package itself. - -When the review Plugin is explicitly selected, its graph is installed wherever -that selected `run` profile is assembled: ordinary non-testing execution, a -nested ``, `xmd syntax` and ``, structural -validation, and the validation `` and `xmd plan` perform on a candidate — -so a program is checked against the vocabulary the later run actually supplies. -An unselected review Plugin contributes no names to execution or inspection. -`xmd test` is a different profile at its root and claims none of these names; a -run child claims them only when its Plugin selection includes the review Plugin. - -`@executablemd/code-review-agent` owns assembly; -`packages/code-review-agent/src/review-components.ts` is the only assembly boundary. Its component and policy -Markdown is loaded from `src/documents/components/` and `src/documents/policies/` -relative to that module's own URL across source, published-package and compiled -distributions. These reads use the Effection filesystem directly, bypassing -`API.Fs` and document-facing `Files` middleware. - -Claiming a name is not the same as narrowing discovery. A caller's `--include` -and a repository's own components resolve exactly as they did before, for every -name outside these forty-one. - #### Declared Markdown A trusted host may hand one execution exact first-party Markdown: the kind of diff --git a/specs/oxlint-policy-spec.md b/specs/oxlint-policy-spec.md new file mode 100644 index 000000000..bbc6833eb --- /dev/null +++ b/specs/oxlint-policy-spec.md @@ -0,0 +1,122 @@ +# Specification: Oxlint Policy + +**Scope**: the repository's built-in Oxlint policy and the gate profile +`deno task lint` runs. + +--- + +## 1. One shared policy, one gate profile + +`oxlint.shared.json` at the repository root is the project's whole built-in +Oxlint policy: plugins, categories, the advisory warnings, the denials and the +shared test override. `.oxlintrc.json` is the gate. It names the shared policy +through a native JSON `extends` path resolved relative to its own location, and +adds the local JavaScript plugin, the blocking rules and one further test +override: + +```text +oxlint.shared.json plugins, categories, warnings, denials, test override +└── .oxlintrc.json gate: local JavaScript plugin and blocking rules +``` + +The gate declares no built-in rule or category of its own. Oxlint merges from +first to last and the later configuration wins, so a gate entry for an inherited +rule would override it; `scripts/tests/oxlint-policy.test.ts` fails if one +appears. + +The gate repeats `"plugins"` because Oxlint reads the plugin set from the entry +configuration alone: an inherited list adds to Oxlint's defaults rather than +replacing them. `oxlint.shared.json` is deliberately not a name Oxlint +discovers on its own, so the file is reachable only through the explicit +`extends`. + +```jsonc +// .oxlintrc.json +{ + "extends": ["./oxlint.shared.json"], + "plugins": ["typescript", "unicorn", "import"], + "jsPlugins": [{ "name": "local", "specifier": "./scripts/oxlint-plugin.js" }], + "rules": { + "eslint/curly": "error", + "local/no-banned-jsdoc-words": "error", + "local/no-module-scoped-registry": "error", + "local/no-section-divider-comments": "error", + "local/no-sync-filesystem": "error", + "local/no-yield-in-finally": "error", + "local/prefer-effection-operation": "error", + "local/prefer-effection-result": "error", + "local/require-scope-bound-event-registration": "error" + }, + "overrides": [ + { + "files": ["**/*.test.ts", "**/*.spec.ts", "**/test/**", "**/tests/**"], + "rules": { "local/no-redundant-test-scope": "error" } + } + ] +} +``` + +Oxlint merges inherited override arrays, so the shared test override and the +gate's both apply to test files. + +Every local rule belongs to the gate. `local/no-sync-filesystem` is the one +whose escape hatch is itself part of the contract: the only accepted +suppression is a single `oxlint-disable-next-line` carrying an adjacent comment +that names the invariant a suspension would break, and no configuration +override, file-wide directive, or unexplained directive is allowed. Oxlint +suppresses a rule's own diagnostics inside a disabled range, so a rule cannot +report a file-wide disable of itself; `scripts/tests/no-sync-filesystem.test.ts` +audits the configuration and every linted source for that contract instead. + +## 2. Rule catalog + +The `correctness` and `suspicious` categories are the project-compatible +baseline, at `"warn"`. `pedantic` and `style` are off. + +Fourteen rules are enabled one at a time at `"warn"`. They are advisory: the +gate fails on errors, and none of them is an error. Ten need only syntax — +`no-unused-vars` (with `_`-prefixed names ignored), `no-inferrable-types`, +`no-empty-function`, `no-empty-object-type`, `no-useless-empty-export`, +`no-unnecessary-type-constraint`, +`no-unnecessary-parameter-property-assignment`, `no-static-only-class`, +`no-console` (allowing `warn` and `error`) and `no-debugger`. Four — +`no-unnecessary-type-assertion`, `no-redundant-type-constituents`, +`no-unnecessary-type-arguments` and `no-unnecessary-boolean-literal-compare` — +need type information and report only under `--type-aware`, which the lint task +does not pass. The shared test override turns `no-console` and +`no-empty-function` off for test files. + +The filename-case, named-export, export-order, function-style, generator-yield, +import-order, key-order, magic-number, and readonly-parameter rules conflict +with the repository's component and generator conventions, and each stays +explicitly `off` so a future category reclassification cannot revive it +silently. The unsafe-access, caught-error, and duplicate-import rules are named +candidates that stay off until adopted individually; no broad category is ever +enabled to obtain one desired rule. + +## 3. Toolchain pin + +Oxlint is pinned to one exact version in both `deno.json` and `package.json`, +and the binary the gate executes reports that version. Its optional type-aware +backend, `oxlint-tsgolint`, is pinned exactly in `deno.json`. The pins are exact +rather than caret ranges: a range plus a frozen lock would let a routine lock +refresh expand the active rule set, and a policy upgrade is meant to be an +explicit act. + +## 4. Test plan + +`scripts/tests/oxlint-policy.test.ts` runs under Deno, Node, and Bun. It reads +the configuration sources and executes `oxlint --print-config` against a +production module and a test module. + +| # | Test | Verify | +|---|------|--------| +| OP1 | Canonical shared policy | The gate's `extends` resolves to `oxlint.shared.json` | +| OP2 | Catalog ownership | Shared owns plugins, categories, denials and the test override; the gate repeats none of it | +| OP3 | Inheritance resolves | `--print-config` succeeds on both paths | +| OP4 | Denials hold | Every conflict and named candidate is off in the merged gate | +| OP5 | Gate blocking rules | `curly` and the local rules stay errors | +| OP6 | Overrides survive | Both test overrides reach the gate | +| OP7 | Override applies | A `tests/` file reports no `no-console`; its sibling does | +| OP8 | Version agreement | `deno.json`, `package.json`, and the executed binary name one version | +| OP9 | Broken inheritance | A missing inherited file fails the invocation instead of printing an empty policy | diff --git a/specs/oxlint-sensor-spec.md b/specs/oxlint-sensor-spec.md deleted file mode 100644 index cc7b692fd..000000000 --- a/specs/oxlint-sensor-spec.md +++ /dev/null @@ -1,1238 +0,0 @@ -# Specification: Oxlint Static Analysis Sensor - -**Extends**: `@executablemd/code-review-agent` spec -**Scope**: Oxlint as a structured signal source for the executable.md review -pipeline, environment compatibility analysis, and import specifier -enforcement for Deno/tsgo interoperability. - ---- - -## 1. Concept - -Oxlint runs through the typed `OxlintDiagnostics` component after -`ReviewContext` has constructed the diff. Its bounded diagnostic value becomes -a structured input to the review pipeline. The LLM receives both the diff and -the diagnostic map, interpreting the density and pattern of violations rather -than individual hits. - -Oxlint runs permissively — all bloat-relevant rules enabled at -`"warn"`, zero rules at `"error"`. It collects signals, not -verdicts. The executable.md agent decides what matters. - -Individual Oxlint violations are weak signals. The composite -metric is **density**: `diagnostics.total / pr.stats.additions` -(violations per added line). A few `no-inferrable-types` hits in a -large PR are noise. But clusters of `no-unused-vars` + -`no-empty-function` + `no-unnecessary-type-assertion` concentrated -in the same files suggest unreviewed generated code. - ---- - -## 2. Architecture - -``` -ReviewPR.md - ├─ → execution failures fail the document - ├─ → scoped exact-host GitHub authentication - ├─ ReviewContext → git diff and PR metadata → pr - ├─ Doctor (as="doctor") - │ ├─ Check: oxlint binary - │ ├─ Check: tsgolint binary - │ ├─ Check: node_modules/ - │ ├─ Check: tsconfig - │ ├─ Scan: scheme specifiers (jsr:, npm:) - │ └─ Probe: type-aware test run - ├─ OxlintDiagnostics (as="rawDiagnostics") - ├─ eval: buildDiagnostics(rawDiagnostics) → diagnostics - │ - └─ DeepInfraProvider (or OllamaProvider) - └─ Instructions - └─ GitHubComment (or stdout) - └─ ReviewBody - ├─ ScopeCheck (unchanged from base spec) - ├─ StructuralBloat (+ OxlintSignals) - ├─ VerbosityCheck (unchanged) - ├─ OxlintSummary (new) - └─ SemanticReview (+ diagnostics context) -``` - -Three layers of concern: - -| Layer | What | Components | -|---|---|---| -| Environment | Can Oxlint run? How much of it? | `Doctor.ts`, `ReviewSetup` | -| Collection | Run Oxlint, normalize output | `OxlintDiagnostics`, `buildDiagnostics` | -| Interpretation | LLM reads signals + diff | `SemanticReview`, `OxlintSummary` | - ---- - -## 3. Deno/tsgo Compatibility - -### 3.1 The problem - -Oxlint's type-aware backend (tsgolint) uses typescript-go (tsgo), -which resolves imports through standard Node module resolution. -Two things Deno projects commonly lack that tsgo requires: - -1. **`tsconfig.json`** — Deno uses `deno.json` `compilerOptions` - instead. tsgo doesn't read `deno.json`. - -2. **Bare specifiers** — Deno-native scheme specifiers (`jsr:`, - `npm:`) in source files are invisible to tsgo's resolver. - Every unresolvable import produces a "Cannot find module" - diagnostic that drowns useful signals. - -### 3.2 The fix: generated tsconfig - -The review workflow generates a `tsconfig.json` in `.reviews/` -that mirrors Deno's defaults. Not committed to the repo — -generated fresh each run, journaled via `durableExec`. - -The effectionx monorepo uses `nodeModulesDir: "auto"` in -`deno.json`, which means `deno install` creates a real -`node_modules/` tree. tsgo resolves bare specifiers through -this tree. - -```json -{ - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "bundler", - "strict": true, - "noEmit": true, - "skipLibCheck": true, - "resolveJsonModule": true, - "lib": ["ESNext", "DOM"], - "types": [] - }, - "include": ["packages/*/src/**/*.ts", "packages/*/*.ts"], - "exclude": ["node_modules", "dist", ".vendor", "**/*.test.ts"] -} -``` - -`moduleResolution: "bundler"` is the closest standard setting to -Deno's resolution — allows extensionless imports and `package.json` -exports. `DOM` in `lib` covers `fetch`, `crypto.subtle`, `URL` and -other web APIs Deno provides natively. `types: []` prevents tsgo -from auto-including `@types/node`. - -### 3.3 The fix: bare specifiers via import map - -Source files must use bare specifiers (`@std/assert`) not scheme -specifiers (`jsr:@std/assert`). The mapping lives in `deno.json` -`imports`: - -```json -{ - "imports": { - "@std/assert": "jsr:@std/assert", - "express": "npm:express@4" - } -} -``` - -Both Deno (via import map) and tsgo (via `node_modules/`) resolve -bare specifiers. This is the only import syntax compatible with -both resolvers. - -### 3.4 Lint plugin: `no-scheme-specifiers` - -A Deno lint plugin that flags `jsr:` and `npm:` scheme specifiers -in source files and auto-fixes them to bare specifiers. Requires -Deno 2.2+. - -#### File: `lint-plugins/no-scheme-specifiers.ts` - -```typescript -const plugin: Deno.lint.Plugin = { - name: "no-scheme-specifiers", - rules: { - "no-scheme-specifiers": { - create(context) { - function checkSource( - node: { source: { value: string } | null }, - ) { - if (!node.source) return; - const source = node.source.value; - if ( - typeof source === "string" - && (source.startsWith("jsr:") - || source.startsWith("npm:")) - ) { - const bare = source - .replace(/^jsr:/, "") - .replace(/^npm:/, "") - .replace(/@[\d^~>=<.*]+$/, ""); - - context.report({ - node: node.source, - message: - `Use bare specifier "${bare}" instead of ` - + `"${source}". Add "${bare}": "${source}" ` - + `to deno.json "imports".`, - fix(fixer) { - return fixer.replaceText( - node.source, - `"${bare}"`, - ); - }, - }); - } - } - - return { - ImportDeclaration: checkSource, - ExportNamedDeclaration: checkSource, - ExportAllDeclaration: checkSource, - }; - }, - }, - }, -}; - -export default plugin; -``` - -#### What it catches - -| Source | Flagged | Suggested bare specifier | -|---|---|---| -| `import { x } from "jsr:@std/assert"` | Yes | `@std/assert` | -| `import { x } from "npm:express@4"` | Yes | `express` | -| `export { y } from "jsr:@std/path"` | Yes | `@std/path` | -| `export * from "npm:lodash"` | Yes | `lodash` | -| `import { x } from "@std/assert"` | No — already bare | -| `import { x } from "node:fs"` | No — builtin | -| `import { x } from "./foo.ts"` | No — relative | - -#### Why not `no-external-import` - -Deno's built-in `no-external-import` was designed for `https://` -URL imports. It also flags `node:` and `bun:` builtins (open issue -denoland/deno_lint#1366). The custom plugin is precise: it flags -only `jsr:` and `npm:` scheme specifiers. - -#### Auto-fix behavior - -`deno lint --fix` replaces the specifier string in the source -file. It does NOT modify `deno.json`. The developer must add the -corresponding import map entry manually. The diagnostic message -tells them exactly what to add. - -#### `deno.json` configuration - -```jsonc -{ - "lint": { - "plugins": ["./lint-plugins/no-scheme-specifiers.ts"], - "rules": { - "include": ["no-scheme-specifiers/no-scheme-specifiers"] - } - } -} -``` - ---- - -## 4. Oxlint Configuration - -### 4.1 One shared policy, two profiles - -`oxlint.shared.json` at the repository root is the project's whole built-in -Oxlint policy. The gate and the sensor are profiles over it, each naming it -through a native JSON `extends` path resolved relative to the profile's own -location: - -```text -oxlint.shared.json plugins, categories, signal catalog, denials, test override -├── .oxlintrc.json gate: local JavaScript plugin and blocking rules -└── .reviews/.oxlintrc.json sensor: advisory, and nothing else -``` - -Inheritance alone does not prevent divergence. Oxlint merges from first to last -and the later configuration wins, so a profile that redeclared an inherited rule -would override it. What holds the two profiles together is what they contain and -what verification enforces: - -- the committed sensor profile declares no built-in rule, category, or override - of its own, so it applies the shared policy unmodified; and -- the sensor's active built-in rule set must remain a subset of the gate's. - -`scripts/tests/oxlint-policy.test.ts` is what makes those two statements binding. -OP2 fails if either profile grows a catalog of its own, and OP5 fails if the -sensor's active set stops being a subset of the gate's for any representative -path. A profile that overrode a shared rule would be caught there, not by Oxlint. - -Both profiles repeat `"plugins"` because Oxlint reads the plugin set from the -entry configuration alone: an inherited list adds to Oxlint's defaults rather -than replacing them. - -`oxlint.shared.json` is deliberately not a name Oxlint discovers on its own, so -the file is reachable only through an explicit `extends`. - -```jsonc -{ - "$schema": "./node_modules/oxlint/configuration_schema.json", - - "plugins": ["typescript", "unicorn", "import"], - - "categories": { - "correctness": "warn", - "suspicious": "warn", - "pedantic": "off", - "style": "off" - }, - - "rules": { - "eslint/no-unused-vars": ["warn", { - "args": "all", - "argsIgnorePattern": "^_", - "varsIgnorePattern": "^_", - "caughtErrorsIgnorePattern": "^_", - "destructuredArrayIgnorePattern": "^_", - "ignoreRestSiblings": true - }], - "typescript/no-inferrable-types": "warn", - "typescript/no-empty-object-type": "warn", - "typescript/no-useless-empty-export": "warn", - "typescript/no-unnecessary-type-constraint": "warn", - "typescript/no-unnecessary-parameter-property-assignment": "warn", - "unicorn/no-static-only-class": "warn", - "eslint/no-empty-function": "warn", - "eslint/no-console": ["warn", { "allow": ["warn", "error"] }], - "eslint/no-debugger": "warn", - - "typescript/no-unnecessary-type-arguments": "warn", - "typescript/no-unnecessary-type-assertion": "warn", - "typescript/no-redundant-type-constituents": "warn", - "typescript/no-unnecessary-boolean-literal-compare": "warn", - - "eslint/func-style": "off", - "eslint/no-magic-numbers": "off", - "eslint/require-yield": "off", - "eslint/sort-keys": "off", - "eslint/sort-imports": "off", - "typescript/prefer-readonly-parameter-types": "off", - "import/exports-last": "off", - "import/group-exports": "off", - "import/no-named-export": "off", - "import/prefer-default-export": "off", - "import/consistent-type-specifier-style": "off", - "unicorn/filename-case": "off", - - "typescript/no-unsafe-assignment": "off", - "typescript/no-unsafe-return": "off", - "typescript/no-unsafe-member-access": "off", - "eslint/preserve-caught-error": "off", - "eslint/no-duplicate-imports": "off" - }, - - "overrides": [ - { - "files": ["**/*.test.ts", "**/*.spec.ts", "**/test/**", "**/tests/**"], - "rules": { - "eslint/no-console": "off", - "eslint/no-empty-function": "off" - } - } - ] -} -``` - -The gate adds the local JavaScript plugin, seven blocking rules, and one further -test override. The sensor adds nothing: - -```jsonc -// .oxlintrc.json -{ - "extends": ["./oxlint.shared.json"], - "plugins": ["typescript", "unicorn", "import"], - "jsPlugins": [{ "name": "local", "specifier": "./scripts/oxlint-plugin.js" }], - "rules": { - "eslint/curly": "error", - "local/no-module-scoped-registry": "error", - "local/no-section-divider-comments": "error", - "local/no-sync-filesystem": "error", - "local/no-yield-in-finally": "error", - "local/prefer-effection-operation": "error", - "local/prefer-effection-result": "error" - }, - "overrides": [ - { - "files": ["**/*.test.ts", "**/*.spec.ts", "**/test/**", "**/tests/**"], - "rules": { "local/no-redundant-test-scope": "error" } - } - ] -} - -// .reviews/.oxlintrc.json -{ - "extends": ["../oxlint.shared.json"], - "plugins": ["typescript", "unicorn", "import"] -} -``` - -Oxlint merges inherited override arrays, so both test overrides apply to the -gate and the shared one applies to the sensor. - -Every local rule belongs to the gate alone. `local/no-sync-filesystem` is the -one whose escape hatch is itself part of the contract: the only accepted -suppression is a single `oxlint-disable-next-line` carrying an adjacent comment -that names the invariant a suspension would break, and no configuration -override, file-wide directive, or unexplained directive is allowed. Oxlint -suppresses a rule's own diagnostics inside a disabled range, so a rule cannot -report a file-wide disable of itself; `scripts/tests/no-sync-filesystem.test.ts` -audits the gate configuration and every linted source for that contract instead. - -### 4.2 Rule catalog - -Two things are enabled, and they are not the same thing. The -`correctness` and `suspicious` categories are the project-compatible baseline -both profiles inherit. The curated signal catalog is the list of 14 rules the -policy names one at a time, and it is the list Doctor reports on and -`categorizeRule` classifies. - -`pedantic` and `style` are off. The filename-case, named-export, export-order, -function-style, generator-yield, import-order, key-order, magic-number, and -readonly-parameter rules conflict with the repository's component and generator -conventions, and each stays explicitly `off` so a future category -reclassification cannot revive it silently. The unsafe-access, caught-error, and -duplicate-import rules are named candidates that stay off until adopted -individually; no broad category is ever enabled to obtain one desired rule. - -The curated rules are split into two groups by whether they require type -information: - -**Syntax-only (10 rules, always available):** - -| Rule | What it catches | -|---|---| -| `no-unused-vars` | Unused variables, functions, imports, types | -| `no-inferrable-types` | `const x: string = "hello"` | -| `no-empty-function` | Empty function bodies, scaffold stubs | -| `no-empty-object-type` | `interface Foo {}`, `type Foo = {}` | -| `no-useless-empty-export` | Redundant `export {}` | -| `no-unnecessary-type-constraint` | `` | -| `no-unnecessary-parameter-property-assignment` | Redundant `this.name = name` in constructor | -| `no-static-only-class` | Classes with only static members | -| `no-console` | `console.log` in production code | -| `no-debugger` | `debugger` statements | - -**Type-aware (4 rules, require tsgolint + tsconfig):** - -| Rule | What it catches | Requires | -|---|---|---| -| `no-unnecessary-type-assertion` | `str as string` when already string | `--type-aware` | -| `no-redundant-type-constituents` | `string \| unknown`, `string \| never` | `--type-aware` | -| `no-unnecessary-type-arguments` | Generic args matching defaults | `--type-aware` | -| `no-unnecessary-boolean-literal-compare` | `x === true` when `x: boolean` | `--type-aware` | - -`SENSOR_RULES` in `packages/code-review-agent/src/categories.ts` is the -TypeScript owner of this catalog — the union of `STRUCTURAL_RULES`, -`VERBOSITY_RULES`, and `TYPE_AWARE_RULES`. Doctor derives its available and -missing rules from it. The native JSON necessarily repeats the list, because the -production sensor runs a standalone Oxlint binary against a JSON configuration; -the conformance suite in §14 is the synchronization boundary. - -### 4.3 Two uses of Oxlint in CI - -| Role | Config | Severity | Blocks merge | Output | -|---|---|---|---|---| -| **Sensor** (executable.md review job) | `.reviews/.oxlintrc.json`, shared policy only | Advisory | No | JSON → LLM | -| **Gate** (separate lint job) | `.oxlintrc.json`, shared policy plus local rules | Blocking for its own rules | Yes | Human-readable | - -### 4.4 Toolchain alignment - -Policy comparison is only meaningful when both uses run the same linter, so the -repository dependency and the binary the review job downloads name one exact -version each: - -| Tool | Version | Declared in | -|---|---|---| -| Oxlint | 1.74.0 | `deno.json`, `package.json`, `EnsureOxlint.md` | -| tsgolint | 0.25.0 | `deno.json`, `EnsureOxlint.md` | - -The pins are exact rather than caret ranges. A range plus a frozen lock would -let a routine lock refresh expand the active rule set without any change to the -production sensor; a policy upgrade is meant to be an explicit act. - ---- - -## 5. New Exports in `@executablemd/code-review-agent` - -### 5.1 `Diagnostics` type - -```typescript -interface OxlintDiagnostic { - ruleId: string; - severity: "error" | "warning"; - message: string; - file: string; - line: number; - column: number; - endLine?: number; - endColumn?: number; -} - -interface DiagnosticGroup { - ruleId: string; - count: number; - files: string[]; - instances: OxlintDiagnostic[]; -} - -interface Diagnostics { - groups: DiagnosticGroup[]; - total: number; - fileCount: number; - ruleCount: number; - byCategory: { - structural: DiagnosticGroup[]; - verbosity: DiagnosticGroup[]; - typeAware: DiagnosticGroup[]; - other: DiagnosticGroup[]; - }; - summary: string; - density: number; -} -``` - -### 5.2 `parseDiagnostics` - -```typescript -function parseDiagnostics( - rawJson: string, - pr: PR, - doctor: DoctorResult, -): Diagnostics; - -function buildDiagnostics( - diagnostics: OxlintDiagnostic[], - pr: PR, - doctor: DoctorResult, -): Diagnostics; -``` - -**Rule categorization:** - -| Category | Rule IDs | -|---|---| -| `structural` | `no-unused-vars`, `no-empty-function`, `no-empty-object-type`, `no-static-only-class`, `no-useless-empty-export`, `no-unnecessary-type-constraint`, `no-unnecessary-parameter-property-assignment`, `no-unnecessary-type-arguments`, `no-unnecessary-type-assertion`, `no-redundant-type-constituents`, `no-unnecessary-boolean-literal-compare` | -| `verbosity` | `no-inferrable-types`, `no-console`, `no-debugger` | -| `typeAware` | All rules requiring `--type-aware` | -| `other` | Everything else | - -Rules may appear in multiple categories. - -**Noise filtering:** When `doctor.recommendation === "type-aware-filtered"`, -`buildDiagnostics` drops diagnostics matching import resolution -noise (`"Cannot find module"`, `"cannot find"`) before grouping. - -`parseDiagnostics` retains the published tolerant parser contract: malformed -JSON and unsupported JSON shapes produce an empty `Diagnostics` value. The -review execution path uses `normalizeOxlintOutput` first; that boundary is -strict and fails malformed output or an unexpected process exit before a -document binding is created. - -**Coverage annotation:** When `doctor.bloatRulesMissing.length > 0`, -summary appends: - -``` -Note: {N} type-aware rules unavailable ({rules}). Density may be understated. -``` - -When `doctor.nativeSpecifiers.count > 0`, summary appends: - -``` -Note: {N} source files use scheme specifiers (jsr:, npm:). -Run `deno lint --fix` with no-scheme-specifiers plugin to migrate. -``` - -**Summary generation:** - -``` -Oxlint: 23 diagnostics across 8 files (7 rules) -Density: 0.12 violations/added-line - - no-unused-vars (7): src/foo.ts, src/bar.ts, src/baz.ts - no-inferrable-types (5): src/foo.ts, src/helpers.ts - no-empty-function (4): src/services/auth.ts, src/services/db.ts - no-unnecessary-type-assertion (3): src/api.ts - no-console (2): src/handlers/webhook.ts - no-unnecessary-type-arguments (1): src/types.ts - no-redundant-type-constituents (1): src/types.ts -``` - -### 5.3 `parseDoctorResult` - -```typescript -function parseDoctorResult(json: string): DoctorResult; -``` - -`parseDoctorResult` remains a published tolerant parser. It applies defaults -to missing fields and returns the default Doctor value for malformed or -non-object JSON. The typed `Doctor` component returns its structured result -directly and does not serialize it for this parser. - -### 5.4 `DoctorResult` type - -```typescript -interface DoctorResult { - oxlintInstalled: boolean; - oxlintVersion: string; - tsgolintInstalled: boolean; - tsgolintVersion: string; - tsconfigExists: boolean; - nodeModulesExists: boolean; - typeAwareAvailable: boolean; - filesAnalyzed: number; - filesSkipped: number; - importErrors: number; - availableRuleIds: string[]; - bloatRulesAvailable: string[]; - bloatRulesMissing: string[]; - recommendation: "type-aware" | "type-aware-filtered" | "syntax-only"; - nativeSpecifiers: { - count: number; - files: string[]; - jsr: number; - npm: number; - }; -} -``` - -### 5.5 Doctor helpers - -```typescript -function summarizeDoctorProbe(input: DoctorProbeInput): DoctorProbeSummary; -function buildDoctorResult( - environment: DoctorEnvironment, - probe: DoctorProbeSummary, -): DoctorResult; -``` - -These helpers accept only the bounded diagnostic representation. The typed -`Doctor` function component keeps process output local and returns the -validated Doctor object directly. - -### 5.6 Package structure - -``` -packages/code-review-agent/ - src/ - review-components.ts declares the graph to every run - components/ - Doctor.ts environment probe (§6.1) - OxlintDiagnostics.ts sensor run + normalization (§6.2) - … the other four reserved registrations - documents/components/ - EnsureOxlint.md provisions the pinned toolchain - OxlintConfig.md sensor profile selection - OxlintSignals.md diagnostic signal extraction - OxlintSummary.md summary or unavailable warning - … the rest of the declared graph - parse-diff.ts - parse-diagnostics.ts - parse-doctor.ts - parse-oxlint.ts - doctor.ts - categories.ts - types.ts - mod.ts -``` - -`EnsureOxlint`, `Doctor` and `OxlintDiagnostics` are package assets: the -installation declares them, so a checkout under review cannot supply its own -sensor component. What stays checkout-relative is the state a run reads and -writes — `.reviews/.oxlintrc.json`, the provisioned binaries under -`.reviews/.oxlint/`, and the generated `tsconfig.oxlint.json`. - ---- - -## 6. Components - -### 6.1 `Doctor.ts` - -Compatibility analysis for the Oxlint static analysis sensor. -Probes the environment to determine which Oxlint capabilities are -available, scans for import specifier compatibility issues, and -recommends a run mode. The typed function component returns the bounded -Doctor object directly. - -Contextual `stat`, `exec`, `glob`, and `readTextFile` operations provide the -observations. Pure parsing and classification live in -`@executablemd/code-review-agent`; raw process output does not become a -document binding. - -**Checks performed:** - -| Check | What | Why | -|---|---|---| -| Oxlint binary | contextual `exec --version` | May not be installed. Without it, all static analysis signals are unavailable. | -| tsgolint binary | contextual `exec --version` | Separate Go binary for type-aware linting via typescript-go. Without it, 4 type-dependent rules are unavailable. | -| `node_modules/` | contextual `stat` | tsgolint resolves imports through Node module resolution. Created by `deno task setup`. | -| Generated tsconfig | contextual `stat` | tsgolint requires tsconfig. Generated by the typed `OxlintConfig` component. | -| Scheme specifiers | `glob` + `readTextFile` | These break tsgo resolution. Doctor reports them and explains the fix. | -| Type-aware probe | Full `oxlint --type-aware` run | Measures what actually works — noise ratio, crash detection, file coverage. | - -**Recommendations:** - -| Value | Meaning | -|---|---| -| `"type-aware"` | All prerequisites met, probe clean, noise < 30%. All 14 bloat rules. | -| `"type-aware-filtered"` | Type-aware works but noise ≥ 30%. Run type-aware, filter import noise in `parseDiagnostics`. | -| `"syntax-only"` | Prerequisites missing or probe crashed. 10 syntax-only rules, 4 type-aware missing. | - -Doctor is the production typed function component. It obtains observations -through contextual `stat`, `exec`, `glob`, and `readTextFile` operations, -passes bounded diagnostics to the package helpers, and returns a structured -Doctor result. A missing prerequisite or recognized type-aware crash selects -`syntax-only`; malformed output and unexpected process exits fail the -enclosing ``. - -### 6.2 `OxlintSignals.md` - -Per-category signal summaries embedded in existing policy documents. - -````markdown ---- -props: - type: object - properties: - groups: - type: array - label: - type: string - required: [groups, label] - additionalProperties: false ---- - -```ts eval -if (groups.length === 0) return; - -const lines = groups.map(g => - `- \`${g.ruleId}\` ×${g.count}: ${g.files.slice(0, 3).join(", ")}${g.files.length > 3 ? ` (+${g.files.length - 3})` : ""}` -); - -return `**Oxlint ${label}:**\n${lines.join("\n")}`; -``` -```` - -### 6.3 `OxlintSummary.md` - -Deterministic summary of diagnostics with doctor status. - -```markdown ---- -props: - type: object - properties: - diagnostics: - type: object - doctor: - type: object - required: [diagnostics, doctor] - additionalProperties: false ---- - - - - - -🟡 Oxlint not installed. Static analysis skipped. - - - - 0}> - -{diagnostics.summary} - - - - 0 - && doctor.oxlintInstalled}> - -*{doctor.bloatRulesMissing.length} type-aware rules unavailable -— install `oxlint-tsgolint` for full coverage.* - - - - -``` - ---- - -## 7. Updated Policy Documents - -### 7.1 `ReviewBody.md` - -```markdown ---- -props: - type: object - properties: - pr: - type: object - diagnostics: - type: object - doctor: - type: object - required: [pr, diagnostics, doctor] - additionalProperties: false ---- - -## PR #{pr.meta.number}: {pr.meta.title} - -**{pr.stats.totalFiles}** files, **+{pr.stats.additions}** / **-{pr.stats.deletions}** - - - - - - - - - - -``` - -### 7.2 `StructuralBloat.md` - -The only change from the base spec: `` appended. -The existing regex checks remain — they operate on the diff (what -was added), while Oxlint operates on the full file (current state). -Both perspectives are complementary. - -```markdown ---- -props: - type: object - properties: - pr: - type: object - diagnostics: - type: object - required: [pr, diagnostics] - additionalProperties: false ---- - - - - - - - - - - - - - - - - -``` - -### 7.3 `SemanticReview.md` - -The LLM prompt includes `diagnostics.summary` and -`diagnostics.density`, and instruction #5 tells it to look for -signal clusters. The LLM correlates Oxlint's structured data with -the raw diff. - -```markdown ---- -props: - type: object - properties: - pr: - type: object - diagnostics: - type: object - doctor: - type: object - required: [pr, diagnostics, doctor] - additionalProperties: false ---- - - 20}> - - - -You are reviewing a TypeScript PR for EXTRANEOUS code only. - -PR: {pr.meta.title} -Description: {pr.meta.body} - -STATIC ANALYSIS SIGNALS: -{diagnostics.summary} -Violation density: {diagnostics.density} per added line. - -Interpret these signals in context. A few inferrable-type warnings -in a large PR are noise. But clusters of unused-vars + -empty-functions + unnecessary-type-assertions concentrated in the -same files suggest unreviewed generated code. - -Report ONLY: -1. Scope creep — changes unrelated to stated purpose -2. Speculative abstractions — new constructs with one consumer -3. Dead constructs — declarations never referenced in diff -4. Wrapper indirection — functions that only forward calls -5. Signal clusters — files where multiple Oxlint rules fire together - -Do NOT flag test helpers, exported types, or style preferences. - -For each finding: FILE, PATTERN, CONCERN, QUESTION for the author. - -If clean: "No extraneous code patterns detected." - -DIFF: -{pr.diffPreview} - - - - - -✅ Small PR — semantic review skipped. - - - -``` - ---- - -## 8. Entry Points - -### 8.1 `ReviewPR.md` (CI with DeepInfra) - -The production entrypoint keeps the workflow hierarchy in Markdown. The -contextual components own Git and GitHub I/O, while root eval blocks only -select files and adapt bounded package values: - -```markdown - - - - - - - - - - - - - -``` - -The root keeps only short eval adapters: it selects changed TypeScript -paths and calls `buildDiagnostics` on the bounded return from -`OxlintDiagnostics`. Git, configuration, Oxlint execution, and normalization -remain in the typed components. `DeepInfraProvider` and `GitHubComment` stay -inside the same `` scope. - -### 8.2 `ReviewPR.local.md` (local with Ollama) - -The local entrypoint has the same component composition and replaces -`DeepInfraProvider`/`GitHubComment` with `OllamaProvider`. Its root eval -adapters select files and build the bounded diagnostics value; no shell -capture or raw Oxlint JSON crosses the document boundary. - ---- - -## 9. CI Workflow - -The review workflow checks out the requested revision, installs the pinned -Deno toolchain, runs `deno task setup`, and executes that checkout's -`./dist/xmd` binary. It passes credentials through the workflow environment -to the lexically scoped `GitHubAuth` component. The root is inside -``, so execution failures produce a nonzero CLI exit while ordinary -review findings remain report text. The journal is uploaded with `if: always()` -and Actions performs no journal or rendered-output postflight parsing. - -### Separate enforcement jobs (unchanged from base spec) - -| Job | Tool | Blocks merge | -|---|---|---| -| `lint` | Oxlint `--type-aware --max-warnings 0` | Yes | -| `dead-code` | Knip | Yes | - ---- - -## 10. What Oxlint Provides That Regex Cannot - -The base spec's `Pattern`, `Ratio`, and `UnusedInDiff` components -operate on added lines in the diff. They are fast and deterministic -but have hard limits: - -| Capability | Regex (base spec) | Oxlint (this spec) | -|---|---|---| -| Unused vars in added code | `UnusedInDiff` (name-counting heuristic) | Full scope analysis | -| Unused vars in existing code touched by PR | No | Lints whole file | -| Type system analysis | No | Via tsgolint | -| Redundant type assertions | No | `no-unnecessary-type-assertion` | -| Redundant type constituents | No | `no-redundant-type-constituents` | -| Unnecessary type arguments | No | `no-unnecessary-type-arguments` | -| Empty function detection | Regex pattern (approximate) | AST-aware (exact) | -| Console statements | Regex pattern | Rule with `allow` list | -| Inferrable types | No | `no-inferrable-types` | -| Cross-scope unused detection | Single-pass name counting | Full lexical scoping | - -Both views are complementary: regex sees the diff (what was added), -Oxlint sees the full file (current state). A file can be clean in -aggregate but the diff added extraneous code, or vice versa. - ---- - -## 11. Density as the Key Metric - -The `density` field (`diagnostics.total / pr.stats.additions`) is -the composite metric the LLM should weight most heavily. - -Calibration thresholds: - -| Density | Interpretation | -|---|---| -| < 0.02 | Clean — experienced contributor, reviewed code | -| 0.02–0.08 | Normal — minor issues, typical development | -| > 0.10 | Elevated — likely unreviewed generated code | - -The executable.md journal is the natural calibration mechanism: after -sufficient reviews, examine the journal to correlate density values -with human reviewer agreement. Adjust thresholds based on observed -false positive and false negative rates. - ---- - -## 12. File Tree - -``` -.reviews/ - ReviewPR.md CI entry point - ReviewPR.local.md Local entry point - tsconfig.oxlint.json Generated (not committed) - journal.jsonl - - components/ - # Base spec components (unchanged) - Finding.md - ReviewSection.md - Instructions.md - GitHubComment.md - DeepInfraProvider.md - OllamaProvider.md - Threshold.md - Pattern.md - Ratio.md - UnusedInDiff.md - DescriptionCheck.md - LinkedIssue.md - ConfigSourceMix.md - AbstractionNames.md - NewDependencies.md - CommentReview.md - ScopeCheck.md - StructuralBloat.md Updated (+ OxlintSignals) - VerbosityCheck.md - SemanticReview.md Updated (+ diagnostics) - ReviewBody.md Updated (+ diagnostics, doctor) - - # New components (this spec) - Doctor.ts Compatibility analysis - ReviewContext.ts PR context and GitHub body - RepositoryInventory.ts Repository file inventory - OxlintDiagnostics.ts Bounded Oxlint execution - OxlintSignals.md Per-category signal list - OxlintSummary.md Diagnostic summary + doctor status - -lint-plugins/ - no-scheme-specifiers.ts Deno lint plugin - -oxlint.shared.json Canonical built-in policy -.oxlintrc.json Gate profile (shared policy + local blocking rules) -.reviews/.oxlintrc.json Sensor profile (shared policy, advisory) - -packages/code-review-agent/ - src/ - parse-diff.ts Unchanged - parse-diagnostics.ts New - doctor.ts New - parse-oxlint.ts New - categories.ts New - types.ts Updated - mod.ts -``` - ---- - -## 13. Eval Block Census (Additions) - -| Document | Eval blocks | Change | -|---|---|---| -| `Doctor.ts` | 0 | Typed contextual probe and bounded result | -| `ReviewContext.ts` | 0 | PR diff and metadata acquisition | -| `RepositoryInventory.ts` | 0 | File and line inventory | -| `OxlintDiagnostics.ts` | 0 | Bounded Oxlint execution | -| `OxlintSignals.md` | 1 | **New** | -| `OxlintSummary.md` | 0 | **New** | -| `ReviewPR.md` | 2 | Modified (composition and binding adapters) | -| `ReviewPR.local.md` | 2 | Modified (same as above) | -| `ReviewBody.md` | 0 | Modified (added diagnostics, doctor props) | -| `StructuralBloat.md` | 0 | Modified (added OxlintSignals) | -| `SemanticReview.md` | 0 | Modified (added diagnostics to prompt) | - -The new contextual components contain no Markdown eval blocks. Root documents -retain only short binding adapters; prompt and policy composition remains -Markdown. - ---- - -## 14. Test Plan - -### Lint plugin: `no-scheme-specifiers` - -| # | Test | Verify | -|---|------|--------| -| NS1 | `jsr:` import flagged | `from "jsr:@std/assert"` → diagnostic | -| NS2 | `npm:` import flagged | `from "npm:express@4"` → diagnostic | -| NS3 | `jsr:` export flagged | `export { y } from "jsr:@std/path"` → diagnostic | -| NS4 | `export *` flagged | `export * from "npm:lodash"` → diagnostic | -| NS5 | Bare specifier clean | `from "@std/assert"` → no diagnostic | -| NS6 | `node:` clean | `from "node:fs"` → no diagnostic | -| NS7 | Relative clean | `from "./foo.ts"` → no diagnostic | -| NS8 | Fix strips `jsr:` | `"jsr:@std/assert"` → `"@std/assert"` | -| NS9 | Fix strips `npm:` + version | `"npm:express@4"` → `"express"` | -| NS10 | Fix preserves scoped packages | `"npm:@types/node@22"` → `"@types/node"` | -| NS11 | Message includes deno.json instruction | Contains `deno.json "imports"` | - -### Oxlint policy conformance - -`scripts/tests/oxlint-policy.test.ts` runs under Deno, Node, and Bun. It reads -the three configuration sources and executes `oxlint --print-config` against a -production module, a test module, and a review component. - -| # | Test | Verify | -|---|------|--------| -| OP1 | Canonical shared policy | Each profile's `extends` resolves to `oxlint.shared.json` | -| OP2 | Catalog ownership | Shared owns plugins, categories, signals, denials, test override; profiles repeat none of it | -| OP3 | Signal options | `no-unused-vars` and `no-console` keep the options density reads | -| OP4 | Inheritance resolves | `--print-config` succeeds for both profiles on all three paths | -| OP5 | Sensor subset | The sensor's active built-in rules are a subset of the gate's | -| OP6 | Curated signals enabled | Every `SENSOR_RULES` entry is `warn` and categorizes as other than `other` | -| OP7 | Denials hold | Every conflict and named candidate is off in both merged profiles | -| OP8 | Sensor advisory | No JavaScript plugin, no `local/*` rule, no error severity | -| OP9 | Gate blocking rules | `curly` and the six local rules stay errors | -| OP10 | Overrides survive | Both test overrides reach the gate, the shared one reaches the sensor | -| OP11 | Override applies | A `tests/` file reports no `no-console`; its sibling does | -| OP12 | Doctor catalog | Available equals `SENSOR_RULES`; syntax-only misses exactly `TYPE_AWARE_RULES` | -| OP13 | Version agreement | `EnsureOxlint.md`, `deno.json`, `package.json`, and the executed binary name one version | -| OP14 | Broken inheritance | A missing inherited file fails the invocation instead of printing an empty policy | - -### Doctor component - -| # | Test | Verify | -|---|------|--------| -| DR1 | Oxlint installed | Version captured, `oxlintInstalled: true` | -| DR2 | Oxlint missing | `NOT_INSTALLED` captured, `oxlintInstalled: false` | -| DR3 | tsgolint installed | Version captured, `tsgolintInstalled: true` | -| DR4 | tsgolint missing | `recommendation: "syntax-only"` | -| DR5 | node_modules exists | `nodeModulesExists: true` | -| DR6 | node_modules missing | Probe skipped, `recommendation: "syntax-only"` | -| DR7 | tsconfig exists | `tsconfigExists: true` | -| DR8 | tsconfig missing | Probe skipped | -| DR9 | Scheme specifiers found | `nativeSpecifiers.count > 0`, files listed | -| DR10 | No scheme specifiers | `nativeSpecifiers.count === 0` | -| DR11 | Probe clean, low noise | `recommendation: "type-aware"` | -| DR12 | Probe noisy (>30%) | `recommendation: "type-aware-filtered"` | -| DR13 | tsgolint crash | `recommendation: "syntax-only"` | -| DR14 | Full replay | No commands re-run, stored doctor result returned | - -### parseDiagnostics - -| # | Test | Verify | -|---|------|--------| -| PD1 | Groups by ruleId | Same ruleId diagnostics grouped together | -| PD2 | Computes density | `total / pr.stats.additions` | -| PD3 | Categorizes rules | `no-unused-vars` in structural, `no-console` in verbosity | -| PD4 | Filters noise when filtered mode | Import noise diagnostics dropped | -| PD5 | Annotates missing rules | Summary includes missing rule note | -| PD6 | Annotates scheme specifiers | Summary includes migration note | -| PD7 | Empty input | `total: 0`, `density: 0`, clean summary | -| PD8 | Malformed JSON | The diagnostic boundary fails rather than hiding an invocation error | - -### Integration - -| # | Test | Verify | -|---|------|--------| -| INT1 | Full pipeline: diff + doctor + oxlint + LLM | ReviewBody renders with all sections | -| INT2 | Doctor fallback: no oxlint | OxlintSummary shows "not installed" | -| INT3 | Doctor fallback: syntax-only | 4 type-aware rules listed as missing | -| INT4 | Signal clusters in SemanticReview prompt | LLM prompt contains diagnostics.summary | -| INT5 | Replay: full journal | No commands, no oxlint, no LLM calls | -| INT6 | Replay: partial (new commit) | Doctor and oxlint re-run for new diff | - ---- - -## 15. What This Does NOT Cover - -| Gap | Why | Mitigation | -|---|---|---| -| `no-unnecessary-type-parameters` | 1 of 2 missing tsgolint rules | LLM detection in SemanticReview prompt | -| Comment quality analysis | No linter rule exists | `CommentReview.md` (LLM-based, base spec) | -| Cross-file unused exports | Requires project-wide graph | Knip CI job (base spec §9) | -| Effection correctness | `yield` vs `yield*`, `async function*` | Separate correctness policy | -| `https://` URL imports | Different problem from scheme specifiers | Deno's built-in `no-external-import` rule | - -## 16. Current document boundary - -The production sensor keeps Markdown responsible for composition and uses -typed function components for contextual I/O: - -```markdown - - - -``` - -`Doctor` uses `stat`, `exec`, `glob`, and `readTextFile`; its parsing, -classification, import-noise aggregation, and result construction live in -`@executablemd/code-review-agent`. `OxlintDiagnostics` invokes Oxlint only -when its file list is nonempty and passes stdout through -`normalizeOxlintOutput`, which retains only `message`, `ruleId`, `severity`, -`file`, `line`, and `column`. It filters changed files in that package. -`buildDiagnostics` consumes that structured result without serializing it back -to JSON. A malformed JSON result, crash, or unexpected invocation exit fails the enclosing -`` instead of becoming an empty diagnostic list. A valid diagnostic -exit may contain zero diagnostics. - -The workflow provisions the checked-out binary through `deno task setup` and -executes `./dist/xmd`. It relies on the CLI exit status, keeps the journal -artifact under `if: always()`, and performs no journal-result or rendered-error -postflight parsing. diff --git a/specs/release-process-spec.md b/specs/release-process-spec.md index 337c402ac..0a6e97a30 100644 --- a/specs/release-process-spec.md +++ b/specs/release-process-spec.md @@ -95,11 +95,7 @@ the release rather than a tidying step afterwards. Only the members whose `name` is an `@executablemd` package change; an unrelated dependency that happens to share the old version number must not. -The bump touches nothing else. PR Review and Repo Analysis prepare and build -the checked-out revision with `deno task setup` and `deno task build`, then run -`./dist/xmd`. They do not -install the latest published release, so a review always understands the -documents at the revision it checks. +The bump touches nothing else. ## 3. Workflows @@ -133,16 +129,6 @@ documents at the revision it checks. subject per target, from one shared step. The `release` job needs the whole `build` matrix, so a failed attestation withholds every binary instead of releasing an unattested one. -- **`review.yml`** and **`repo-analysis.yml`**: install the repository-pinned - Deno and pnpm actions, run `deno task setup` and `deno task build`, and - execute the checked-out `./dist/xmd` against the checked-out Markdown root. - Neither passes a component include: the review and repository-analysis - component graph is declared by the code-review package embedded in that - binary (§10), so the components a review runs come from the installation - rather than from the checkout being reviewed. Their CI roots use `` - error mode, so execution failures fail the workflow through the CLI exit - status. Journals and reports are uploaded with `if: always()`; Actions does - not interpret journal records or rendered error markers. - **`publish-packages.yml`** (`push: tags v*`): GENERATED by `scripts/gen-publish-workflow.md` — an executable markdown document that expands the root `workspace` entries (including one-level globs such as @@ -713,8 +699,8 @@ The checks that hold this together, each proving a different build: not the checkout. A binary with no `node_modules` and no module graph to add to either loads an external ESM module and runs it, or it cannot, and nothing else can tell. It also proves the binary describes only the engine's own - language until a Plugin is named, and gains the review graph when one is. It - runs in the `smoke` job for the same reason. + language until a Plugin is named, and gains that Plugin's vocabulary when one + is. It runs in the `smoke` job for the same reason. - `scripts/tests/packaged-document.test.ts` holds the canonical compile inputs to the document *directories* that exist and are not empty, because which packages ship documents is the one thing no build discovers for itself. @@ -750,10 +736,6 @@ maintained: `PACKAGED_DOCUMENTATION` below. An entry here is for a package the binary executes Markdown out of without importing; naming one nothing imports would embed bytes no code can reach. -- `UNEMBEDDED_PACKAGES` — the packages whose assets the binary deliberately does - not carry, so the discovery sweep below does not demand them. The code-review - package is the one: it is selected with `--plugin` and reads its own assets - from wherever the operator installed it. - `PACKAGED_DOCUMENTS` — each package's `src/documents/`, embedded whole (§9). - `PACKAGED_DOCUMENTATION` — each package's `components.md`, named individually because the directories they sit in are package source, and embedding those @@ -778,11 +760,7 @@ provides. discoverable kinds and holds the lists to what it finds in **both** directions — a missing entry ships a binary without its asset, a stale one embeds nothing while reading as coverage, and a redundant one is rejected against the -whole-package entry that already covers it. `UNEMBEDDED_PACKAGES` hides a -package from that sweep, which is exactly the kind of claim that rots, so the -same test checks it: a CLI production module importing one — or the CLI package -depending on it — fails there, because the package would then be in the binary -with its assets missing rather than deliberately absent. +whole-package entry that already covers it. ### The documentation smoke diff --git a/specs/testing-spec.md b/specs/testing-spec.md index a1a4da1d7..dec2e1a83 100644 --- a/specs/testing-spec.md +++ b/specs/testing-spec.md @@ -380,9 +380,8 @@ installs the same Plugin **values** again inside that scope, with command `run` line. What it reinstalls is the run profile: the bundled Git Plugin, which the test root itself does not carry, plus whatever the invocation selected. A child of an invocation that named no `--plugin` therefore receives Git's thirteen names and -nothing else, and one that selected the code-review Plugin receives the review -graph too — which claims nothing at the test root and claims all forty-one of -its names in the child. +nothing else, and one that selected a Plugin also receives whatever that Plugin +contributes for command `run`. `host="workflow"`, and the `` scope it requires, are specified in issue #454 and are not built: a host that provides no workflow profile refuses diff --git a/tsconfig.node.json b/tsconfig.node.json index 2b4944e2a..194545022 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -19,7 +19,6 @@ "@executablemd/acp": ["./packages/acp/mod.ts"], "@executablemd/cli": ["./packages/cli/src/cli.ts"], "@executablemd/cli/api": ["./packages/cli/api.ts"], - "@executablemd/code-review-agent": ["./packages/code-review-agent/mod.ts"], "@executablemd/core": ["./packages/core/mod.ts"], "@executablemd/durable-streams": ["./packages/durable-streams/mod.ts"], "@executablemd/runtime": ["./packages/runtime/mod.ts"], @@ -39,7 +38,6 @@ "include": [ "packages/acp/**/*.ts", "packages/cli/**/*.ts", - "packages/code-review-agent/**/*.ts", "packages/core/**/*.ts", "packages/durable-streams/**/*.ts", "packages/git/**/*.ts",