168 lines
9.2 KiB
Markdown
168 lines
9.2 KiB
Markdown
# Lifecycle foundation v1
|
|
|
|
`architecture.md` remains the authoritative design. This milestone provides only
|
|
deterministic job state, checkpoint storage, locking and inbox admission. It does
|
|
not execute interpretation, hypotheses, diagnosis, tools or LLMs. The separate
|
|
CaseBuilder module now uses these checkpoint APIs for deterministic construction.
|
|
State APIs allow future phase implementations to confirm their work; recording
|
|
a state is not evidence that this foundation executed that phase.
|
|
|
|
## Package state and service state
|
|
|
|
The package manifest is authoritative for identity, generation, revision, state
|
|
and routing. Detailed progress is stored as immutable JSON artifacts at
|
|
`history/checkpoints/<hash-of-step-id>.json`. The manifest's
|
|
`artifacts.lifecycle_checkpoint` points to the latest artifact. Each checkpoint
|
|
links to its predecessor, with format/version, identities, output generation,
|
|
analysis revision, previous/current state, routing snapshot, optional resume state, step identity,
|
|
request hash, source generation and content fingerprint, creation time and opaque
|
|
structured `details`. `checkpoint-v1.schema.json` defines the JSON contract.
|
|
Readers additionally check paths, cycles, state values, generation/revision order,
|
|
identity and agreement with the manifest for a matching generation. Checkpoint
|
|
JSON reads use the configurable manifest-byte limit as their security cap.
|
|
|
|
Existing `analysis/current.json` is not repurposed as worker state. The checkpoint
|
|
namespace and manifest checkpoint pointer are reserved to the lifecycle service.
|
|
Checkpoints are outside frozen analysis revision directories. Every successful
|
|
checkpoint uses PackageWriter to create and validate one new package generation;
|
|
all old artifacts remain. No checkpoint exists only in an external database.
|
|
|
|
Service layout:
|
|
|
|
```text
|
|
<workspace>/
|
|
inbox/ incoming files of arbitrary names/extensions
|
|
quarantine/<unique-id>/ package.zip and validation_error.json
|
|
cache/<raw-archive-sha256>.zip exact immutable input archives
|
|
cases/<case-hash>/<package-hash>/
|
|
g00000001.zip immutable confirmed generations
|
|
current.json reconstructible local lookup index
|
|
outbox/ reserved for later analyzed-package delivery
|
|
review/ reserved for later human-review delivery
|
|
runtime/
|
|
current_job.json coarse worker summary, or null when idle
|
|
locks/worker.lock persistent lock inode/file
|
|
locks/case-<case-hash>.lock
|
|
```
|
|
|
|
Opaque IDs are hashed before use in filesystem paths, including for locking.
|
|
Stored-generation filenames are internal lookup conventions and are always
|
|
checked against the manifest; incoming filenames never supply identity or state.
|
|
Runtime includes identities, input generation, analysis revision, target stage,
|
|
start time and last confirmed checkpoint time. It contains no detailed progress.
|
|
Runtime and local index JSON are written via temporary file, flush/fsync and
|
|
atomic replacement. They can be stale after a crash and are never authoritative.
|
|
Current generation is recovered from fully validated managed packages. Corrupt
|
|
managed storage is reported rather than silently rolling back to an older file.
|
|
|
|
## Transitions and retry
|
|
|
|
Normal transitions follow the architecture's linear sequence from NEW through
|
|
HANDED_OFF. REOPENED proceeds to BUILDING_CASE. Active phases may checkpoint
|
|
progress in the same state, become BLOCKED, or become FAILED_TO_ANALYZE.
|
|
HANDED_OFF and FAILED_TO_ANALYZE can become REOPENED. Skips, regressions and
|
|
unrelated BLOCKED resumes are rejected.
|
|
|
|
The initial VALIDATED to BUILDING_CASE transition starts revision 1 if it is
|
|
still zero. REOPENED increments analysis revision exactly once; subsequently
|
|
entering BUILDING_CASE preserves it. The revision summary counter is maintained
|
|
without altering other history counters. Package generation changes independently.
|
|
These are explicit controller operations, not inferred from filenames.
|
|
|
|
BLOCKED requires a nonblank reason and timezone-aware retry timestamp. The
|
|
checkpoint records the interrupted active state; routing becomes inbox with an
|
|
incremented retry count. Resume returns only to that state, only after the retry
|
|
time, and clears the reason/timestamp while retaining retry count. The retry
|
|
clock is UTC-aware. FAILED_TO_ANALYZE routes logically to review; OUTPUT_READY
|
|
and HANDED_OFF route logically to outbox; active/reopened work routes to cases.
|
|
The state and routing fields remain separate, and intake preserves a returning
|
|
package's routing. Only fresh NEW packages are advanced to VALIDATED on admission.
|
|
|
|
This milestone does not create diagnoses, failure reports or Change Requests and
|
|
does not deliver packages to coding agents. Outbox/review are initialized but
|
|
physical output delivery belongs to the future phase controller. Detailed
|
|
checkpoint payloads and artifact pointer updates are caller-supplied contracts;
|
|
they receive structural validation, not diagnostic interpretation.
|
|
|
|
## Idempotence, locking and recovery
|
|
|
|
The content fingerprint hashes sorted internal paths and uncompressed bytes,
|
|
with canonical JSON for the manifest. Archive compression, order and filename
|
|
do not affect identity. Re-delivery of a known input generation and fingerprint
|
|
does not create a new generation. Each checkpoint carries its source receipt,
|
|
so this remains true when the package moves to a service without old local files.
|
|
A seen generation with different content is quarantined as a conflict. Newer
|
|
returning packages must preserve current protected files, checkpoint lineage and
|
|
analysis revision; they cannot replace prior content or erase history. The input
|
|
manifest remains authoritative for externally performed work on newer generations.
|
|
|
|
`advance` requires a stable nonblank `step_id`. Repeating it with the same target,
|
|
details, artifact content and retry request returns the latest confirmed package
|
|
without rerunning a write. Reusing the ID for a different request is an error.
|
|
Artifact additions and the checkpoint are committed in the same package. External
|
|
side effects are outside this transaction and need their own idempotence policy.
|
|
|
|
Nonblocking native OS locks use msvcrt on Windows and flock on POSIX. The coarse
|
|
runtime file models one worker, guarded by a worker lock. Case locks additionally
|
|
use case identity, independent of package identity. There is no timeout-based lock
|
|
stealing and no waiting on blocked cases. The OS releases locks when a process
|
|
exits, including a crash. Lock files remain in place to avoid inode replacement
|
|
races; do not delete them while workers are active. Shared network filesystems
|
|
require a separate distributed locking strategy and are not supported here.
|
|
|
|
Commit sequence:
|
|
|
|
1. Validate input, checkpoint lineage and requested transition under locks.
|
|
2. Atomically write the coarse runtime summary.
|
|
3. Publish the fully validated next generation via PackageWriter.
|
|
4. Update the reconstructible local index, then clear runtime to null.
|
|
5. For inbox admission, acknowledge the original only after publication, checking
|
|
that its raw bytes still match the cached snapshot.
|
|
|
|
Before step 3, retry repeats only the unconfirmed operation. After step 3, the
|
|
published package and embedded receipt prove completion even if steps 4 or 5
|
|
never happened. A retry repairs local metadata and does not increment generation.
|
|
File fsync and atomic visibility are provided, without claiming portable
|
|
directory-metadata durability through power loss. Crashes may leave unpublished
|
|
temporary files or incomplete quarantine directories; those are not treated as
|
|
confirmed generations. Garbage collection is outside this milestone.
|
|
|
|
## Public API
|
|
|
|
```python
|
|
from viano_bug_analysis.lifecycle import (
|
|
Workspace, InboxProcessor, CheckpointStore, State, LifecycleError,
|
|
)
|
|
|
|
workspace = Workspace("service-data")
|
|
processor = InboxProcessor(workspace)
|
|
results = processor.poll(max_jobs=100)
|
|
|
|
store = CheckpointStore(workspace)
|
|
try:
|
|
package = store.advance(
|
|
"case-A", "stable-package", State.BUILDING_CASE,
|
|
step_id="revision-1:start-case-building",
|
|
details={"inputs_collected": []},
|
|
)
|
|
except LifecycleError as error:
|
|
print(error.code, str(error))
|
|
```
|
|
|
|
`poll` is a single pass, not a daemon. It validates bounded cached snapshots,
|
|
admits valid cases, preserves invalid originals in quarantine with a structured
|
|
error report, and continues to the next input after per-input errors. Oversized
|
|
archives are moved directly to quarantine without creating an oversized cache
|
|
copy. Due BLOCKED local cases are resumed once; future retries remain deferred.
|
|
Deferred or busy inputs do not consume the `max_jobs` admission limit, so they
|
|
cannot indefinitely crowd fresh work out of a poll. Such a pass may return more
|
|
than `max_jobs` results and inspect all pending files. No diagnostic work runs.
|
|
|
|
Results are ACCEPTED, DUPLICATE, QUARANTINED, DEFERRED, BUSY, RESUMED or ERROR,
|
|
with paths and optional error code/message. Conflicting/corrupt incoming records
|
|
are quarantined; infrastructure failures leave inbox input pending. A changed
|
|
producer file is not acknowledged. Workspace storage must be service-owned on a
|
|
local filesystem supporting hard links, and producers must publish complete
|
|
files atomically without modifying them during processing. Symlink inputs are
|
|
rejected. This is not a hardened multi-tenant filesystem boundary.
|