Files

9.2 KiB

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:

<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

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.