Skip to content

feat: strict cache mode and CHTYPES_CACHE_UNUSABLE (#486, part 2) - #505

Merged
EricAndrechek merged 3 commits into
mainfrom
strict-cache-486-strict
Oct 6, 2026
Merged

EricAndrechek merged 3 commits into
mainfrom
strict-cache-486-strict

Conversation

@EricAndrechek

@EricAndrechek EricAndrechek commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

Summary

Part 2 of public issue #486: strict mode and one cache-error code, in all four bindings. It is built on #503 (part 1) and carries its commits until #503 merges; merge #503 first. The diff of this part alone is the last two commits.

  • CHTYPES_CACHE_UNUSABLE, exit status 9, added to spec/fetch-v1/constants.json and generated into all four bindings and fetch-v1.md §8. It names the exact path, a reason and the errno name, with one sentence in every binding: <path> is unusable as a cache: <reason> (<errno>).

    reason raised when
    unreadable_root K1: the root or unpacked/sha256/ cannot be listed (strict)
    not_a_directory K1: the root or unpacked/sha256/ is not a directory (strict)
    unreadable_entry K2: an entry or its verified.json cannot be read (strict)
    unacceptable_record K3 with no blobs to re-verify from (strict)
    layout_0x K4, a 0.x registry dir used as the cache (strict)
    unwritable any mode: a write the fetch layer needed under the cache failed
  • Strict mode, opt-in: Go FetchOptions.StrictCache, Python FetchOptions(strict_cache=True), TypeScript strictCache: true, Rust FetchOptions::strict_cache, else CHTYPES_CACHE_STRICT=1 (0 is off), and --strict on fetch, list, verify and where. Every fault of the cache (K1 to K4), and K1 or K2 of an existing system dir, is the new error, checked before any system dir is read, so nothing falls through. where --strict checks the root before printing it, and verify --strict that verified nothing exits 1 (CHTYPES_ARTIFACT_MISSING). Strict by default for an explicit cache is not in this change.

  • The default mode keeps "unreadable is absent", now in every binding and on every Python, and warns once per path: <path> could not be read (<errno>); treated as not installed. Set CHTYPES_CACHE_STRICT=1 to make this an error. The warning is in Resolved.warnings, in the MISSING message, and on stderr from the CLI's list and verify.

What a caller saw before and sees now

"Before" is this branch's base, part 1 (measured by the run on the tests-only commit, below, with the Go-written cache read as nobody). A system dir holds the same build in the "+ system dir" cells.

fault before now, default mode now, strict mode
root 000 (K1) Go and TypeScript: MISSING, exit 1, no word of the fault; with a system dir, its build, silently. Python 3.13: a raw PermissionError, exit 1, no code, with or without a system dir. Rust: CHTYPES_SOURCE_UNREACHABLE, exit 3, with or without a system dir all four: MISSING with a warning naming the root; with a system dir, its build, with the warning all four: exit 9, unreadable_root, path = the root, with or without a system dir
unpacked/sha256 000 (K1) the same as root 000 the same, naming unpacked/sha256 unreadable_root, path = unpacked/sha256
entry 000 (K2) Go, Python, TypeScript: MISSING silently, or the system dir's build silently. Rust: SOURCE_UNREACHABLE, exit 3 the warning names the entry unreadable_entry, path = the entry
record 000 (K2) the same as entry 000 the warning names the record unreadable_entry, path = the record
garbage record, no blobs (K3) MISSING, except Rust over a Python-written cache: exit 1 with another code (inferred: CHTYPES_ARTIFACT_CORRUPT, from parsing that cache's index.json, whose entries carry no mediaType or size) MISSING (a K3 record is absent, as before), in all four unacceptable_record, path = the record
a 0.x registry dir (K4) MISSING with the 0.x hint (part 1) unchanged layout_0x, path = the dir
a failed write under the cache Go *UsageError (CLI exit 2), Python a raw OSError (CLI exit 1), TypeScript UsageError (CLI exit 2), Rust CHTYPES_SOURCE_UNREACHABLE (exit 3) CHTYPES_CACHE_UNUSABLE, unwritable, exit 9 the same

The new public names, and their doc rows

  • Go: CodeCacheUnusable, ErrCacheUnusable, ArtifactError.OSError, FetchOptions.StrictCache.
  • Python: CacheUnusableError (with path, reason, os_error), CODE_CACHE_UNUSABLE, FetchOptions.strict_cache.
  • TypeScript: CacheUnusableError (with path, reason, osError), the CacheFault type, FetchOptions.strictCache.
  • Rust: Error::CacheUnusable(CacheFault), CacheFault (with path, reason, os_error), FetchOptions::strict_cache.
  • bindings-v1.md §4 gains the "cache unusable" class row and "A cache fault" bullet, and §6 names the strict option; fetch-v1.md §1 gains "The cache faults". The names the doc gives in prose are entries in scripts/parity-surface-allow.json, beside the existing fetch-option and code entries. parity-surface is green in all four in CI: 0 findings and 0 allowlist problems (Go 266 matched, Python 240, TypeScript 236, Rust 238).
  • TypeScript's CacheUnusableError is an ArtifactError like every other fetch error, and isChtypesError is true for it; that TypeScript's ArtifactError is not a ChtypesError is Fetch layer: Go --frozen re-downloads on a warm cache; TS --lock pins one platform; TS ArtifactError isn't a ChtypesError; Python index.json duplicates (measured, 1.0.2) #487's item 3, left to that issue.

Test plan

Each new test ran on the unfixed code first: the tests-only commit 2b9cd21 ran in CI before the implementation (it is part 1's code with the new tests; its interop job, its ci run).

  • v1-cache-interop part 7, writer x reader x fault x mode. Each writer's cache is faulted seven ways; each reader runs fetch --offline, list --offline and verify as nobody, in the default mode and with --strict, and again with /opt/chtypes/v1 (a default system dir) holding the build: 4 x 4 x 7 x 2 x 2 = 448 cells. A positive control proves nobody is refused the faulted path ("Permission denied") or the row is INVALID. A cell passes only when the reader's exit status, code, unusable path and reason, warned paths and answer equal the expected ones, and every reader in the row gives the same observation. On the base: 356 cells red: every default K1/K2 cell (no warning; Python's raw error; Rust's exit 3), the garbage-record row over the Python-written cache (Rust disagreed), and every strict cell (--strict unknown, exit 2). On the head (job): 448/448 ok, 112 per block (default, strict, each with and without the system dir), and every earlier part of the job ok too (4x4 16/16, the 0.x rows 12/12, both line tables 16/16, concurrency 5/5, read-only 8/8, cross-uid 16/16).
  • cli-parity: verify --strict of an empty cache exits 1, list --offline --strict and where --strict of a good one succeed, in all four CLIs. On the base: red in all four (exit 2).
  • Per binding: every fault in both modes through resolve_installed, list_installed, verify_installed and an offline ensure (and Go's and TypeScript's probe), each chmod proved by a refused read first and skipped loudly as root; option-then-environment precedence; a strict, unreadable system dir; a failed write as unwritable; the public class and its fields through the registry; and the CLI's --strict. On the base these do not compile or import (the names are new); their failure is shown by mutants instead, run locally: removing the probe fails 5 Go tests and 19 Python tests, and removing the write mapping fails exactly the Go and the Python unwritable test.

Local gates on the head: go build/vet/test ./..., uv run pytest -q, tsc --noEmit (both configs), cargo check --locked --all-targets (also with --all-features) with clippy and fmt, gen-constants --check and --selftest, abi-v1/gen.py --check, genfixtures --check, cache-interop.py --selftest, the four lints, markdownlint, dprint, policy-merge-check.py --check-guide and --selftest, check-selftests-wired.py, check-cli-parity.sh go|python, parity-surface.py run --only go python (green), and the Go and Python conformance runners (249/249 each). cache-interop.py --faults-as-self with the Go and Python CLIs: 56/56.

MERGE NOTES

  • Merge fix: cache modes follow the umask, one root order, lookups create nothing, a 0.x hint (#486, part 1) #503 first; this branch is stacked on it.
  • Adds a public error code and public names in all four bindings, so it is an API minor for every package. Touches protected paths (spec/, scripts/, .github/, the fetch layers), so it does not enqueue itself.
  • v1-cache-interop creates /opt/chtypes/v1 (a default system dir) with sudo install on the hosted runner and leaves it empty.
  • cli-parity gains three --strict cases.
  • Rust's fetch layer no longer turns an unreadable record, entry or index.json into an error in the default mode (it was SOURCE_UNREACHABLE or ARTIFACT_CORRUPT); that is the one-answer rule, and strict mode is where they are errors.

Related issues

Refs #486

🤖 Generated with Claude Code

…t in cli-parity (#486)

The cross-binding tests for public issue #486's strict mode and its one
cache-error code, ahead of the implementation, so the run on this commit
shows them failing on the unfixed code:

- v1-cache-interop part 7: each writer's cache is faulted seven ways
  (none, root 000, unpacked/sha256 000, an entry 000, a record 000, a
  garbage record with no blobs, a 0.x registry dir), and every reader
  runs fetch --offline, list --offline and verify as nobody, in the
  default mode and with --strict, and again with /opt/chtypes/v1 (a
  default system dir) holding the build. A positive control proves the
  reader is refused the faulted path. Every reader must give the same
  answer: not installed with a warning naming the path (or from the
  system dir), or exit 9 CHTYPES_CACHE_UNUSABLE naming the path and the
  reason, never a fall-through. --faults-as-self runs it locally.
- cli-parity: verify --strict of an empty cache exits 1, and list
  --offline --strict and where --strict of a good one succeed, in all
  four CLIs.

Refs #486

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Public issue #486's strict mode and its one cache-error code, in all four
bindings:

- CHTYPES_CACHE_UNUSABLE (exit 9) joins spec/fetch-v1/constants.json and
  is generated into all four. It names the path, a reason
  (unreadable_root, not_a_directory, unreadable_entry,
  unacceptable_record, layout_0x, unwritable) and the errno name, with
  one sentence in every binding: Go *ArtifactError{Code:
  CodeCacheUnusable, Path, Reason, OSError} and ErrCacheUnusable, Python
  and TypeScript CacheUnusableError, Rust Error::CacheUnusable(CacheFault).
- Strict mode, opt-in: Go FetchOptions.StrictCache, Python strict_cache,
  TypeScript strictCache, Rust strict_cache, CHTYPES_CACHE_STRICT=1, and
  --strict on every CLI command. Every fault of the cache (K1 to K4) and
  of an existing system dir (K1, K2) is the new error, checked before any
  system dir is read, so nothing falls through. where --strict checks the
  root; verify --strict of nothing exits 1.
- The default mode keeps "unreadable is absent" in every binding (Python
  before 3.14 raised a raw PermissionError, Rust SOURCE_UNREACHABLE) and
  warns once per path, in Resolved.warnings, the MISSING message and the
  CLI's list and verify.
- A write the fetch layer needed under the cache that failed is the new
  error with reason unwritable, in every mode: it was Go's and
  TypeScript's UsageError, a raw Python OSError, and Rust's
  SOURCE_UNREACHABLE.

bindings-v1.md section 4 gains the class row and the fault's fields,
fetch-v1.md section 1 "The cache faults", and the parity allowlist the
names the doc gives in prose. Each CHANGELOG has the same Added and
Changed bullets.

Refs #486

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@EricAndrechek
EricAndrechek marked this pull request as ready for review October 6, 2026 16:53
@EricAndrechek
EricAndrechek added this pull request to the merge queue Oct 6, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to invalid changes in the merge commit Oct 6, 2026
@EricAndrechek
EricAndrechek added this pull request to the merge queue Oct 6, 2026
Merged via the queue into main with commit d698560 Oct 6, 2026
@EricAndrechek
EricAndrechek deleted the strict-cache-486-strict branch October 6, 2026 17:22
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