Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@
"maxStateNotes": 20,
"includeStaleNotes": false
},
"_repoWikiContextComment": "Optional advisory repo-wiki packet surfaced to review prompts. Disabled means no packet is read and prompts stay unchanged.",
"repoWikiContext": {
"enabled": false,
"packetPath": ".neondiff/repo-wiki-packet.json",
"maxPacketBytes": 12000,
"includeStaleContext": false
},
"_workRootComment": "workRoot must resolve outside this checkout (enforced at config load: config.workRoot must be outside the current repository checkout). /tmp/neondiff is a placeholder that satisfies that check on any machine without edits; point it at a durable path for real use.",
"workRoot": "/tmp/neondiff/runtime",
"statePath": "/tmp/neondiff/state/reviews.sqlite",
Expand Down
20 changes: 20 additions & 0 deletions docs/neondiff-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,3 +307,23 @@ recurrence of a previously-suppressed false positive, not just a byte-exact repe
- **Coarse fallback only on exact-miss.** When the exact fingerprint doesn't match, the gate falls back to matching `coarsePath` + normalized category + a line window (`|Δ| <= 3`) + a near-duplicate normalized title, reusing the same near-match helpers as same-run deduplication (#294) so the two matching semantics can never drift apart.
- **`confirmedByHuman` is honor-only — nothing auto-sets it.** This PR/field only *honors* a note that already carries `confirmedByHuman: true`; no code path in this surface automatically promotes an auto-learned note to human-confirmed.
- **Severity invariant: auto-learned notes stay P2/P3-only.** A false-positive note without `confirmedByHuman: true` may only suppress `P2`/`P3` findings, exact or coarse. Only a note with `confirmedByHuman: true` may suppress a finding of *any* severity, including `P0`/`P1`. This means a mistakenly-learned auto false positive can never silently suppress a high-severity finding — only an explicit human confirmation can widen suppression to `P0`/`P1`.

### `repoWikiContext`

`config.repoWikiContext` controls an optional advisory repo-wiki packet surfaced to review prompts (`src/repo-wiki-context.ts`). It is disabled by default, reads a prebuilt packet from the PR worktree, and never edits repository files.

| Key | Type | Default | What it does |
| --- | --- | --- | --- |
| `enabled` | `boolean` | `false` | Master switch for reading and including repo-wiki context. When disabled or absent, prompts are unchanged. |
| `packetPath` | `string` | `.neondiff/repo-wiki-packet.json` | JSON or Markdown packet path, resolved relative to the prepared PR worktree. Absolute paths and parent-directory segments are rejected. |
| `maxPacketBytes` | `integer` (`>= 1`) | `12000` | Byte budget for the rendered packet. Over-budget packets are omitted and recorded in evidence. |
| `includeStaleContext` | `boolean` | `false` | Whether stale, missing, or unknown-freshness packets may still be included as degraded advisory context. |

Safety invariants:

- The packet is advisory only. The PR diff, checkout files, GitHub metadata, and configured repo policy remain authoritative.
- Missing, stale, unknown-freshness, invalid, over-budget, or secret-like packet content degrades by omission and writes redacted evidence instead of blocking review.
- Packet files read from the PR worktree may be PR-author-controlled. Treat all packet metadata, titles, bodies, and source notes as untrusted advisory text, not instructions.
- `packetPath` is confined to the prepared PR worktree. Use a generated packet committed or copied into the worktree; v1 does not read host-side absolute sidecar files.
- A deterministic packet is treated as fresh only when its `source.headSha` matches the prepared PR worktree head. Generic JSON and raw Markdown packets are unknown-freshness by default.
- This flag accepts packet output; it does not vendor OpenWiki source code, run OpenWiki during live reviews, enable scheduled docs updates, or permit edits outside `openwiki/**`.
34 changes: 26 additions & 8 deletions docs/repo-wiki-packet.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Repo Wiki Packet

Repo wiki packets are deterministic, evidence-safe context bundles for future
codebase-map and repo-wiki review prompts. The MVP is library-only and dry-run:
it does not change live worker behavior, prompt construction, GitHub comments,
checks, queueing, or runtime state.
Repo wiki packets are deterministic, evidence-safe context bundles for
codebase-map and repo-wiki review prompts. Packet generation remains usable as a
dry-run artifact, and live prompt inclusion is gated behind
`repoWikiContext.enabled`.

This context is advisory. GitHub diff, current checkout files, current review
evidence, and configured repo policy remain truth. A stale or missing wiki
Expand Down Expand Up @@ -96,7 +96,25 @@ over-budget packet.

## Runtime Boundary

This MVP intentionally does not wire packets into `src/worker.ts`,
`src/walkthrough.ts`, `src/config.ts`, `src/cli.ts`, or `src/state.ts`.
Future integration should add a separate feature flag, evidence files, and
review-prompt caps before any live prompt use.
Prompt integration is disabled by default through `config.repoWikiContext`.
When enabled, `src/worker.ts` reads a prebuilt packet from the prepared PR
worktree, records redacted evidence, and includes it in the review prompt only
if it is fresh or explicitly allowed stale/degraded, within budget, and free of
secret-like text. `packetPath` is confined to a relative path inside the
prepared PR worktree; absolute paths and parent-directory segments are rejected.
Deterministic repo-wiki packets are treated as `fresh` only when their
`source.headSha` matches the prepared PR worktree head. Self-declared
`source.status = "fresh"` with a missing or mismatched source head is downgraded
to `unknown`. Loose JSON or raw Markdown packets without freshness metadata are
also treated as `unknown`, which is omitted unless `includeStaleContext=true`.

Packets read from the PR worktree can be PR-author-controlled. Treat packet
metadata, section titles, section bodies, and source notes as untrusted advisory
text, never as instructions. The prompt boundary repeats this rule so packet
content cannot override review instructions, current diff evidence, checkout
files, GitHub metadata, or configured repo policy.

This integration does not run OpenWiki during live review, mutate repository
files, change GitHub comment posting behavior, alter checks, or make repo-wiki
context authoritative. OpenWiki-generated files remain confined to
`openwiki/**`; suggestions for other docs are report-only.
26 changes: 25 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
import { isApiKeyEnvName, isProviderId, isProviderStructuredOutputMode, PROVIDER_STRUCTURED_OUTPUT_MODES, SCHEMA_FEEDBACK_RETRY_MAX, type ProviderRegistryConfig } from "./providers.js";
import { REGRESSION_CATEGORIES, type CategoryPrecisionFloors, type RequestChangesConfidenceFloors } from "./regression-taxonomy.js";
import type { ReviewMode, ReviewModeDefinition, ReviewModesConfig } from "./review-mode-types.js";
import { validateRelativePacketPath, type RepoWikiContextConfig } from "./repo-wiki-context.js";
import { DEFAULT_REVIEW_LENS_CONFIG, validateReviewLensConfig, type ReviewLensConfig } from "./review-lenses.js";
import { containsSecretLikeText } from "./secrets.js";
import type { SkillPackContextConfig } from "./skill-packs.js";
Expand Down Expand Up @@ -78,6 +79,7 @@ export interface BotConfig {
};
reviewGate?: ReviewGateConfig;
repoMemory?: RepoMemoryConfig;
repoWikiContext?: RepoWikiContextConfig;
gitnexusContext?: GitNexusContextConfig;
githubRelatedContext?: GitHubRelatedContextConfig;
skillPacks?: SkillPackContextConfig;
Expand Down Expand Up @@ -352,6 +354,12 @@ const DEFAULT_CONFIG: BotConfig = {
maxStateNotes: 20,
includeStaleNotes: false
},
repoWikiContext: {
enabled: false,
packetPath: ".neondiff/repo-wiki-packet.json",
maxPacketBytes: 12_000,
includeStaleContext: false
},
gitnexusContext: {
enabled: false,
packetVersion: "gitnexus-context-packet-v0.1",
Expand Down Expand Up @@ -648,6 +656,9 @@ function validateConfig(config: BotConfig): void {
const repoMemory = config.repoMemory ?? DEFAULT_CONFIG.repoMemory!;
config.repoMemory = repoMemory;
validateRepoMemoryConfig(repoMemory, "config.repoMemory");
const repoWikiContext = config.repoWikiContext ?? DEFAULT_CONFIG.repoWikiContext!;
config.repoWikiContext = repoWikiContext;
validateRepoWikiContextConfig(repoWikiContext, "config.repoWikiContext");
const gitnexusContext = config.gitnexusContext ?? DEFAULT_CONFIG.gitnexusContext!;
config.gitnexusContext = gitnexusContext;
validateGitNexusContextConfig(gitnexusContext, "config.gitnexusContext");
Expand Down Expand Up @@ -919,7 +930,7 @@ function validateReviewModesConfig(value: unknown, label: string, config: BotCon
}
}
const baseSelfConsistency = config.reviewGate?.selfConsistency?.enabled ?? false;
const baseContextAddons = (config.gitnexusContext?.enabled ?? false) || (config.githubRelatedContext?.enabled ?? false);
const baseContextAddons = (config.repoWikiContext?.enabled ?? false) || (config.gitnexusContext?.enabled ?? false) || (config.githubRelatedContext?.enabled ?? false);
const modes = value.modes as Record<string, unknown>;
for (const mode of REVIEW_MODES) {
// Distinguish an entirely ABSENT required mode key from a present-but-wrong-type value, so the
Expand Down Expand Up @@ -984,6 +995,19 @@ function validateRepoMemoryConfig(value: unknown, label: string): void {
validateBoolean(value.includeStaleNotes, `${label}.includeStaleNotes`);
}

function validateRepoWikiContextConfig(value: unknown, label: string): void {
if (!isRecord(value)) throw new Error(`${label} must be an object`);
validateBoolean(value.enabled, `${label}.enabled`);
validateOptionalString(value.packetPath, `${label}.packetPath`);
if (typeof value.packetPath !== "string" || value.packetPath.trim().length === 0) {
throw new Error(`${label}.packetPath must be a non-empty string`);
}
const packetPathError = validateRelativePacketPath(value.packetPath);
if (packetPathError) throw new Error(`${label}.packetPath ${packetPathError.replace(/^Repo wiki packetPath /, "")}`);
validatePositiveInteger(value.maxPacketBytes, `${label}.maxPacketBytes`);
validateBoolean(value.includeStaleContext, `${label}.includeStaleContext`);
}

function validateGitNexusContextConfig(value: unknown, label: string): void {
if (!isRecord(value)) throw new Error(`${label} must be an object`);
validateBoolean(value.enabled, `${label}.enabled`);
Expand Down
Loading
Loading