Release 0.16.0: repository context - #23
Conversation
…nd GitHub trees (#17 T8) Repository context (#17) needs to know which files exist at the reviewed commit before it can match contract globs or resolve a path. This adds the optional listing surface and its first adapter. - forges/base.py: the frozen PathListing(paths, complete) dataclass, the MAX_LISTING_PAGES = 20 page cap for paged adapters, and the optional Protocol method list_paths(ref, *, sha) -> PathListing | None after get_pr_history. Callers resolve it with getattr, it never raises, and it returns None when no listing can be fetched. - forges/replay.py: ReplayForge.list_paths delegates to the inner forge like get_file_content, passing the caller's sha through, and gives None when the inner forge has no listing or raises. LocalDiffForge stays without one. - forges/github.py: one Git Trees API request (git/trees/{sha}?recursive=1) keeping only blob entries, sorted and deduplicated, with complete = not truncated. An empty sha, a transport error, a non-2xx status, a non-JSON body or a body without a tree list gives None, logged at DEBUG. - tests/test_forge_github.py: the timeout sweep pins the adapter's public method set, so list_paths is driven there and the routed session answers the trees route. - tests/test_issue_17_list_paths.py: new, 37 tests. 🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
Adds tests/fixtures/issue17/, a small Java repo fixture that reproduces the two repository-context misses from issue #17: an OpenAPI idempotency-key contract that sits outside the diff and disagrees with a new migration's unique index, and a TransportConfig mutual-exclusion constraint that changes in one diff chunk while a service in another chunk violates it. repo/ holds the working tree at the PR head (Java sources under src/main/java/, an OpenAPI spec under api/openapi/, and three Liquibase changesets under db/changelog/). pr.diff adds the 003 migration and modifies TransportConfig.java and ConnectorService.java; every added line matches the repo/ head content, verified by a new head-consistency test. cases.json carries the two "error" labels a resolver should surface, each anchored on an added diff line with a must_match regex. tests/test_issue_17_fixture.py checks the fixture structurally: cases.json loads and its labels anchor, build_chunks(max_files_per_chunk=1) puts TransportConfig.java and ConnectorService.java in different chunks, the OpenAPI spec and the earlier two migrations exist under repo/ but are absent from the diff, every added diff line matches repo/ at its new line number, and the fixture text uses only placeholder identities (acme, example.com). 🤖 Authored with Claude Code — Claude Sonnet 5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
Adds src/prxref/forges/repo_dir.py holding RepoDir, a confined local
directory reader that stands in for a PR head with no network access, for
the upcoming --repo-dir flag (wired by a later seat). RepoDir.read(path)
matches the forge adapters' get_file_content contract byte-for-byte
(missing/directory/unsafe-path/.git/oversized/binary all read as None,
otherwise utf-8 with errors="replace", 512 KiB ceiling, size checked from a
stat before the file is read). RepoDir.list_files() walks the tree, never
follows a symlinked directory, skips .git at any depth, omits symlinks that
escape the root, and caps at 100_000 files, returning (paths, complete).
The confinement check resolves real paths and compares against root plus
a path separator, not a string prefix, so a sibling directory that extends
the root's name (".../r2" next to ".../r") is never mistaken for being
inside it; a dedicated regression test and a falsifiability run against a
naive startswith(root) check both confirm this.
list_files() returns a plain tuple rather than the PathListing dataclass
because that type is landing in forges/base.py under a parallel seat (T8);
the wiring seat (T15) wraps this return value.
tests/test_repo_dir_reader.py covers normal reads, missing/directory
targets, unsafe paths (absolute, "..", backslash, empty, NUL), symlinked
files and directories both inside and outside the root, .git exclusion,
the 512 KiB boundary, NUL-byte and invalid-utf8 handling, sorted POSIX
listing output, the file-count cap at and past the boundary, and the
ValueError raised for a non-existent root.
🤖 Authored with Claude Code
— Claude Sonnet 5 via Claude Code
Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
Add PRXREF_REPO_CONTEXT (choice off|diff|repo, default off), PRXREF_REPO_CONTEXT_MAX_CHARS (int, default 12000, _Range(0)), PRXREF_CONTEXT_CONTRACT_GLOBS (list key defaulting to the OQ5 built-in contract-glob set; a set value replaces the default, empty reads as unset) and PRXREF_CONTEXT_EXCLUDE_GLOBS (list key, default empty, added to a floor a later seat owns) to config._DEFAULTS and its _INT_KEYS/_LIST_KEYS/_CHOICE_KEYS/_RANGES tables, per decisions.md "Config keys" (D2: all four surfaces land in this one seat). Document all four on every surface: the config.py module docstring (key table and the list-key sentence), .env.example, and docs/env-vars.md, and recompute the two stated key-count totals in docs/env-vars.md (63->67 configuration keys, 64->68 accepted variable names with the legacy alias) with code rather than by hand. Add _KEYS_0_16 and TestKeys016AreDocumented to tests/test_config.py, mirroring the 0.15 substring-trap guard, and a new tests/test_issue_17_config.py pinning load_config behaviour: defaults, exact case-sensitive matching for PRXREF_REPO_CONTEXT (like PRXREF_FAIL_ON), the open-zero _Range(0) rejection of 0 and negative values for the max-chars key, and the replace/empty-is-unset contract for the contract-globs default. Nothing reads these keys yet; wiring lands in later tasks (T13/T14). 🤖 Authored with Claude Code — Claude Sonnet 5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
Add src/prxref/repo_context.py, the pure core the 0.16.0 repository-context feature builds on: the ContextEntry record (rendered() in the referenced_definitions "path:line: text" shape, record() for the run record), the REASONS admission ranks and KINDS, language_of (chunk_context's map plus .java), definition_regexes (a Java type-declaration regex for class, interface, record, enum and @interface after modifiers and same-line annotations; chunk_context's regexes otherwise), referenced_names (first-seen identifiers minus keywords, names defined on the added lines, and for Java the JDK names and non-type-like names), and find_definitions (the referenced_definitions scan loop generalized to any file's text, first definition per name, skip_lines, max_lines, the 512 KiB byte cap). chunk_context.py is untouched: Java stays out of its _definition_regexes because referenced_definitions runs with the feature off (D1). The module reuses chunk_context's underscore helpers so both scan and render alike. Tests: tests/test_repo_context_defs.py (65 tests). 🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
Add the optional Forge.list_paths to the two paged adapters, behaving as T8's GitHub method does: file paths only, sorted and deduplicated, never raising, None on an empty sha with no request. GitLab walks repository/tree?recursive=true with per_page=_PAGE_SIZE and page=1, 2, 3 ..., keeps type == "blob" entries, and ends the walk when the X-Next-Page header is absent or empty (never on a short page), as the 2026-09-24 keyless probe of gitlab.com showed. Bitbucket Server walks the documented /files?at=<sha> listing with start/limit paging and ends on isLastPage (or its absence) or a missing nextPageStart, as the activity walk does. The endpoint shape comes from the REST documentation and was not probed live (no public instance), so it is UNVERIFIED. Both cap the walk at MAX_LISTING_PAGES (imported from forges.base). A walk the cap stops, or a failure on a later page, returns the paths read so far with complete=False; a failure on the first page returns None. Failures log at DEBUG, as get_file_content does. Tests: tests/test_issue_17_list_paths_paged.py (52 tests). 🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
Bitbucket Cloud walks GET /2.0/repositories/{owner}/{repo}/src/{sha}/ with
max_depth=64 and pagelen=100, then follows each page's next URL verbatim
with no params of its own, capped at MAX_LISTING_PAGES. Only commit_file
entries are kept. The listing is complete=False when the cap stops a walk
with a next still pending, when a later page cannot be read (the paths
read so far are returned), or when a commit_directory sits at the depth
limit. That last rule is inferred from the orchestrator's max_depth probe,
not documented by Bitbucket, and the docstring says so.
Azure DevOps makes one items?recursionLevel=Full request at the commit
through the adapter's _get and _json helpers, keeps blob entries that are
not folders, and strips the leading slash Azure puts on every path. An
x-ms-continuationtoken header marks the listing complete=False and is not
followed, because continuation on this endpoint is undocumented and was
never observed.
Both return None for an empty sha (no request) and for a failed first
request, log only at DEBUG, and never raise. Tests are in
tests/test_issue_17_list_paths_bbc_ado.py.
🤖 Authored with Claude Code
— Claude Opus 5.5 via Claude Code
Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
New pure module src/prxref/repo_contracts.py (stdlib json and re only, no
YAML library, no I/O) that cuts the slice of a contract file a changed
route, operation, schema or table points at:
- normalize_name folds idempotency_keys, idempotency-key and IdempotencyKey
to one key; route_key makes {id}, :id, <id> and <int:id> routes compare
equal to a spec path.
- openapi_yaml_excerpts slices by indentation under paths:, the enclosing
HTTP-method block of an operationId, and components.schemas (plus the
Swagger 2.0 definitions:), skipping block-scalar text.
- openapi_json_excerpts and json_schema_excerpts do the same through
json.loads, with a best-effort line found by following the key path.
- sql_excerpts finds CREATE TABLE, ALTER TABLE and CREATE [UNIQUE] INDEX ON
statements; comments, quoted strings and $$ bodies do not end a statement,
and a Liquibase --changeset line or a GO line does.
- liquibase_excerpts slices XML, YAML and JSON changeSets holding
tableName, baseTableName or referencedTableName.
Every excerpt is capped at 40 lines and 2000 chars, and a cut block ends
with an "N more lines" marker line that counts toward both caps. An
excerpt that another one already shows in full is dropped. The regexes
stay linear on long whitespace runs.
Tests: tests/test_repo_context_contracts.py (63), with every fixture inline.
🤖 Authored with Claude Code
— Claude Opus 5.5 via Claude Code
Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
…17 T12) Add src/prxref/repo_reader.py, the one reader a review run shares across its parallel chunk workers when repository context is on. RepoReader wraps a fetch(path) and an optional lister(): - read(path) is cached and single-flight per path: concurrent callers share one fetch, and a None result is cached, so a miss costs one fetch per run. It is not capped; its fetches count in stats()["reads"] only. - chunk_reader() returns a new callable per chunk that enforces MAX_CHUNK_READS (16) per chunk and MAX_RUN_READS (200) across all chunk readers. A cached or in-flight path is served without counting; a path refused at a cap returns None without a fetch, stays uncached, and sets read_cap_hit. - listing() calls the lister at most once per run, single-flight, and caches its result, with a raise or a non-PathListing read as None. - exclude(path) makes a path unreadable, uncounted and unlisted at the one boundary every read crosses; an exclude that raises fails closed. - stats() snapshots {"reads", "read_cap_hit", "listing"} for the run record. forge_reader(forge, ref, sha) mirrors _make_file_reader's gate (None without get_file_content or with an empty sha) and passes ref and sha to both get_file_content and list_paths; repo_dir_reader(repo_dir) wraps a RepoDir. orchestrator.py is untouched: the wiring seat builds the reader and maps stats() onto the run record. Tests: tests/test_orchestrator_repo_reader.py (49 tests). 🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
… T5) New pure module src/prxref/repo_crosschunk.py with diff_definitions(chunk, all_files, read) -> list[ContextEntry]. It closes issue #17 miss (b): a type changed in one chunk (TransportConfig's new compact constructor) is now shown to the worker of another chunk that calls it (ConnectorService), together with the changed lines inside that type, not just its declaration. Names are the union of repo_context.referenced_names over the chunk files' added lines. Every diff file with definition regexes is searched with find_definitions, reading at the PR head or, with no reader or a None read, scanning each contiguous run of known hunk lines on its own so entries keep true new-file line numbers and never show a line the hunks lack. Skips: this chunk's own hunk lines, js/python files in the chunk when a reader is given (referenced_definitions already covers them, D1), and removed files. A hit in a file outside the chunk that has + lines anywhere in the PR is "cross-chunk" and also yields one change entry per contiguous + run after the definition line and before the next definition indented no deeper (capped at MAX_CHANGE_LINES = 12 plus a "... N more changed lines" line); every other hit is "diff-file". Output is ordered by (REASONS rank, path, line) and deduplicated on (path, line). No budget: T7 owns it. Tests: tests/test_repo_context_crosschunk.py, including the issue17 fixture end to end with a reader and from hunks only. 🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
Issue #17, task T3. New pure module src/prxref/repo_resolve.py turns a referencing file, its text and the names referenced_names reports into an ordered list of Candidate(name, path, reason) files for the caller to read. It does no I/O: reading candidates and running find_definitions is T7's job. - Imports first: Java same-organization imports (first two package segments) under the source root derived from the package line, wildcard and nested-type imports included; Python from-imports at the root and under src/, relative dots counted from the file; TS/JS relative import/export specifiers probed as .ts, .tsx, .d.ts, .js, /index.ts. - Then the Java same-package convention <dir>/<Name>.java for names no single-name import binds. - Then a name search over a listing: same-language files whose stem matches (exact, case-insensitive, Python snake_case), deepest shared directory first, at most 3 per name. - Dead-candidate filters: a complete listing drops absent import and convention paths; third-party Java, stdlib Python and bare TS/JS imports give no candidate and are not name-searched; languages without definition regexes resolve nothing. Tests: tests/test_repo_context_resolve.py (81 tests, pure), including the issue17 fixture: TransportConfig resolves by path-convention and the Spring imports give no candidate. 🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
…test The nested-type test named dedup but never produced two entries on one (path, line), so removing the dedup loop left the suite green. Making the nested record's declaration an added line puts Outer's change run and Inner's definition on the same line; the test now pins that the definition entry wins that tie, and a no-dedup mutation goes red. 🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
Repository context (PRXREF_REPO_CONTEXT=repo) needs a zero-network way to run against a fixture or a checked-out PR head, so eval cases gain an optional repo_dir field: a directory holding the repository at the PR head, read and listed by repository context in place of a forge. - EvalCase gains repo_dir: str | None = None as its LAST field, so no positional construction shifts. - _CASE_KEYS gains "repo_dir" directly after "context_file", which fixes the allowed: list in the unknown-field message and the key order case_to_json writes; the cases.json form joins a relative repo_dir onto the dataset directory as context_file is joined, and requires an existing directory; the case-*/ directory form picks up a repo/ subdirectory as ticket.md becomes context_file. - case_to_json/case_from_json_record round-trip repo_dir, both set and None; a 0.15-shaped case.json record with no repo_dir key still loads. - The issue #17 acceptance fixture gains "repo_dir": "repo" and L1's must_match loosens from re:(?i)connector_id to re:(?i)connector, so it also matches a correct finding written as "connectorId" or "the connector" (the fixture-seat correction carried into this wave). - docs/evals.md documents the field for both dataset forms. This seat builds only the eval-case half. The --repo-dir CLI flag, the threading into evals._run_case and the orchestrator, the run-record fields and the README CLI Flags entry belong to seat T15b, which reads case.repo_dir once it lands. 🤖 Authored with Claude Code — Claude Sonnet 5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
…ct entries (#17) Adds the T6 surface to src/prxref/repo_contracts.py for issue #17's misses (a) and (c): a migration whose unique index disagrees with an OpenAPI contract outside the diff, and a route whose operation and schemas the worker never sees. - ContractTriggers and contract_triggers(): route literals (Spring, JAX-RS, FastAPI/Flask, Express; class-level @RequestMapping/@path, Blueprint url_prefix and APIRouter prefix joined when the head text is given), tables (T4's _SQL_HEADS and _bare_table, REFERENCES targets, Liquibase tableName/baseTableName/referencedTableName in XML, YAML and JSON), referenced names, and operation ids. Every scan is linear on a 512 KiB line; the INDEX ... ON head is bounded at the next CREATE/ALTER. - literal_contract_paths() and select_contract_files(): the run's contract files from the globs, the listing and the diff paths. - earlier_migrations(): up to MAX_EARLIER_MIGRATIONS = 4 same-directory predecessors under a natural sort. - contract_excerpts(): extension dispatch to T4's excerpters, plus whole-file fragments matched by stem. - contract_entries(): the per-chunk entry point for T7, reading at most MAX_SPEC_FILES = 6 ranked spec files plus earlier migrations, through the reader it is given, and returning ContextEntry(kind="contract", reason="contract") ordered and deduplicated by (path, line). Tests: tests/test_repo_context_triggers.py (82), including the issue17 fixture end to end. 🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
…ude floor) Issue #17, task T7. build_unit_context (new src/prxref/repo_unit.py) combines the three repository-context sources for one worker chunk: diff_definitions (diff and repo), contract_entries and the resolver (repo with a reader only), called in admission-rank order so a capped reader spends its reads on the higher-ranked sources first. Every read goes through a guard that refuses an excluded path without calling the reader (an exclude that raises fails closed). Entries at excluded paths are dropped, the rest ordered by (REASONS rank, path, line), deduplicated on (path, line), and admitted against one character budget that stops at the first entry that does not fit; the omitted line goes to the block of the first entry left out and does not count toward the budget. "off" returns EMPTY_UNIT with no read, and "repo" with no reader equals "diff" with no reader (OQ2). repo_context.py gains EXCLUDE_FLOOR and exclude_predicate(extra_globs), which matches the floor and the extra globs as two separate match_globs lists, so a "!" in PRXREF_CONTEXT_EXCLUDE_GLOBS can never re-admit a label file. chunk_context.render_context_blocks gains extra_def_lines (under the one definitions header) and contract_lines (under the new CONTRACT_HEADER); with the defaults it is byte-identical to before (D1), pinned against a verbatim oracle copy. Nothing calls the new code yet; T13 wires it. Tests: tests/test_repo_context_unit.py (94), including the issue17 fixture end to end through the real capped reader, the observed read order, the chunk cap degrading, and the budget, order and exclusion rules on canned sources. 🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
orchestrate_review takes five new keyword-only parameters after
max_findings_per_rule: repo_context ("off" by default), repo_context_max_chars
(12000), context_contract_globs, context_exclude_globs and repo_dir. An
unknown level raises ValueError before any forge call.
Off is byte-identical to a run without the kwargs; the run record gains a
repo_context key that is None when off. On, the run builds one RepoReader
(repo_dir first, else the forge at the head sha), takes the listing and the
contract files once per run in repo mode, and builds each chunk's unit
context inside its worker before the first attempt. Diff-file reads go
through the shared uncapped read and every other path through the chunk's
capped reader. A unit build that raises gives that chunk the empty unit and
one WARNING. The timeout retry carries neither new block and marks the
chunk's row retry_dropped. Repo mode with no reader or no listing logs one
WARNING naming PRXREF_REPO_CONTEXT. The trace gains one chunk context event
per chunk and one repo_context ok event per run.
The CLI and JSON output are not wired yet (T14); tests/test_cli.py and
tests/test_run_record.py carry NOT_WIRED_YET / NOT_IN_JSON_YET tripwires
for that seat to remove.
🤖 Authored with Claude Code
— Claude Opus 5.5 via Claude Code
Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
The live check V1 found that GitLab's REST repository/tree?recursive=true
listing returns every directory before any file across the whole recursive
walk. Under the 20-page cap, a project with more than 2000 entries lost
files, and gitlab-org/gitlab came back with zero paths and complete=False.
list_paths now walks GraphQL's files-only tree.blobs connection first
(POST https://{host}/api/graphql, variables p/ref/after, the same
PRIVATE-TOKEN header and timeout as every other request), following
endCursor while hasNextPage for at most MAX_LISTING_PAGES pages. The cap,
or a later-page failure, keeps the paths read so far with complete=False.
When the first GraphQL page is unusable, the unchanged REST walk answers.
That covers a transport failure, a non-2xx status, a non-JSON body, a
non-empty errors array, a null data.project, any other wrong shape, and an
empty first page with hasNextPage false. GraphQL answers a sha it cannot
resolve with that empty page, so REST decides between 404 -> None and an
empty tree. The fallback reason is logged at DEBUG.
Tests: tests/test_issue_17_gitlab_graphql.py (58 new, including the
directories-first defect with a control). T9's REST tests pass unedited.
🤖 Authored with Claude Code
— Claude Opus 5.5 via Claude Code
Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
A reply with no text (or whitespace only) and a non-truncation finish
reason failed its unit at parse time ("no parseable content") and was
never asked again, though the provider billed it. The review of 0.15.0's
own pull request lost 1 of 8 chunks this way.
_invoke_and_parse now repeats that call once, with the same prompt and
the same budget, after one WARNING naming the unit label and the finish
reason. It never retries a reply stopped at the budget (length or
max_tokens, the truncation path), a non-empty reply that fails to parse,
or a call that raised. Tokens and elapsed_ms cover both calls, model is
the second call's, and cost folds through costs.combine_reported: the
sum when both calls reported a figure, else None. A second empty reply
fails the unit with the same error as before. The chunk and the systemic
sweep share the path. Worst case per chunk is 4 invoke calls with the
orchestrator's timeout retry, 2 for the sweep; the module, review_chunk
and review_systemic docstrings are corrected to say so.
Tests: tests/test_empty_reply_retry.py (23 cases, including one run
through orchestrate_review).
🤖 Authored with Claude Code
— Claude Opus 5.5 via Claude Code
Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
Seat T14 (T14 + T15b). _run_review now passes the four repository-context settings (repo_context, repo_context_max_chars, context_contract_globs, context_exclude_globs) from load_config to orchestrate_review, and takes a new repo_dir keyword. `review --repo-dir PATH` is opened as a RepoDir right after the prompts directory loads, before make_forge and the LLM client, so a path that is not an existing directory (or a blank one, which would otherwise resolve to the working directory) exits 2 naming --repo-dir with no network I/O. It is checked and passed even while PRXREF_REPO_CONTEXT is off; the webhook daemon passes none. --format json gains repo_context right after rule_counts (null when off), and -v prints one `repo context:` line after `spec:` when the record has one. prxref eval threads each case's repo_dir into the review, and RUN_CONFIG_KEYS ends with the four settings, so run.json records them. Docs: README gets the --repo-dir bullet and the repo_context key in the --format json list; docs/evals.md lists the sixteen allowlisted settings. T13's tripwires are removed (NOT_WIRED_YET, NOT_IN_JSON_YET) and the pinned JSON key orders move deliberately, including two pins the brief did not name (tests/test_eval_run.py's exact run_review kwargs and tests/test_cli_scoped_rules.py's scoped_rules-to-sampling distance). New tests: tests/test_cli_repo_context.py (34), including an end-to-end eval of the issue17 fixture with no network and no LLM. 🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
Adds tests/test_issue_17_acceptance.py, 20 tests. Each one drives orchestrate_review end to end over tests/fixtures/issue17, using the real reviewer, a read-and-list fake forge and a capturing LLM, and asserts on what a user sees: the prompts, the run record and the trace. The checks: - both labeled chunks carry their evidence, with an off-run control; - the record rows and the trace events carry their reasons; - off is byte-identical to the RELEASED 0.15.0, in three kwarg variants, with a repo-level control; - a 300-char budget holds and prints the omitted-entries line; - LocalDiffForge makes zero reads at repo and at diff; - repo_dir on LocalDiffForge gives the check-1 evidence; - the exclude floor holds against a "!**/cases.json" negation, with a schema control and a name-search control; - read-cap starvation (D-L) keeps the spec read, with a control that routes every read through the capped reader; - one worker and four workers give equal records and prompts. tests/fixtures/issue17/harness.py is version-neutral (orchestrate_review, PRRef, PRData, InvokeResult only). tests/fixtures/issue17/make_golden.py runs it under `uv run --no-project --with prxref==0.15.0` from outside the repository. golden_off_prompts.json is that run's output: two design points (the fixture, and the fixture plus a Python file whose chunk renders the pre-0.16 dependency and definitions blocks). The tip reproduces it byte for byte, and no source file changes. 🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
Add the 0.16.0 CHANGELOG section (repository context, Java definitions, the three new budget and glob keys, bounded reads, --repo-dir, eval support, the repo_context record, per-forge listing, the empty-reply retry fix, and known limitations), a README "Repository Context" section, the --repo-dir bullet moved into the main CLI Flags list, the -v bullet's repo context line (text output only), a listing paragraph per forge in docs/forges.md, the corrected PRXREF_REPO_CONTEXT row and an empty-reply note on PRXREF_TRACE_DIR in docs/env-vars.md, the repository-context and empty-reply paragraphs in docs/llm.md, and a "Measuring repository context" recipe in tests/evals/README.md. 🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
Bump the version to 0.16.0 in pyproject.toml, src/prxref/__init__.py and uv.lock. Date the CHANGELOG's [0.16.0] section and fix its link references: [Unreleased] now compares v0.16.0...HEAD, with [0.16.0] and [0.15.0] tag links added. Rewrite HANDOFF.md for 0.16.0: the #17 module map, entry points and caps, the GitLab GraphQL listing and the empty-reply retry; this release's lessons; the recounted config coupling (67 keys, 1 alias, 68 names); how the release was built; the verified counts; the live checks; and the open items, including #20 and #21. Correct README's description of the repo_context record's units: it is {"chunks": [...]}, one row per chunk in chunk order. 🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
The sdist ships tests/ (183 files); task and check IDs (T#, V1-V3, "seat", "wave", "OQ#", "map-#") from every prior build's internal coordination mean nothing to a reader and are banned from public artifacts. Strip them from comments and docstrings across src/ and tests/, keeping public issue numbers (#10-#17), version numbers, commit SHAs, and cross-references to other test files intact. Also correct three comments that had gone stale as their described work landed: repo_dir.py's docstring said the --repo-dir CLI flag "belongs to a later seat" (it is wired in prxref.cli now); formatter.py's ImportError comment said "reviewer seat not landed yet" (reviewer.py has existed and imported cleanly for several releases); and two tests (test_issue_05 and test_issue_06) described gates ("apply_hedge_gate", match_class's kind parameter) as future work when both now exist and are exercised for real. No code, test logic, string literal a test asserts on, or fixture changed: an AST-equality check (docstrings stripped) over every changed file proves it, with a control confirming the check detects a real literal change. PYTHONDONTWRITEBYTECODE=1 uv run pytest -q stays at exactly 7675 passed; ruff check src tests stays clean. 🤖 Authored with Claude Code — Claude Sonnet 5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
prxref automated review: ApprovedPR: Release 0.16.0: repository context · files reviewed: 113 🟥 0 error · 🟧 1 warning · 🔍 0 spec · ⬜ 0 outofscope
Reviewed by prxref · model=z-ai/glm-5.3-flash · 215446 tok · 118.6s
|
The PRXREF_TRACE_FILE row named eight events and said each carries start, then ok or fail. 0.16 adds a `chunk` `context` event per chunk and a `repo_context` `ok` event per run whenever PRXREF_REPO_CONTEXT is not off, and earlier releases had already added `sweep`, `heartbeat` `tick`, `chunk` `retry` and the one-off input and pass events with their own phases. The row now lists them all, with the fields of the two new ones. Found by prxref's own review of the 0.16.0 release PR. 🤖 Authored with Claude Code — Claude Opus 5.5 via Claude Code Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459
| json_mode=True, | ||
| ) | ||
| except Exception as e: # noqa: BLE001 | ||
| logger.warning("worker review failed for %s: %s", label, e) |
There was a problem hiding this comment.
🤖 🟧 [WARNING] Chunk worker failure swallowed into a warning after a billed LLM call (src/prxref/reviewer.py:643)
The digest shows except Exception as e: # noqa: BLE001 followed by logger.warning("worker review failed for %s: %s", label, e) on the chunk-worker path. The chunk's LLM call has already been billed (docs/llm.md notes both calls' tokens are summed), and reducing the failure to a log line means the paid call's result is lost with no retry or persisted state, unlike the empty-reply path which the release explicitly fixed to retry.
Reviewed by prxref · model=z-ai/glm-5.3-flash
Release 0.16.0. The full notes are in
CHANGELOG.mdunder[0.16.0], and the handoff with live-check results is inHANDOFF.md.What's in it
PRXREF_REPO_CONTEXT=off|diff|repo). Atdiff, a chunk worker also sees definitions from the PR's other changed files and the lines another chunk changes inside a type it uses. Atrepoit also sees definitions from files outside the diff (through imports, Java's same-package convention and a file-name search) and a### Contract excerptsblock: the OpenAPI operation or schema, JSON Schema, or earlier migration a changed route, table or name matches.offsends prompts byte-identical to 0.15.0.PRXREF_REPO_CONTEXT,PRXREF_REPO_CONTEXT_MAX_CHARS,PRXREF_CONTEXT_CONTRACT_GLOBS,PRXREF_CONTEXT_EXCLUDE_GLOBS(added to a floor that always keeps eval labels,.env*and key files out)--repo-dir PATHreads files from a local checkout of the PR head instead of the forge--format jsonand the run record gainrepo_context;-vprints arepo context:line in text outputrepo_dirfieldVerified
236ce08: 7675 tests pass andruff check src testsis clean.uv buildmakesprxref-0.16.0.tar.gzandprxref-0.16.0-py3-none-any.whl, and both pass the leak scans with 0 non-low hits. The version is 0.16.0 inpyproject.toml,__init__.pyanduv.lock.list_pathsmatched a raw walk on GitHub, GitLab, Bitbucket Cloud and Azure DevOps (19 of 19 checks, no LLM call). With GLM 5.3 Flash, the Repository context outside the diff: cross-file definitions, contract excerpts, cross-chunk links #17 fixture and the bundled eval cases (28 LLM calls, no failed case) sent byte-identical prompts within each level, andrepoadded only the context block. Bundled recall was 7 of 8 at bothoffanddiff. An HTTP 201 that exists only in the injected OpenAPI excerpt appeared in all 3reporuns' findings and in none of the 3offruns. End to end on #3: 0.1.7: stay on the best account automatically (best --auto, on-limit hook, statusline) multi-auto-claude-sub#5, the GitHub forge and--repo-dirbuilt the same 110 entries, andoffrecordedrepo_context: null. Identical prompts gave 3 findings and 0, so the effect on findings is unmeasured and the default staysoff. The thinking model neededPRXREF_LLM_MAX_TOKENS=32768andPRXREF_LLM_TIMEOUT=900.Closes #17
🤖 Authored with Claude Code
Claude-Session-Id: f915b0ff-b27c-46f3-bc7c-430dca61d459