Skip to content

feat: scaffold V1 — configs, action metadata, pure review core#1

Merged
aliasunder merged 23 commits into
mainfrom
feat/scaffold-v1
Jul 10, 2026
Merged

feat: scaffold V1 — configs, action metadata, pure review core#1
aliasunder merged 23 commits into
mainfrom
feat/scaffold-v1

Conversation

@aliasunder

Copy link
Copy Markdown
Owner

What

Initial scaffold of the umm-actually GitHub Action: repo tooling, action metadata, Docker packaging, CI, and the complete pure (no-I/O) core of the V1 review pipeline with full unit coverage.

Contents

Tooling — ESM, Node 24, strict tsconfig (noUncheckedIndexedAccess, exactOptionalPropertyTypes), typescript-eslint strict with type assertions banned (assertionStyle: never), Prettier (semi: false), vitest.

Action packagingaction.yml (inputs: model, fallback_model, max_findings [uncapped by default], severity_threshold, conventions_file, phases, context_budget_tokens, trace_related_files, pr_number; outputs: findings_count, review_url, model_used, skipped_reason) with image: Dockerfile during development — a release workflow will flip this to a digest-pinned GHCR image. Multi-stage node:24-slim Dockerfile. src/main.ts is a stub that fails loudly; the pipeline lands in the next PR.

Pure core modules

  • config.ts — Zod validation/coercion over all-string action inputs
  • github/event.ts — event payload → PrContext (complete for pull_request, needs-fetch for issue_comment)
  • diff/commentable-lines.ts + diff/annotate-diff.ts — pure transforms over parse-diff output; the annotated diff prints exactly the commentable line set (enforced by a cross-module test)
  • review/finding.ts — finding schema (severity + confidence + mandatory failure_scenario) and its strict-structured-output JSON schema
  • review/phases.ts — dimension text as constants; combined phase for V1, additive seam for V2 multi-phase
  • review/prompt.ts — deterministic system/user prompt assembly (diff-anchored tracing scope, proof-of-work analysis field, severity rubric, anchoring contract)
  • review/select-findings.ts — threshold filter, overlap dedupe (keeps higher severity), severity sort, optional cap
  • review/comment-mapping.ts — findings → inline review comments with anchor validation, near-miss snapping, and beyond-diff findings routed to the review body

Tests — 68 tests across 9 files against shared fixtures (real multi-hunk diff with rename/add/delete/binary cases, event payloads, sample LLM response). Key mutations verified to fail the right tests (commentable-line computation, multi-line comment start/end ordering).

CI — format check, lint, test, build, Docker build smoke; SHA-pinned actions, least-privilege permissions, persist-credentials: false.

Notes

  • Model suggestions render as plain diff fences rather than GitHub one-click suggestion blocks — suggestion blocks replace the commented range verbatim, which an LLM-supplied diff can't guarantee; deferred until the mapping can validate alignment.
  • fixtures/sample.diff line counts were hand-verified against parse-diff output.

Verification

  • npm test — 68/68 green
  • npm run lint, npm run prettier:check, npm run build — clean
  • docker build — succeeds; docker run exits 1 with the expected not-implemented error

🤖 Generated with Claude Code

- action.yml: inputs/outputs per V1 design, runs.using docker
- Multi-stage node:24-slim Dockerfile (build → slim runtime)
- Tooling: strict tsconfig, typescript-eslint strict (no as/!),
  Prettier semi:false, vitest, Node 24
- Pure core modules: config parsing (Zod over string inputs),
  event payload → PrContext resolution, commentable-line + annotated-diff
  transforms over parse-diff, finding schema with strict structured-output
  JSON schema, phase definitions (combined V1, V2 seam), prompt assembly,
  finding selection (threshold/dedupe/optional cap), review comment mapping
  with near-miss snapping and beyond-diff body routing
- 68 tests incl. cross-module annotate↔commentable consistency check
- CI: format, lint, test, build, docker build smoke (SHA-pinned actions)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because your workspace is out of credits. Ask your workspace admin to add credits to resume reviews. Manage billing

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@aliasunder, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 34 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d12c09a2-77c4-434b-b098-2e29a0c5c9f4

📥 Commits

Reviewing files that changed from the base of the PR and between 2f4b7bd and de18702.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (5)
  • AGENTS.md
  • package.json
  • src/__tests__/config.test.ts
  • src/config.ts
  • src/main.ts
📝 Walkthrough

Walkthrough

This PR adds repository configuration, documentation, Docker and CI setup, and GitHub Action metadata. It also introduces TypeScript modules and tests for structured logging, action config parsing, GitHub event resolution, diff annotation and commentable-line computation, review finding schemas and selection, review phases and prompt building, and mapping findings into review comments and review-body text. The entrypoint is still a stub that fails with a not-yet-implemented message.

Estimated code review effort: 4 (Complex) | ~75 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: scaffolding the V1 action with configs, action metadata, and pure review-core code.
Description check ✅ Passed The description is detailed and directly matches the changeset, covering tooling, packaging, core modules, tests, and CI.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@socket-security

socket-security Bot commented Jul 9, 2026

Copy link
Copy Markdown

@socket-security

socket-security Bot commented Jul 9, 2026

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm @emnapi/runtime is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: package-lock.jsonnpm/vitest@4.1.10npm/@emnapi/runtime@1.11.1

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@emnapi/runtime@1.11.1. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 security issue, 3 other issues, and left some high level feedback:

Security issues:

  • BlueOak-1.0.0: Open-source license could not be identified (link)

General comments:

  • In action.yml, the model input description says "no provider prefix" but the example anthropic/claude-sonnet-4-6 includes one; consider clarifying this to avoid confusion about the expected slug format.
  • In comment-mapping.ts, the SNAP_DISTANCE constant is currently hard-coded to 3; you may want to either make this configurable via action inputs or document the rationale so future changes to diff behavior are easier to reason about.
  • The Dockerfile runs npm ci twice (once before build and once with --omit=dev); consider restructuring the build steps to avoid reinstalling dependencies and instead prune dev deps after compilation for faster, leaner builds.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `action.yml`, the `model` input description says "no provider prefix" but the example `anthropic/claude-sonnet-4-6` includes one; consider clarifying this to avoid confusion about the expected slug format.
- In `comment-mapping.ts`, the `SNAP_DISTANCE` constant is currently hard-coded to 3; you may want to either make this configurable via action inputs or document the rationale so future changes to diff behavior are easier to reason about.
- The Dockerfile runs `npm ci` twice (once before build and once with `--omit=dev`); consider restructuring the build steps to avoid reinstalling dependencies and instead prune dev deps after compilation for faster, leaner builds.

## Individual Comments

### Comment 1
<location path="src/github/event.ts" line_range="36-37" />
<code_context>
+  }),
+})
+
+const issueCommentEventSchema = z.object({
+  issue: z.object({
+    number: z.int().positive(),
+    // Present only when the issue is actually a pull request
+    pull_request: z.looseObject({}).optional(),
+  }),
</code_context>
<issue_to_address>
**suggestion:** Using `z.looseObject({})` is discouraged and may be brittle against future zod changes.

`pull_request` is currently typed as `z.looseObject({}).optional()`, but `looseObject` is being deprecated in Zod. Please switch to `z.object({}).catchall(z.any()).optional()` (or a more specific shape if you start using its properties) to keep the same behavior while avoiding a deprecated API and clarifying that `pull_request` contents are intentionally ignored.

```suggestion
    // Present only when the issue is actually a pull request.
    // We intentionally ignore the contents of `pull_request`, but allow any shape.
    pull_request: z.object({}).catchall(z.any()).optional(),
```
</issue_to_address>

### Comment 2
<location path="Dockerfile" line_range="10-11" />
<code_context>
+COPY src ./src
+RUN npx tsc
+
+# Prune to production dependencies only
+RUN npm ci --omit=dev
+
+# Runtime stage: dist + production node_modules, nothing else
</code_context>
<issue_to_address>
**suggestion (performance):** Running `npm ci` twice in the build stage is redundant and adds build time.

The first `npm ci` for dev dependencies is later wiped by `npm ci --omit=dev`, causing a full reinstall of `node_modules` in the same stage. Consider keeping the dev install in a separate layer (or stage), running `tsc` there, and then installing only production dependencies in another layer/stage. This avoids duplicate installs, speeds up builds, and keeps the image structure clearer while preserving runtime behavior.
</issue_to_address>

### Comment 3
<location path="src/review/__tests__/comment-mapping.test.ts" line_range="65-42" />
<code_context>
+    })
+  })
+
+  it("degrades to a single-line comment when end_line falls in a different hunk", () => {
+    const finding = makeFinding({ line: 5, end_line: 142 })
+
+    const mapped = mapFindingsToReview({
+      findings: [finding],
+      commentableByPath: makeCommentableByPath(),
+    })
+
+    expect(mapped.comments[0]).toMatchObject({ line: 5, side: "RIGHT" })
+    expect(mapped.comments[0]?.start_line).toBeUndefined()
+  })
+
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests for multi-line edge cases: end_line equal to line, end_line before line, and non-commentable end_line

`multiLineEnd` currently guards several edge cases (end_line === line, end_line < line, and end_line not in rightLines), but the tests only exercise the different-hunk case. To better lock in the intended behavior, please add tests for:
- end_line === line → degrades to a single-line comment.
- end_line < line → degrades to a single-line comment.
- end_line not in commentable.rightLines (e.g., in a gap inside the hunk) → degrades to a single-line comment.
These will ensure future changes don’t accidentally allow invalid multi-line ranges.

Suggested implementation:

```typescript
  it("degrades to a single-line comment when end_line falls in a different hunk", () => {
    const finding = makeFinding({ line: 5, end_line: 142 })

    const mapped = mapFindingsToReview({
      findings: [finding],
      commentableByPath: makeCommentableByPath(),
    })

    expect(mapped.comments[0]).toMatchObject({ line: 5, side: "RIGHT" })
    expect(mapped.comments[0]?.start_line).toBeUndefined()
  })

  it("degrades to a single-line comment when end_line equals line", () => {
    const finding = makeFinding({ line: 143, end_line: 143 })

    const mapped = mapFindingsToReview({
      findings: [finding],
      commentableByPath: makeCommentableByPath(),
    })

    expect(mapped.comments[0]).toMatchObject({ line: 143, side: "RIGHT" })
    expect(mapped.comments[0]?.start_line).toBeUndefined()
  })

  it("degrades to a single-line comment when end_line is before line", () => {
    const finding = makeFinding({ line: 145, end_line: 143 })

    const mapped = mapFindingsToReview({
      findings: [finding],
      commentableByPath: makeCommentableByPath(),
    })

    expect(mapped.comments[0]).toMatchObject({ line: 145, side: "RIGHT" })
    expect(mapped.comments[0]?.start_line).toBeUndefined()
  })

  it("degrades to a single-line comment when end_line is not commentable in the hunk", () => {
    const finding = makeFinding({ line: 143, end_line: 144 })

    const commentableByPath = makeCommentableByPath()
    const originalFile = commentableByPath.get("src/greeter.ts")!

    // Create a gap inside the hunk by omitting line 144 from rightLines
    commentableByPath.set("src/greeter.ts", {
      ...originalFile,
      rightLines: new Set([
        1, 2, 3, 4, 5, 6,
        141, 142, 143, /* 144 omitted */ 145, 146, 147,
      ]),
    })

    const mapped = mapFindingsToReview({
      findings: [finding],
      commentableByPath,
    })

    expect(mapped.comments[0]).toMatchObject({ line: 143, side: "RIGHT" })
    expect(mapped.comments[0]?.start_line).toBeUndefined()
  })


const makeCommentableByPath = (): Map<string, CommentableFile> =>

```

These changes assume `makeFinding` and `mapFindingsToReview` are already imported and available in this test file, consistent with the existing tests. If the test suite relies on `makeCommentableByPath` being pure (returning a new Map each time), the mutation inside the "end_line is not commentable in the hunk" test is safe because it only affects that local instance. If there are other fixtures or helpers that depend on the exact `rightLines` shape, verify that no other tests in the same `describe` block reuse the mutated `commentableByPath` instance.
</issue_to_address>

### Comment 4
<location path="package-lock.json" line_range="2083-2098" />
<code_context>

</code_context>
<issue_to_address>
**security (license/minimatch):** BlueOak-1.0.0: Open-source license could not be identified

The obligations of the `BlueOak-1.0.0` license for this code could not be determined automatically. Unknown licenses may carry obligations or restrictions and should be reviewed manually to ensure compliance

*Source: trivy*
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/github/event.ts
Comment thread Dockerfile Outdated
Comment thread src/review/__tests__/comment-mapping.test.ts Outdated
Comment thread package-lock.json
aliasunder and others added 3 commits July 8, 2026 23:44
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…model description, snap rationale

- Dockerfile: separate prod-deps stage instead of double npm ci in one stage
- comment-mapping tests: end_line === line, end_line < line, non-commentable
  end_line all degrade to single-line comments
- action.yml: model description no longer contradicts its own example
- SNAP_DISTANCE: documented why 3

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- CODE_OF_CONDUCT.md (Contributor Covenant 2.1), CONTRIBUTING.md,
  SECURITY.md (incl. OpenRouter data-flow disclosure + prompt-injection
  threat model), CHANGELOG.md seed
- .gitleaks.toml (extends default ruleset), .coderabbit.yaml
- husky pre-commit (tsc --noEmit + lint-staged: eslint --fix + prettier)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/github/event.ts (1)

58-58: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract multi-clause conditional into a named boolean.

As per coding guidelines, multi-clause conditionals should be extracted into named booleans with affirmative names. This improves readability at the call site.

♻️ Suggested refactor
-  if (eventName === "pull_request" || eventName === "pull_request_target") {
+  const isPullRequestEvent =
+    eventName === "pull_request" || eventName === "pull_request_target"
+
+  if (isPullRequestEvent) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/github/event.ts` at line 58, The conditional in the event handling logic
is a multi-clause check and should be extracted into a named affirmative boolean
for readability. In the code around the eventName check in the event processing
path, create a descriptive boolean (for example, one indicating whether the
event is a pull request event) and use that in the if statement instead of
comparing eventName inline. Keep the change localized to the event handling
logic so the call site reads clearly.

Source: Coding guidelines

eslint.config.ts (1)

23-30: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Broaden the test glob and keep assertion checks strict.

This override misses *.spec.ts, and downgrading the assertion rules to warnings lets tests bypass the repo-wide no-as/! policy. Please cover both **/*.{test,spec}.ts and keep these rules at error.

As per coding guidelines, “Do not use type assertions (as) or non-null assertions (!) — use runtime guards or schema validation to narrow.”

♻️ Proposed fix
   {
-    files: ["**/__tests__/**/*.ts", "**/*.test.ts"],
+    files: ["**/__tests__/**/*.ts", "**/*.{test,spec}.ts"],
     rules: {
-      "`@typescript-eslint/consistent-type-assertions`": [
-        "warn",
-        { assertionStyle: "never" },
-      ],
-      "`@typescript-eslint/no-non-null-assertion`": "warn",
+      "`@typescript-eslint/consistent-type-assertions`": [
+        "error",
+        { assertionStyle: "never" },
+      ],
+      "`@typescript-eslint/no-non-null-assertion`": "error",
     },
   },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@eslint.config.ts` around lines 23 - 30, The ESLint override in the config is
too narrow and too permissive: update the test file glob in the top-level config
to include both test and spec files, and keep the assertion-related rules
enforced as errors rather than warnings. Adjust the override where the `files`
pattern and the `@typescript-eslint/consistent-type-assertions` and
`@typescript-eslint/no-non-null-assertion` settings are defined so
`**/*.{test,spec}.ts` is covered and the repo-wide no-`as`/`!` policy remains
strict.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Dockerfile`:
- Around line 16-23: The runtime stage in the Dockerfile currently starts from
node:24-slim and runs the ENTRYPOINT as root because there is no USER directive.
Add a non-root user in this runtime stage and switch to it before ENTRYPOINT,
keeping the existing COPY steps for prod-deps and build artifacts intact. Use
the runtime stage symbols around COPY and ENTRYPOINT to place the user setup and
ensure the container executes /app/dist/src/main.js as that non-root user.
- Around line 1-5: `npm ci --omit=dev` is still triggering the `prepare`
lifecycle script, which fails because `husky` is a devDependency not present in
the Docker image. Update the Dockerfile’s `prod-deps` and build stages to run
`npm ci` with `--ignore-scripts` so npm skips `prepare` entirely, and keep the
change aligned with the existing dependency-install steps (`prod-deps`/build
`RUN npm ci`).

In `@src/review/finding.ts`:
- Around line 33-44: Add a cross-field validation on findingSchema so end_line
cannot be less than line, since lineRange() assumes a non-inverted range and
duplicate overlap checks can fail otherwise. Keep the schema definition in
src/review/finding.ts using the existing findingSchema symbol, and implement the
check with a .refine() so the inferred type stays unchanged while validation
happens at runtime; this should remain compatible with z.toJSONSchema() output.

---

Nitpick comments:
In `@eslint.config.ts`:
- Around line 23-30: The ESLint override in the config is too narrow and too
permissive: update the test file glob in the top-level config to include both
test and spec files, and keep the assertion-related rules enforced as errors
rather than warnings. Adjust the override where the `files` pattern and the
`@typescript-eslint/consistent-type-assertions` and
`@typescript-eslint/no-non-null-assertion` settings are defined so
`**/*.{test,spec}.ts` is covered and the repo-wide no-`as`/`!` policy remains
strict.

In `@src/github/event.ts`:
- Line 58: The conditional in the event handling logic is a multi-clause check
and should be extracted into a named affirmative boolean for readability. In the
code around the eventName check in the event processing path, create a
descriptive boolean (for example, one indicating whether the event is a pull
request event) and use that in the if statement instead of comparing eventName
inline. Keep the change localized to the event handling logic so the call site
reads clearly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ad3cca53-6451-480f-b55d-9d38bebc7ed0

📥 Commits

Reviewing files that changed from the base of the PR and between 2f44de8 and c0fe1ff.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (45)
  • .coderabbit.yaml
  • .dockerignore
  • .github/workflows/ci.yml
  • .gitignore
  • .gitleaks.toml
  • .husky/pre-commit
  • .nvmrc
  • .prettierrc.json
  • AGENTS.md
  • CHANGELOG.md
  • CODE_OF_CONDUCT.md
  • CONTRIBUTING.md
  • Dockerfile
  • SECURITY.md
  • action.yml
  • eslint.config.ts
  • fixtures/issue_comment.review.json
  • fixtures/openrouter.response.json
  • fixtures/pull_request.opened.json
  • fixtures/sample.diff
  • package.json
  • src/__tests__/config.test.ts
  • src/__tests__/test-logger.ts
  • src/config.ts
  • src/diff/__tests__/annotate-diff.test.ts
  • src/diff/__tests__/commentable-lines.test.ts
  • src/diff/annotate-diff.ts
  • src/diff/commentable-lines.ts
  • src/github/__tests__/event.test.ts
  • src/github/event.ts
  • src/logger.ts
  • src/main.ts
  • src/review/__tests__/comment-mapping.test.ts
  • src/review/__tests__/finding.test.ts
  • src/review/__tests__/make-finding.ts
  • src/review/__tests__/phases.test.ts
  • src/review/__tests__/prompt.test.ts
  • src/review/__tests__/select-findings.test.ts
  • src/review/comment-mapping.ts
  • src/review/finding.ts
  • src/review/phases.ts
  • src/review/prompt.ts
  • src/review/select-findings.ts
  • tsconfig.json
  • vitest.config.ts

Comment thread Dockerfile Outdated
Comment thread Dockerfile
Comment thread src/review/finding.ts
aliasunder and others added 9 commits July 9, 2026 13:27
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the openai-SDK-with-baseURL approach. The official SDK is
generated from OpenRouter's OpenAPI spec, so provider routing preferences
and usage accounting are typed first-class instead of untyped extra-body
passthrough. The base SDK, not @openrouter/agent — the action makes
single structured-output completion calls, not tool-orchestrated loops.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per-run cost report (model, tokens, USD from OpenRouter usage accounting)
written to the workflow step summary, disableable per repo.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Convention: INPUT_* via @actions/core getInput; GITHUB_* ambient env via
env-var with an injectable env record (vault-cortex idiom). Codified in
AGENTS.md; first usage lands with main.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pure modules (diff/, review/) may not runtime-import I/O modules
(github/, openrouter/, context/) or SDK packages; type-only imports
allowed. Tests exempt (fixture reads). Verified the rule fails on a
planted violation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Levels with LOG_LEVEL threshold, V8-captured source location
(filename:line) on info+, child loggers merging context props, and
function-valued props resolved at emit time. JSON lines to stdout,
error level to stderr. Replaces the thin @actions/core wrapper;
failures still surface as Actions annotations via core.setFailed at
the pipeline boundary. 7 behavioral tests (79 total).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…g ranges

- npm ci in both Docker stages now passes --ignore-scripts: the prepare
  script runs husky (a devDependency), which broke the prod-deps stage
  with exit 127. All runtime deps are pure JS, so no install scripts
  are needed.
- lineRange() in select-findings now orders its ends, so a model-emitted
  inverted range (end_line < line) can't slip past the duplicate overlap
  check. Regression test added (mutation-checked).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/__tests__/logger.test.ts (1)

6-36: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract shared captureStream helper to eliminate duplication.

captureStdout and captureStderr are nearly identical — only the target stream differs. A shared helper reduces duplication and makes future stream capture (e.g., custom streams) trivial.

♻️ Proposed refactor
-const captureStdout = (): { lines: () => WrittenLine[] } => {
-  const writeSpy = vi
-    .spyOn(process.stdout, "write")
-    .mockImplementation(() => true)
-  onTestFinished(() => writeSpy.mockRestore())
-  return {
-    lines: () =>
-      writeSpy.mock.calls.map((call): WrittenLine => {
-        const written = call[0]
-        if (typeof written !== "string")
-          throw new Error("expected string write")
-        return JSON.parse(written)
-      }),
-  }
-}
-
-const captureStderr = (): { lines: () => WrittenLine[] } => {
-  const writeSpy = vi
-    .spyOn(process.stderr, "write")
-    .mockImplementation(() => true)
-  onTestFinished(() => writeSpy.mockRestore())
-  return {
-    lines: () =>
-      writeSpy.mock.calls.map((call): WrittenLine => {
-        const written = call[0]
-        if (typeof written !== "string")
-          throw new Error("expected string write")
-        return JSON.parse(written)
-      }),
-  }
-}
+const captureStream = (
+  stream: typeof process.stdout | typeof process.stderr,
+): { lines: () => WrittenLine[] } => {
+  const writeSpy = vi
+    .spyOn(stream, "write")
+    .mockImplementation(() => true)
+  onTestFinished(() => writeSpy.mockRestore())
+  return {
+    lines: () =>
+      writeSpy.mock.calls.map((call): WrittenLine => {
+        const written = call[0]
+        if (typeof written !== "string")
+          throw new Error("expected string write")
+        return JSON.parse(written)
+      }),
+  }
+}
+
+const captureStdout = () => captureStream(process.stdout)
+const captureStderr = () => captureStream(process.stderr)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/__tests__/logger.test.ts` around lines 6 - 36, The `captureStdout` and
`captureStderr` helpers duplicate the same stream-spying logic, so extract a
shared `captureStream` helper and reuse it from both functions. Keep the
existing behavior in `logger.test.ts` by passing the target stream
(`process.stdout` or `process.stderr`) and returning the same `lines()` reader,
while preserving the `onTestFinished` restore and JSON parsing logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@eslint.config.ts`:
- Around line 49-52: The second no-restricted-imports pattern in
eslint.config.ts is missing allowTypeImports: true, so type-only imports from
`@actions/`** and `@openrouter/`** are incorrectly blocked even though they should
be allowed. Update the restricted-imports configuration in the
no-restricted-imports rule so the pattern that contains group: ["node:fs",
"node:fs/**", "`@actions/`**", "`@openrouter/`**"] also includes allowTypeImports:
true, matching the intent of the first pattern and preserving type-only imports.

In `@src/logger.ts`:
- Line 1: The logger currently reads LOG_LEVEL from raw process.env at module
load time, which makes log-level behavior non-injectable and fragile in tests.
Update the logger setup in logger.ts to use env-var with an injectable env
record, and move the LOG_LEVEL lookup into createLogger so callers can pass a
custom env object for tests. Keep the existing logger behavior, but ensure
createLogger (and any helper it uses) accepts the env override and no longer
depends on module-level env access.

---

Nitpick comments:
In `@src/__tests__/logger.test.ts`:
- Around line 6-36: The `captureStdout` and `captureStderr` helpers duplicate
the same stream-spying logic, so extract a shared `captureStream` helper and
reuse it from both functions. Keep the existing behavior in `logger.test.ts` by
passing the target stream (`process.stdout` or `process.stderr`) and returning
the same `lines()` reader, while preserving the `onTestFinished` restore and
JSON parsing logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4d45d119-7de6-4404-81da-5d034119d3c3

📥 Commits

Reviewing files that changed from the base of the PR and between c0fe1ff and 0aa3030.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (13)
  • AGENTS.md
  • CONTRIBUTING.md
  • Dockerfile
  • action.yml
  • eslint.config.ts
  • package.json
  • src/__tests__/config.test.ts
  • src/__tests__/logger.test.ts
  • src/__tests__/test-logger.ts
  • src/config.ts
  • src/logger.ts
  • src/review/__tests__/select-findings.test.ts
  • src/review/select-findings.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • package.json
  • action.yml
  • src/config.ts
  • src/review/tests/select-findings.test.ts
  • Dockerfile
  • src/tests/config.test.ts
  • src/review/select-findings.ts

Comment thread eslint.config.ts
Comment thread src/logger.ts Outdated

@aliasunder aliasunder left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Phase 1: PR Review

5 findings across 5 files (dimensions: correctness, security/performance, docs). 3 would-fix (trivial), 2 flagged as needs-design-decision.

  • [D1] src/diff/annotate-diff.ts:43\ No newline at end of file markers render as phantom content lines with duplicated line numbers (verified against parse-diff source + runtime)
  • [D1] src/logger.ts:83JSON.stringify throws on circular refs/BigInt, so a log call can crash the action
  • [D4] src/review/prompt.ts:79 — untrusted file content can close the </file> wrapper (prompt-injection delimiter escape)
  • [D4] Dockerfile:2 — mutable node:24-slim base tag vs. the repo's SHA-pinning convention for actions
  • [D6] AGENTS.md:23 — structure tree lists modules that don't exist yet (openrouter/, context/, orchestrate.ts, octokit wrappers)

Verified clean: ci.yml action SHAs match their claimed version tags (checked via GitHub API), SECURITY.md claims match the implementation, comment-mapping hunk/snap edge cases guarded, all 80 tests pass.

Verdict: ship-with-minor-fixes


🔍 ship-check · pr-review · claude-fable-5

Comment thread src/diff/annotate-diff.ts Outdated
Comment thread src/logger.ts Outdated
Comment thread src/review/prompt.ts Outdated
Comment thread Dockerfile Outdated
Comment thread AGENTS.md Outdated
…logger env

- The second no-restricted-imports pattern (node:fs + SDKs) now sets
  allowTypeImports: true, matching the first pattern and the documented
  intent — each pattern is evaluated independently, so the flag did not
  carry over.
- LOG_LEVEL now resolves through env-var with an injectable env record
  (per AGENTS.md env conventions) instead of raw process.env property
  access at module load. createLogger takes an optional env option,
  child loggers inherit it. 3 new tests (mutation-checked); the default
  threshold test now injects an empty record so ambient LOG_LEVEL can't
  flip it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@aliasunder aliasunder left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Phase 2: Code Quality

6 findings across 5 files (naming 1, structure 2, comments 1, simplicity 2, module conventions 0).

Two additional items flagged without inline comments (design decisions, not posted as findings):

  • The severity list is duplicated between src/config.ts (SEVERITY_LEVELS) and src/review/finding.ts (FINDING_SEVERITIES), held consistent by a cross-module test. Single source of truth would be simpler, but importing across the two would couple action-input parsing to the LLM wire schema — the consistency test is a defensible decoupling. Worth an explicit call either way.
  • prompt.ts interpolates path/reason into <file …> attributes unquoted-unescaped — same injection family as the Phase 1 </file> delimiter finding already deferred as a design decision; resolve them together.

🔍 ship-check · code-quality · claude-fable-5

Comment thread src/config.ts Outdated
Comment thread src/review/finding.ts Outdated
Comment thread src/review/comment-mapping.ts
Comment thread src/review/comment-mapping.ts Outdated
Comment thread src/review/select-findings.ts Outdated
Comment thread src/diff/annotate-diff.ts Outdated
…crash guard, refactors

pr-review findings:
- annotate-diff: filter parse-diff's '\ No newline at end of file' marker
  changes — they duplicated the previous change's line number and rendered
  as phantom content lines, undermining anchoring (new fixture + test)
- logger: JSON.stringify wrapped with a degraded fallback so circular
  refs / BigInt in log data can't crash the action (new test)
- AGENTS.md: mark not-yet-landed modules (openrouter/, context/,
  orchestrate.ts, github/ octokit wrappers) as planned

code-quality findings:
- config: extract shared parsePositiveInteger; optional variant composes it
- finding: severityRank → SEVERITY_RANK (constant-table naming consistency)
- comment-mapping: capture end_line local (drops dead null re-check);
  extract 40-line map callback to classifyFinding
- select-findings: single sort with full comparator before dedupe (was two
  sort passes); kept duplicate now deterministic on severity ties
- annotate-diff: name LINE_NUMBER_WIDTH once — deleted-row padding was
  silently coupled to padStart(6)

Both behavioral fixes mutation-checked. 85 tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@aliasunder aliasunder left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Phase 3: Test Audit

5 test quality findings, 6 coverage gaps across 9 files (10 test files audited, 85 tests, all passing).

All 6 coverage gaps were mutation-verified in a sandbox copy: each listed mutation was applied to the production code and the full suite stayed green, proving the behavior is currently unpinned. No coverage regressions (no tests exist on main to regress against). Prior-phase findings fixed in 70d3145 were re-checked and are not re-reported; the fixes for the phantom no-newline lines and the logger serialization guard both carry solid dedicated tests.


🔍 ship-check · test-audit · claude-fable-5

Comment thread src/__tests__/logger.test.ts Outdated
Comment thread src/review/__tests__/prompt.test.ts Outdated
Comment thread src/diff/__tests__/annotate-diff.test.ts
Comment thread src/diff/__tests__/annotate-diff.test.ts Outdated
Comment thread src/review/__tests__/comment-mapping.test.ts Outdated
Comment thread src/review/select-findings.ts Outdated
Comment thread src/github/event.ts
Comment thread src/logger.ts
Comment thread src/review/comment-mapping.ts
Comment thread src/review/prompt.ts
…pendabot

- Untrusted-content wrappers (file, conventions, diff, prior_findings) now
  carry a per-run random suffix (generateDelimiterNonce, 12 hex chars) so a
  malicious PR containing a literal closing tag can't break out of the
  wrapper into instruction position; path/reason attributes escape double
  quotes. buildUserPrompt takes a required delimiter_nonce param (no
  callers yet — orchestrate.ts lands next PR). 4 new tests incl. breakout
  regression (mutation-checked). SECURITY.md documents the defense.
- Dockerfile base image digest-pinned on all three stages (same idiom as
  SHA-pinned actions in ci.yml) — the image builds on the consumer's
  runner, so a mutated tag flows into the container holding both tokens.
  New .github/dependabot.yml (docker ecosystem, weekly) restores the patch
  stream the pin would otherwise lose. Local docker build verified against
  the pinned digest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@aliasunder aliasunder left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Orchestrator triage — resolution for the deferred severity-duplication finding from the code-quality phase (severity list defined in both src/config.ts and src/review/finding.ts, with no consistency guard). Decision recorded in the inline comment on src/review/finding.ts for the implementing agent.


🔍 ship-check · orchestrator-triage · claude-fable-5

Comment thread src/review/finding.ts

@aliasunder aliasunder left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Phase 4: Bug Check

9 bugs found across 7 files (all inline below). Reviewed at 70d3145 per dispatch; the PR head has since moved to ac6e817 (delimiter nonce + attribute escaping, Dockerfile digest pin, dependabot) — none of the 9 findings are fixed by those commits, all anchors verified against the current head, and finding 9 is new in ac6e817 itself.

Confidence: 4 high, 5 medium, 0 low

By dimension: description-vs-implementation 3 · boundary/off-by-one 1 · behavioral consistency 3 · input validation 1 · platform/encoding 1

Verified non-findings (checked, clean): both CI action SHA pins resolve to exactly the tags their comments claim (checked via the GitHub API); parse-diff's no-newline-marker behavior matches the annotate-diff doc comment (verified against parse-diff 0.12.0); finding.ts's strict-structured-output schema claims hold (every key required, additionalProperties false — verified on the generated schema); .dockerignore excludes tests/fixtures so the Docker build stays consistent with tsconfig's include; ENTRYPOINT path matches the rootDir output layout; generateDelimiterNonce matches SECURITY.md's 12-hex example.


🔍 ship-check · bug-check · claude-fable-5

Comment thread src/review/comment-mapping.ts Outdated
Comment thread src/logger.ts Outdated
Comment thread src/logger.ts Outdated
Comment thread src/review/prompt.ts Outdated
Comment thread src/review/prompt.ts
Comment thread tsconfig.json Outdated
Comment thread src/review/comment-mapping.ts
Comment thread src/review/comment-mapping.ts Outdated
Comment thread CHANGELOG.md Outdated
…rds, prompt hardening, severity single-source

test-audit (11 findings): sentinel emits for silent no-op tests, positive
anchors before absence assertions, fixture-count anchor on the cross-module
loop, full toBe on deterministic annotate/comment output, and coverage for
required-integer empty rejection, cross-file dedupe, pull_request_target,
malformed issue_comment, warn routing, debug source omission, LOG_LEVEL
case-insensitivity, SNAP_DISTANCE boundaries, and null PR body.

bug-check (9 findings):
- comment-mapping: snap is now bounded by SNAP_DISTANCE itself (a nearby
  pure-deletion hunk could snap 488 lines away); suggestion fences size to
  the longest backtick run; body findings render suggestions
- logger: throwing lazy props record a placeholder instead of crashing;
  per-call data resolves through the same lazy path as child props
- prompt: truncation calls toWellFormed (no lone surrogates); PR title/
  description/branch wrapped in nonce-tagged pr_metadata block, closing the
  SECURITY.md claim gap
- tsconfig: module/moduleResolution NodeNext — compiler now enforces the
  .js-extension ESM rule the docs declare
- CHANGELOG/CONTRIBUTING: release-workflow claims reworded to future tense

severity single-source (orchestrator decision, phases pattern): config
validates severity_threshold shape only; review/finding.ts owns the
vocabulary via resolveSeverityThreshold (startup fail-fast when the
orchestrator lands). SEVERITY_LEVELS removed from config.

Also extracted the shared captureStream test helper (CodeRabbit nitpick).
24 new tests (109 total), key fixes mutation-checked.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@aliasunder

Copy link
Copy Markdown
Owner Author

Responses to the two review-body items that have no inline thread:

CodeRabbit nitpick (logger.test.ts — shared captureStream helper): Fixed in d53b3a8 as proposed — captureStdout/captureStderr are now one-line wrappers over a shared captureStream.

Phase 2 code-quality body items:

  • Severity-list duplication — resolved per the orchestrator-triage decision on the finding.ts thread (phases pattern, implemented in d53b3a8; maintainer re-confirmed the disposition against the original discussion).
  • path/reason attribute escaping in prompt.ts — resolved together with the delimiter finding as requested: ac6e817 added per-run randomized delimiter tags plus double-quote escaping on both attributes, and d53b3a8 extended the nonce wrapper to the PR metadata section.

🔍 ship-check · pr-monitor · claude-fable-5

aliasunder and others added 6 commits July 9, 2026 19:37
…braces

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ed parsing

@actions/core already ships getBooleanInput, which enforces the YAML 1.2
core-schema boolean list (true|True|TRUE / false|False|FALSE) and throws
on anything else — no reason to reinvent that. RawInputs now carries
booleans pre-parsed from the collection boundary (main.ts, wired next PR
with the orchestrator); the booleanString transform is gone and the
schema validates them as plain z.boolean(). One behavior shift: a bad
boolean input now fails at collection with getBooleanInput's error
rather than inside parseConfig's aggregated message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Module layering: before writing a parsing/validation helper, check the
  SDK's utility surface (getBooleanInput replaced the hand-rolled
  boolean-string transform); 'validation lives in config.ts' scopes our
  rules, not the platform's.
- Code style: block bodies for any multiline function response;
  expression bodies only for one-liners.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…oundary

collectRawInputs gathers strings via getInput and booleans via
getBooleanInput (strict YAML 1.2 list), then parseConfig validates —
bad inputs now fail loudly today instead of waiting for the orchestrator
PR. Verified end-to-end: cost_summary=maybe fails with the YAML 1.2
error; valid inputs reach the pipeline stub.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ut fix

- AGENTS.md env rule: event name/payload come from @actions/github
  context (it reads GITHUB_EVENT_NAME/GITHUB_EVENT_PATH and parses the
  JSON) — the old bullet's GITHUB_EVENT_PATH example steered toward
  hand-reading what the SDK already provides, the same trap as
  booleanString. env-var remains for genuinely ambient values.
- AGENTS.md structure tree: main.ts line now describes what exists
  (input collection/validation) with pipeline + outputs marked next PR.
- Remove @octokit/webhooks-types — devDependency imported nowhere;
  runtime payload validation is the zod schemas in event.ts.

Verified clean in the same sweep: all ci.yml convention claims (SHA
pins, persist-credentials, least-privilege permissions), no raw
process.env reads outside the injectable logger default, parse-diff/
zod/toJSONSchema/getInput/setFailed all genuinely used.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant