feat: add case package validation foundation

This commit is contained in:
Nico committed 2026-10-13 12:01:28 +02:00
1 parent 2e590d5e12
commit 112487242a
18 files changed
+806 -1

No files matched your search

+125
View File
@@ -0,0 +1,125 @@
# Case package format v1
The authoritative design is `architecture.md`. This document specifies only the
project-independent package foundation. A package is a long-lived, self-contained
case record in a ZIP archive. Its filename and extension carry no program meaning.
Exactly one root `manifest.json` is the authoritative entry point.
## Identity, state and routing
`format` is `viano-bug-case-package`; `format_version` is integer `1`.
`package_id` is an opaque nonblank string identifying the case package throughout
its lifetime. Enriching and emitting it again preserves this identity and, in a
future writer, increases `package_generation` (a nonnegative integer).
`case_id` identifies the case. `analysis_revision` is a separate nonnegative
integer, initially zero; it is independent of package generation.
The reader does not derive or increment any of these values.
`project` requires generic `type` and `profile` strings and accepts `display_name`.
`created_at` and `updated_at` are RFC-3339 timestamps with timezone; updated time
must not precede creation time.
Timestamp validation rejects leap seconds because Python's datetime model cannot
represent them; lowercase `t`/`z` and fractional seconds are supported.
`state` uses the lifecycle states defined in the architecture: NEW, VALIDATED,
BUILDING_CASE, INTERPRETING_CASE, GENERATING_HYPOTHESES,
INVESTIGATING_HYPOTHESES, BUILDING_DIAGNOSIS, OUTPUT_READY, HANDED_OFF,
BLOCKED, FAILED_TO_ANALYZE, REOPENED.
`routing` is separate and requires:
- `destination`: null or inbox, quarantine, cases, outbox, review; a logical route,
never a filesystem path. Cache and runtime are service storage, not routes.
- `reason`: null or nonblank explanatory string.
- `retry_count`: nonnegative integer, initially zero.
- `retry_not_before`: null or RFC-3339 timestamp.
No state/route combination triggers execution. This milestone checks the contract
only, without implementing routing, retry, locking or lifecycle transitions.
## References and integrity
`artifacts` requires `case`, `diagnosis`, `change_request`, each null or an internal
file path. Additional artifact pointers follow the same rule. `history` requires
nonnegative counters `analysis_revisions`, `reproductions`, `evidence`, `handoffs`,
`feedback`, and nullable `events_ref`. Counters are supplied summaries; the reader
does not infer them from directory names or impose diagnostic semantics.
`integrity` maps internal file paths to `{ "sha256": "<64 lowercase hex digits>",
"size": <uncompressed byte count> }`. All supplied records are checked. A fresh
package may have an empty map. Producers should cover important immutable
artifacts. The format foundation does not infer which project artifacts are
important. Hashes detect changes, not authenticity. The manifest cannot reference
or hash itself. Every non-null reference must resolve to an actual ZIP file.
The bundled `manifest-v1.schema.json` uses JSON Schema Draft 2020-12 and only local
schema references; validation requires no network. Unknown extension fields are
accepted and retained. Unsupported format versions, duplicate JSON keys,
non-UTF-8 manifests and nonstandard numeric constants are rejected.
## Preservation
`original/` and completed `analysis/revisions/NNNN/` are immutable. New evidence,
feedback and revisions are appended. Only explicitly designated index/pointer
files (`manifest.json`, `analysis/current.json`) may later be updated. Unknown
files are preserved unchanged. Validator and reader never write, extract, delete
or rewrite anything. Historical append-only and output-superset comparisons,
atomic generation and generation increments belong to a future writer; they
cannot be proven from one standalone incoming archive.
## ZIP acceptance and configurable security policy
This implementation accepts stored and deflated ZIP entries, including ZIP64
within configured limits. It rejects encryption, symlinks and other special
filesystem entries, unsafe paths, duplicate entries, case-insensitive collisions,
and file/directory conflicts. Paths must be relative POSIX paths without empty,
dot or parent segments, backslashes, colons or control characters. Explicit empty
directory entries are accepted. Paths are rejected, never silently repaired.
The reader does not extract files; future extraction requires its own checks.
`PackageLimits` is a configurable operational security policy. These defaults
are **not immutable limits of the v1 package format** and do not appear in the
manifest schema:
| Limit | Default |
|---|---:|
| Archive bytes | 512 MiB |
| Expanded total bytes | 2 GiB |
| Expanded file bytes | 256 MiB |
| Manifest bytes | 1 MiB |
| ZIP entries (including directories) | 10,000 |
| Per-entry compression ratio | 200:1 |
Declared sizes are checked before reading. Actual reads are bounded and total
bytes are counted while streaming every file for CRC verification; listed SHA-256
hashes are checked during the same pass. Manifest parsing is bounded separately.
Malformed archive, I/O, JSON, schema and integrity failures produce structured
validation issues. Programmer errors and process-level failures are not hidden.
Input files must not be modified concurrently while open; the reader retains the
same file handle and archive used for validation.
## Public API
```python
from viano_bug_analysis.package import (
PackageLimits, PackageValidator, PackageReader, PackageReadError,
)
result = PackageValidator(PackageLimits()).validate("renamed-package.bin")
if not result.valid:
for issue in result.issues:
print(issue.code, issue.path, issue.message)
try:
with PackageReader("renamed-package.bin") as package:
manifest = package.manifest # defensive copy, including extensions
inventory = package.files # all file paths, including unknown files
data = package.read_bytes("original/bug_report.json")
except PackageReadError as error:
issues = error.result.issues
```
Read access outside the context is a programming error (`RuntimeError`). Invalid
packages and missing member requests raise `PackageReadError` with structured
details. `read_bytes` is bounded by policy but materializes a single file in memory;
validation itself streams. No artifact interpretation is performed.