diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 35c1950..c962b78 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -4,29 +4,40 @@ Graph Engineering separates portable graph meaning from language-specific execution. ```text -TypeScript builder ─┐ -YAML / JSON ────────┼─> versioned Graph IR ─> compiler ─> durable scheduler -Python builder ─────┘ │ │ - │ ├─ node executors - │ ├─ event/checkpoint store - │ ├─ artifact store - │ └─ telemetry exporters - └─ plan / visualize / audit +Canonical Graph IR JSON ─> compiler ─┬─> in-memory ready-queue scheduler + ├─> event-sourced start/resume + └─> plan / Mermaid / DOT / audit + +Independent JSON items ─> standalone bounded pipeline ─> terminal item results ``` The protocol in `spec/` defines serialization, stable diagnostics, events, and conformance fixtures. Native runtimes may use idiomatic APIs internally, but their observable behavior must agree on the shared corpus. +General TypeScript/Python builders, YAML input, ArtifactStore/LockManager, +SQLite/PostgreSQL/S3, distributed workers, and telemetry exporters remain target +surfaces. Local memory/JSONL events and file checkpoints exist today. Recovery +currently rebuilds from the authoritative event history; checkpoint +acceleration is not wired into scheduling. + ## Execution principles - An edge exists only when data or control policy genuinely flows. - Independent ready nodes run concurrently up to explicit limits. -- Pipelines stream independent items; barriers exist only for cross-item needs. -- Model output is validated before downstream consumption. -- Every cycle and dynamic expansion has semantic and hard resource limits. +- The standalone pipeline streams independent items through bounded buffers; + Graph IR `edge.mode: "stream"` remains declarative and is not durable item + streaming. +- Pure router and settled-barrier evaluators are deterministic; scheduler-level + conditional routing and deadline/quorum waiting remain planned. +- Executor output is checked for detached portable JSON before downstream + consumption; runtime JSON Schema validation remains planned. +- Every future cycle and dynamic expansion must have semantic and hard resource + limits; the current scheduler rejects implicit cycles and does not execute + dynamic GraphPatch revisions. - Node results are persisted as they succeed, so a crash does not discard an - entire parallel stage. + entire parallel stage when callers use event-sourced start/resume. Plain + `runGraph` remains in-memory. - State recovery never pretends non-idempotent external effects are exactly once. See `spec/README.md` for protocol details and the master plan under diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cf763b..95e5ecc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,14 +19,22 @@ migration note. side effects may retry within their original budgets, while omitted or non-idempotent declarations surface `IN_DOUBT_SIDE_EFFECT`. Resuming a valid terminal run returns its recorded result with no new event or executor call. +- Native standalone `runPipeline` and `run_pipeline` APIs with lazy source + intake, bounded end-to-end backpressure, per-stage concurrency and retry, + input/completion delivery order, explicit stop/drop/dead-letter outcomes, + cooperative cancellation, and shared cross-language behavioral cases. +- Evidence-gated progress scanning and a full Day 1-21 delivery control surface: + 107 registered tasks, a dependency graph, ownership map, coverage matrix, + three organic-growth plans, and a 178-item stable-v1/RC release checklist. ### In progress - Scheduler checkpoint acceleration, replay/fork, and distributed lease/fencing providers. Recovery correctness currently comes from the complete event stream; checkpoint files are not wired into the scheduler. -- Streaming pipelines, conditional edge lowering, verifier panels, and bounded - runtime loops. +- Graph IR stream-edge lowering and durable item recovery, conditional edge + lowering, verifier panels, and bounded runtime loops. The new standalone + pipeline deliberately does not claim these graph/durability semantics. ## [0.1.0-alpha.1] - 2026-07-26 diff --git a/README.md b/README.md index 153766c..efa3545 100644 --- a/README.md +++ b/README.md @@ -26,13 +26,15 @@ decisions while scheduler-level conditional routing remains an explicit v1 goal. The project is an early alpha. The DAG compiler, ready-queue schedulers, safe project initializer, machine-readable CLI, structured failure handling, retries, -timeouts, budgets, settled barrier/router decisions, safe Mermaid/DOT rendering, -pattern constructors, local event/checkpoint stores, read-only MCP server, and -development progress scanner are executable today. Both native runtimes also -provide event-sourced durable start/resume: committed successes are reused after -process loss and unsafe ambiguous effects fail closed. Checkpoint acceleration, -replay/fork, distributed leases, and the broader v1 surface remain under active -development; the repository does not silently mock unfinished capabilities. +timeouts, bounded concurrency and attempt budgets, settled barrier/router +decisions, standalone bounded pipelines with backpressure, safe Mermaid/DOT +rendering, pattern constructors, local event/checkpoint stores, a read-only MCP +server, and a development progress scanner are executable today. Both native +runtimes also provide event-sourced durable start/resume: committed successes +are reused after process loss and unsafe ambiguous effects fail closed. +Checkpoint acceleration, replay/fork, distributed leases, Graph IR stream +execution, and the broader v1 surface remain under active development; the +repository does not silently mock unfinished capabilities. > **Source-only alpha:** npm and PyPI packages are not published yet. Clone this > repository to try the current release candidate; registry publication remains @@ -90,6 +92,7 @@ uv run --project python pytest python/tests | Stable compiler diagnostics | Yes | Yes | | Ready-queue DAG scheduler | Yes | Yes | | Bounded concurrency | Yes | Yes | +| Standalone bounded pipeline and backpressure | Yes | Yes | | Retry, timeout, attempt budget | Yes | Yes | | Failure isolation and named ports | Yes | Yes | | Shared compiler/runtime conformance | Yes | Yes | @@ -100,7 +103,7 @@ uv run --project python pytest python/tests | Event-sourced scheduler start/resume | Yes | Yes | | Scheduler checkpoint acceleration | Not yet | Not yet | | Read-only validation/planning MCP | Yes | Uses the same portable IR | -| Streaming and scheduler-applied routers/verifier panels/loops | Target v1 | Target v1 | +| Graph IR streaming and scheduler-applied routers/verifier panels/loops | Target v1 | Target v1 | ## Design commitments @@ -144,6 +147,7 @@ python3 scripts/check-python-artifacts.py - [Runtime semantics](spec/runtime-semantics.md) - [Persistence semantics](spec/persistence-semantics.md) - [Durable recovery semantics](spec/durable-recovery-semantics.md) +- [Bounded pipeline semantics](spec/pipeline-semantics.md) - [Primitive semantics](spec/primitives-semantics.md) - [21-day delivery plan](codex_plans/Graph-Engineering-21-Day-Master-Plan.md) diff --git a/ROADMAP.md b/ROADMAP.md index adbef3f..7c27d53 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -25,7 +25,10 @@ earned by executable cross-language tests. - [ ] Scheduler checkpoint acceleration; correctness already rebuilds from the authoritative event history. - [ ] Replay and fork with immutable lineage. -- [ ] Pipeline buffers/backpressure and explicit barrier policies. +- [x] Standalone native pipeline buffers/backpressure with structured terminal + outcomes and shared TypeScript/Python behavioral cases. +- [ ] Graph IR-integrated/durable item streaming and scheduler-integrated + deadline/quorum barrier policies. - [ ] Deterministic routers, verifier verdicts, quorum/unknown outcomes, reflection, and bounded loop-until-dry primitives. - [ ] Deterministic mock, OpenAI, Anthropic, Gemini, OpenAI-compatible, HTTP, shell, diff --git a/SECURITY.md b/SECURITY.md index 71e27e0..b75b7fa 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -19,10 +19,17 @@ Until v1, only the newest pre-release is supported. After v1, the latest minor release receives security fixes. The policy will be revised before a second stable major version. -## Security defaults - -- Shell, network, filesystem writes, and secrets require explicit capabilities. -- MCP mutation is disabled by default. -- Prompt and response bodies are not recorded by default. -- External side effects require idempotency declarations or human approval. -- Worktree cleanup never targets an unresolved broad path. +## Security boundary and target defaults + +The alpha validates graph structure, bounds scheduler work, keeps MCP read-only, +and gates ambiguous durable retries using the declared side-effect class. Node +executors still inherit the host process's ambient filesystem, network, shell, +and environment authority; capability metadata is not yet an enforcement +boundary. Run untrusted executors only inside isolation you configure outside +the runtime. + +Target-v1 defaults are deny-by-default shell, network, filesystem-write, and +secret capabilities; explicit approval for non-idempotent external effects; +prompt/response capture off by default; and path-resolved worktree cleanup. +These are requirements, not claims about the current alpha. See the detailed +[security boundary](docs/SECURITY.md#controls-implemented-in-alpha). diff --git a/codex_logs/daily/2026-07-26.md b/codex_logs/daily/2026-07-26.md index 5e54b79..825c32e 100644 --- a/codex_logs/daily/2026-07-26.md +++ b/codex_logs/daily/2026-07-26.md @@ -251,3 +251,78 @@ Canonical IR -> language models/builders -> compiler -> deterministic scheduler dependency-review, and Python/JavaScript CodeQL check passed. The pull request remains intentionally unmerged and review-required; protected-branch policy was not bypassed. + +## 18:22 UTC bounded-pipeline and full-plan control checkpoint + +- Ran parallel full-plan, pipeline, architecture, security, delivery, and growth + audits after every assigned agent explicitly read the 355-line master plan. + The supporting plan set now exceeds 7,000 lines, including deep Graph IR, + runtime, persistence, security, builder/YAML, redaction, release, ownership, + dependency, growth, and 178-leaf acceptance documents. These documents record + Open work honestly and do not count file presence as implemented capability. +- Expanded the executable registry from 91 to 107 unique tasks after the gap + audit found 16 missing control/implementation lanes. Added independent native + adapter and redaction tasks, approval authority, runtime chaos versus durable + operations, privacy/usability, support/education, canonical npm distribution, + release-leaf mapping, historical candidate revalidation, and a final + fail-closed release roll-up. A fresh graph check found no duplicate ID, + dangling dependency, or cycle. +- Completed the standalone bounded pipeline in TypeScript and Python with lazy + source admission, bounded queues and global in-flight work, stage concurrency, + retry/timeout, stop/drop/dead-letter outcomes, ordered/completion delivery, + cooperative cancellation, cleanup, and a hard 2,048-stage construction bound. + Eight shared cases agree across languages. +- Independent adversarial review caught and closed three late defects before the + milestone commit: TypeScript accepted explicit-null configuration defaults; + Python policy extension keys such as `max_concurrency` could be silently + rewritten and change canonical hashes; and Python did not close an overflowing + stage iterator. Two new shared policy fixtures now prove exact canonical/hash + parity for both null and numeric extension values. +- Final local evidence is green: workspace build/typecheck/lint/test; Core 41, + Persistence 27, Primitives 147, Runtime 115, Patterns 93, MCP 15 and CLI 131 + tests; Python 587 tests plus 2 subtests, Ruff and strict mypy; 14 Graph IR + fixtures plus all runtime/persistence/durable/router/barrier/pipeline joins; + seven npm tarball-content and clean-install checks; Python wheel/sdist checks; + 193 local Markdown links; production audit with no known vulnerability; and + diff hygiene. +- Committed the immutable code/protocol milestone as + `3df201db016f2e74d4ee9a96bc28ddabe0d010d8`, authored and committed by + `reacher-z ` without co-author trailers. The registry now + binds D7 completion evidence to that revision; push and the stacked draft PR + follow after the control/document commit. +- Verified that the real user-level `graph-progress.timer` remains active on its + 30-minute schedule. The scanner has explicit evidence supersession and + timestamp-integrity rules covered by 17 tests; it reports planned and external + dependencies as waiting rather than falsely complete. +- Started the next critical path at full parallelism: native TypeScript and + Python D2 builder/safe-YAML lanes, plus the canonical D9 redaction protocol. + Redaction remains a critical open implementation blocker: current durable + events expose more raw fields than the initial input/output finding, so stable + release claims remain no-go until both language implementations, migration, + sink-byte canaries, and independent security review pass. + +## 18:50 UTC bounded-pipeline delivery checkpoint + +- Committed the full-plan audit, 107-task dependency/evidence registry, + architecture and growth controls, and hardened progress scanner as + `a39dbdc3f7349965aa2a843dfaebd9475744e040`. Both milestone commits use + `reacher-z ` as author and committer and contain no + co-author trailer. +- Pushed `feat/pipeline-runtime` and opened stacked draft pull request + [#15](https://github.com/reacher-z/GraphEngineering/pull/15) against + `feat/durable-recovery`. Protected `main` remains untouched and the draft was + not merged. +- All 13 reported GitHub gates passed: Node 20 and 22, Python 3.11 through 3.13, + protocol fixtures, cross-language conformance, npm package artifacts, + progress scanner, dependency review, JavaScript/TypeScript CodeQL, Python + CodeQL, and the aggregate CodeQL result. +- Reconfirmed the user-level `graph-progress.timer` is active with its next + 30-minute run scheduled. The latest manual scan reports 107 tasks, 42 healthy, + 65 dependency-waiting, zero stale/blocked/integration-risk/warning items, and + 6 of 77 evidence gates satisfied. +- Froze D2 authoring, safe-YAML, strict typed-port, and initial-revision + semantics plus a machine-readable identity schema and ADR. Added initial + positive, negative, typed diagnostic, identity, and YAML safety fixtures; + these remain work in progress until both native lanes, CLI, three-way shared + conformance, package-install smoke, independent review, and immutable + candidate evidence all pass. diff --git a/codex_logs/decisions/ADR-0001-standalone-bounded-pipeline.md b/codex_logs/decisions/ADR-0001-standalone-bounded-pipeline.md new file mode 100644 index 0000000..eaa5314 --- /dev/null +++ b/codex_logs/decisions/ADR-0001-standalone-bounded-pipeline.md @@ -0,0 +1,92 @@ +# ADR-0001: Ship the first bounded pipeline as a standalone runtime primitive + +- Status: accepted for v1alpha1 implementation +- Date: 2026-07-26 +- Owners: main, TypeScript runtime, Python runtime +- Canonical contract: `spec/pipeline-semantics.md` + +## Context + +Graph IR already reserves `edge.mode: "stream"`, but both native graph +schedulers currently execute one node once and retain one portable JSON +`NodeResult` per node. Durable history identifies attempts by run and node, and +`EdgeEmitted` records one value-edge marker after node success. It has no item +identity, item offset, acknowledgement, queue state, window/join policy, or +per-item attempt history. + +Directly interpreting a stream edge would therefore change node readiness, +input/output contracts, attempt and budget accounting, graph outputs, fan-in, +event folding, crash recovery, and replay at once. Treating an async iterator as +an ordinary node JSON value would instead be a false implementation: it is not +portable, hashable, recoverable, or safely replayable. + +The Day-5 plan still needs an executable pipeline in which independent items +occupy different stages concurrently, slow consumers exert source backpressure, +buffers stay bounded, retries stop, and failures remain structured. + +## Decision + +The first implementation is a standalone, in-memory `runPipeline` / +`run_pipeline` primitive shared by the TypeScript and Python runtime packages. +It has: + +- a synchronous factory and single-pass asynchronous result iterator; +- synchronous or asynchronous sources; +- immutable ordered stage contracts; +- bounded stage queues and a bounded global in-flight credit window; +- a hard item admission limit and statically bounded maximum attempts; +- input-order or completion-order terminal delivery; +- bounded deterministic retries and attempt timeouts; +- explicit stop, drop, and dead-letter policies; +- structured item and run failures rather than `null` placeholders; +- cooperative cancellation, explicit close, and task/listener cleanup; and +- shared cross-language fixtures and a normative protocol document. + +The primitive does not activate `edge.mode: "stream"` and does not claim +item-level durability. When called inside a graph node, the entire pipeline is +part of that node attempt and can be replayed as a unit under existing recovery +rules. + +## Safety and resource decisions + +The source cannot be pulled until an in-flight credit is available. Credit is +released only when a terminal result reaches the consumer. This makes consumer +pressure propagate to the source rather than hiding an unbounded result list. + +`maxItems` defaults to a finite value and is checked before each source pull. +The product of `maxItems` and the sum of per-stage attempt bounds must be a safe +integer. Infinite or adversarial sources consequently have bounded work even +when callers forget to cancel. + +Input ordering applies only to terminal delivery. Internal stage flow remains +completion-driven so a fast later item can enter a downstream stage while a slow +earlier item is still upstream. The reorder buffer remains bounded by the +in-flight window. + +Retries are at-least-once and require idempotent external effects. Timeout and +cancellation can detach non-cooperative user code after observing its eventual +outcome; they cannot revoke an external side effect already initiated. + +## Consequences + +This delivers useful pipeline/backpressure semantics without destabilizing the +ordinary or durable DAG scheduler. It also creates a precise test bed for future +stream-edge work. + +The tradeoff is that a standalone pipeline is not yet a first-class graph edge, +cannot be checkpointed per item, and does not support stream joins, windows, +materializing barriers, replay, or fork. Documentation must keep that boundary +visible. + +## Required follow-up before Graph IR stream lowering + +1. Add portable item identity and ordinal rules. +2. Specify per-edge enqueue, consume, acknowledgement, and offset events. +3. Define crash reconstruction for bounded queues and in-flight handlers. +4. Define value/stream fan-in: merge, zip, window, and materializing barrier. +5. Bind per-item attempts, budgets, idempotency keys, and external effects. +6. Define stream graph outputs, cancellation, replay, fork, and schema evolution. +7. Add cross-process leases/fencing before distributed workers advance a stream. + +Until those contracts exist and pass cross-language crash conformance, +`edge.mode: "stream"` remains declarative only. diff --git a/codex_logs/task-registry.json b/codex_logs/task-registry.json index 2e857d9..b34bedd 100644 --- a/codex_logs/task-registry.json +++ b/codex_logs/task-registry.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "updated_at": "2026-07-26T13:40:44Z", + "updated_at": "2026-07-26T18:22:08Z", "repository": ".", "scan_policy": { "warning_after_minutes": 60, @@ -8,6 +8,9 @@ "nudge_cooldown_minutes": 120, "escalate_same_blocker_after_scans": 2 }, + "evidence_policy": { + "required_for_assigned_at_or_after": "2026-07-26T16:20:00Z" + }, "tasks": [ { "id": "D1-SPEC-001", @@ -524,6 +527,1354 @@ "risk": "high", "blocker": null, "next_action": "Monitor protected-branch CI and review feedback without weakening the recovery contract" + }, + { + "id": "D7-PIPELINE-SPEC-012", + "title": "Cross-language bounded pipeline and backpressure contract", + "owner": "main + /root/pipeline_spec", + "status": "completed", + "work_package": "WP4-WP11", + "depends_on": ["D1-SPEC-001", "D3-TS-RUNTIME-002", "D3-PY-RUNTIME-002"], + "expected_artifacts": [ + "spec/pipeline-semantics.md", + "spec/conformance/pipeline.case.json" + ], + "expected_tests": [ + "fast items cross later stages without a whole-stage barrier", + "bounded admission and source backpressure", + "ordered structured terminal outcomes", + "bounded retries, stop policy, and cooperative cancellation", + "bounded stage-configuration pull count" + ], + "evidence_required": true, + "test_evidence": [ + {"requirement": "fast items cross later stages without a whole-stage barrier", "result": "passed", "recorded_at": "2026-07-26T16:40:00Z", "reference": "spec/conformance/pipeline.case.json: fast-item-crosses-stage-without-barrier"}, + {"requirement": "bounded admission and source backpressure", "result": "passed", "recorded_at": "2026-07-26T16:40:00Z", "reference": "spec/conformance/pipeline.case.json: slow-consumer-backpressures-source"}, + {"requirement": "ordered structured terminal outcomes", "result": "passed", "recorded_at": "2026-07-26T16:40:00Z", "reference": "shared pipeline fixture reports compared by tools/conformance/run.mjs"}, + {"requirement": "bounded retries, stop policy, and cooperative cancellation", "result": "passed", "recorded_at": "2026-07-26T16:40:00Z", "reference": "native pipeline regression suites and shared retry/stop cases"}, + {"requirement": "bounded stage-configuration pull count", "result": "passed", "recorded_at": "2026-07-26T17:40:00Z", "reference": "spec/pipeline-semantics.md: maxStages default/hard limit and overflow rule"} + ], + "completion_evidence": ["spec/pipeline-semantics.md", "spec/conformance/pipeline.case.json", "codex_logs/decisions/ADR-0001-standalone-bounded-pipeline.md"], + "assigned_at": "2026-07-26T14:58:00Z", + "started_at": "2026-07-26T14:58:00Z", + "last_heartbeat": "2026-07-26T18:18:36Z", + "completed_at": "2026-07-26T18:18:36Z", + "risk": "high", + "blocker": null, + "next_action": "Hold the standalone bounded-pipeline contract stable while native implementations and red-team tests land" + }, + { + "id": "D7-TS-PIPELINE-012", + "title": "TypeScript bounded streaming pipeline runtime", + "owner": "/root/ts_pipeline_impl (reassigned after no artifact from /root/pipeline_ts)", + "status": "completed", + "work_package": "WP4", + "depends_on": ["D7-PIPELINE-SPEC-012", "D3-TS-RUNTIME-002"], + "expected_artifacts": ["packages/runtime"], + "expected_tests": [ + "bounded stage queues", + "no false barrier", + "early-consumer cleanup", + "failure and cancellation accounting", + "bounded stage-configuration pull count" + ], + "evidence_required": true, + "test_evidence": [ + {"requirement": "bounded stage queues", "result": "passed", "recorded_at": "2026-07-26T16:55:00Z", "reference": "packages/runtime/test/pipeline.test.ts; runtime test suite"}, + {"requirement": "no false barrier", "result": "passed", "recorded_at": "2026-07-26T16:55:00Z", "reference": "packages/runtime/test/pipeline.test.ts: fast later item regression"}, + {"requirement": "early-consumer cleanup", "result": "passed", "recorded_at": "2026-07-26T16:55:00Z", "reference": "packages/runtime/test/pipeline.test.ts: close/return and hostile cleanup regressions"}, + {"requirement": "failure and cancellation accounting", "result": "passed", "recorded_at": "2026-07-26T16:55:00Z", "reference": "packages/runtime/test/pipeline.test.ts: adversarial cancellation and formatter regressions"}, + {"requirement": "bounded stage-configuration pull count", "result": "passed", "recorded_at": "2026-07-26T17:40:00Z", "reference": "packages/runtime/test/pipeline.test.ts: infinite stage iterable stops at maxStages + 1 without source construction"} + ], + "completion_evidence": ["packages/runtime/src/pipeline.ts", "packages/runtime/test/pipeline.test.ts", "runtime full suite and strict typecheck", "commit:3df201db016f2e74d4ee9a96bc28ddabe0d010d8"], + "assigned_at": "2026-07-26T14:58:00Z", + "started_at": "2026-07-26T14:58:00Z", + "last_heartbeat": "2026-07-26T18:18:36Z", + "completed_at": "2026-07-26T18:18:36Z", + "risk": "high", + "blocker": null, + "next_action": "Maintain the 115-test runtime surface while cross-language and repository gates run" + }, + { + "id": "D7-PY-PIPELINE-012", + "title": "Python bounded streaming pipeline runtime", + "owner": "/root/py_pipeline_impl (reassigned after no artifact from /root/pipeline_py)", + "status": "completed", + "work_package": "WP4", + "depends_on": ["D7-PIPELINE-SPEC-012", "D3-PY-RUNTIME-002"], + "expected_artifacts": ["python/src/graph_engineering"], + "expected_tests": [ + "bounded asyncio stage queues", + "no false barrier", + "async source backpressure", + "failure and cancellation accounting", + "bounded stage-configuration pull count" + ], + "evidence_required": true, + "test_evidence": [ + {"requirement": "bounded asyncio stage queues", "result": "passed", "recorded_at": "2026-07-26T16:55:00Z", "reference": "python/tests/test_pipeline.py; full pytest suite"}, + {"requirement": "no false barrier", "result": "passed", "recorded_at": "2026-07-26T16:55:00Z", "reference": "python/tests/test_pipeline.py: fast later item regression"}, + {"requirement": "async source backpressure", "result": "passed", "recorded_at": "2026-07-26T16:55:00Z", "reference": "python/tests/test_pipeline.py: bounded pull and slow consumer regressions"}, + {"requirement": "failure and cancellation accounting", "result": "passed", "recorded_at": "2026-07-26T16:55:00Z", "reference": "python/tests/test_pipeline.py: adversarial cancellation and hostile exception regressions"}, + {"requirement": "bounded stage-configuration pull count", "result": "passed", "recorded_at": "2026-07-26T18:18:36Z", "reference": "python/tests/test_pipeline.py: under/exact/overflow/infinite max_stages and iterator-close regressions; 59 pipeline tests"} + ], + "completion_evidence": ["python/src/graph_engineering/pipeline.py", "python/tests/test_pipeline.py", "full pytest, Ruff, and strict mypy", "commit:3df201db016f2e74d4ee9a96bc28ddabe0d010d8"], + "assigned_at": "2026-07-26T14:58:00Z", + "started_at": "2026-07-26T14:58:00Z", + "last_heartbeat": "2026-07-26T18:18:36Z", + "completed_at": "2026-07-26T18:18:36Z", + "risk": "high", + "blocker": null, + "next_action": "Maintain the 587-test native suite and 59-test pipeline surface while later graph-integrated streaming remains explicit" + }, + { + "id": "D7-PIPELINE-CONFORMANCE-013", + "title": "Cross-language pipeline conformance, red-team, docs, and release gates", + "owner": "main", + "status": "completed", + "work_package": "WP4-WP10-WP11", + "depends_on": ["D7-TS-PIPELINE-012", "D7-PY-PIPELINE-012"], + "expected_artifacts": [ + "tools/conformance", + "docs/CONCEPTS.md", + "packages/runtime/README.md", + "python/README.md" + ], + "expected_tests": [ + "shared trace and terminal projection parity", + "adversarial cancellation and cleanup", + "bounded stage-configuration parity", + "full workspace and package gates" + ], + "evidence_required": true, + "test_evidence": [ + {"requirement": "shared trace and terminal projection parity", "result": "passed", "recorded_at": "2026-07-26T17:48:00Z", "reference": "corepack pnpm test:conformance: 8 bounded-pipeline cases"}, + {"requirement": "adversarial cancellation and cleanup", "result": "passed", "recorded_at": "2026-07-26T18:18:36Z", "reference": "115 TypeScript runtime tests and 59 Python pipeline tests, including hostile cleanup and cancellation races"}, + {"requirement": "bounded stage-configuration parity", "result": "passed", "recorded_at": "2026-07-26T18:18:36Z", "reference": "maxStages/max_stages limit+1 tests, explicit-null configuration rejection, iterator cleanup, and 8 shared pipeline cases"}, + {"requirement": "full workspace and package gates", "result": "passed", "recorded_at": "2026-07-26T18:18:36Z", "reference": "commit 3df201db016f2e74d4ee9a96bc28ddabe0d010d8; pnpm build/typecheck/lint/test/conformance/check:packages/check:packed-install/audit:prod; Python 587 tests, Ruff, strict mypy, wheel/sdist; docs 193 links"} + ], + "completion_evidence": ["commit:3df201db016f2e74d4ee9a96bc28ddabe0d010d8", "spec/pipeline-semantics.md", "tools/conformance/run.mjs", "packages/runtime/test/pipeline.test.ts", "python/tests/test_pipeline.py", "independent cross-language max-stage/null/lifecycle review"], + "assigned_at": "2026-07-26T15:06:03Z", + "started_at": "2026-07-26T16:20:00Z", + "last_heartbeat": "2026-07-26T18:18:36Z", + "completed_at": "2026-07-26T18:18:36Z", + "risk": "high", + "blocker": null, + "next_action": "Keep the standalone contract stable; graph-integrated durable stream edges remain a separate unclaimed task" + }, + { + "id": "CTRL-PLAN-COVERAGE-001", + "title": "Evidence-backed master-plan coverage and delivery control documents", + "owner": "main + parallel reviewers", + "status": "completed", + "work_package": "WP0-WP12", + "depends_on": [], + "expected_artifacts": ["codex_plans/delivery/master-plan-coverage-matrix.md", "codex_plans/delivery/task-dependency-graph.md", "codex_plans/delivery/agent-ownership-map.md", "codex_plans/delivery/release-checklist.md"], + "expected_tests": ["Day 1-21 coverage", "ten-pattern coverage", "mandatory-test and threshold coverage", "documentation link validation"], + "test_evidence": [ + {"requirement": "Day 1-21 coverage", "result": "passed", "recorded_at": "2026-07-26T16:55:00Z", "reference": "task-dependency-graph D01-D21 structural audit"}, + {"requirement": "ten-pattern coverage", "result": "passed", "recorded_at": "2026-07-26T16:55:00Z", "reference": "coverage matrix and dependency graph P01-P10 structural audits"}, + {"requirement": "mandatory-test and threshold coverage", "result": "passed", "recorded_at": "2026-07-26T16:55:00Z", "reference": "release-checklist REL inventory: 178 unique open evidence rows"}, + {"requirement": "documentation link validation", "result": "passed", "recorded_at": "2026-07-26T16:55:00Z", "reference": "corepack pnpm check:docs: 83 local Markdown links"} + ], + "completion_evidence": ["codex_plans/delivery/master-plan-coverage-matrix.md", "codex_plans/delivery/task-dependency-graph.md", "codex_plans/delivery/agent-ownership-map.md", "codex_plans/delivery/release-checklist.md"], + "assigned_at": "2026-07-26T16:20:00Z", + "started_at": "2026-07-26T16:20:00Z", + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "high", + "blocker": null, + "next_action": "Maintain all four control documents as implementation evidence lands; never close Open release rows by inference" + }, + { + "id": "CTRL-EVIDENCE-002", + "title": "Make scanner completion depend on explicit test and release evidence", + "owner": "platform lane", + "status": "completed", + "work_package": "WP11", + "depends_on": ["CTRL-PLAN-COVERAGE-001"], + "expected_artifacts": ["tools/progress-scanner", "codex_logs/task-registry.json"], + "expected_tests": ["missing expected-test evidence is integration risk", "test timestamps and references are preserved", "completion references are mandatory", "latest evidence record supersedes earlier outcomes without deleting history", "pre-assignment and future evidence timestamps are rejected"], + "test_evidence": [ + {"requirement": "missing expected-test evidence is integration risk", "result": "passed", "recorded_at": "2026-07-26T17:05:00Z", "reference": "test_required_completion_evidence_must_cover_every_expected_test"}, + {"requirement": "test timestamps and references are preserved", "result": "passed", "recorded_at": "2026-07-26T17:05:00Z", "reference": "17-test progress-scanner suite and latest scan completion_evidence projection"}, + {"requirement": "completion references are mandatory", "result": "passed", "recorded_at": "2026-07-26T17:05:00Z", "reference": "test_required_completion_evidence_can_make_completed_task_healthy and missing-reference branch"}, + {"requirement": "latest evidence record supersedes earlier outcomes without deleting history", "result": "passed", "recorded_at": "2026-07-26T17:44:00Z", "reference": "test_latest_evidence_record_supersedes_an_earlier_failure"}, + {"requirement": "pre-assignment and future evidence timestamps are rejected", "result": "passed", "recorded_at": "2026-07-26T17:44:00Z", "reference": "test_evidence_timestamps_cannot_predate_assignment_or_claim_the_future"} + ], + "completion_evidence": ["tools/progress-scanner/graph_progress.py", "tools/progress-scanner/tests/test_graph_progress.py", "tools/progress-scanner/README.md", "17-test progress-scanner suite", "manual 90-task scan with explicit satisfied/open evidence totals"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": "2026-07-26T16:55:00Z", + "last_heartbeat": "2026-07-26T17:44:00Z", + "risk": "high", + "blocker": null, + "next_action": "Maintain the migration cutoff, append-only latest-record semantics, timestamp integrity, exact passing records, and completion references for every new completed task" + }, + { + "id": "D2-BUILDERS-YAML-020", + "title": "General TypeScript/Python graph builders and YAML compiler parity", + "owner": "TS + Python lanes; main owns fixtures", + "status": "in_progress", + "work_package": "WP1-WP7", + "depends_on": ["D1-SPEC-001"], + "expected_artifacts": ["packages/core", "python/src/graph_engineering", "spec/conformance"], + "expected_tests": ["builder hash parity", "YAML/JSON equivalence", "revision and typed-port diagnostics", "optional IR fields reject explicit null in both languages"], + "test_evidence": [ + {"requirement": "optional IR fields reject explicit null in both languages", "result": "passed", "recorded_at": "2026-07-26T17:48:00Z", "reference": "4 shared GE1007 fixtures plus exhaustive Python matrix over 27 known optional fields; cross-language conformance passed for 12 graph fixtures"} + ], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": "2026-07-26T17:15:00Z", + "last_heartbeat": "2026-07-26T18:22:08Z", + "risk": "high", + "blocker": null, + "next_action": "Freeze the general builder and safe YAML contract now that the 27-field explicit-null parity gap is closed" + }, + { + "id": "D3-PY-CLI-021", + "title": "Native Python CLI and cross-language command-envelope parity", + "owner": "Python + platform lanes", + "status": "planned", + "work_package": "WP7", + "depends_on": ["D2-BUILDERS-YAML-020", "D3-CLI-002"], + "expected_artifacts": ["python/src/graph_engineering", "python/pyproject.toml", "docs/CLI.md"], + "expected_tests": ["init/validate/compile/plan/doctor parity", "graph and grapheng entry points", "stable exit codes"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "medium", + "blocker": null, + "next_action": "Implement after the common builder and YAML surface is frozen" + }, + { + "id": "D4-TRACE-SUBGRAPH-022", + "title": "Nested subgraphs, explicit reducers, artifact edges, and trace-view contract", + "owner": "main + runtime lanes", + "status": "planned", + "work_package": "WP1-WP2-WP8", + "depends_on": ["D2-BUILDERS-YAML-020", "D3-TS-RUNTIME-002", "D3-PY-RUNTIME-002", "D6-DURABLE-SPEC-010"], + "expected_artifacts": ["spec", "packages/runtime", "python/src/graph_engineering"], + "expected_tests": ["namespace and checkpoint scope", "concurrent state reducer", "artifact-ref and executable stream edges"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "high", + "blocker": null, + "next_action": "Define subgraph and edge execution semantics without weakening current DAG compatibility" + }, + { + "id": "D6-ROUTER-BARRIER-023", + "title": "Scheduler-integrated conditional routers and quorum/deadline barriers", + "owner": "main + TS/Python runtime lanes", + "status": "planned", + "work_package": "WP4", + "depends_on": ["D7-PIPELINE-CONFORMANCE-013", "D5-TS-ROUTER-005", "D5-PY-ROUTER-005"], + "expected_artifacts": ["spec", "packages/runtime", "python/src/graph_engineering", "spec/conformance"], + "expected_tests": ["conditional edge execution", "deadline/quorum/missing statistics", "confidence escalation", "route decision event emission"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "high", + "blocker": null, + "next_action": "Start contract design after the standalone pipeline milestone is review-ready" + }, + { + "id": "D7-CYCLE-SPEC-024", + "title": "Bounded cycle, convergence, seen-set, and GraphPatch contract", + "owner": "main", + "status": "planned", + "work_package": "WP5", + "depends_on": ["D1-SPEC-001", "D3-TS-RUNTIME-002", "D3-PY-RUNTIME-002"], + "expected_artifacts": ["spec/cycle-semantics.md", "spec/graph-patch.schema.json", "spec/conformance"], + "expected_tests": ["until-dry convergence", "hard iteration/time/cost/node limits", "malicious patch rejection", "replayed exit reason"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "high", + "blocker": null, + "next_action": "Freeze loop and append-only revision invariants before implementation" + }, + { + "id": "D7-TS-CYCLES-025", + "title": "TypeScript bounded cycles and GraphPatch runtime", + "owner": "TypeScript lane", + "status": "planned", + "work_package": "WP5", + "depends_on": ["D7-CYCLE-SPEC-024"], + "expected_artifacts": ["packages/runtime", "packages/core"], + "expected_tests": ["global seen set", "bounded while/untilDry/evaluator optimizer", "patch dry run and budget gate"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "high", + "blocker": null, + "next_action": "Wait for cycle and GraphPatch contract freeze" + }, + { + "id": "D7-PY-CYCLES-026", + "title": "Python bounded cycles and GraphPatch runtime", + "owner": "Python lane", + "status": "planned", + "work_package": "WP5", + "depends_on": ["D7-CYCLE-SPEC-024"], + "expected_artifacts": ["python/src/graph_engineering"], + "expected_tests": ["global seen set", "bounded while/untilDry/evaluator optimizer", "patch dry run and budget gate"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "high", + "blocker": null, + "next_action": "Wait for cycle and GraphPatch contract freeze" + }, + { + "id": "D7-CYCLE-CONFORMANCE-027", + "title": "Cross-language cycle, convergence, and dynamic-patch conformance", + "owner": "main + independent reviewer", + "status": "planned", + "work_package": "WP5-WP11", + "depends_on": ["D7-TS-CYCLES-025", "D7-PY-CYCLES-026"], + "expected_artifacts": ["tools/conformance", "spec/conformance"], + "expected_tests": ["seen-set parity", "every exit reason", "hard-stop resource accounting", "adversarial patches"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "high", + "blocker": null, + "next_action": "Run only after both native cycle runtimes pass focused suites" + }, + { + "id": "D8-CHAOS-OPS-030", + "title": "Runtime chaos and durable operational-control acceptance join", + "owner": "main + independent QA reviewer", + "status": "planned", + "work_package": "WP2-WP7-WP11", + "depends_on": ["D8-RUNTIME-CHAOS-084", "D9-OPS-CONTROL-085"], + "expected_artifacts": ["tests/chaos", "codex_logs/release-evidence/runtime-operations"], + "expected_tests": ["bounded retry and non-cooperative cleanup campaign", "status/watch/inspect/logs", "pause/resume/cancel/retry", "independent cross-language acceptance"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "high", + "blocker": null, + "next_action": "Join the independent runtime-chaos and durable operational-command evidence" + }, + { + "id": "D9-DURABLE-EXT-SPEC-031", + "title": "Leases, replay/fork, approvals, artifacts, and checkpoint-acceleration contract", + "owner": "main", + "status": "planned", + "work_package": "WP3", + "depends_on": ["D6-DURABLE-CONFORMANCE-011", "D9-REDACTION-039"], + "expected_artifacts": ["spec", "codex_plans/architecture/persistence-and-recovery.md"], + "expected_tests": ["dual-resume exclusion", "replay/fork lineage", "stale approval", "non-idempotent confirmation", "snapshot rebuild"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "high", + "blocker": null, + "next_action": "Extend the authoritative event-history model without creating a second source of truth" + }, + { + "id": "D9-TS-DURABLE-EXT-032", + "title": "TypeScript leases, replay/fork, artifacts, SQLite, and approvals", + "owner": "TypeScript lane", + "status": "planned", + "work_package": "WP3", + "depends_on": ["D9-DURABLE-EXT-SPEC-031", "D9-TS-REDACTION-087"], + "expected_artifacts": ["packages/persistence", "packages/runtime"], + "expected_tests": ["LockManager CAS", "SQLite restart", "ArtifactStore hash", "fork lineage", "approval expiry"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "high", + "blocker": null, + "next_action": "Wait for durable extension contract" + }, + { + "id": "D9-PY-DURABLE-EXT-033", + "title": "Python leases, replay/fork, artifacts, SQLite, and approvals", + "owner": "Python lane", + "status": "planned", + "work_package": "WP3", + "depends_on": ["D9-DURABLE-EXT-SPEC-031", "D9-PY-REDACTION-088"], + "expected_artifacts": ["python/src/graph_engineering"], + "expected_tests": ["LockManager CAS", "SQLite restart", "ArtifactStore hash", "fork lineage", "approval expiry"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "high", + "blocker": null, + "next_action": "Wait for durable extension contract" + }, + { + "id": "D9-DURABLE-EXT-CONFORMANCE-034", + "title": "Cross-language extended durability and race conformance", + "owner": "main + independent reviewer", + "status": "planned", + "work_package": "WP3-WP11", + "depends_on": ["D9-TS-DURABLE-EXT-032", "D9-PY-DURABLE-EXT-033", "D9-APPROVAL-077", "D9-REDACTION-CONFORMANCE-089"], + "expected_artifacts": ["tools/conformance", "spec/conformance"], + "expected_tests": ["every checkpoint crash window", "dual resume race", "replay/fork parity", "router replay without rejudgment", "stale approval binding", "artifact corruption", "redaction migration join"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "high", + "blocker": null, + "next_action": "Wait for both native durable extension lanes" + }, + { + "id": "D9-REDACTION-039", + "title": "Correct durable redaction semantics and eliminate the false redacted wire signal", + "owner": "main protocol lane + independent security reviewer", + "status": "in_progress", + "work_package": "WP3-WP9-WP11", + "depends_on": ["D6-DURABLE-CONFORMANCE-011"], + "expected_artifacts": ["spec/redaction-semantics.md", "codex_plans/architecture/security-and-isolation.md", "codex_plans/delivery/d9-redaction-implementation-brief.md"], + "expected_tests": ["canonical wire flag truth table", "sink-before-write transform and policy contract", "replay identity and authority invariants", "legacy misleading-history migration contract"], + "assigned_at": "2026-07-26T17:45:00Z", + "started_at": "2026-07-26T18:18:36Z", + "last_heartbeat": "2026-07-26T18:22:08Z", + "risk": "critical", + "blocker": null, + "next_action": "Freeze sink-before-write redaction and wire-flag truth semantics before extending the durable event schema" + }, + { + "id": "D10-BUDGET-SPEC-035", + "title": "Portable token, money, time, node, model, and pricing contract", + "owner": "main", + "status": "planned", + "work_package": "WP6", + "depends_on": ["D6-DURABLE-SPEC-010", "D7-CYCLE-SPEC-024"], + "expected_artifacts": ["spec/budget-semantics.md", "spec/conformance"], + "expected_tests": ["atomic reservations", "hard pre-schedule stop", "pricing snapshot identity", "model-tier routing"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "high", + "blocker": null, + "next_action": "Freeze deterministic units and reservation semantics" + }, + { + "id": "D10-TS-BUDGET-036", + "title": "TypeScript budget ledger, model router, and cost reporting", + "owner": "TypeScript lane", + "status": "planned", + "work_package": "WP6", + "depends_on": ["D10-BUDGET-SPEC-035", "D9-DURABLE-EXT-CONFORMANCE-034"], + "expected_artifacts": ["packages/runtime", "packages/cli"], + "expected_tests": ["concurrent reservation", "budget cancellation", "tier fallback", "cost JSON envelope"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "high", + "blocker": null, + "next_action": "Wait for budget contract" + }, + { + "id": "D10-PY-BUDGET-037", + "title": "Python budget ledger, model router, and cost reporting", + "owner": "Python lane", + "status": "planned", + "work_package": "WP6", + "depends_on": ["D10-BUDGET-SPEC-035", "D9-DURABLE-EXT-CONFORMANCE-034"], + "expected_artifacts": ["python/src/graph_engineering"], + "expected_tests": ["concurrent reservation", "budget cancellation", "tier fallback", "cost JSON envelope"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "high", + "blocker": null, + "next_action": "Wait for budget contract" + }, + { + "id": "D10-BUDGET-CONFORMANCE-038", + "title": "Cross-language hard-budget and model-routing conformance", + "owner": "main + independent reviewer", + "status": "planned", + "work_package": "WP6-WP11", + "depends_on": ["D10-TS-BUDGET-036", "D10-PY-BUDGET-037"], + "expected_artifacts": ["tools/conformance", "spec/conformance"], + "expected_tests": ["same reservation verdicts", "no budget escape", "same stable cost envelopes", "100 randomized contention runs"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "high", + "blocker": null, + "next_action": "Wait for both native budget lanes" + }, + { + "id": "D11-VERIFY-SPEC-040", + "title": "Verifier, judge, citation, reflection, quorum, and unknown-state contract", + "owner": "main", + "status": "planned", + "work_package": "WP5", + "depends_on": ["D6-ROUTER-BARRIER-023", "D9-APPROVAL-077"], + "expected_artifacts": ["spec/verification-semantics.md", "spec/conformance"], + "expected_tests": ["pass/reject/abstain", "insufficient quorum becomes unknown", "versioned rubrics", "citation evidence retention"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "high", + "blocker": null, + "next_action": "Freeze vote preservation, tie-break, and human escalation rules" + }, + { + "id": "D11-TS-VERIFY-041", + "title": "TypeScript verifier panels, judges, citation checks, and reflection", + "owner": "TypeScript lane", + "status": "planned", + "work_package": "WP5", + "depends_on": ["D11-VERIFY-SPEC-040", "D10-BUDGET-CONFORMANCE-038"], + "expected_artifacts": ["packages/runtime", "packages/patterns"], + "expected_tests": ["maker/verifier context isolation", "diverse lenses", "judge synthesis", "unknown and human gate"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "high", + "blocker": null, + "next_action": "Wait for verifier contract" + }, + { + "id": "D11-PY-VERIFY-042", + "title": "Python verifier panels, judges, citation checks, and reflection", + "owner": "Python lane", + "status": "planned", + "work_package": "WP5", + "depends_on": ["D11-VERIFY-SPEC-040", "D10-BUDGET-CONFORMANCE-038"], + "expected_artifacts": ["python/src/graph_engineering"], + "expected_tests": ["maker/verifier context isolation", "diverse lenses", "judge synthesis", "unknown and human gate"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "high", + "blocker": null, + "next_action": "Wait for verifier contract" + }, + { + "id": "D11-VERIFY-CONFORMANCE-043", + "title": "Cross-language verification, citation, and human-gate conformance", + "owner": "main + independent reviewer", + "status": "planned", + "work_package": "WP5-WP11", + "depends_on": ["D11-TS-VERIFY-041", "D11-PY-VERIFY-042"], + "expected_artifacts": ["tools/conformance", "spec/conformance", "examples/cited-research"], + "expected_tests": ["vote and verdict parity", "citation failure", "abstention and unknown", "stale human approval"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "high", + "blocker": null, + "next_action": "Wait for both native verifier lanes" + }, + { + "id": "D12-ISOLATION-SPEC-044", + "title": "Capability, approval, worktree, process, container, and merge-gate threat contract", + "owner": "main + security reviewer", + "status": "planned", + "work_package": "WP9", + "depends_on": ["D7-CYCLE-SPEC-024", "D9-REDACTION-039", "D9-APPROVAL-077"], + "expected_artifacts": ["spec/security-policy.schema.json", "codex_plans/architecture/security-and-isolation.md", "docs/THREAT_MODEL.md"], + "expected_tests": ["deny by default", "authority non-expansion", "untrusted model/tool content", "cleanup and lease safety"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "critical", + "blocker": null, + "next_action": "Threat-model host boundaries and capability grant lifecycle before implementation" + }, + { + "id": "D12-TS-ISOLATION-045", + "title": "TypeScript capability enforcement and worktree/process/container providers", + "owner": "TypeScript + security lanes", + "status": "planned", + "work_package": "WP9", + "depends_on": ["D12-ISOLATION-SPEC-044"], + "expected_artifacts": ["packages/isolation", "packages/runtime"], + "expected_tests": ["path/network/secret denial", "worktree lease and merge conflicts", "port/temp/cache/database namespace isolation"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "critical", + "blocker": null, + "next_action": "Wait for the isolation contract and adversarial fixture design" + }, + { + "id": "D12-PY-ISOLATION-046", + "title": "Python capability enforcement and worktree/process/container providers", + "owner": "Python + security lanes", + "status": "planned", + "work_package": "WP9", + "depends_on": ["D12-ISOLATION-SPEC-044"], + "expected_artifacts": ["python/src/graph_engineering"], + "expected_tests": ["path/network/secret denial", "worktree lease and merge conflicts", "port/temp/cache/database namespace isolation"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "critical", + "blocker": null, + "next_action": "Wait for the isolation contract and adversarial fixture design" + }, + { + "id": "D12-ISOLATION-REDTEAM-047", + "title": "Cross-language isolation, prompt-injection, escape, and merge red-team suite", + "owner": "independent security reviewer", + "status": "planned", + "work_package": "WP9-WP11", + "depends_on": ["D12-TS-ISOLATION-045", "D12-PY-ISOLATION-046", "D11-VERIFY-CONFORMANCE-043"], + "expected_artifacts": ["tests/security", "tools/conformance"], + "expected_tests": ["prompt injection", "path traversal and symlink race", "port/process/container escape", "worktree conflict and stale lease"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "critical", + "blocker": null, + "next_action": "Review implementations independently and retain every exploit regression" + }, + { + "id": "D13-ADAPTER-SPEC-048", + "title": "Provider and tool adapter capability/conformance contract", + "owner": "main", + "status": "planned", + "work_package": "WP6-WP7", + "depends_on": ["D12-ISOLATION-REDTEAM-047"], + "expected_artifacts": ["spec/adapter-semantics.md", "spec/conformance"], + "expected_tests": ["capability discovery", "structured output and tools", "streaming usage", "retry/rate/circuit/fallback/cancel"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "high", + "blocker": null, + "next_action": "Define a mock-first adapter contract without embedding vendor SDK types" + }, + { + "id": "D13-ADAPTERS-049", + "title": "Cross-language official adapter conformance join", + "owner": "main + independent conformance reviewer", + "status": "planned", + "work_package": "WP6-WP7", + "depends_on": ["D13-TS-ADAPTERS-081", "D13-PY-ADAPTERS-082"], + "expected_artifacts": ["tools/conformance", "spec/conformance", "codex_logs/release-evidence/adapters"], + "expected_tests": ["shared deterministic mock parity", "HTTP/shell/MCP envelope parity", "fallback cancellation and redaction parity", "live vendor results remain opt-in and separately identified"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "critical", + "blocker": null, + "next_action": "Join both native adapter lanes with an independent shared-fixture review" + }, + { + "id": "D13-DX-051", + "title": "Doctor, Graph Ready score, badge, pattern picker, and adapter diagnostics", + "owner": "platform lane", + "status": "planned", + "work_package": "WP7-WP10", + "depends_on": ["D13-ADAPTERS-049"], + "expected_artifacts": ["packages/cli", "docs"], + "expected_tests": ["deterministic G0-G4 score", "top-three remediation", "badge rendering", "provider/store/security doctor"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "medium", + "blocker": null, + "next_action": "Freeze scoring rubric against implemented capabilities" + }, + { + "id": "D14-API-FREEZE-050", + "title": "Public alpha API, Graph IR, diagnostics, and compatibility freeze", + "owner": "main + language maintainers", + "status": "planned", + "work_package": "WP1-WP12", + "depends_on": ["D13-ADAPTERS-049", "D3-PY-CLI-021", "D4-TRACE-SUBGRAPH-022"], + "expected_artifacts": ["docs/API.md", "docs/CLI.md", "CHANGELOG.md"], + "expected_tests": ["public export audit", "TS/Python API parity", "machine-envelope stability", "deprecation and migration policy"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "high", + "blocker": null, + "next_action": "Run only after provider and Python CLI surfaces exist" + }, + { + "id": "D14-MCP-PLUGINS-052", + "title": "Policy-gated runtime MCP extensions and plugin SDK/discovery", + "owner": "platform + runtime lanes", + "status": "planned", + "work_package": "WP7", + "depends_on": ["D14-API-FREEZE-050", "D12-ISOLATION-REDTEAM-047"], + "expected_artifacts": ["packages/mcp-server", "packages/plugin-sdk", "python/src/graph_engineering"], + "expected_tests": ["read-only default", "explicit mutation enablement", "human approval", "plugin list/doctor and capability denial"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "critical", + "blocker": null, + "next_action": "Preserve read-only default while designing opt-in mutation policy" + }, + { + "id": "D14-PATTERN-SKELETONS-053", + "title": "Cross-language complete-bundle skeletons for all ten patterns", + "owner": "patterns + docs lanes", + "status": "planned", + "work_package": "WP10", + "depends_on": ["D2-BUILDERS-YAML-020", "D11-VERIFY-CONFORMANCE-043", "D12-ISOLATION-REDTEAM-047", "D13-ADAPTERS-049"], + "expected_artifacts": ["patterns", "examples"], + "expected_tests": ["ten named directories", "YAML/JSON/TS/Python entry points", "common budget/permission/failure/resume fixtures"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "high", + "blocker": null, + "next_action": "Create skeletons only when runtime capabilities can execute them honestly" + }, + { + "id": "D15-STORAGE-WORKERS-054", + "title": "Production PostgreSQL/S3 storage and lease-coordinated worker mode", + "owner": "TS + Python persistence lanes", + "status": "planned", + "work_package": "WP3-WP8", + "depends_on": ["D9-DURABLE-EXT-CONFORMANCE-034", "D13-ADAPTERS-049", "D14-API-FREEZE-050"], + "expected_artifacts": ["packages/persistence", "python/src/graph_engineering", "deploy"], + "expected_tests": ["shared storage conformance", "worker loss and lease takeover", "network/store/artifact chaos", "migration and cleanup"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "critical", + "blocker": null, + "next_action": "Define integration-test infrastructure and consistency boundaries" + }, + { + "id": "D15-EXPLORER-060", + "title": "React Graph Explorer and OpenTelemetry observability stack", + "owner": "platform + observability lanes", + "status": "planned", + "work_package": "WP8-WP10", + "depends_on": ["D15-STORAGE-WORKERS-054", "D11-VERIFY-CONFORMANCE-043", "D14-API-FREEZE-050", "D9-REDACTION-CONFORMANCE-089"], + "expected_artifacts": ["apps/explorer", "packages/observability", "docs/OBSERVABILITY.md"], + "expected_tests": ["static/live topology", "checkpoint time travel", "critical path/utilization/barrier/verdict views", "console/JSONL/OTLP exporters"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "high", + "blocker": null, + "next_action": "Freeze trace and redaction envelopes before UI implementation" + }, + { + "id": "D15-PERFORMANCE-061", + "title": "Reproducible latency, throughput, recovery, and first-run benchmarks", + "owner": "performance + DX reviewers", + "status": "planned", + "work_package": "WP11-WP12", + "depends_on": ["D15-STORAGE-WORKERS-054", "D15-EXPLORER-060", "D14-API-FREEZE-050", "D14-PATTERN-SKELETONS-053"], + "expected_artifacts": ["benchmarks", "docs/BENCHMARKS.md"], + "expected_tests": ["baseline provenance", "10 percent regression gate", "pipeline versus barrier", "first run under five minutes"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "high", + "blocker": null, + "next_action": "Design reproducible hardware/runtime metadata and variance policy" + }, + { + "id": "D16-SECURITY-062", + "title": "Security preflight, redaction, fuzzing, SBOM, and zero-blocker audit", + "owner": "independent security + release lanes", + "status": "planned", + "work_package": "WP9-WP11-WP12", + "depends_on": ["D12-ISOLATION-REDTEAM-047", "D15-STORAGE-WORKERS-054", "D15-EXPLORER-060", "D14-MCP-PLUGINS-052", "D9-REDACTION-CONFORMANCE-089"], + "expected_artifacts": ["docs/THREAT_MODEL.md", "SECURITY.md", "sbom", "tests/security"], + "expected_tests": ["secret/static/dependency/license scans", "fuzz/property tests", "policy/redaction regressions", "no unaccepted high or critical finding"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "critical", + "blocker": null, + "next_action": "Run preflight only against complete runtime and adapter surfaces" + }, + { + "id": "D17-BETA-063", + "title": "Immutable Beta integration artifact and defect burn-down", + "owner": "main + release lanes", + "status": "planned", + "work_package": "WP10-WP11-WP12", + "depends_on": ["D16-SECURITY-062", "D15-PERFORMANCE-061", "D14-PATTERN-SKELETONS-053"], + "expected_artifacts": ["docs/API.md", "CHANGELOG.md", "codex_logs/release-evidence/beta"], + "expected_tests": ["immutable Beta source and package digests", "complete Beta API and quickstart", "zero repository-owned open P0/P1", "known exclusions and defect disposition"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "critical", + "blocker": null, + "next_action": "Build an immutable Beta artifact after security preflight; external usability is tracked independently" + }, + { + "id": "D18-COMPAT-BENCH-064", + "title": "Cross-platform compatibility, scale, randomized-failure, and regression audit", + "owner": "QA + performance + language lanes", + "status": "planned", + "work_package": "WP11", + "depends_on": ["D17-BETA-063"], + "expected_artifacts": [".github/workflows", "benchmarks", "codex_logs/release-evidence"], + "expected_tests": ["Linux macOS Windows", "Node 20 and 22", "Python 3.11 3.12 3.13", "100-way and 1000-node", "100 randomized fault runs"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "critical", + "blocker": null, + "next_action": "Add platform runners and versioned benchmark baselines after beta surface freeze" + }, + { + "id": "D19-RC-065", + "title": "Release-candidate freeze, clean install/upgrade, and migration matrix", + "owner": "main + release managers", + "status": "planned", + "work_package": "WP11-WP12", + "depends_on": ["D18-COMPAT-BENCH-064", "D14-API-FREEZE-050", "CTRL-PATTERNS-071", "D18-EDUCATION-ASSETS-083", "D18-SUPPORT-READINESS-080", "CTRL-DOCS-073"], + "expected_artifacts": ["CHANGELOG.md", "docs/UPGRADING.md", "codex_logs/release-evidence"], + "expected_tests": ["clean npm/Python install", "upgrade from alpha and beta", "all docs and snippets", "signed RC source artifacts"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "critical", + "blocker": null, + "next_action": "Freeze only after all compatibility and defect gates are evidenced" + }, + { + "id": "D20-PROVENANCE-066", + "title": "Trusted npm/PyPI provenance assembly and non-publishing rehearsal", + "owner": "main + release security", + "status": "planned", + "work_package": "WP12", + "depends_on": ["D19-RC-065", "D16-SECURITY-062"], + "expected_artifacts": [".github/workflows", "sbom", "codex_logs/release-evidence/provenance"], + "expected_tests": ["trusted publisher identity", "provenance verification", "checksum and SBOM match", "no high or critical blocker"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "critical", + "blocker": null, + "external_gate": "Requires authenticated registry and hosting authority for final rehearsal and publication", + "next_action": "Prepare non-publishing rehearsals and request only the minimum external credentials at the release gate" + }, + { + "id": "D21-RELEASE-067", + "title": "Stable-v1 or complete-RC release, site, launch, and support operation", + "owner": "main + all lanes", + "status": "planned", + "work_package": "WP12", + "depends_on": ["D20-PROVENANCE-066", "CTRL-RELEASE-ROLLUP-086"], + "expected_artifacts": ["codex_logs/release-evidence", "docs/SUPPORT.md", "site", "CHANGELOG.md"], + "expected_tests": ["candidate digest matches signed roll-up", "stable versus complete-RC decision enforced", "published artifact verification", "support and incident operation"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "critical", + "blocker": null, + "external_gate": "Stable release depends on all evidence gates and external publishing authority; otherwise the required output is a transparent complete RC", + "next_action": "Do not label stable until the release checklist has no open mandatory gate" + }, + { + "id": "PATTERN-01-RESEARCH", + "title": "Complete multi-source research diamond pattern bundle", + "owner": "patterns lane", + "status": "planned", + "work_package": "WP10", + "depends_on": ["D14-PATTERN-SKELETONS-053", "D7-PIPELINE-CONFORMANCE-013", "D6-ROUTER-BARRIER-023", "D9-DURABLE-EXT-CONFORMANCE-034", "D10-BUDGET-CONFORMANCE-038"], + "expected_artifacts": ["patterns/multi-source-research"], + "expected_tests": ["mock e2e", "failure/resume", "TS/Python/YAML/JSON", "budgets permissions and launch guides"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "medium", + "blocker": null, + "next_action": "Upgrade the existing constructor/showcase into the complete pattern-bundle gate" + }, + { + "id": "PATTERN-02-CITED", + "title": "Complete cited deep-research and citation-verification pattern bundle", + "owner": "patterns lane", + "status": "planned", + "work_package": "WP10", + "depends_on": ["D14-PATTERN-SKELETONS-053", "PATTERN-01-RESEARCH", "D11-VERIFY-CONFORMANCE-043", "D13-ADAPTERS-049"], + "expected_artifacts": ["patterns/cited-deep-research"], + "expected_tests": ["citation verifier", "abstention/unknown", "mock e2e and resume", "all launch guides"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "high", + "blocker": null, + "next_action": "Wait for verifier and provider surfaces" + }, + { + "id": "PATTERN-03-AUTH", + "title": "Complete route-authentication security-sweep pattern bundle", + "owner": "patterns + security lanes", + "status": "planned", + "work_package": "WP9-WP10", + "depends_on": ["D14-PATTERN-SKELETONS-053", "D6-ROUTER-BARRIER-023", "D12-ISOLATION-REDTEAM-047", "D16-SECURITY-062"], + "expected_artifacts": ["patterns/route-auth-security-sweep"], + "expected_tests": ["one worker per route", "adversarial verification", "injected auth gaps", "least privilege and resume"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "high", + "blocker": null, + "next_action": "Wait for isolation and verifier surfaces" + }, + { + "id": "PATTERN-04-DIFF", + "title": "Complete diff-risk router and diverse judge-panel pattern bundle", + "owner": "patterns lane", + "status": "planned", + "work_package": "WP10", + "depends_on": ["D14-PATTERN-SKELETONS-053", "D6-ROUTER-BARRIER-023", "D11-VERIFY-CONFORMANCE-043", "D12-ISOLATION-REDTEAM-047"], + "expected_artifacts": ["patterns/diff-risk-review"], + "expected_tests": ["small/heavy routing", "correctness security performance judges", "unknown gate", "failure/resume"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "medium", + "blocker": null, + "next_action": "Upgrade the route constructor after scheduler-integrated routing and judges" + }, + { + "id": "PATTERN-05-UNTIL-DRY", + "title": "Complete loop-until-dry bug-discovery pattern bundle", + "owner": "patterns lane", + "status": "planned", + "work_package": "WP5-WP10", + "depends_on": ["D14-PATTERN-SKELETONS-053", "D7-CYCLE-CONFORMANCE-027", "D10-BUDGET-CONFORMANCE-038", "D11-VERIFY-CONFORMANCE-043"], + "expected_artifacts": ["patterns/loop-until-dry"], + "expected_tests": ["dedupe against all seen", "two dry rounds", "diverse verification", "hard budget exit and resume"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "high", + "blocker": null, + "next_action": "Replace the static constructor with an executable convergence demonstration" + }, + { + "id": "PATTERN-06-MIGRATION", + "title": "Complete file-migration worktree and test-gate pattern bundle", + "owner": "patterns + isolation lanes", + "status": "planned", + "work_package": "WP9-WP10", + "depends_on": ["D14-PATTERN-SKELETONS-053", "D9-DURABLE-EXT-CONFORMANCE-034", "D12-ISOLATION-REDTEAM-047", "D16-SECURITY-062"], + "expected_artifacts": ["patterns/file-migration"], + "expected_tests": ["isolated writes", "test gate and loopback", "merge conflict", "cleanup and resume"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "critical", + "blocker": null, + "next_action": "Wait for worktree and merge-node enforcement" + }, + { + "id": "PATTERN-07-CI", + "title": "Complete CI failure-sweeper pattern bundle", + "owner": "patterns lane", + "status": "planned", + "work_package": "WP10", + "depends_on": ["D14-PATTERN-SKELETONS-053", "D8-RUNTIME-CHAOS-084", "D9-DURABLE-EXT-CONFORMANCE-034", "D9-APPROVAL-077", "D12-ISOLATION-REDTEAM-047", "D13-ADAPTERS-049"], + "expected_artifacts": ["patterns/ci-failure-sweeper"], + "expected_tests": ["independent failure routing", "bounded retries", "provider-free fixture", "permissions and resume"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "medium", + "blocker": null, + "next_action": "Implement on the frozen pattern bundle surface" + }, + { + "id": "PATTERN-08-DEPS", + "title": "Complete dependency-update sweeper pattern bundle", + "owner": "patterns + security lanes", + "status": "planned", + "work_package": "WP9-WP10", + "depends_on": ["D14-PATTERN-SKELETONS-053", "D6-ROUTER-BARRIER-023", "D9-DURABLE-EXT-CONFORMANCE-034", "D12-ISOLATION-REDTEAM-047", "D13-ADAPTERS-049", "D16-SECURITY-062"], + "expected_artifacts": ["patterns/dependency-update-sweeper"], + "expected_tests": ["per-package fanout", "license/security gates", "isolated updates", "failure/resume"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "high", + "blocker": null, + "next_action": "Implement after isolation and policy enforcement" + }, + { + "id": "PATTERN-09-PR", + "title": "Complete PR babysitter pattern bundle", + "owner": "patterns lane", + "status": "planned", + "work_package": "WP10", + "depends_on": ["D14-PATTERN-SKELETONS-053", "D6-ROUTER-BARRIER-023", "D9-DURABLE-EXT-CONFORMANCE-034", "D9-APPROVAL-077", "D11-VERIFY-CONFORMANCE-043", "D13-ADAPTERS-049", "D14-MCP-PLUGINS-052"], + "expected_artifacts": ["patterns/pr-babysitter"], + "expected_tests": ["event polling without duplicate action", "human gate", "bounded wait", "resume and permissions"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "high", + "blocker": null, + "next_action": "Implement only with durable waits and explicit write authority" + }, + { + "id": "PATTERN-10-ECOSYSTEM", + "title": "Complete scheduled ecosystem-scan pattern bundle", + "owner": "patterns lane", + "status": "planned", + "work_package": "WP10", + "depends_on": ["D14-PATTERN-SKELETONS-053", "D7-CYCLE-CONFORMANCE-027", "D9-DURABLE-EXT-CONFORMANCE-034", "D10-BUDGET-CONFORMANCE-038", "D13-ADAPTERS-049", "D15-STORAGE-WORKERS-054"], + "expected_artifacts": ["patterns/ecosystem-scan"], + "expected_tests": ["parallel sources", "cross-set dedupe/rank", "schedule-safe replay", "provider and cost budgets"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "medium", + "blocker": null, + "next_action": "Implement after adapters, budgets, and durable operations" + }, + { + "id": "CTRL-PATTERNS-071", + "title": "Verify all ten patterns satisfy the complete bundle gate", + "owner": "independent DX + runtime reviewers", + "status": "planned", + "work_package": "WP10-WP11", + "depends_on": ["PATTERN-01-RESEARCH", "PATTERN-02-CITED", "PATTERN-03-AUTH", "PATTERN-04-DIFF", "PATTERN-05-UNTIL-DRY", "PATTERN-06-MIGRATION", "PATTERN-07-CI", "PATTERN-08-DEPS", "PATTERN-09-PR", "PATTERN-10-ECOSYSTEM"], + "expected_artifacts": ["patterns", "codex_logs/release-evidence/patterns.md"], + "expected_tests": ["ten mock e2e runs", "ten failure/resume runs", "all language and launcher variants", "all diagrams budgets and permissions"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "critical", + "blocker": null, + "next_action": "Reject directory-count evidence; require every complete-bundle field" + }, + { + "id": "CTRL-ACCEPTANCE-070", + "title": "Prove every mandatory test and quantitative release threshold", + "owner": "main + independent QA/release reviewers", + "status": "planned", + "work_package": "WP11-WP12", + "depends_on": ["D18-COMPAT-BENCH-064", "D16-SECURITY-062", "D19-RC-065", "D17-USABILITY-076"], + "expected_artifacts": ["codex_plans/delivery/release-checklist.md", "codex_logs/release-evidence"], + "expected_tests": ["90/85 coverage", "250 tests per language", "all mandatory scenarios", "platform matrix", "external usability gates"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "critical", + "blocker": null, + "external_gate": "External usability rows require real third-party evidence", + "next_action": "Keep all 178 release-checklist evidence slots Open until exact artifacts exist" + }, + { + "id": "CTRL-GROWTH-072", + "title": "Organic launch assets, metrics, experiments, community, and support plan", + "owner": "growth + community lane", + "status": "planned", + "work_package": "WP10-WP12", + "depends_on": ["D13-DX-051", "D15-EXPLORER-060"], + "expected_artifacts": ["codex_plans/growth/launch-plan.md", "codex_plans/growth/content-calendar.md", "codex_plans/growth/metrics-and-experiments.md", "site"], + "expected_tests": ["reproducible demo assets", "bilingual launch copy", "channel-specific drafts", "privacy-safe activation and retention metrics"], + "assigned_at": "2026-07-26T16:45:30Z", + "started_at": null, + "last_heartbeat": "2026-07-26T16:45:30Z", + "risk": "high", + "blocker": null, + "external_gate": "6,000+ organic stars is an observed stretch outcome and cannot be guaranteed or manufactured", + "next_action": "Prepare honest assets and controlled activation goals; never purchase or automate stars" + }, + { + "id": "CTRL-DOCS-073", + "title": "Complete every planned research, architecture, delivery, growth, course, and operations document", + "owner": "docs lane + domain reviewers", + "status": "in_progress", + "work_package": "WP10", + "depends_on": ["D14-API-FREEZE-050", "D15-EXPLORER-060", "D15-PERFORMANCE-061", "D16-SECURITY-062", "CTRL-PATTERNS-071", "D18-EDUCATION-ASSETS-083"], + "expected_artifacts": ["codex_plans/research", "codex_plans/architecture", "codex_plans/delivery", "codex_plans/growth", "docs"], + "expected_tests": ["planned-document inventory", "link and snippet checks", "implementation-status audit", "English and Chinese launch essentials"], + "assigned_at": "2026-07-26T16:20:00Z", + "started_at": "2026-07-26T16:20:00Z", + "last_heartbeat": "2026-07-26T18:22:08Z", + "risk": "high", + "blocker": null, + "next_action": "Fill the exact open-document list in the coverage matrix while implementations advance" + }, + { + "id": "CTRL-RELEASE-MAP-074", + "title": "Machine-map every release-checklist leaf to live tasks", + "owner": "integration lane (primary); independent QA review", + "status": "planned", + "work_package": "WP11-WP12", + "depends_on": ["CTRL-PLAN-COVERAGE-001"], + "expected_artifacts": ["codex_plans/delivery/release-task-map.json", "scripts/check-release-task-map.mjs", "scripts/tests/release-task-map.test.mjs"], + "expected_tests": ["all 178 release IDs map exactly once", "blocking classification is explicit", "every mapped task exists", "duplicate dangling and cyclic mappings fail closed"], + "evidence_required": true, + "assigned_at": "2026-07-26T18:09:04Z", + "started_at": null, + "last_heartbeat": "2026-07-26T18:09:04Z", + "risk": "critical", + "blocker": null, + "next_action": "Create the 178-of-178 machine-readable mapping and its negative fixtures" + }, + { + "id": "CTRL-EVIDENCE-BACKFILL-075", + "title": "Candidate revalidation overlay for historical completed tasks", + "owner": "integration lane (primary); independent release review", + "status": "planned", + "work_package": "WP11-WP12", + "depends_on": ["CTRL-EVIDENCE-002"], + "expected_artifacts": ["codex_logs/release-evidence/task-revalidation.json", "scripts/check-evidence-closure.mjs"], + "expected_tests": ["every release ancestor has candidate revision and digest", "commands environments and results are immutable", "reviewer and exclusions are present", "historical status without overlay contributes zero release weight"], + "evidence_required": true, + "assigned_at": "2026-07-26T18:09:04Z", + "started_at": null, + "last_heartbeat": "2026-07-26T18:09:04Z", + "risk": "critical", + "blocker": null, + "next_action": "Define and validate the append-only candidate revalidation manifest" + }, + { + "id": "D17-USABILITY-076", + "title": "External Beta usability and consent evidence", + "owner": "product-quality lane (primary); external participants provide evidence", + "status": "planned", + "work_package": "WP10-WP11-WP12", + "depends_on": ["D17-BETA-063", "D16-PRIVACY-079"], + "expected_artifacts": ["codex_logs/usability/method.json", "codex_logs/usability/consent-manifest.json", "codex_logs/usability/disposition.json"], + "expected_tests": ["at least five authentic external reports", "at least 80 percent finish in 300 seconds", "failed attempts are retained and retested", "zero open P0 or P1 finding"], + "evidence_required": true, + "assigned_at": "2026-07-26T18:09:04Z", + "started_at": null, + "last_heartbeat": "2026-07-26T18:09:04Z", + "risk": "critical", + "blocker": null, + "external_gate": "Requires real consenting third-party testers and elapsed-time evidence; agents may prepare but never fabricate reports", + "next_action": "Prepare the consent-safe study method after the immutable Beta and privacy contract exist" + }, + { + "id": "D9-APPROVAL-077", + "title": "Canonical approval idempotency and authority-binding contract", + "owner": "integration protocol lane", + "status": "planned", + "work_package": "WP3-WP9-WP11", + "depends_on": ["D6-DURABLE-SPEC-010", "D9-REDACTION-039"], + "expected_artifacts": ["spec/approval-semantics.md", "spec/conformance/approval.case.json"], + "expected_tests": ["approve reject revoke and expire", "graph run revision and action binding", "stale approval rejection", "stable idempotency key and redacted audit"], + "evidence_required": true, + "assigned_at": "2026-07-26T18:09:04Z", + "started_at": null, + "last_heartbeat": "2026-07-26T18:09:04Z", + "risk": "critical", + "blocker": null, + "next_action": "Freeze approval authority and stale-decision semantics before durable implementation" + }, + { + "id": "D14-NPM-DIST-078", + "title": "Canonical unscoped npm distribution and graph binaries", + "owner": "TypeScript distribution lane (primary); package QA review", + "status": "planned", + "work_package": "WP10-WP11", + "depends_on": ["D14-API-FREEZE-050", "D3-CLI-002"], + "expected_artifacts": ["packages/graph-engineering/package.json", "codex_logs/decisions/ADR-canonical-npm-distribution.md", "scripts/check-canonical-distribution.mjs"], + "expected_tests": ["clean tarball install on Node 20 and 22", "no workspace references", "graph and grapheng binaries work", "package contains real supported functionality"], + "evidence_required": true, + "assigned_at": "2026-07-26T18:09:04Z", + "started_at": null, + "last_heartbeat": "2026-07-26T18:09:04Z", + "risk": "critical", + "blocker": null, + "external_gate": "Live rehearsal requires current npm namespace authority; no empty name-squatting package may be published", + "next_action": "Record the package-boundary ADR and implement a non-publishing clean-install rehearsal" + }, + { + "id": "D16-PRIVACY-079", + "title": "Telemetry usability and gallery consent retention and redaction policy", + "owner": "product-quality lane (primary); independent security review", + "status": "planned", + "work_package": "WP10-WP11-WP12", + "depends_on": ["D9-REDACTION-039", "D12-ISOLATION-REDTEAM-047"], + "expected_artifacts": ["docs/PRIVACY.md", "codex_logs/schemas/consent.schema.json", "codex_logs/schemas/retention.schema.json"], + "expected_tests": ["telemetry and capture default off", "PII canary does not escape", "minimal retention is enforced", "withdrawal deletion and gallery consent are auditable"], + "evidence_required": true, + "assigned_at": "2026-07-26T18:09:04Z", + "started_at": null, + "last_heartbeat": "2026-07-26T18:09:04Z", + "risk": "high", + "blocker": null, + "external_gate": "A human data owner must approve retention and consent policy before real participant data is collected", + "next_action": "Define default-off collection and withdrawal semantics before usability recruitment" + }, + { + "id": "D18-SUPPORT-READINESS-080", + "title": "Pre-publish support incident rollback and registry readiness", + "owner": "integration operations lane (primary); QA and security review", + "status": "planned", + "work_package": "WP10-WP11-WP12", + "depends_on": ["D16-SECURITY-062", "D18-COMPAT-BENCH-064"], + "expected_artifacts": ["docs/SUPPORT.md", "docs/INCIDENT_RESPONSE.md", "codex_logs/release-evidence/support-readiness.json"], + "expected_tests": ["SUP01 through SUP08 are evidenced", "rollback deprecate yank and forward-fix rehearsal", "redacted support bundle", "incident tabletop and acknowledged roster"], + "evidence_required": true, + "assigned_at": "2026-07-26T18:09:04Z", + "started_at": null, + "last_heartbeat": "2026-07-26T18:09:04Z", + "risk": "critical", + "blocker": null, + "external_gate": "Requires acknowledged human support roster and registry authority for the final rehearsal", + "next_action": "Prepare support runbooks and a non-destructive tabletop before RC freeze" + }, + { + "id": "D13-TS-ADAPTERS-081", + "title": "TypeScript model HTTP shell and MCP adapters", + "owner": "TypeScript adapter lane", + "status": "planned", + "work_package": "WP7-WP11", + "depends_on": ["D13-ADAPTER-SPEC-048"], + "expected_artifacts": ["packages/adapters/package.json", "packages/adapters/src", "packages/adapters/test"], + "expected_tests": ["deterministic mock normal CI", "provider and HTTP envelopes", "shell and MCP capability denial", "rate circuit fallback cancellation and redaction"], + "evidence_required": true, + "assigned_at": "2026-07-26T18:09:04Z", + "started_at": null, + "last_heartbeat": "2026-07-26T18:09:04Z", + "risk": "high", + "blocker": null, + "external_gate": "Vendor credentials are opt-in only and never required by normal CI", + "next_action": "Implement the deterministic mock adapter before any vendor SDK integration" + }, + { + "id": "D13-PY-ADAPTERS-082", + "title": "Python model HTTP shell and MCP adapters", + "owner": "Python adapter lane", + "status": "planned", + "work_package": "WP7-WP11", + "depends_on": ["D13-ADAPTER-SPEC-048"], + "expected_artifacts": ["python/src/graph_engineering/adapters", "python/tests/adapters"], + "expected_tests": ["deterministic mock normal CI", "provider and HTTP envelopes", "shell and MCP capability denial", "rate circuit fallback cancellation and redaction"], + "evidence_required": true, + "assigned_at": "2026-07-26T18:09:04Z", + "started_at": null, + "last_heartbeat": "2026-07-26T18:09:04Z", + "risk": "high", + "blocker": null, + "external_gate": "Vendor credentials are opt-in only and never required by normal CI", + "next_action": "Implement the deterministic mock adapter before any vendor SDK integration" + }, + { + "id": "D18-EDUCATION-ASSETS-083", + "title": "Executable course case studies demo and bilingual education assets", + "owner": "product-quality and docs lane", + "status": "planned", + "work_package": "WP10-WP11-WP12", + "depends_on": ["CTRL-PATTERNS-071", "D15-EXPLORER-060", "D17-BETA-063", "D14-API-FREEZE-050"], + "expected_artifacts": ["docs/course/manifest.json", "docs/case-studies/manifest.json", "docs/demo/manifest.json", "codex_plans/growth/content-calendar.md"], + "expected_tests": ["14 executable course steps", "four authentic cases including failure", "90-second uncut demo", "claim version bilingual and link checks"], + "evidence_required": true, + "assigned_at": "2026-07-26T18:09:04Z", + "started_at": null, + "last_heartbeat": "2026-07-26T18:09:04Z", + "risk": "high", + "blocker": null, + "external_gate": "External case studies require explicit publication consent and withdrawal handling", + "next_action": "Create the executable asset manifest after patterns and public APIs are frozen" + }, + { + "id": "D8-RUNTIME-CHAOS-084", + "title": "Cross-language runtime retry cancellation and non-cooperative chaos campaign", + "owner": "product-quality lane (primary); native runtime reviewers", + "status": "planned", + "work_package": "WP4-WP11", + "depends_on": ["D7-CYCLE-CONFORMANCE-027", "D7-PIPELINE-CONFORMANCE-013"], + "expected_artifacts": ["tests/chaos/runtime", "codex_logs/release-evidence/runtime-chaos.json"], + "expected_tests": ["bounded deterministic seeds", "no deadlock task leak or unbounded retry", "exact attempt and cancellation accounting", "non-cooperative work is detached safely"], + "evidence_required": true, + "assigned_at": "2026-07-26T18:09:04Z", + "started_at": null, + "last_heartbeat": "2026-07-26T18:09:04Z", + "risk": "high", + "blocker": null, + "next_action": "Define the reproducible fault seed corpus and resource-leak oracle" + }, + { + "id": "D9-OPS-CONTROL-085", + "title": "Durable operational CLI and Python command surface", + "owner": "product-quality and platform lane", + "status": "planned", + "work_package": "WP7-WP11", + "depends_on": ["D9-DURABLE-EXT-CONFORMANCE-034", "D8-RUNTIME-CHAOS-084"], + "expected_artifacts": ["packages/cli/src", "python/src/graph_engineering/cli", "docs/CLI.md"], + "expected_tests": ["status watch inspect and logs", "pause resume cancel and retry", "stable JSON envelopes and exits", "race and stale-operation rejection"], + "evidence_required": true, + "assigned_at": "2026-07-26T18:09:04Z", + "started_at": null, + "last_heartbeat": "2026-07-26T18:09:04Z", + "risk": "high", + "blocker": null, + "next_action": "Freeze the operational state machine after extended durability conformance" + }, + { + "id": "CTRL-RELEASE-ROLLUP-086", + "title": "Final candidate-bound stable-versus-complete-RC decision", + "owner": "integration release lane (primary); independent R3 review", + "status": "planned", + "work_package": "WP11-WP12", + "depends_on": ["D20-PROVENANCE-066", "CTRL-ACCEPTANCE-070", "CTRL-PATTERNS-071", "CTRL-DOCS-073", "CTRL-GROWTH-072", "D17-USABILITY-076", "D18-SUPPORT-READINESS-080", "CTRL-EVIDENCE-BACKFILL-075", "CTRL-RELEASE-MAP-074"], + "expected_artifacts": ["codex_logs/release-evidence/final-rollup.json", "codex_logs/release-evidence/blockers.json", "codex_logs/release-evidence/signed-decision.md"], + "expected_tests": ["every blocking leaf is Green or decision is complete RC no-release", "reopened evidence invalidates downstream decision", "candidate and artifact digests match", "independent reviewer signs scope and exclusions"], + "evidence_required": true, + "assigned_at": "2026-07-26T18:09:04Z", + "started_at": null, + "last_heartbeat": "2026-07-26T18:09:04Z", + "risk": "critical", + "blocker": null, + "external_gate": "Stable publication requires distinct reviewer and publishing authority after the roll-up passes", + "next_action": "Implement the fail-closed roll-up only after release mapping and evidence overlay exist" + }, + { + "id": "D9-TS-REDACTION-087", + "title": "TypeScript sink-before-write durable redaction", + "owner": "TypeScript persistence lane", + "status": "planned", + "work_package": "WP3-WP9-WP11", + "depends_on": ["D9-REDACTION-039"], + "expected_artifacts": ["packages/persistence/src/redaction.ts", "packages/runtime/test/redaction.test.ts"], + "expected_tests": ["wire flag matches protected representation", "every TypeScript sink is canary-free", "replay identity is preserved", "legacy false-flag history follows migration policy"], + "evidence_required": true, + "assigned_at": "2026-07-26T18:09:04Z", + "started_at": null, + "last_heartbeat": "2026-07-26T18:09:04Z", + "risk": "critical", + "blocker": null, + "next_action": "Wait for the canonical redaction contract and then implement sink-before-write protection" + }, + { + "id": "D9-PY-REDACTION-088", + "title": "Python sink-before-write durable redaction", + "owner": "Python persistence lane", + "status": "planned", + "work_package": "WP3-WP9-WP11", + "depends_on": ["D9-REDACTION-039"], + "expected_artifacts": ["python/src/graph_engineering/redaction.py", "python/tests/test_redaction.py"], + "expected_tests": ["wire flag matches protected representation", "every Python sink is canary-free", "replay identity is preserved", "legacy false-flag history follows migration policy"], + "evidence_required": true, + "assigned_at": "2026-07-26T18:09:04Z", + "started_at": null, + "last_heartbeat": "2026-07-26T18:09:04Z", + "risk": "critical", + "blocker": null, + "next_action": "Wait for the canonical redaction contract and then implement sink-before-write protection" + }, + { + "id": "D9-REDACTION-CONFORMANCE-089", + "title": "Cross-language redaction and sink-byte security join", + "owner": "integration lane (primary); independent QA and security review", + "status": "planned", + "work_package": "WP3-WP9-WP11", + "depends_on": ["D9-TS-REDACTION-087", "D9-PY-REDACTION-088"], + "expected_artifacts": ["spec/conformance/redaction.case.json", "scripts/check-secret-canaries.mjs", "codex_logs/release-evidence/redaction/manifest.json"], + "expected_tests": ["identical flag migration identity and errors", "journal checkpoint artifact stdout stderr log trace error and support bytes contain no canary", "negative fixtures detect every seeded leak", "independent security disposition is signed"], + "evidence_required": true, + "assigned_at": "2026-07-26T18:09:04Z", + "started_at": null, + "last_heartbeat": "2026-07-26T18:09:04Z", + "risk": "critical", + "blocker": null, + "next_action": "Join both native redaction lanes with shared fixtures and sink-byte canary scans" } ] } diff --git a/codex_plans/Graph-Engineering-21-Day-Master-Plan.md b/codex_plans/Graph-Engineering-21-Day-Master-Plan.md index 3da4466..29d22df 100644 --- a/codex_plans/Graph-Engineering-21-Day-Master-Plan.md +++ b/codex_plans/Graph-Engineering-21-Day-Master-Plan.md @@ -1,6 +1,6 @@ # Graph Engineering: 21-Day Dual-Language Open-Source Platform Plan -Status: **Approved and executing — source alpha 1 released** +Status: **Approved and executing — source alpha 1 released; durable recovery and bounded pipeline delivered; D2 authoring and D9 redaction contracts in progress** Started: **2026-07-26** Repository: (public) Current release: @@ -33,6 +33,54 @@ The Day-21 6,000-star number is a breakout growth OKR, not an engineering guaran Day 21 must produce all planned assets and at least a complete beta/release candidate. Stable v1 ships only if recovery, security, cross-language conformance, package provenance, and external usability gates pass. Quality takes precedence over falsely labeling an incomplete build as production-ready. +### Live execution checkpoint — 2026-07-26 + +This checkpoint is append-only evidence of plan execution; it does not remove +or weaken any later-day acceptance gate. + +| Plan area | State | Evidence / next gate | +|---|---|---| +| Day 1-4 foundations | Partial; current alpha slice green | Public source alpha, canonical IR/compiler, deterministic ready-queue schedulers, native TS/Python parity, a TypeScript CLI/read-only MCP, security checks, and package rehearsal are green. General builders/YAML, Python CLI, the trace viewer, and several planned control documents remain open. | +| Day 5 barriers and routing primitives | Partial | Settled all/minimum/percentage barriers and deterministic single/multicast routing have shared pure-evaluator parity; scheduler-integrated waiting, deadlines/quorum, conditional edge execution, and durable route replay remain open. | +| Day 5 pipeline/backpressure | Delivered for the standalone native scope | Commit `3df201d` provides lazy bounded TypeScript/Python pipelines, backpressure, ordered/completion delivery, stop/drop/dead-letter policies, cancellation/cleanup, a hard 2,048-stage construction bound, 8 shared cases, 115 TS runtime tests and 59 Python pipeline tests. Graph IR stream-edge lowering and durable item recovery remain separate open scope. | +| Day 6 failure envelopes | Partially complete | Scheduler failures, retry/timeout/cancellation, upstream isolation, invalid input/output, and attempt budgets are structured; pipeline terminal/run failures are the active parity slice. | +| Day 9 durable execution | Delivered for immutable local DAG scope | Commit-before-release event-sourced start/resume, exact tagged binary64 JSON, crash-window handling, terminal idempotence, and bidirectional terminal-history interop are in draft PR #14. Leases, checkpoint acceleration, replay/fork, approvals, and distributed stores remain explicit follow-ups. | +| Release/growth | Active | Public repository and `v0.1.0-alpha.1` exist; protected main and CI/CodeQL are green. The controlled objective remains trustworthy activation/adoption; 6,000+ organic stars is a breakout OKR, not a manufactured or guaranteed result. | + +Current critical path after the pipeline milestone is: bounded convergent cycles +and their hard budgets; cost/model routing; verifier/judge/reflection semantics; +then worktree/process isolation and provider adapters. Completed work is not +counted as evidence for excluded functionality merely because it landed ahead +of its calendar day. + +Two independent full-plan audits on 2026-07-26 found that the registry covered +only the implemented alpha slice through the active pipeline work. Days 8-21, +ten complete patterns, and most hard release thresholds were not yet represented +as executable tasks. Therefore “healthy” scanner output is a liveness/artifact +signal only and must not be read as master-plan completion. The canonical +coverage matrix and dependency backlog under `codex_plans/delivery/` must stay +synchronized until every row is evidence-backed; missing external adoption or +publishing authority remains an explicit gate rather than an inferred success. + +Superseding execution update: the registry now contains 107 concrete controls +covering the entire calendar, every runtime lane, all ten pattern bundles, +mandatory acceptance evidence, documentation, provenance, privacy, support, +release-leaf mapping, historical candidate revalidation, and organic growth. +The delivery directory now contains a day-by-day coverage matrix, a dependency +graph, an ownership/write-lease map, and a 178-item release checklist. Planned, +waiting, and external-gate rows remain visibly non-complete; this expansion fixes +the scanner's former scope blind spot but does not itself satisfy any product or +release gate. + +The 107-task checkpoint incorporates the full-plan gap audit's 16 missing +controls (`074`-`089`): independent TS/Python adapter and redaction lanes, +approval authority, runtime chaos versus durable operations, privacy/usability, +education/support readiness, canonical npm distribution, release-leaf mapping, +candidate evidence backfill, and a final fail-closed release roll-up. The task +graph is unique, has no dangling dependency and is acyclic. The scanner reports +completed pipeline evidence as 6 of 77 required gates satisfied; every other +planned or external outcome remains open. + ## 2. Planning, logs, and agent operations This file is the canonical plan. Supporting execution documents live below `codex_plans/`, while evidence and progress live below `codex_logs/`. diff --git a/codex_plans/architecture/graph-ir-and-schema.md b/codex_plans/architecture/graph-ir-and-schema.md new file mode 100644 index 0000000..28ce344 --- /dev/null +++ b/codex_plans/architecture/graph-ir-and-schema.md @@ -0,0 +1,667 @@ +# Graph IR and schema architecture + +Status: implementation-aligned Day 2 freeze ledger, 2026-07-26 + +The canonical persistent contract is +[`spec/graph.schema.json`](../../spec/graph.schema.json). This document explains +that contract, the compiler behavior implemented around it, and the work still +required before the complete Day 2 promise can be called frozen. It does not +add fields, reinterpret existing fields, or grant runtime behavior merely +because a word appears in the schema. + +Normative serialization rules and stable compiler codes live in the +[protocol README](../../spec/README.md). Entrypoint/output rationale and the +controlled namespace are recorded in +[ADR-0001](./ADR-0001-explicit-entrypoints.md) and +[ADR-0002](./ADR-0002-protocol-namespace.md). Runtime execution is a separate +contract described by [runtime semantics](./runtime-semantics.md). + +The [21-day master plan](../Graph-Engineering-21-Day-Master-Plan.md) is the +delivery source. Its current evidence ledger correctly marks Day 2 and the full +Graph IR/compiler capability as **Partial** in the +[coverage matrix](../delivery/master-plan-coverage-matrix.md). + +## 1. What the IR is + +Graph IR v1alpha1 is a language-neutral JSON document that describes graph +identity, external contracts, explicit roots and outputs, node declarations, +data-dependency edges, and optional graph policies. TypeScript and Python have +native projections of the same document; neither language model is normative. + +```mermaid +flowchart LR + A[JSON Graph document] --> P[Portable JSON capture] + P --> S[Envelope/schema validation] + S --> T[Topology and policy validation] + T --> C[Canonical JSON snapshot] + C --> H[Lowercase SHA-256 graph hash] + T --> L[Deterministic topological layers] +``` + +Today, that pipeline supports an immutable static DAG subset. The document can +*name* node kinds, edge modes, state schema, policy objects, and condition/map +annotations that have no complete compiler or scheduler implementation. The +schema is therefore a serialization vocabulary, not a capability-negotiation +result. + +### Current capability labels + +| Label | Meaning in this document | +| --- | --- | +| **Validated** | Both native compilers accept/reject the stated document shape and shared fixtures cover the portable observation. | +| **Has local runtime meaning** | The current DAG runtime consumes the field for the narrowly described behavior. | +| **Opaque/declarative** | The field is retained and hashed, but its inner semantics are not validated or executed by core. | +| **Vocabulary only** | A literal is allowed so the IR can evolve, but no generic execution contract exists for it. | +| **Not implemented** | No supported authoring, compilation, migration, or runtime surface currently provides the promised feature. | + +## 2. Protocol identity, namespace, and versions + +Several strings that look like “version” have distinct jobs and must never be +collapsed: + +| Field | Current value/example | Meaning | +| --- | --- | --- | +| JSON Schema dialect | `https://json-schema.org/draft/2020-12/schema` | How tooling interprets the schema document itself. | +| Schema `$id` | `https://reacher-z.github.io/GraphEngineering/schemas/v1alpha1/graph.schema.json` | Stable identity/location for this schema resource. | +| Graph `apiVersion` | `graphengineering.reacher-z.github.io/v1alpha1` | Protocol discriminator used by readers and compilers. | +| Graph `kind` | `Graph` | Resource type within the protocol family. | +| `metadata.version` | Caller-owned non-empty string such as `1.0.0` | Application graph version; currently not required to be SemVer and not used for protocol dispatch. | +| `graphHash` | SHA-256 of the complete canonical document | Exact content identity of one Graph IR snapshot. | +| Event `graphRevision` | Positive safe integer; current durable DAG uses revision `1` | Revision identity in run history, not a field in GraphSpec and not evidence that GraphPatch exists. | + +The namespace uses `reacher-z.github.io` because it is controlled by the +project. It must not be shortened to an unowned vanity domain. Released +`apiVersion` strings are immutable identifiers even if a future custom domain +becomes available; redirects may improve discovery but must not rewrite stored +documents. + +The canonical schema is under `spec/`. The CLI and MCP currently carry +byte-identical bundled copies at +[`packages/cli/assets/spec/graph.schema.json`](../../packages/cli/assets/spec/graph.schema.json) +and +[`packages/mcp-server/schemas/v1alpha1/graph.schema.json`](../../packages/mcp-server/schemas/v1alpha1/graph.schema.json). +Those are distribution artifacts, not independent sources. A Day 2 freeze must +make drift detection an explicit test rather than relying on manual copying. + +## 3. Canonical JSON and graph identity + +The TypeScript implementation lives in +[`canonical.ts`](../../packages/core/src/canonical.ts); Python exposes the same +surface through +[`canonical.py`](../../python/src/graph_engineering/canonical.py). Compilation +binds canonical text and its hash to one detached snapshot so later caller +mutation cannot change execution while leaving the reported hash unchanged. + +### Frozen v1alpha1 algorithm + +For the currently supported numeric corpus: + +1. capture one portable JSON snapshot without invoking caller-owned accessors; +2. preserve array order exactly; +3. recursively sort object property names by Unicode code point; +4. serialize compact JSON with no insignificant whitespace; +5. encode the result as UTF-8; and +6. compute SHA-256 and render lowercase hexadecimal. + +The shared [diamond fixture](../../spec/conformance/diamond.graph.json) hashes +to the value recorded in +[`expected.json`](../../spec/conformance/expected.json). Both native compilers +also agree on its declaration-sensitive topological layers. + +### What changes the hash + +The hash covers the complete accepted document, not only executable topology. +The following changes therefore produce a different graph identity: + +- metadata, labels, descriptions, or caller graph version; +- node or edge array order, even if reachability is unchanged; +- any JSON Schema, config, policy, resource, cache, or isolation value; +- endpoint ports, edge annotations, and edge modes; +- adding or removing an optional field; +- legal JSON `null` versus field absence (the schema separately decides where + `null` is valid); and +- any spelling, Unicode scalar, number representation after normalization, or + value change that alters canonical bytes. + +Object insertion order does not affect the hash because keys are sorted. +Repeated host-language aliases are snapshotted by value. Negative zero is +serialized as JSON zero. These are canonicalization properties, not semantic +equivalence rules: the project has no graph-normalization pass that sorts node +or edge arrays for arbitrary callers. + +### Numeric limitation + +Graph canonicalization accepts finite JSON numbers and rejects integer values +outside JavaScript's interoperable safe-integer range. However, the v1alpha1 +cross-language graph-hash corpus intentionally contains no fractional numbers. +Full RFC 8785 number serialization has not been adopted. Consequently: + +- the diamond hash is a valid cross-language compatibility commitment; +- safe-integer graph documents stay inside the proven profile; +- fractional values may be accepted locally but are not yet a fully frozen + byte-equivalent cross-language hash contract; and +- Day 2 cannot claim a complete numeric freeze until an exact number profile + and adversarial cross-language vectors are accepted. + +Do not “fix” this by rounding, stringifying numbers implicitly, or tolerating +non-finite values. A future contract must either adopt a precise canonical +number algorithm or require explicit decimal/scaled-integer encodings in the +affected fields. + +## 4. Portable JSON boundary + +Graph IR is JSON, not an arbitrary TypeScript or Python object graph. Before a +hash or compilation result is trusted, values must be finite, detached, and +representable consistently in both languages. + +The current boundary rejects, as applicable to the host language: + +- `undefined`, functions, symbols, bigint, NaN, and infinities; +- integers outside `-(2^53-1)` through `2^53-1`; +- cyclic containers and sparse arrays; +- accessor-backed, hidden, symbolic, or extra array properties; +- hostile proxies and custom-prototype/class instances; and +- object-key collisions created by Unicode surrogate normalization. + +Valid JSON `null` is data, never a silent failure placeholder. Repeated object +references are copied rather than retained as shared mutable aliases. Unicode +surrogate pairs are normalized consistently, lone surrogates are escaped into +UTF-8-safe JSON, and object keys are compared by code point rather than +JavaScript UTF-16 code unit ordering. + +The compiler reports unsafe graph input as `GE1007_INVALID_GRAPH` and does not +leak an accessor/proxy exception cause. Authors should still pass parsed JSON or +plain data rather than rely on implementation-specific rejection behavior for +exotic host objects. + +This portable boundary validates representation. It does **not** prove that an +embedded object is a valid JSON Schema, mapping language, condition language, +provider configuration, resource policy, or isolation policy. + +## 5. Root envelope + +The root object rejects unknown fields and requires: + +- `apiVersion` and `kind`; +- `metadata`; +- `inputSchema` and `outputSchema`; +- non-empty `entrypoints` and non-empty named `outputs`; +- `nodes`; and +- `edges`. + +`stateSchema` and `policies` are optional. `nodes` and `edges` are structurally +allowed to be empty arrays, but a valid non-empty entrypoint and output +reference cannot resolve against an empty node set, so semantic compilation +will fail. + +In JSON Schema, “optional” means the property may be absent; it does not make +the property nullable. The current optional metadata, endpoint, node, edge, +retry, and known-policy fields do not include `null` in their declared types. +`config` may legally be null because it deliberately accepts any portable JSON +value, and null can also appear inside an otherwise valid schema/config object. + +There is a current Day 2 parity defect at this boundary: TypeScript follows the +canonical schema and returns `GE1007_INVALID_GRAPH` for explicit null in fields +such as `metadata.description`, `stateSchema`, endpoint `port`, or node `retry`, +while the Python Pydantic projection currently accepts those values through its +`T | None` declarations. Until Python distinguishes “absent” from “present but +null” during validation and a shared negative fixture locks the rule, nullability +parity is **not frozen**. The canonical schema remains authoritative. + +#### Reproduced nullability variants + +The following read-only probe started from +[`diamond.graph.json`](../../spec/conformance/diamond.graph.json), made exactly +one JSON change per copy, and compiled each copy with the currently built +TypeScript core and current Python package. The JSON column is a minimal +replacement fragment, not a new extension syntax. + +| Suggested shared fixture | Single JSON replacement | TypeScript actual | Python actual | +| --- | --- | --- | --- | +| `invalid-null-metadata-description.graph.json` | `{ "metadata": { "description": null } }` | `valid: false`; `GE1007_INVALID_GRAPH` | `valid: true`; no diagnostic | +| `invalid-null-state-schema.graph.json` | `{ "stateSchema": null }` | `valid: false`; `GE1007_INVALID_GRAPH` | `valid: true`; no diagnostic | +| `invalid-null-output-port.graph.json` | `{ "outputs": { "result": { "node": "merge", "port": null } } }` | `valid: false`; `GE1007_INVALID_GRAPH` | `valid: true`; no diagnostic | +| `invalid-null-node-retry.graph.json` | `{ "nodes": [{ "id": "split", "retry": null }] }` | `valid: false`; `GE1007_INVALID_GRAPH` | `valid: true`; no diagnostic | + +Each shared fixture must otherwise be a complete, valid graph and introduce +exactly one explicit-null defect; abbreviated fragments above must never replace +required sibling fields. The expected corpus entry for every case is +`valid: false` with exactly `GE1007_INVALID_GRAPH`. This keeps the fix +schema-led: Python should reject the documents rather than TypeScript relaxing +the canonical contract. + +### Metadata + +Metadata is closed to unknown properties. `name` must match +`^[a-z][a-z0-9-]{0,62}$`; `version` is any non-empty string. Description is +optional text and labels are an optional string-to-string map. + +Metadata participates in the graph hash. Labels can advertise a versioned +required capability to external tooling, as the TypeScript pattern package +does, but core does not negotiate or enforce such a label. An annotation is not +a substitute for an implementation check. + +### Graph input, output, and state schemas + +Each schema-valued field is currently validated only as a JSON object. Core +does not meta-validate it against Draft 2020-12, resolve external references, +or enforce it against graph/node runtime values. The declarations are retained +and hashed so a future validator can do that work without redesigning the +envelope. + +`stateSchema` is especially easy to overclaim. Its presence does not create a +shared state store, state transaction, reducer, conflict detector, checkpoint +scope, or concurrent-write semantics. It is **opaque/declarative** today. + +## 6. Entrypoints and outputs + +Entrypoints and outputs are explicit by accepted architectural decision: + +- `entrypoints` is a non-empty unique list of node IDs; +- each entrypoint must resolve to a declared node; +- an entrypoint cannot have an incoming edge; +- reachability starts only from those listed roots; +- independent zero-indegree roots are legal only when each is listed; and +- every declared node must be reachable from at least one entrypoint. + +The runtime gives raw graph input to entrypoint nodes. It does not infer roots +from array position or indegree. + +`outputs` is a non-empty mapping from public result names to `{ node, port? }` +endpoints. Each referenced node must exist. At runtime, the optional port names +a property on that node's result; missing data becomes a structured output +binding failure. The compiler does not currently prove that the port exists in +the node's declared output schema or that the assembled result conforms to the +graph output schema. + +Public output names themselves have no additional naming grammar in the +current JSON Schema. Code that handles them must use safe map/object practices +rather than assuming identifier syntax. + +## 7. Nodes + +Every node object is closed to unknown fields and requires `id`, `kind`, +`inputSchema`, `outputSchema`, and `config`. + +### Node identity and order + +Node IDs match `^[A-Za-z][A-Za-z0-9_.-]{0,127}$` and must be unique. Declaration +order is semantically observable: it is the deterministic tie-break for peers +that become ready together, it contributes to topological layer order, and it +changes canonical bytes. Builders must never depend on incidental map or set +iteration to choose it. + +### Node kind vocabulary + +The allowed literals are `agent`, `model`, `tool`, `transform`, `subgraph`, +`router`, `barrier`, `validator`, and `human`. + +Acceptance of a literal proves only that the node can be represented. The +current scheduler dispatches caller-supplied executors and does not provide a +generic model, tool, nested subgraph, router, durable barrier, validator, or +human-approval implementation based on `kind`. Transform and barrier have +TypeScript identity defaults, but those defaults do not implement the semantic +feature suggested by the name. + +### Node contracts and policy-shaped fields + +- `inputSchema` and `outputSchema` are required opaque JSON objects today. +- `config` accepts any portable JSON value and is interpreted only by the + selected executor/pattern. +- `retry` has bounded attempts, delays, multiplier, and jitter vocabulary. +- `timeoutMs` has a positive timer-safe integer bound. +- `cache`, `resources`, and `isolation` are optional opaque JSON objects. +- `sideEffects` is `none`, `idempotent`, or `non-idempotent`. + +Current local scheduling uses retry and timeout fields. Durable recovery uses +`sideEffects` to decide whether an interrupted activity can safely retry. +Cache/resource/isolation objects are retained but not enforced. A declaration +cannot restrict filesystem, network, tool, process, or secret access without a +future capability/isolation provider. + +## 8. Edges and ports + +Every edge object is closed to unknown fields and requires a unique valid `id`, +`from`, and `to`. Each endpoint requires a non-empty node name and may include a +non-empty port name. Source and target nodes must exist. + +For the current DAG runtime, an edge is a required dependency. A source port +selects a property from the producer result. A target port chooses the key in +the consumer input map; without one, the source node ID is the key. Duplicate +target binding keys are detected during runtime binding as structured failure. + +This is **named-port binding**, not typed-port compilation. The compiler does +not currently: + +- resolve a port declaration inside an input/output JSON Schema; +- reject a source port absent from the producer schema; +- reject a target port absent from the consumer schema; +- prove producer and consumer schema compatibility; +- validate edge `schema` against either endpoint; or +- detect duplicate target binding keys statically. + +### Edge vocabulary versus execution + +| Field/value | Schema/compiler treatment | Current DAG runtime treatment | +| --- | --- | --- | +| `from` / `to` node | Existence and DAG topology validated | Required dependency and value binding | +| endpoint `port` | Non-empty string only | Property selection / input-key binding | +| `mode: value` | Accepted and hashed | One terminal value per producer | +| `mode: stream` | Accepted and hashed | No stream lowering, item identity, queue, offset, ack, or backpressure semantics | +| `mode: artifact-ref` | Accepted and hashed | No ArtifactStore contract or reference validation | +| `map` | Must be an object if present | Not lowered or evaluated | +| `condition` | Must be an object if present | Not lowered; all statically reachable branches still run | +| `schema` | Must be an object if present | Not meta-validated or applied to the transferred value | + +An omitted edge mode is accepted, but authors should not use omission as a +portable promise for future mode negotiation until the default is explicitly +frozen in the normative contract. + +Ordinary cycles are rejected. The existence of condition annotations or a +statically unrolled TypeScript `loopUntilDry` pattern does not enable an +executable conditional cycle or early stop. + +## 9. Graph policies + +Unlike the closed root/node/edge objects, `policies` intentionally permits +extension keys. Known v1alpha1 properties are: + +| Policy | Structural validation | Current enforcement | +| --- | --- | --- | +| `maxConcurrency` | Positive safe integer | Bounds local/durable active node attempts | +| `maxDynamicNodes` | Non-negative safe integer | No effect because dynamic graph expansion is absent | +| `maxDepth` | Positive safe integer | Compiler rejects excess static topological layers | +| `maxFanOut` | Positive safe integer | Compiler rejects excess static outgoing edge count | +| `maxTotalAttempts` | Positive safe integer | Bounds local/durable node attempts and retry reservations | +| `maxDurationMs` | Positive timer-safe integer | Value/timer ceiling validated; no complete graph-duration stop contract is implemented | +| `maxCostUsd` | Non-negative finite number | Retained only; no provider usage accounting or cost reservation exists | + +Unknown policy fields being accepted means they can be preserved and hashed; it +does not mean either runtime enforces them. A feature that requires an unknown +policy must use a versioned capability contract and fail closed when the runtime +cannot honor it. Silent “best effort” would make safety budgets fictional. + +## 10. Compiler stages and implemented diagnostics + +TypeScript compilation is implemented by +[`compiler.ts`](../../packages/core/src/compiler.ts) with dependency-free +envelope checks in +[`schema-validation.ts`](../../packages/core/src/schema-validation.ts). Python +implements the corresponding behavior in +[`compiler.py`](../../python/src/graph_engineering/compiler.py) and strict +[Pydantic models](../../python/src/graph_engineering/models.py). + +The observable compile pipeline is: + +1. safely snapshot portable JSON; +2. validate closed envelopes, required fields, literals, shapes, and numeric + bounds; +3. compute canonical graph text/hash for a structurally valid snapshot; +4. index node and edge identities; +5. resolve edge endpoints, entrypoints, and output nodes; +6. build adjacency and deterministic topological layers; +7. reject cycles, incoming edges to entrypoints, and unreachable nodes; and +8. enforce static max-fan-out and max-depth policies. + +Identity/reference failures stop topology analysis so the compiler does not +manufacture cascaded cycle/reachability diagnoses from an ambiguous index. +Messages and host exception classes may differ; stable codes and associated +portable identifiers are the compatibility surface. + +| Code | Implemented trigger | +| --- | --- | +| `GE1001_DUPLICATE_NODE` | More than one node declares the same ID. | +| `GE1002_DUPLICATE_EDGE` | More than one edge declares the same ID. | +| `GE1003_MISSING_SOURCE` | An edge source node does not exist. | +| `GE1004_MISSING_TARGET` | An edge target node does not exist. | +| `GE1005_CYCLE` | Static adjacency contains a cycle. | +| `GE1006_UNREACHABLE_NODE` | A declared node is not reachable from an explicit entrypoint. | +| `GE1007_INVALID_GRAPH` | Portable JSON, envelope, literal, shape, identifier, or numeric validation fails. | +| `GE1008_MISSING_ENTRYPOINT` | An entrypoint string does not name a node. | +| `GE1009_MISSING_OUTPUT` | A public output endpoint does not name a node. | +| `GE1010_ENTRYPOINT_HAS_INCOMING` | A declared entrypoint has an incoming edge. | +| `GE1101_MAX_FAN_OUT` | A node's static outgoing edge count exceeds policy. | +| `GE1102_MAX_DEPTH` | Static topological layer count exceeds policy. | + +The shared negative corpus currently covers duplicate node, missing endpoint, +cycle, unreachable node, incoming-entrypoint, unsafe budget, and oversized +timer cases. It does not yet provide one shared fixture for every code or every +invalid field/path combination. + +### Validation explicitly absent + +Current compilation does not establish: + +- full JSON Schema validity or runtime input/output conformance; +- typed-port existence or producer/consumer compatibility; +- router exhaustiveness, defaults, or condition-language validity; +- concurrent state-write conflicts or reducer correctness; +- capability authorization or resource/isolation feasibility; +- `maxCostUsd` or provider/model budget enforceability; +- dynamic fan-out/GraphPatch cardinality and permission limits; +- explicit loop-node boundedness or convergence; or +- nested subgraph namespace/checkpoint correctness. + +Those are master-plan requirements, not hidden behavior behind +`GE1007_INVALID_GRAPH`. + +## 11. Cross-language evidence + +The shared coordinator +[`tools/conformance/run.mjs`](../../tools/conformance/run.mjs) compares native +TypeScript and Python compilation results. The current canonical/compile +evidence includes: + +- the exact diamond SHA-256 and its three topological layers; +- invalid graph verdicts and ordered stable diagnostic codes for the fixtures + listed in [`expected.json`](../../spec/conformance/expected.json); +- declaration-order tie breaking for topological peers; +- explicit root/reachability behavior; +- finite timer and safe-integer budget ceilings; +- safe handling of mutation, aliases, hostile accessors/proxies, cycles, + sparse arrays, Unicode edge cases, and non-portable numbers in package tests; + and +- preservation of required `config: null` plus the generic canonical + distinction between a legal null value and an absent property. + +This evidence proves one bounded DAG/compiler slice. It does not prove every +Draft 2020-12 keyword, every fractional number spelling, YAML equivalence, +builder parity, typed schemas, full node-kind behavior, or future migrations. +It also does not cover the known optional-field nullability divergence described +above; that gap must not be hidden behind the otherwise green fixture set. + +## 12. Authoring and advanced IR gaps + +The following distinctions are mandatory in README tables, release notes, +examples, and launch content. + +| Promised surface | Evidence that exists | Missing work / honest current label | +| --- | --- | --- | +| General TypeScript builder | Four zero-side-effect topology constructors exist in [`@graph-engineering/patterns`](../../packages/patterns/README.md) | No general builder for arbitrary nodes/edges/typed ports; **not implemented** | +| General Python builder | Strict GraphSpec Pydantic models exist | No fluent/general parity builder or Python pattern set; **not implemented** | +| YAML authoring | JSON is accepted by the TypeScript CLI | No YAML loader, duplicate-key policy, tag/anchor safety profile, canonical conversion, or parity fixtures; **not implemented** | +| Typed ports | Endpoint port strings bind values at runtime | No declared port registry, schema extraction, assignability algorithm, or compile-time compatibility diagnostic; **not implemented** | +| Runtime schema contracts | Schema objects are required and hashed | No Draft 2020-12 meta-validation or node/edge/graph value validation; **not implemented** | +| Shared graph state | `stateSchema` is accepted | No state instance, transaction, reducer, conflict check, or persistence semantics; **vocabulary only** | +| Nested subgraphs | `kind: subgraph` is accepted | No embedded/reference form, namespace expansion, input/output mapping, policy inheritance, checkpoint scope, or trace lineage; **vocabulary only** | +| Stream/artifact edges | Edge mode literals are accepted | No Graph IR stream scheduler or ArtifactStore lowering; **vocabulary only** | +| Conditions and mappings | Opaque objects are accepted and hashed | No portable expression language, compiler, sandbox, or scheduler application; **declarative only** | +| Dynamic `GraphPatch` | `GraphPatched` exists in the event-type enum | No patch document schema, compiler API, revision transition, authorization/budget check, dry run, durable fold, or runtime execution; **not implemented** | +| Node/edge/schema content hashes | Whole-graph canonical SHA-256 exists | No stable individual content hashes or dependency/revision manifest; **not implemented** | + +The `GraphPatched` event name is reserved vocabulary. The current durable +contract intentionally runs one immutable compiled DAG at graph revision `1`. +Emitting an arbitrary event with that type cannot create a valid patch or +change the compiled graph. + +Likewise, pattern constructors that emit condition annotations clearly state +that the current scheduler runs every statically reachable branch. Their +existence is authoring evidence, not routing or early-stop execution evidence. + +## 13. Compatibility and migration policy + +v1alpha1 is pre-stable, but compatibility still requires deliberate versioning. +The following rules protect fixtures, durable histories, and users while the +contract evolves. + +### Reader behavior + +- Unsupported `apiVersion` fails; it is never guessed from `$id`, metadata, or + filename. +- Root, metadata, node, edge, endpoint, and retry objects reject unknown fields. +- Unknown graph policy fields are preserved/hashed but have no implied support. +- Required capabilities must be negotiated explicitly and fail closed when + absent; an ignored safety field is worse than a clear incompatibility. +- A reader must not silently upgrade, delete, default, reorder, or coerce data + before reporting the original document's hash. + +Because most envelopes reject unknown properties, adding even an optional root, +node, or edge field breaks older strict readers. Such a change is not safely +backward compatible merely because a new reader considers the field optional. +It requires either a new protocol version or a previously defined, versioned +extension container with clear ignore/fail behavior. + +### Writer behavior + +- Writers emit exactly one supported `apiVersion`/`kind` pair. +- Deterministic builders define node/edge declaration order instead of relying + on hash-map iteration. +- Defaults that affect identity are materialized or omitted consistently across + languages; a schema-legal explicit null must not be collapsed into absence, + while null must still be rejected where the schema does not allow it. +- Opaque configs and annotations carry their own controlled version identity. +- Writers must never claim a required runtime capability only because the + schema accepts the associated field. + +### Migration shape + +No general migration tool exists today. Before the first incompatible change, +the project needs a deterministic migration contract that: + +1. selects the source schema from the original `apiVersion`; +2. validates and records the original canonical bytes and hash; +3. applies one named, versioned, pure transformation; +4. emits a new document with a new protocol identity when required; +5. compiles it through the target compiler; +6. reports source/target hashes and a semantic/topology diff; +7. never mutates or overwrites the source document by default; and +8. has byte-identical TypeScript/Python/YAML-to-JSON fixtures. + +Durable resume must continue to require the graph hash bound by `RunCreated`. +A changed graph, including metadata-only changes, cannot be substituted into an +existing immutable run. Future GraphPatch revisions require their own protocol +and lineage; migration is not a back door for patching live history. + +### Compatibility test matrix + +Before stable release, CI must cover: + +- current reader/current writer in both languages; +- every supported old reader/new writer and new reader/old writer combination; +- canonical bytes/hash before and after migrations; +- unknown field/version/capability failures; +- absence versus explicit null, including deterministic rejection wherever the + field does not admit null; +- node/edge order and Unicode-key determinism; +- safe-integer boundaries and the adopted fractional-number profile; and +- durable history rejection when graph identity or revision mismatches. + +## 14. Day 2 freeze exit gate + +The calendar's Day 2 headline gate is “byte-equivalent canonical IR.” The +master plan's detailed Day 2 promise also includes builders/schema types, +JSON Schema and negative fixtures, stable component hashes, and the authoring +path from TypeScript, Python, YAML, and JSON. The gate must be assessed against +the complete promise, not only one green diamond hash. + +### Current gate status + +| Exit criterion | State on 2026-07-26 | Evidence or blocker | +| --- | --- | --- | +| Canonical Graph v1alpha1 schema under controlled namespace | Green for current document | Canonical schema plus accepted namespace ADR | +| Native strict TS/Python GraphSpec projections | Green for current envelope | Core types/validator and Pydantic models | +| Byte-identical canonical JSON/hash | Green for safe-integer shared diamond; Partial overall | Fractional number profile/corpus is not frozen | +| Shared positive/negative compiler corpus | Partial | Core DAG cases exist; not every stable diagnostic or advanced validation has a fixture | +| Optional-field nullability parity | Open defect | Schema/TypeScript reject present null; Python currently accepts it | +| General TypeScript builder | Open | Pattern-specific constructors are insufficient | +| General Python builder | Open | Models are not a parity builder | +| Safe deterministic YAML loader | Open | CLI is JSON-only | +| Graph/node/edge/schema content hashes | Partial/Open | Whole graph hash only | +| Typed ports and schema compatibility | Open | Port strings bind dynamically; compiler does not analyze schemas | +| State/reducer conflict validation | Open | `stateSchema` has no execution model | +| Subgraph namespace/checkpoint contract | Open | Node kind literal only | +| Dynamic revision/GraphPatch schema | Open | Event name only; durable graph remains immutable revision 1 | +| Policy/budget/capability/loop validation promised by plan | Partial/Open | Static fan-out/depth and numeric shapes exist; broader semantics do not | +| Schema bundle drift detection | Open control | Current CLI/MCP copies are byte-identical but need an automated source-of-truth assertion | +| Compatibility/migration ADR and fixtures | Open | No migration surface exists | + +Therefore Day 2 is **Partial**, not fully exited. The implemented canonical DAG +slice is useful and conformance-backed, but it does not satisfy the full +authoring and semantic-validation scope. + +### Required evidence to close the gate + +The integration owner may mark Day 2 complete only after all of the following +are present: + +1. **Protocol freeze record:** approved schema/namespace/version ADR, complete + field-presence rules, default rules, and an explicit numeric canonicalization + decision. +2. **Source-of-truth enforcement:** CI proves every bundled schema equals the + canonical `spec/` file and published schema URLs resolve to the same bytes. +3. **Four authoring paths:** arbitrary equivalent graphs built through JSON, + safe YAML, TypeScript builder, and Python builder compile to byte-identical + canonical JSON and the same hash. +4. **Builder safety:** deterministic declaration ordering, collision handling, + immutable snapshots, explicit-null preservation, no implicit roots/outputs, + and no capability claim beyond the emitted IR. +5. **Canonical corpus:** arrays, nested Unicode keys, lone surrogates, empty + values, explicit null, safe-integer edges, negative zero, and the chosen + fractional-number policy have shared golden bytes and hashes. +6. **Component identity:** graph, node, edge, and schema content/revision hash + rules are either implemented with cross-language vectors or explicitly + removed/deferred by an approved plan amendment. +7. **Compiler completeness:** port/schema compatibility, schema meta-validation, + state/reducer conflicts, router exhaustiveness, policy/budget/capability + requirements, and loop/dynamic limits have stable diagnostics and negative + fixtures—or each is explicitly moved to a later version without misleading + Day 2 completion language. +8. **Advanced vocabulary contracts:** subgraph, state, stream/artifact, and + GraphPatch shapes either receive normative contracts or remain explicitly + rejected/declarative at compile/runtime boundaries. +9. **Migration tests:** unsupported versions fail predictably and every + supported migration is pure, non-destructive, traceable, and hash-audited. +10. **Parity evidence:** TypeScript and Python package tests, fixture validation, + and the full cross-language coordinator pass from a clean checkout with no + skipped case. + +The freeze is a protocol commitment, not a prohibition on later features. +Later work can add a new version or a previously designed extension, but it +cannot retroactively reinterpret a v1alpha1 hash or turn declarative vocabulary +into execution without an explicit contract, conformance corpus, and migration +story. + +## 15. Review checklist for every IR change + +Before merging any change that touches graph structure or canonicalization, +reviewers must answer: + +- Is `spec/graph.schema.json` still the only semantic source? +- Does the change require a new `apiVersion`, schema `$id`, or extension + namespace? +- Will old strict readers reject it, ignore it, or mis-execute it? +- Does it change canonical bytes for an existing valid document? +- Are array order, explicit null, Unicode, and numeric behavior deterministic? +- Are all new limits finite and safe in JavaScript and Python? +- Are TS and Python models, compilers, builders, bundled schemas, and public docs + aligned? +- Is there a shared positive vector and a negative/adversarial vector? +- Does a diagnostic need a new stable code rather than an overloaded message? +- Is any schema field being mistaken for runtime implementation? +- Does durable history bind or reject the new graph identity correctly? +- Is a deterministic migration available when existing documents are affected? + +If any answer is unknown, the capability remains Partial/Open and the current +version must fail clearly rather than infer behavior. That discipline is what +makes Graph IR a cross-language protocol instead of two similarly named object +models. diff --git a/codex_plans/architecture/persistence-and-recovery.md b/codex_plans/architecture/persistence-and-recovery.md new file mode 100644 index 0000000..d859505 --- /dev/null +++ b/codex_plans/architecture/persistence-and-recovery.md @@ -0,0 +1,473 @@ +# Persistence and recovery architecture + +Status: implementation-aligned Day 9 freeze ledger, 2026-07-26 + +This document explains the persistence and recovery slice that exists today, +the authority boundaries it relies on, and the work still required before the +complete Day 9 durable-execution promise can be called delivered. It does not +replace the normative [persistence semantics](../../spec/persistence-semantics.md), +[durable recovery semantics](../../spec/durable-recovery-semantics.md), +[event schema](../../spec/event.schema.json), +[checkpoint schema](../../spec/checkpoint.schema.json), or +[Durable JSON schema](../../spec/durable-json.schema.json). If this document and +a versioned specification disagree, the specification wins. + +The current implementation is a strong local slice: TypeScript and Python can +persist and resume one immutable revision-1 DAG, reuse committed successes, +recover bounded safe attempts, and fail closed on an in-doubt external effect. +It is not a distributed coordinator, a production storage system, or an +exactly-once activity engine. + +## 1. Current boundary at a glance + +```mermaid +flowchart LR + G[Immutable compiled DAG] --> D[Durable start or resume] + D --> F[Validate and fold full event history] + F --> J[CAS journal] + J --> E[(Authoritative event stream)] + E --> F + J --> X[Caller executor] + X --> J + + E -. authoritative prefix .-> C[(Checkpoint cache)] + C -. acceleration only; not integrated .-> F + + J -. not implemented .-> L[Lease / LockManager] + J -. not implemented .-> A[ArtifactStore] +``` + +| Surface | TypeScript | Python | Current claim | +| --- | --- | --- | --- | +| Event envelope | [`events.ts`](../../packages/persistence/src/events.ts) | [`events.py`](../../python/src/graph_engineering/events.py) | Strict v1alpha1 envelope and portable structured validation | +| Event store | [`EventStore`](../../packages/persistence/src/event-store.ts), memory and JSONL adapters | [`EventStore`](../../python/src/graph_engineering/persistence/event_store.py), memory and JSONL adapters | Last-sequence CAS and inclusive reads within one coordinating process | +| Checkpoint store | [`CheckpointStore`](../../packages/persistence/src/checkpoints.ts) and atomic file adapter | [`CheckpointStore`](../../python/src/graph_engineering/persistence/checkpoint_store.py) and atomic file adapter | Standalone content-hashed local snapshots; not used by durable scheduling | +| Durable value codec | [`durable-json.ts`](../../packages/runtime/src/durable-json.ts) | [`durable_json.py`](../../python/src/graph_engineering/durable_json.py) | Exact cross-language finite binary64 transport through tagged JSON | +| Durable scheduler | [`durable.ts`](../../packages/runtime/src/durable.ts) | [`durable.py`](../../python/src/graph_engineering/durable.py) | Event-sourced start/resume for one static local DAG revision | + +“Local” is a correctness qualifier. The TypeScript serial queue and Python +per-event-loop process locks coordinate store instances inside one process. +They are implementation plumbing, not a public `LockManager`, lease, fencing +token, heartbeat, or cross-process ownership protocol. + +## 2. Event authority + +The append-only event stream is the sole durable authority for scheduler work. +An in-memory result, an executor return value, a checkpoint, a log message, or a +caller assertion cannot authorize a dependent or prove that an attempt +completed. Recovery derives its state by validating and folding the committed +event prefix. + +This yields five rules: + +1. A node attempt is claimed only by a committed `NodeStarted`. +2. An executor result is reusable only after its outcome event is committed. +3. A dependent can observe a success only after `NodeSucceeded` and all ordered + `EdgeEmitted` events commit. +4. A terminal result is trusted only when it is consistent with every explicit + folded node outcome. +5. A checkpoint can accelerate reconstruction but can never create, erase, or + supersede an event fact. + +### Authoritative commit batches + +| Commit | Meaning after successful append | What remains forbidden before success | +| --- | --- | --- | +| `RunCreated` + `RunStarted` | The run identity, original input, graph hash, implementation hash, and attempt budget exist | Starting executor work or silently treating the stream as another run | +| `NodeScheduled` + `NodeStarted` | One attempt and its global budget slot are claimed, with bound input and recovery identity | Calling the executor | +| `NodeSucceeded` + ordered `EdgeEmitted` | The detached output and every outgoing value fact are reusable | Releasing a dependent or reporting durable success | +| `NodeAttemptFailed` + optional `NodeRetried` | The attempt outcome is known; a retry reservation is durable when present | Inventing an unrecorded retry or resetting backoff/budgets | +| `NodeSettledWithoutAttempt` | A deterministic failed/skipped result exists without a new attempt | Releasing dependants based only on an in-memory failure | +| `RunSucceeded`, `RunFailed`, or `RunCancelled` | The tagged terminal snapshot is final | Appending another event or rerunning an executor on terminal resume | + +The writer computes `payloadHash` over canonical JSON for every event `data` +object. The recovery fold also validates event IDs, timestamps, run and graph +identity, sequence, payload hashes, node/edge identity, attempt progression, +retry reservations, budgets, output hashes, activity keys, edge batches, and +terminal projection consistency. + +These hashes provide integrity checks, not origin authentication. There is no +signature, MAC, trusted timestamp authority, or append authorization layer. A +hostile actor who can rewrite the complete private store can construct new +bytes and hashes; protecting the directory and storage credentials remains an +operator responsibility. + +## 3. Event versions and compare-and-swap + +An event stream belongs to one safe `runId`. Its version is its last sequence, +not its length: + +- a missing stream has version `-1`; +- the first event has sequence `0`; +- append at version `V` must contain contiguous sequences beginning at `V + 1`; +- a successful append returns the new last sequence; and +- an empty append is a CAS-checked no-op. + +`read(runId, fromSequence)` is inclusive and sequence ordered. Readers reject a +truncated, blank, non-UTF-8, malformed, wrong-run, invalid, or non-contiguous +record. They never skip a bad line and continue with an apparently healthy +history. + +Error precedence is intentional. Implementations validate request identity, +version shape, and container shape and detach caller input first. Inside the +per-run critical section they then validate existing storage, compare CAS, and +only after a match validate the new event envelopes. Therefore existing +corruption wins over a stale write, and `VERSION_CONFLICT` wins over defects in +a stale append payload. + +The durable journal serializes its own append calls and uses the last successful +sequence as the next expectation. It maps a resume CAS loss to +`RESUME_CONFLICT`, latches the first durability failure, and stops intentional +new scheduling. A losing resume invokes no executor because `RunResumed` must +commit before continuation begins. + +CAS is necessary but insufficient for distributed ownership. Two coordinators +can both execute application code before one loses a later append race. CAS +detects a stale durable write; it does not fence a stale worker, revoke +credentials, stop an external request, or establish a lease. + +### Local JSONL durability + +Both local JSONL adapters append canonical UTF-8 JSON records, flush and +`fsync` successful writes, and validate the complete stream on the next +operation. A newly created stream directory entry is also synchronized within +the documented local-filesystem assumptions. + +A process or disk failure during append can leave an uncertain tail. A +truncated tail is detected as `CORRUPT_EVENT_LOG`; automatic truncation, repair, +salvage, replication, compaction, and retention are out of scope. Callers must +not infer success from an append that did not return successfully. + +## 4. Checkpoints are caches, not authority + +The file checkpoint stores are implemented in both languages. A checkpoint +contains `runId`, `checkpointId`, last applied `sequence`, `createdAt`, `state`, +and `contentHash`. Saving performs the following local atomic-replacement +sequence: + +1. validate safe identifiers, timestamp, sequence, and state; +2. detach the state and calculate its canonical content hash; +3. write a private temporary file in the destination directory; +4. flush and `fsync` the temporary file; +5. atomically rename/replace it; and +6. `fsync` the containing directory. + +Loading validates UTF-8/JSON, the closed envelope, safe identifiers, directory +and filename identity, safe-integer-only state, and `contentHash`. Listing +returns summaries without state, ordered by sequence and then checkpoint ID by +Unicode code point. + +Checkpoint v1alpha1 state deliberately excludes floating-point numbers. It +accepts null, booleans, strings, arrays, objects, and integers in the JavaScript +safe range. Runtime values containing finite decimals can still be embedded by +first encoding them as Tagged Durable JSON, whose representation contains only +strings, arrays, booleans, null, and safe integers. + +### Scheduler integration status + +The durable start/resume options do not accept a `CheckpointStore`, and the +scheduler does not save, load, list, or trust checkpoints. Every resume folds +the complete event history. Thus checkpoint files currently provide a tested +storage primitive but no recovery acceleration. + +Future acceleration must preserve this order: + +```text +load and validate authoritative event stream + -> consider a checkpoint whose sequence is within that stream + -> verify graph/input/implementation and history-prefix identity + -> rebuild any missing suffix from events + -> continue only from the event-derived projection +``` + +A scheduler checkpoint will need at least the last applied sequence, +graph/input/implementation hashes, a history-prefix hash, total attempts, and +node projections in graph declaration order. Missing, stale, corrupt, +ahead-of-tail, or projection-inconsistent checkpoints must be ignored with a +structured recovery warning and rebuilt from events. They must never hide event +corruption. None of this scheduler integration or warning behavior is +implemented yet. + +## 5. Tagged Durable JSON and identity hashes + +The ordinary runtime accepts portable finite JSON, including non-integer +binary64 values. Checkpoint-safe canonical JSON cannot rely on TypeScript and +Python producing identical decimal spellings for every binary64 value. Tagged +Durable JSON solves that transport problem without weakening the checkpoint +number profile. + +| Runtime value | Tagged form | +| --- | --- | +| null | `["n"]` | +| boolean | `["b", true]` | +| string | `["s", "text"]` | +| safe integer | `["i", 42]` | +| finite non-integer binary64 | `["f", "3ff8000000000000"]` | +| array | `["a", [encoded items...]]` | +| object | `["o", [["key", encoded value]...]]` | + +The float payload is the exact sixteen-character lowercase big-endian IEEE-754 +bit pattern. Negative zero and integer-valued doubles normalize to the integer +form. Object keys are strictly increasing by Unicode code point, making +duplicates and noncanonical order invalid. Decoders reject unknown tags, wrong +arity, unsafe integers, non-finite floats, integer-valued float tags, and +unsorted or duplicate keys. + +Inputs, outputs, implementation IDs, activity identities, and terminal result +snapshots are hashed as lowercase SHA-256 over canonical UTF-8 JSON for their +tagged representation. Hash equality is therefore a cross-language byte-level +claim for the shared domain, not a comparison of host-language float text. + +## 6. Stable activity keys and the side-effect boundary + +Every scheduled node records its detached input, `inputHash`, declared +`sideEffects`, and logical activity key. The key is: + +```text +durableJsonHash([ + "activity/v1alpha1", runId, graphRevision, nodeId, inputHash +]) +``` + +Attempt is deliberately absent. A retry of the same logical activity receives +the same `activityKey` and `idempotencyKey`; `attemptId` remains distinct and +includes the attempt number. The runtime can preserve and pass the stable key, +but the executor must actually forward it to an external system that provides +idempotent semantics. + +| Declaration on an interrupted attempt | Recovery behavior | Caller obligation | +| --- | --- | --- | +| `none` | May retry automatically within node/global budgets | The executor must truly have no external effect requiring reconciliation | +| `idempotent` | May retry with the same activity key | Forward the key to, and rely on, an external idempotency boundary | +| `non-idempotent` | Do not reinvoke; fail with `IN_DOUBT_SIDE_EFFECT` | Reconcile outside this API | +| omitted | Same fail-closed behavior as non-idempotent | Add an honest declaration or reconcile outside this API | + +External effects are at-least-once. The irreducible ambiguity is a crash after +the external system commits but before `NodeSucceeded` commits. The event log +correctly says only that the attempt was open; it cannot inspect or roll back +the outside system. Stable keys, remote idempotency, reconciliation, +compensation, and human approval are application responsibilities. + +There is no independent durable activity ledger and no approval callback or +approval event flow in the current durable scheduler. Event-schema literals +such as `HumanInputRequested` and `HumanInputReceived` are vocabulary only for +this slice; the durable history fold does not implement an approval protocol. + +## 7. Start and resume lifecycle + +Start and resume are deliberately separate APIs. Start never silently resumes, +and resume never silently creates a run or accepts replacement input. + +### Start + +1. Snapshot and compile the graph; enforce the durable timer bounds. +2. Snapshot portable graph input and hash the caller-supplied non-empty + `implementationId`. +3. Read the stream and reject a non-empty history as `RUN_ALREADY_EXISTS`. +4. CAS-append `RunCreated` and `RunStarted` from version `-1`. A racing start is + rejected as `RUN_ALREADY_EXISTS`. +5. Commit each attempt claim before executor dispatch. +6. Commit each outcome before releasing dependent work. +7. Commit one terminal event containing the exact tagged graph result. + +`RunCreated` binds revision `1`, canonical graph hash, original detached input +and its hash, implementation hash, and effective total-attempt budget. The +`implementationId` is an assertion, not code attestation: a caller can +dishonestly reuse a label for changed code. + +### Resume + +1. Snapshot and compile the supplied graph and read the complete stream. +2. Reject an empty stream as `RUN_NOT_FOUND`. +3. Validate every event envelope and fold every semantic transition. +4. Verify graph, original input, implementation, attempts, retry reservations, + outputs, activity identities, and any terminal projection. +5. If history is terminal, return the recorded result with zero new events, + zero checkpoint writes, and zero executor calls. +6. Otherwise CAS-append `RunResumed` before invoking an executor. A losing CAS + returns `RESUME_CONFLICT` and invokes no executor. +7. Reuse committed successes, preserve pending retry availability, and convert + each open attempt to canonical `NODE_EXECUTION_INTERRUPTED` history. +8. Retry only a safe activity with remaining node and global budget; otherwise + settle or fail closed according to the side-effect rule. + +Cancellation signals are process-local and do not survive a crash. A committed +`RunCancelled` is terminal. If the process exits before that event commits, +resume reasons from the durable node history rather than assuming that a prior +in-memory cancellation completed. + +## 8. Crash-window ledger + +The table distinguishes what is durable from what was merely observed by one +process. “Resume action” always assumes the old coordinator has stopped; the +current local store cannot enforce that precondition. + +| Crash or loss window | Authoritative history | Resume action | Remaining risk | +| --- | --- | --- | --- | +| Before `RunCreated` + `RunStarted` commits | Missing stream | A new start may create the run; resume returns `RUN_NOT_FOUND` | An append with uncertain return must be inspected, not guessed | +| After run creation, before any attempt claim | Run identity only | Append `RunResumed`, then schedule ready roots | No executor output exists to recover | +| After `NodeScheduled`, before `NodeStarted` | A reservation without a claim, if such a valid prefix exists | Preserve the schedule identity and append the matching start without duplicating `NodeScheduled` | Local torn-write/corruption rules still apply | +| After `NodeStarted`, before executor entry | Open attempt | Record interruption; retry only under safe side-effect and budget rules | Conservatively treated the same as unknown execution | +| While executor is running | Open attempt | Same interruption path | Synchronous or remote work may have continued after process loss | +| External system committed, before success append | Open attempt | Reuse the activity key only for an idempotent retry; otherwise stop in doubt | No universal exactly-once answer exists | +| Output returned/validated, before `NodeSucceeded` batch commits | Open attempt | Do not reuse the in-memory output; recover as interrupted | Pure computation may be repeated | +| Success batch committed, before dependent release | Committed success and emitted edges | Reuse output; release dependent from folded state; never rerun producer | This is the central successful-node invariant | +| Failed attempt committed without a retry reservation | Known terminal attempt failure | Reuse the failure and settle descendants as required | No hidden retry may be invented | +| `NodeAttemptFailed` + `NodeRetried` committed, before delay or next start | Known failure plus exclusive next-attempt reservation | Wait only the remaining absolute delay and consume the reserved attempt once | Clock representation and budgets remain validated | +| `NodeSettledWithoutAttempt` committed, before dependent release | Explicit failed/skipped node outcome | Reuse the result and release dependants from the fold | No attempt is retroactively charged | +| Terminal event committed, before API return | Complete terminal result | Return it byte-semantically unchanged; append and execute nothing | Caller may have missed the original response but not the result | +| Event append fails or returns an impossible version | Last successful prefix only | Latch `DURABILITY_STORE_FAILED`; stop new scheduling and recover from the store later | In-flight external effects may still need reconciliation | +| Checkpoint write fails | Event stream remains authoritative | Current scheduler is unaffected because it does not use checkpoints | Future acceleration must warn/fallback without changing facts | + +## 9. Threat and failure matrix + +| Threat or failure | Present control and observable outcome | Boundary / operator action | +| --- | --- | --- | +| Path traversal through IDs | Strict 1–128 character identifier grammar; `.`/`..` forbidden; filenames use SHA-256 identifiers; `UNSAFE_IDENTIFIER` | Symlink and hostile shared-directory hardening are outside the alpha model; use a private directory | +| Caller mutates append/checkpoint input | Inputs are snapshotted before asynchronous persistence | Do not pass exotic host objects; only portable JSON is supported | +| Stale append | Expected-version mismatch causes `VERSION_CONFLICT` and writes nothing | Retry only after intentionally rereading and reconciling state | +| Racing start or resume | Start maps the creation CAS loss to `RUN_ALREADY_EXISTS`; resume maps its claim loss to `RESUME_CONFLICT` and invokes no executor | CAS does not fence work already launched by another coordinator | +| Two live coordinators | A later stale append is rejected | Unsupported and unsafe for external effects; stop the old process before resume | +| Torn or malformed JSONL | Complete-stream validation returns `CORRUPT_EVENT_LOG`; no record is skipped | Repair/salvage is manual and unspecified; preserve evidence before intervention | +| Checkpoint truncation, tampering, wrong identity, or unsafe number | Load/list returns `CORRUPT_CHECKPOINT` | Scheduler acceleration is absent; future integration must ignore with warning and rebuild from events | +| Stale or ahead checkpoint | Normatively cannot authorize work | No scheduler check exists yet because checkpoints are not integrated | +| Graph, input, or implementation mismatch | `GRAPH_HASH_MISMATCH`, `INPUT_HASH_MISMATCH`, or `IMPLEMENTATION_MISMATCH` before executor invocation | `implementationId` is caller attestation, not a signed build identity | +| Semantically contradictory history | Complete fold returns `INVALID_RUN_HISTORY` | Do not skip or synthesize around the contradiction | +| Duplicate/invalid generated event ID or clock | Writer latches `DURABILITY_STORE_FAILED`; history rejects duplicate IDs and invalid timestamps | Inject deterministic valid factories in tests; fix the producer before retrying | +| Store I/O or impossible returned version | First failure is latched as `DURABILITY_STORE_FAILED`; successful in-memory outcome is not reported as durable | Inspect authoritative storage and reconcile possible external effects | +| Attempt/retry/budget forgery | Fold validates contiguous attempts, exclusive retry reservations, per-node/global budgets, and concurrency | A consistent malicious full-history rewrite is not prevented cryptographically | +| Process loss during executor | Open attempt becomes `NODE_EXECUTION_INTERRUPTED` | Retry only `none`/`idempotent`; otherwise reconcile out of band | +| Effect committed outside, success not committed inside | Stable activity key and fail-closed side-effect policy | External idempotency, compensation, or human decision is required | +| Sensitive input/output persisted | Event envelopes default `redacted: true`; telemetry and prompt/response capture remain off by default | Tagged values still contain application data; encryption-at-rest and field-level redaction are not implemented | +| Hostile local user rewrites all bytes | Envelope, content, and semantic hashes detect accidental or inconsistent mutation | No signature/MAC/ACL layer; filesystem and credential security are operator-owned | +| Unbounded history growth | Correctness comes from a complete fold | No compaction, retention, or checkpoint acceleration; long-run latency/storage limits remain open | +| Network filesystem or disk semantics differ | Only documented local-filesystem assumptions are claimed | Use neither JSONL nor file checkpoints as a production distributed store | + +## 10. Cross-language evidence + +The language-neutral coordinator +[`tools/conformance/run.mjs`](../../tools/conformance/run.mjs) executes native +TypeScript and Python implementations and compares portable machine output. The +shared evidence currently includes: + +- [`run-created.event.json`](../../spec/conformance/run-created.event.json) for + the common event envelope; +- [`checkpoint-basic.json`](../../spec/conformance/checkpoint-basic.json) for + canonical checkpoint bytes, content hash, restart load, and summary shape; +- [`durable-json.case.json`](../../spec/conformance/durable-json.case.json) for + every tagged value kind, exact float bits, Unicode code-point ordering, and + malformed/noncanonical negatives; +- [`durable-resume.case.json`](../../spec/conformance/durable-resume.case.json) + for committed-success reuse, interrupted-attempt advancement, stable activity + identity, dependent input reconstruction, and terminal idempotence; and +- [`strict-rfc3339.case.json`](../../spec/conformance/strict-rfc3339.case.json) + for strict shared timestamp acceptance. + +The coordinator compares memory and JSONL CAS versions, empty CAS, inclusive +reads, restart persistence, checkpoint hashes, unsafe-ID codes, durable executor +calls and attempts, reused/interrupted node lists, activity-key stability, and +terminal resume. It also exchanges terminal histories in both directions so +each runtime consumes histories produced by the other. + +Package-local suites provide deeper fault evidence: + +- TypeScript persistence corruption and atomicity tests live under + [`packages/persistence/test`](../../packages/persistence/test), with durable + crash and semantic-forgery tests in + [`durable.test.ts`](../../packages/runtime/test/durable.test.ts). +- Python persistence tests live in + [`test_event_store.py`](../../python/tests/test_event_store.py) and + [`test_checkpoint_store.py`](../../python/tests/test_checkpoint_store.py), + with durable crash and history tests in + [`test_durable_scheduler.py`](../../python/tests/test_durable_scheduler.py). + +This evidence proves parity only for the named local observations. It does not +exercise two OS processes, lease expiry, fencing, SQLite, PostgreSQL, S3, +artifact loss, approval races, replay/fork lineage, distributed workers, or a +hostile storage administrator. + +## 11. Explicitly unimplemented capabilities + +The following boundaries are release-protective. Schema vocabulary, roadmap +text, an internal mutex, or a standalone storage interface is not evidence that +the operational capability exists. + +| Capability | Current reality | Required before claiming it | +| --- | --- | --- | +| Leases | Not implemented | Versioned ownership, expiry/renewal, monotonic fencing token, stale-owner tests, and defined clock/partition behavior | +| Public `LockManager` | Not implemented | Cross-process provider contract, lease/fencing integration, structured failures, and both-language adapters | +| `ArtifactStore` | Not implemented | Content/identity schema, atomic publication, authorization, garbage collection, corruption handling, and recovery fixtures | +| SQLite store | Not implemented | Transactional EventStore/checkpoint/artifact design, migrations, concurrent-process CAS, crash tests, and parity | +| PostgreSQL store | Not implemented; planned production adapter | Transaction/isolation contract, migrations, connection failure handling, lease/fencing integration, and multi-host tests | +| S3 artifact/checkpoint store | Not implemented; planned production adapter | Object consistency/version contract, integrity metadata, atomic publish protocol, retry policy, and fault tests | +| Scheduler checkpoint acceleration | Specified but not integrated | Prefix/projection validation, stale/ahead/corrupt fallback warnings, suffix fold, and equivalence tests against full replay | +| Replay | Explicitly outside durable v1alpha1 | Recorded-activity policy, deterministic decision reuse, lineage, new-run identity, and conformance corpus | +| Fork | Explicitly outside durable v1alpha1 | Parent/history reference, fork point rules, mutable inputs/implementation policy, lineage events, and parity tests | +| Approval/reconciliation workflow | Not implemented | Durable request/decision events, authorization, stale-decision handling, timeout/escalation, CLI/API callback, and audit tests | +| Non-idempotent confirmation | Not implemented | The approval protocol above; current behavior always fails closed with `IN_DOUBT_SIDE_EFFECT` | +| Distributed workers | Not implemented | Worker protocol, queue/claim semantics, leases, heartbeats, fencing, ownership transfer, cancellation, and chaos suite | + +There is likewise no universal exactly-once effect guarantee, repair service, +compaction/retention engine, at-rest encryption layer, or cryptographic history +signature. PostgreSQL, S3, and distributed-worker completion belongs primarily +to the later production-storage milestone; their absence must not be hidden by +calling the Day 9 local slice “production durable.” + +## 12. Day 9 exit gates + +The master plan's literal Day 9 outcome—“successful nodes never rerun”—is green +for a valid, authoritative local immutable-DAG history. The broader Day 9 +deliverable remains **Partial, strong local-DAG slice**, matching the +[coverage matrix](../delivery/master-plan-coverage-matrix.md), because storage, +checkpoint, coordination, artifact, and intervention extensions remain open. + +### Frozen local-slice gate + +| Gate | State | Evidence required to preserve the state | +| --- | --- | --- | +| Event stream is the only recovery authority | Green | Normative specs, full semantic fold, corruption tests | +| Last-sequence CAS and strict read contract match across languages | Green locally | Shared persistence report plus memory/JSONL package tests | +| Attempt claim commits before executor invocation | Green | Start/crash tests must observe `NodeStarted` before handler entry | +| Success and edge emissions commit before dependent release | Green | Blocking-store tests and durable history validation | +| A committed successful node is never rerun on resume | Green | Shared durable-resume fixture and terminal/interrupted crash tests | +| Terminal resume is read-only and idempotent | Green | Zero new events, checkpoint writes, and executor calls | +| Exact finite JSON identity is portable | Green for Tagged Durable JSON v1alpha1 | Shared valid/invalid codec corpus and bidirectional history exchange | +| Open effects fail safely | Green for declared local policy | Stable key reuse for idempotent work; `IN_DOUBT_SIDE_EFFECT` otherwise | +| Standalone file checkpoint integrity is portable | Green as a storage primitive | Shared content hash, restart, atomic replace, corruption, and order tests | + +Any change to event ordering, event data shape, activity-key composition, +attempt accounting, terminal projections, checkpoint hashing, or error +precedence must update the canonical specification and shared fixtures before +either language implementation diverges. + +### Gates still required for complete Day 9 delivery + +Day 9 cannot be marked fully Green until all of the following have durable +evidence: + +1. A versioned extension specification defines lease/fencing ownership, + `LockManager`, `ArtifactStore`, checkpoint acceleration, replay/fork lineage, + and approval/reconciliation semantics without weakening event authority. +2. Native TypeScript and Python implementations expose equivalent SQLite, + locking, artifact, checkpoint-accelerated recovery, replay/fork, and approval + behavior with stable structured failures. +3. Checkpoint fault injection proves missing, stale, corrupt, ahead-of-tail, and + inconsistent caches always fall back to the same event-derived result. +4. Crash injection covers every authoritative batch boundary, retry + reservation, terminal commit, artifact publication, approval decision, and + external-effect ambiguity. +5. A real dual-coordinator suite proves one owner through lease expiry, + renewal, fencing, takeover, stale-worker writes, and process death; CAS-only + tests do not satisfy this gate. +6. Replay and fork fixtures prove lineage, recorded-versus-reexecuted activity + behavior, deterministic results under fakes, and terminal/history parity. +7. Non-idempotent recovery has an auditable authorized decision path instead of + either silent reinvocation or an unrecorded manual workaround. +8. The cross-language coordinator and package-local suites are green for every + new provider, with docs and capability matrices naming both guarantees and + non-guarantees. + +PostgreSQL, S3, and distributed workers retain their later production-storage +exit gates even after local Day 9 is complete. Until those gates pass, operators +must run one coordinator against private local storage, stop it before resume, +and treat every external activity as at-least-once. diff --git a/codex_plans/architecture/runtime-semantics.md b/codex_plans/architecture/runtime-semantics.md new file mode 100644 index 0000000..7a360e5 --- /dev/null +++ b/codex_plans/architecture/runtime-semantics.md @@ -0,0 +1,520 @@ +# Runtime architecture and implementation boundary + +Status: implementation-aligned planning baseline, 2026-07-26 + +This document explains how the runtime pieces that exist today fit together and +where their boundaries are. It is not a replacement for the protocol. The +normative contracts remain [runtime semantics](../../spec/runtime-semantics.md), +[pipeline semantics](../../spec/pipeline-semantics.md), +[primitive semantics](../../spec/primitives-semantics.md), +[persistence semantics](../../spec/persistence-semantics.md), and +[durable recovery semantics](../../spec/durable-recovery-semantics.md). If this +architecture note and a versioned specification disagree, the specification +wins and this note must be corrected. + +The purpose of this boundary document is to prevent three dangerous capability +inflations: + +1. a deterministic evaluator must not be advertised as scheduler integration; +2. an in-memory item pipeline must not be advertised as Graph IR streaming; +3. local event-sourced continuation must not be advertised as distributed or + exactly-once execution. + +The delivery schedule is owned by the +[21-day master plan](../Graph-Engineering-21-Day-Master-Plan.md), while its +evidence status is tracked in the +[master-plan coverage matrix](../delivery/master-plan-coverage-matrix.md). + +## Capability labels used here + +| Label | Meaning | +| --- | --- | +| **Implemented local slice** | Public TypeScript and Python code exists, is bounded, and participates in shared conformance for the stated slice. | +| **Pure evaluator** | Deterministic library logic exists, but it neither waits for work nor changes graph scheduling. | +| **Specified, not integrated** | A normative contract exists, but the main graph scheduler does not expose the complete behavior. | +| **Planned** | The roadmap describes the capability, but current users must not rely on it. | +| **Explicitly excluded** | The current contract deliberately declines the guarantee, usually because a later protocol/storage revision is required. | + +“Implemented” in this document never means production-hard distributed +operation. It means the exact local, in-memory or local-filesystem boundary +named in the row. + +## Runtime topology at a glance + +```mermaid +flowchart LR + IR[Graph IR v1alpha1] --> C[Compile and validate] + C --> S[Local DAG ready queue] + S --> E[Caller-supplied executors] + E --> V[Portable JSON boundary] + V --> S + S --> R[Run result] + + P[Standalone item source] --> Q[Bounded pipeline queues] + Q --> PS[Ordered per-item stages] + PS --> D[Terminal item delivery] + + B[Settled barrier input] --> BE[Pure barrier evaluator] + RT[Route request] --> RE[Pure route evaluator] + + IR --> DR[Durable start or resume] + DR --> ES[Append-only event store] + ES --> DR + DR --> E +``` + +These are four related surfaces, not one hidden mega-runtime: + +- `runGraph` / `run_graph` is the local, non-persistent DAG scheduler. +- `runPipeline` / `run_pipeline` is a standalone bounded item-flow engine. +- settled barrier and route selection APIs are pure evaluators. +- durable start/resume is a separate event-sourced continuation path for one + immutable compiled DAG. + +There are no implicit calls from the local DAG scheduler into pipeline, +primitive, durable, provider, or distributed-worker subsystems. + +## 1. Local DAG scheduler + +### Implemented execution shape + +The TypeScript implementation is rooted in +[scheduler.ts](../../packages/runtime/src/scheduler.ts); the Python equivalent +is [scheduler.py](../../python/src/graph_engineering/scheduler.py). Both compile +before executing, reject invalid graphs, copy values across a portable JSON +boundary, and return structured run and node results. + +```mermaid +stateDiagram-v2 + [*] --> Compile + Compile --> Rejected: invalid graph + Compile --> Ready: valid graph + Ready --> Running: concurrency slot and budget + Running --> Succeeded: valid output + Running --> RetryWait: retryable failure and budget + RetryWait --> Ready: bounded delay expires + Running --> Failed: terminal failure + Ready --> Skipped: upstream failure or cancellation + Succeeded --> Ready: required inputs now satisfied + Succeeded --> AssembleOutput: all named output nodes succeeded + AssembleOutput --> [*] + Failed --> [*]: all runnable work settled + Skipped --> [*]: all runnable work settled +``` + +This diagram shows the scheduler mechanism, not a new protocol state machine. +The conceptual state vocabulary remains in the normative runtime specification. +The current public result projection intentionally exposes only terminal node +statuses `succeeded`, `failed`, and `skipped`. Internal ready, running, and +retry-wait phases are not a stable public introspection API. + +The current run result exposes `succeeded`, `failed`, or `cancelled`. Although +the protocol state model reserves `paused`, the public local scheduler has no +operational pause API. Durable `resume` means continuing a persisted history +after interruption; it is not an implementation of interactive pause/unpause. + +### Readiness and determinism invariants + +- Every declared entrypoint is an independent root and receives the graph + input. An entrypoint with an incoming edge is rejected during compilation. +- Every node must be reachable from an explicit entrypoint. The scheduler does + not infer accidental roots. +- A node becomes ready only after all required incoming producers have + succeeded. There are no implicit topological-layer barriers. +- Simultaneously ready nodes are considered in `GraphSpec.nodes` declaration + order. Concurrency can change completion order, but not the tie-break rule. +- `maxConcurrency` bounds active node attempts. Retry delays and settled nodes + do not consume an active attempt slot. +- A producer result is copied and validated before it can be bound downstream. + A sibling cannot mutate another sibling's view of the same result. +- A failed producer causes its dependent, not-yet-started descendants to settle + as structured `UPSTREAM_FAILED` skips. Independent branches may continue. +- The scheduler never replaces a failed branch with `null`. JSON `null` remains + valid data and is distinguishable from a missing binding. +- Graph output is assembled solely from named endpoint bindings. An endpoint + port selects a property; absence becomes a structured output-binding failure. + +For a non-entry node, incoming values form a mapping. A target port is the key +when present; otherwise the source node ID is the key. A source port selects a +property before binding. Colliding input keys fail rather than silently +overwrite one another. + +### Attempt, retry, and timeout bounds + +Retries are finite and remain part of one logical node. A retry is possible +only while the node policy allows another attempt and the graph-wide total +attempt budget has capacity. Backoff and per-attempt timeouts use bounded +values validated by the compiler/runtime boundary. Exhaustion produces a +structured terminal failure rather than a hidden extra attempt. + +The stable failure surface currently includes executor lookup, executor +failure, timeout, cancellation, invalid portable output, upstream failure, +input binding, output binding, total-attempt exhaustion, and interrupted +durable attempts. Exact codes and field-presence rules belong to the normative +specifications and conformance fixtures; this architecture note deliberately +does not duplicate their schema. + +### Cancellation boundary + +Cancellation is cooperative: + +1. once caller cancellation is observed, the scheduler stops intentionally + dispatching new attempts; +2. active handlers receive the language-native cancellation signal; +3. active nodes that do not complete successfully settle with structured + cancellation failure information; +4. nodes that never start settle as structured skips; and +5. caller cancellation has run-status precedence over ordinary branch failure. + +An asynchronous handler can observe cancellation promptly. Arbitrary +synchronous user code cannot be forcibly preempted by either runtime. Its +external side effects may continue even after the orchestrator has stopped +waiting, so application code must use cooperative asynchronous boundaries and +idempotency where effects are involved. + +### Executor and validation boundary + +Executors are capabilities supplied by the caller. The local runtimes do not +ship a model-provider registry, invoke an LLM by node kind, or infer tool access. +TypeScript provides deterministic identity defaults for transform and barrier +node kinds; this convenience does not turn a barrier node into a quorum-aware +wait primitive. + +Values crossing graph input, bound node input, retry attempt input, executor +output, edge binding, and public graph output are finite detached JSON. This +rejects language-only values, aliases, cycles, non-finite numbers, unsafe +integers, and mutation leaks. It is structural portable-JSON validation, not +runtime enforcement of arbitrary node `inputSchema` / `outputSchema` JSON +Schema declarations. + +### Scheduler backpressure boundary + +For the DAG scheduler, backpressure means only bounded active attempts through +`maxConcurrency` and bounded total attempts. Each node produces one terminal +result. The ready queue is in memory, and there is no byte-based queue limit, +item credit, stream offset, acknowledgement, or durable queue spill. Those +concepts belong to future Graph IR stream execution, not this scheduler. + +## 2. Standalone bounded pipeline + +The current pipeline implementation is +[pipeline.ts](../../packages/runtime/src/pipeline.ts) and +[pipeline.py](../../python/src/graph_engineering/pipeline.py). Its complete +contract is frozen in [pipeline semantics](../../spec/pipeline-semantics.md). + +The factory is synchronous and lazy: it validates and snapshots stage/options +configuration without advancing the source. A run is single-pass and has one +consumer. Once consumption begins, items can overlap across stages while every +individual item still visits its stages in declaration order. + +```mermaid +flowchart LR + C[Acquire global credit] --> P[Pull one source item] + P --> Q0[Boundary queue 0] + Q0 --> S1[Stage 1] + S1 --> Q1[Boundary queue 1] + Q1 --> S2[Stage 2] + S2 --> T[Terminal item result] + T --> O[Consumer delivery] + O --> C +``` + +### Pipeline invariants + +- `accepted - emitted` never exceeds `maxInFlight`; credit is obtained before + pulling the next source item. +- Every inter-stage queue is bounded by `bufferCapacity`; a running handler is + not counted as queued. +- `maxItems` is a hard intake budget. At the limit the engine does not perform + a speculative extra source read. +- Total handler attempts are bounded by `maxItems` multiplied by the sum of + each stage's configured maximum attempts. +- A fast later item can enter a downstream stage while a slow earlier item is + still upstream. There is no whole-stage barrier. +- Ordered delivery changes terminal emission order only; it does not serialize + internal execution. +- Every accepted item becomes `succeeded`, `failed`, `dropped`, or `cancelled`. + Structured failure data is preserved, and `inputBound` distinguishes valid + JSON `null` from absence. +- Only execution failure and timeout are retry candidates. Retry delay is + bounded, cancellable, and consumes no stage concurrency slot. +- `dead-letter`, `drop`, and `stop` are explicit terminal policies. `stop` + prevents further intentional intake while already accepted work drains. +- Consumer delivery releases global credit; an open consumer that stops + reading without closing can intentionally keep completion pending. + +### Pipeline cancellation and cleanup + +Caller cancellation or explicit consumer close stops source intake, wakes +queue and retry waiters, signals active handlers, and accounts for every +accepted unfinished item. No new handler attempt starts after cancellation is +observed. Source and iterator cleanup is explicit and errors during cleanup do +not erase the first run-level failure. + +As with the graph scheduler, synchronous source iteration and synchronous +handler code cannot be preempted. Non-cooperative asynchronous outcomes are +detached and observed so late rejection does not become an unhandled failure, +but cancellation cannot roll back an external action already committed. + +### Pipeline summary precedence + +The completion summary is cancelled when caller cancellation or consumer close +wins. Otherwise a run-level failure or failed item makes it failed, then a +dropped item makes it failed, and only a fully successful drain is succeeded. +Counts reconcile accepted items with terminal statuses after a normal drain or +cancelled cleanup. + +### Deliberate separation from Graph IR + +The pipeline is **not** a lowering of `edge.mode: "stream"`. If a caller invokes +it inside a graph executor, the entire pipeline is one graph-node attempt. A +process crash can therefore replay that whole attempt, and inner stage attempts +do not consume `runGraph`'s graph-wide attempt budget. The current pipeline has +no persisted item identities, offsets, acknowledgements, queue recovery, +windows, joins, materializing barrier, replay, or fork. + +External effects executed by a stage are at-least-once under retry. The engine +does not promise exactly-once delivery or exactly-once side effects. + +## 3. Barrier and router evaluators + +TypeScript exposes the pure implementations in +[barrier.ts](../../packages/primitives/src/barrier.ts) and +[router.ts](../../packages/primitives/src/router.ts); Python exposes +[barrier.py](../../python/src/graph_engineering/primitives/barrier.py) and +[router.py](../../python/src/graph_engineering/primitives/router.py). + +### Settled barrier + +The evaluator consumes an already-settled collection and a deterministic +`all`, `minimum`, or `percentage` policy. It returns whether the threshold is +satisfied plus accepted/rejected/missing evidence and a stable reason. It does +not wait for unfinished work, create a timer, cancel laggards, schedule nodes, +or mutate item status. + +Consequently, current support is a **pure evaluator**, not scheduler-integrated +barrier execution. A full runtime barrier still needs arrival accounting, +quorum/deadline behavior, missing-item policy, cancellation behavior, durable +decision events, and recovery rules. + +### Route selection + +The route evaluator validates an already-produced classifier request against a +declared allowlist/policy and deterministically returns selected route keys. It +does not invoke a classifier, execute an edge, or schedule a destination. + +The local ready-queue scheduler currently does not lower `edge.condition`, does +not apply the evaluator result to graph topology, and does not record/replay a +route-selection event. Routing therefore remains evaluator-complete but +scheduler-incomplete. + +Both evaluators are model-free, tool-free, clock-free, storage-free, and +network-free. They return detached immutable/immutable-style portable results; +judgment belongs in a caller-provided model node upstream of the evaluator. + +## 4. Durable continuation and recovery + +The durable entrypoints are separate from `runGraph`: TypeScript uses +[durable.ts](../../packages/runtime/src/durable.ts), while Python uses +[durable.py](../../python/src/graph_engineering/durable.py). Start and resume are +distinct operations. A missing history is never silently treated as start, and +an existing history is never silently restarted. + +The current recovery unit is one immutable, compiled DAG revision. `RunCreated` +binds graph hash, input, implementation identity, and execution budgets. Tagged +Durable JSON preserves finite portable JSON, including exact binary64 values, +without weakening checkpoint-safe numeric rules. + +### Event-sourced state machine + +```mermaid +sequenceDiagram + participant O as Orchestrator + participant E as Event store + participant X as Executor + O->>E: commit NodeScheduled + NodeStarted + E-->>O: append accepted + O->>X: run attempt with activity key + X-->>O: result or structured failure + O->>O: validate portable output + O->>E: commit NodeSucceeded / retry / terminal settlement + E-->>O: append accepted + O->>O: release newly-ready dependants +``` + +The event stream is the source of truth. The central invariant is +**commit-before-release**: a successful result cannot make dependants ready +until its success and emitted values are durably appended. A deterministic +settlement without an executor attempt is likewise appended before downstream +readiness changes. + +An append failure latches a durability failure; the in-memory outcome is not +reported as durable success. A compare-and-swap loss becomes +`RESUME_CONFLICT` and is never blindly retried. The process that loses the race +must stop coordinating the run, although already-dispatched external work may +still require idempotency protection. + +### Resume boundary + +Resume reads, validates, and folds the complete history before it calls an +executor. It appends a resume record before continuing. Previously successful +nodes are reused and never re-executed. A valid terminal history is returned +exactly as recorded, without a new event, checkpoint write, or executor call. + +An open attempt found after process loss has an unknown external outcome: + +- `sideEffects: "none"` may retry within its original budgets; +- `sideEffects: "idempotent"` may retry with the same stable activity key; and +- `sideEffects: "non-idempotent"` or an omitted declaration fails closed as an + in-doubt side effect. + +This is at-least-once activity execution, not universal exactly-once effects. +A crash after an external system commits but before `NodeSucceeded` is appended +is inherently ambiguous unless that system honors the activity key or an +application-specific approval/reconciliation flow resolves it. + +### Storage and checkpoint boundary + +Memory and local JSONL event stores implement contiguous per-run sequences, +compare-and-swap append, strict corruption detection, and private local storage +assumptions. File checkpoints are atomic and content-hashed caches. Scheduler +correctness currently comes from full event-history folding; checkpoint-based +recovery acceleration is not integrated. + +Local serialization is within one process. CAS is not a distributed lease, and +the old coordinator must be stopped before another resumes a run. Multi-process +or multi-host workers require leases, fencing tokens, heartbeats, ownership +transfer, and a production database/object-store contract that do not exist in +this slice. + +## 5. Cross-language evidence + +The language-neutral coordinator is +[tools/conformance/run.mjs](../../tools/conformance/run.mjs). It launches native +TypeScript and Python reporters and compares portable observations, not stack +traces, class names, or uncontrolled wall-clock order. + +| Implemented slice | Shared evidence | +| --- | --- | +| Compile/canonical DAG | [diamond graph](../../spec/conformance/diamond.graph.json) and negative graph fixtures under `spec/conformance/` | +| Ready queue without layer barrier | [runtime ready-queue case](../../spec/conformance/runtime-ready-queue.case.json) | +| Invalid output and cancellation | [invalid-output case](../../spec/conformance/runtime-invalid-output.case.json) and [cancellation case](../../spec/conformance/runtime-cancellation.case.json) | +| Settled barrier and route selection | [barrier case](../../spec/conformance/settled-barrier.case.json) and [route case](../../spec/conformance/route-selection.case.json) | +| Local persistence | [checkpoint vector](../../spec/conformance/checkpoint-basic.json), [event vector](../../spec/conformance/run-created.event.json), and strict timestamp cases | +| Durable recovery/interchange | [durable resume case](../../spec/conformance/durable-resume.case.json) and [Durable JSON case](../../spec/conformance/durable-json.case.json) | +| Standalone bounded pipeline | [pipeline case](../../spec/conformance/pipeline.case.json) | + +Current conformance joins exercise canonical compile output, ready-queue +ordering, invalid output, cancellation, pure primitives, persistence, durable +continuation, terminal-history interchange, and bounded pipeline observations. +Package-local tests add fault injection and language-specific API checks. + +Passing these fixtures proves parity only for the named observations. It does +not prove production reliability, exhaustive schedules, provider correctness, +distributed safety, future feature support, performance, or popularity. + +## 6. Failure and recovery boundaries + +| Boundary | Current guarantee | Outside the guarantee | +| --- | --- | --- | +| Executor failure | Structured terminal/retry outcome within bounded policy | Rollback of already committed external effects | +| Invalid value | Rejected at portable JSON/binding boundary | Full runtime JSON Schema enforcement | +| Cancellation | No intentional new attempt after observation; cooperative signal and complete accounting | Force-stopping synchronous code or undoing side effects | +| DAG overload | Active attempts and total attempts are bounded | Stream/byte queue pressure and durable spill | +| Pipeline overload | Global item credit and every boundary queue are bounded | Durable queues or cross-process consumers | +| Local crash in `runGraph` | No recovery promise | Any implicit conversion to durable mode | +| Crash in durable run | Full event fold, committed success reuse, side-effect safety gate | Exactly-once effects, replay/fork, distributed takeover | +| Event/checkpoint corruption | Strict failure; never skip corrupt records silently | Automatic repair of torn/corrupt local state | +| Concurrent resume | CAS conflict fails closed | Lease-based coordination or fencing of stale workers | + +Failures remain values or events at every boundary. Missing work, rejected work, +timeout, cancellation, and in-doubt side effects must never be collapsed into an +unexplained `null` or an apparently successful empty result. + +## 7. Explicitly not implemented + +The following table is release-protective. Documentation, examples, CLI output, +and marketing must not imply these capabilities until their freeze gate is +green in both languages. + +| Capability | Current status | Required completion evidence | +| --- | --- | --- | +| Graph IR stream-edge lowering | Not implemented | Versioned item identity, offsets/acks, bounded durable queues, recovery and cross-language fixtures | +| Stream joins, windows, materializing barriers | Not implemented | Explicit ordering/watermark/failure semantics plus crash matrix | +| Runtime `edge.map` and `edge.condition` | Not implemented | Deterministic lowering, validation, events, resume/replay parity | +| Scheduler barrier quorum/deadline | Pure evaluator only | Arrival state machine, missing/timeout policy, event history and fixtures | +| Scheduler route application | Pure evaluator only | Route event, selected-edge scheduling, unselected-branch settlement and recovery | +| Explicit cycles and `untilDry` | Rejected/not implemented | Bounded convergence contract, seen-set semantics, budgets and durable iteration identity | +| Dynamic graph patches/fan-out | Not implemented | Revision lineage, authorization, hard cardinality/depth/attempt limits and replay | +| Verifiers, judge panels, reflection, citation gates | Not implemented runtime primitives | Typed policies, evidence lineage, deterministic aggregation and adversarial fixtures | +| Unknown/abstain and human approval gates | Not implemented | Durable decision state, callbacks/CLI/API, timeout/escalation and audit trail | +| Worktree/process/container isolation | Not implemented | Capability policy, filesystem ownership, cleanup, merge conflict and threat model tests | +| Provider/model/tool adapters | Not implemented | Explicit capability injection, error taxonomy, record/replay and secret-redaction tests | +| Per-node model routing and token/money budgets | Not implemented | Provider-neutral accounting, hard enforcement, overflow/failure semantics and fixtures | +| Runtime JSON Schema node I/O validation | Not implemented | Shared validator profile and error-path parity; portable JSON checks remain active | +| Operational pause/unpause | Not implemented | Public state/API, safe-point definition, durable event semantics and cancellation interaction | +| Scheduler checkpoint acceleration | Specified, not integrated | Checkpoint validation/fallback fault matrix proving event stream remains authoritative | +| Replay and fork | Explicitly excluded from current durable revision | Lineage schema, recorded-activity behavior and deterministic comparison suite | +| Distributed workers | Not implemented | Worker protocol, leases, heartbeats, fencing, ownership transfer and chaos tests | +| PostgreSQL/S3 production stores | Not implemented | Adapter contract, migration, cross-process races, corruption and availability tests | +| OpenTelemetry/live Explorer/time travel | Not implemented | Opt-in privacy policy, stable event projection, redaction and bounded retention | + +Telemetry and prompt/response capture remain off by default even after +observability surfaces arrive. External side effects remain at-least-once and +must be idempotent or explicitly approved; no topology can waive that rule. + +## 8. Freeze points and change control + +The master plan names five architectural freeze points: + +| Day | Freeze point | Interpretation for the current repository | +| --- | --- | --- | +| 2 | Canonical IR | Graph v1alpha1 schema, canonical form, entrypoint/output decisions, and diagnostics are the baseline for implemented DAG work. | +| 6 | State machines | Current local scheduler and pure primitive behavior must remain cross-language aligned; unintegrated future states are not claimed. | +| 9 | Event/checkpoint semantics | Local persistence and durable continuation use the event stream as authority; checkpoint acceleration and distributed locks remain outside the gate. | +| 14 | Public alpha API | Names, exports, documentation, examples, and structured failure shapes require compatibility review before promotion. | +| 19 | Complete feature freeze | Applies only when all planned capability/evidence rows are green; this repository has not reached that full-product gate merely because the current slices pass. | + +The standalone pipeline has its own active v1alpha1 contract and conformance +slice. That freezes the bounded in-memory API behavior; it does not pre-approve +Graph IR streaming or durable item-flow design. + +Any observable runtime change must be reviewed in this order: + +1. decide whether the existing specification already defines the behavior; +2. update the canonical `spec/` contract first when protocol behavior changes; +3. add or update a shared fixture that isolates the observation; +4. implement matching TypeScript and Python behavior; +5. run focused package tests and the full cross-language coordinator; +6. update user documentation and the capability/coverage matrices; and +7. record an ADR when the change affects an established architectural boundary. + +A one-language implementation, a unit test without a shared fixture, or a +roadmap checkbox is insufficient to advance a capability label. Until every +required layer is present, documentation must use “planned,” “pure evaluator,” +or “specified, not integrated” with the missing boundary named explicitly. + +## 9. Acceptance checklist for future runtime work + +A runtime feature is ready to move from planned to implemented only when all of +the following are true: + +- its input, output, failure, cancellation, retry, and recovery state machines + are bounded and versioned; +- deterministic plumbing stays in code while model judgment remains an + explicit executor capability; +- no failure or missing branch is silently converted to `null`; +- concurrency, fan-out, retry, item/byte buffering, time, and cost limits are + explicit where relevant; +- side-effect replay behavior and idempotency/approval requirements are stated; +- crash windows are enumerated and tested at every authoritative commit point; +- TypeScript and Python expose equivalent portable observations; +- shared conformance fixtures and package-local negative tests are green; +- public documentation states both the guarantee and its non-guarantees; and +- telemetry, prompts, responses, secrets, and artifacts remain private by + default unless the caller opts in. + +This checklist is intentionally stricter than “the happy path runs.” The +project's runtime architecture is credible only when users can see exactly +which graph shape is executable, what survives a crash, and where responsibility +returns to the caller. diff --git a/codex_plans/architecture/security-and-isolation.md b/codex_plans/architecture/security-and-isolation.md new file mode 100644 index 0000000..6eaf491 --- /dev/null +++ b/codex_plans/architecture/security-and-isolation.md @@ -0,0 +1,909 @@ +# Security and isolation architecture + +Status: current-alpha boundary and target-v1 gate plan, 2026-07-26 + +Graph Engineering coordinates code that may read files, call networks, spend +provider budget, mutate external systems, and persist sensitive results. The +orchestrator is therefore part of the security boundary, but the current alpha +is **not** a sandbox and is **not production hardened**. + +The public [security policy](../../SECURITY.md) governs vulnerability reporting +and supported versions. The detailed [security guide](../../docs/SECURITY.md) +defines operator guidance and target-v1 principles. This planning document maps +those principles to current code, attack cases, review controls, and the Day 12, +Day 16, and Day 20 exit gates. It does not turn a target into an implemented +feature. + +Delivery authority remains the +[21-day master plan](../Graph-Engineering-21-Day-Master-Plan.md), the +[coverage matrix](../delivery/master-plan-coverage-matrix.md), the +[ownership/review map](../delivery/agent-ownership-map.md), and the +[stable-v1 release checklist](../delivery/release-checklist.md). The coverage +matrix correctly marks security/isolation **Open** and the security preflight +**Partial**. + +## 1. Claim vocabulary + +| Label | Meaning | +| --- | --- | +| **Implemented control** | Code and tests provide the narrowly stated behavior in the current alpha. | +| **Risk reduction** | Useful validation or bounding exists, but it is not an authorization or containment boundary. | +| **Deployment responsibility** | The caller/operator must enforce the control outside Graph Engineering today. | +| **Target v1** | Required design or release gate with no complete accepted implementation yet. | +| **Release blocker** | Stable v1 is prohibited until candidate-bound evidence satisfies the named acceptance test. | + +The phrases “declared,” “hashed,” “bounded,” “read-only,” and “redacted” do not +automatically mean “authorized,” “authenticated,” “isolated,” or “secret-safe.” +Each guarantee below names the exact layer that enforces it. + +## 2. Current ambient-authority boundary + +The most important current fact is simple: + +> A node executor has the ambient authority of the host process that runs it. + +TypeScript `NodeExecutor` functions run in the orchestrator's Node.js process. +Python handlers run in the orchestrator's Python process. Unless the application +adds an external sandbox, executor code can read the process environment, use +the host filesystem, open network connections, launch subprocesses, access +locally available credentials, and interfere with other in-process work. + +Graph IR fields named `resources` and `isolation` are opaque declarations. +`sideEffects` is a caller-supplied assertion used for one durable recovery +decision; the runtime does not verify that a supposedly idempotent remote action +really is idempotent. A metadata label advertising a runtime capability is not a +grant and is not enforced by the scheduler. + +Timeout and cancellation are cooperative. They bound how long the orchestrator +intentionally waits or schedules, but cannot forcibly stop arbitrary synchronous +code or undo an external mutation. A timed-out executor may keep running and may +commit an effect after the run has already reported a timeout. + +There is no current built-in provider, shell, filesystem, tool, or mutating MCP +adapter. That keeps the package-owned attack surface smaller, but it does not +restrict custom executor code. Applications must run only reviewed executors +under an operating-system identity whose existing permissions are acceptable. + +### Current-versus-target authority flow + +```mermaid +flowchart LR + U[Untrusted Graph IR and input] --> C[Compiler and portable JSON checks] + C --> S[Local scheduler] + S --> X[Caller executor in same host process] + X --> ENV[Ambient environment and secrets] + X --> FS[Host filesystem and subprocess] + X --> NET[Network and external accounts] + S --> E[Local events and results] + + OP[Target operator policy] -. not implemented .-> CAP[Target capability intersection] + CAP -. not implemented .-> ISO[Target worktree/process/container boundary] + ISO -. future narrow access .-> ENV + ISO -. future narrow access .-> FS + ISO -. future narrow access .-> NET +``` + +The dotted target path is required work. It must not be inferred from the +current `resources`, `isolation`, `sideEffects`, node kind, or metadata labels. + +## 3. Security objectives and non-goals + +Target v1 needs the following properties: + +1. least authority per node and per external account; +2. policy decisions outside model/tool output; +3. hard, compositional execution/resource limits; +4. isolation between parallel writers and untrusted processes; +5. durable, attributable approval and effect evidence; +6. data minimization and redaction before any sink; +7. honest at-least-once external-effect semantics; and +8. recovery that cannot change graph, capability, or approval identity. + +Even a completed v1 must not claim that arbitrary generated code, third-party +plugins, MCP servers, provider SDKs, containers, or external services are +trustworthy. Container isolation is not a complete boundary when privileged +flags, host sockets, broad mounts, credentials, or a vulnerable kernel are +exposed. + +## 4. Assets + +Security review protects confidentiality, integrity, and availability across +these asset classes: + +| Asset | Confidentiality concern | Integrity/availability concern | +| --- | --- | --- | +| Source repositories and worktrees | Private code, unreleased changes, embedded credentials | Unauthorized edits, destructive cleanup, false merge, lost work | +| Environment and secret material | Provider/API tokens, cloud credentials, signing keys, SSH agents | Credential replacement, scope expansion, accidental rotation/revocation | +| User/graph data | Prompts, inputs, outputs, retrieved content, customer/PII data | Tampered inputs, forged evidence, cross-tenant disclosure | +| External accounts and services | Account/tenant identifiers and request content | Duplicate mutation, wrong target, irreversible action, rate-limit/ban | +| Provider and compute budgets | Usage and pricing data may be sensitive | Cost exhaustion, unbounded attempts, denial of service | +| Durable histories/checkpoints | Inputs and outputs may be embedded in records | Forged history, rollback, truncation, divergent resume | +| Future artifacts | Reports, patches, binaries, model evidence | Poisoning, substitution, oversized storage, cross-run access | +| Findings and verification evidence | Sensitive vulnerability detail | False approval, omitted rejection, source/citation tampering | +| Release artifacts and package identity | Publishing tokens and private provenance metadata | Dependency/package substitution, unsigned artifact, tag/digest mismatch | +| Operational availability | Runtime topology and failures | Deadlock, process exhaustion, queue growth, stale coordinator activity | + +No graph hash, checkpoint content hash, or event payload hash authenticates an +author. Current SHA-256 values establish content identity/corruption signals, +not permission, provenance, non-repudiation, or resistance to an attacker who +can rewrite the file and recompute the hash. + +## 5. Threat actors and untrusted principals + +The design assumes the following may be malicious, compromised, or simply +wrong: + +- a graph author, graph input producer, or dynamic-planning result; +- prompt content, a model response, retrieved text, webpage, issue, repository + file, generated patch, embedding, or citation; +- an MCP server, tool description, tool response, provider response, or SDK + error payload; +- a custom executor, plugin, dependency, repository hook, build script, or + package lifecycle script; +- another run/node that produces an artifact or shared-state value; +- a stale orchestrator or competing process attempting to resume the same run; +- a local user/process with access to event/checkpoint directories; +- an external service that commits an action before its reply is lost; +- a contributor or compromised CI/dependency/publishing account; and +- a hurried operator or approver who misunderstands the target, diff, or scope. + +The operator policy and reviewed runtime implementation are intended trusted +computing inputs, but they remain fallible and require independent review. Human +approval text copied from an untrusted source is data; only an authenticated, +bound decision from the approval system can authorize an action. + +## 6. Trust boundaries + +### B1 — untrusted document to compiler + +Graph IR, configs, schemas, and inputs cross from caller data into core. Current +portable JSON capture and structural/topology validation reduce parser, +mutation, and unbounded-topology risk. They do not authorize executor actions or +validate every declared JSON Schema. + +### B2 — scheduler to executor + +This is the largest current gap. Scheduling, input snapshotting, attempt bounds, +and cancellation exist, but there is no process/capability boundary. The +executor receives in-process code authority and can bypass Graph IR declarations. + +### B3 — executor to tools, files, network, and secrets + +Graph Engineering currently has no enforcement point here. The host OS, +container, service account, firewall, secret manager, and application wrappers +are deployment controls. Target v1 must mediate access rather than trust an +executor's self-description. + +### B4 — orchestrator to durable storage + +Current local stores validate records, IDs, sequence, and hashes, but use a +private-local-directory threat model. There is no tenant authorization, +encryption, keyed authenticity, cross-process lease, or stale-worker fencing. + +### B5 — MCP host to local read-only server + +The MCP server is a separate local stdio process. It deliberately exposes only +validation, planning, and a package-owned schema. Stdio provides no identity or +authentication; the spawning host decides which model/user can call it. + +### B6 — source to CI, package, and registry + +Actions, dependencies, build tools, package contents, release identities, and +registries cross a supply-chain boundary. Current CI hardening is useful, while +trusted publishing, SBOMs, checksums, attestations, secret/license scans, and +candidate provenance remain open. + +### B7 — proposal to human approval + +No runtime approval system exists. Target v1 must show the exact action and bind +the decision to graph/revision/node/input/capability/diff identity. A prompt +asking “is this okay?” is not an approval boundary. + +## 7. Controls implemented in the current alpha + +### 7.1 Graph structure and portable data + +The canonical compiler in +[`packages/core`](../../packages/core/src/compiler.ts) and the native +[Python compiler](../../python/src/graph_engineering/compiler.py) provide these +risk reductions: + +- reject malformed/unknown closed-envelope fields; +- reject unsafe host values, cycles, sparse arrays, non-finite numbers, and + unsafe integers at portable boundaries; +- reject duplicate node/edge IDs and missing endpoints; +- require explicit roots/outputs and reject unreachable nodes; +- reject implicit graph cycles; +- enforce static max fan-out and max depth; and +- bind accepted content to a canonical graph hash. + +These checks prevent several accidental or malicious malformed graphs from +reaching executors. They do not validate typed ports, arbitrary embedded JSON +Schema, capability declarations, edge maps/conditions, router exhaustiveness, +dynamic patches, state reducers, or executor code. + +### 7.2 Scheduler and pipeline bounds + +The [TypeScript scheduler](../../packages/runtime/src/scheduler.ts) and +[Python scheduler](../../python/src/graph_engineering/scheduler.py) bound active +node attempts with concurrency policy, bound the total attempt count, implement +bounded per-node retry/timeout, preserve structured failures, and propagate +cooperative cancellation. + +The standalone pipelines add finite item intake, global in-flight credit, +bounded boundary queues, per-stage concurrency, bounded retries, and explicit +stop/drop/dead-letter outcomes. Those controls constrain the current local +algorithm; they do not isolate stage handlers or make Graph IR streaming safe. + +Unimplemented resource dimensions include a complete graph-duration hard stop, +dynamic node cardinality, token/money accounting, CPU, memory, process count, +file descriptors, disk bytes, network bytes, and external provider quota. + +### 7.3 Structured failure containment + +Invalid input/output, missing executors, upstream failure, timeout, +cancellation, binding error, attempt exhaustion, and persistence/recovery +failures remain explicit results or events. The runtime never treats a missing +or failed branch as a successful `null` value. + +This protects control-flow integrity and auditability. It does not contain a +malicious process, reverse an effect, or guarantee that an application does not +discard/log the structured detail incorrectly. + +### 7.4 Read-only MCP surface + +The current MCP package is intentionally narrow. Its +[server registration](../../packages/mcp-server/src/server.ts) exposes exactly: + +- `graph_validate`; +- `graph_plan`; and +- `graph_get_schema` plus one fixed schema resource. + +There is no graph execution, runtime state, persistence, arbitrary file path, +network, provider, shell, subprocess, MCP proxy, or mutation tool. The handlers, +not the client-facing annotations, are the safety property. `readOnlyHint` and +`destructiveHint: false` are informative metadata only. + +The [bounded compiler worker](../../packages/mcp-server/src/limits.ts) limits a +submitted graph to 524,288 serialized UTF-8 bytes, 2,048 nodes, 8,192 edges, and +a 2,000 ms compiler deadline. Cancellation/timeout terminates the worker thread. +The fixed schema path performs no network lookup. + +Remaining MCP boundaries are explicit: + +- stdio is not authenticated; +- the host must decide who can launch/call the server; +- the SDK parses JSON-RPC before the application measures graph bytes, so the + bound is not a transport-frame or process-memory limit; +- a worker thread is not an OS sandbox; +- returned identifiers/diagnostics remain untrusted display data; and +- future mutation/runtime tools require a new capability/approval threat review. + +### 7.5 Local persistence integrity and path controls + +The current +[JSONL event store](../../packages/persistence/src/jsonl-event-store.ts) and +[file checkpoint store](../../packages/persistence/src/file-checkpoint-store.ts) +implement: + +- a restrictive run/checkpoint identifier grammar; +- filename derivation from SHA-256 so caller text is not a path segment; +- strict envelopes, UTF-8/JSON parsing, contiguous sequence, and wrong-run + detection; +- expected-version compare-and-swap within documented local assumptions; +- event append fsync and directory sync for newly created streams; +- checkpoint private temporary creation, fsync, atomic rename, directory sync, + and content-hash verification; and +- corruption failure instead of silently skipping a bad record. + +These are durability and accidental-corruption/path-injection controls. They do +not protect a shared directory from a hostile local user, symlink attack, +tampering with recomputed unkeyed hashes, data disclosure, tenant confusion, or +cross-process coordination. Local queues serialize only within one process. + +Operators must place stores in a private directory owned by a least-privileged +runtime account. The file checkpoint store is not currently part of scheduler +recovery, so its hash cannot be presented as an authorization or resume claim. + +### 7.6 Durable recovery and external effects + +The [TypeScript durable runtime](../../packages/runtime/src/durable.ts) and +[Python durable runtime](../../python/src/graph_engineering/durable.py) bind one +immutable graph, original input, implementation identity, and attempt budget to +the event history. They commit an attempt claim before dispatch, commit success +before releasing dependants, fold complete history on resume, and use stable +activity keys. + +After process loss, an open effect-free or declared-idempotent activity may +retry under the original budgets. An omitted or non-idempotent side-effect class +fails closed as `IN_DOUBT_SIDE_EFFECT`. Terminal resume calls no executor. + +This is valuable replay-risk containment, but: + +- initial non-idempotent execution does not pass an approval gate; +- idempotency is trusted metadata and the application must send the activity key + to the external service; +- there is no durable activity ledger, reconciliation, compensation, or approval + callback; +- a crash after remote commit but before `NodeSucceeded` is ambiguous; +- CAS cannot fence a stale coordinator before external work; and +- external effects remain at least once, never universal exactly once. + +### 7.7 Current CI and supply-chain controls + +Repository workflows currently provide: + +- read-only default workflow permissions, with only CodeQL receiving the + required `security-events: write` grant; +- commit-SHA-pinned GitHub Actions and checkout with credential persistence off; +- frozen pnpm/uv lockfile installation in normal CI; +- TypeScript/Python build, type, lint, tests, artifact-content checks, and + cross-language conformance; +- production npm audit at moderate severity; +- pull-request dependency review failing at moderate severity; +- scheduled/pull-request CodeQL for JavaScript/TypeScript and Python; and +- Dependabot for npm, Python, and GitHub Actions. + +These controls are source/CI evidence, not a completed release provenance +chain. No accepted candidate-bound secret scan, Python/system dependency scan, +license inventory, SBOM, checksum manifest, build attestation, trusted npm/PyPI +publishing rehearsal, or independent provenance verification exists yet. + +## 8. Privacy and redaction release blocker + +There is no runtime redaction engine. No built-in telemetry exporter currently +ships, so product telemetry and provider prompt capture are absent by default; +however, node inputs/outputs exist in memory and calling applications can log +them. Durable execution also persists sensitive payloads: + +- `RunCreated.data.input` contains the encoded original graph input; and +- `NodeSucceeded.data.output` contains the encoded successful node output. + +Both native durable journals currently set the event envelope field +`redacted: true` while writing those full values. That flag is **not evidence of +actual redaction**. The mismatch between the flag and stored bytes is a +**release blocker** because a consumer could reasonably treat it as a safety +claim. + +Stable v1 cannot pass `I06`, `T26`, `Q08`, `SC07`, or `SC13` until the semantics +are corrected. The resolution must be explicit: either implement and specify +real pre-sink redaction/capture policy, or change/remove the field/default so it +truthfully describes the payload. Documentation alone is insufficient. + +### Canary-secret acceptance criteria + +For a clean packaged TypeScript run and a clean packaged Python run: + +1. seed unique canary strings separately into graph input, model prompt/mock + input, executor output, thrown error, tool response, artifact content, and + support-bundle/log metadata; +2. run success, retry, timeout, cancellation, durable crash/resume, and failure + paths with capture at its default setting; +3. recursively scan raw event journal bytes, checkpoint bytes, artifact bytes, + stdout, stderr, application/runtime logs, trace-export bytes, error reports, + support bundles, and generated diagnostics; +4. assert that no canary byte sequence or obvious encoded form appears in any + sink that the default policy does not explicitly authorize; +5. where payload capture is explicitly enabled, assert that redaction happens + before persistence/export, that access is separately authorized, and that + the event flag accurately describes the stored payload; +6. record scanner version/config, exact candidate digests, fixtures, raw + machine-readable results, false-positive dispositions, and independent + security review; and +7. include a negative control proving the scanner detects an intentionally + seeded unsafe fixture. + +The hard acceptance statement is: + +> Under default configuration, a canary secret must not occur in journal, +> checkpoint, artifact, log, trace, error-report, or support-bundle bytes. + +If a product contract requires durable node values for recovery, it must define +an encrypted/authorized sensitive-payload channel or store only protected +references; setting `redacted: true` over raw payload bytes cannot satisfy this +criterion. + +## 9. Target capability and approval model — not implemented + +Target v1 requires positive grants with deny as the default. The effective node +authority is the intersection of: + +```text +operator maximum + ∩ graph-requested capabilities + ∩ executor/provider-supported capabilities + ∩ run-specific approval +``` + +Missing information narrows authority. A model, router, planner, tool response, +child graph, or dynamic patch can request a path but cannot enlarge this set. + +### Required capability dimensions + +- stable tool/MCP operation identities and argument constraints; +- filesystem roots with independent read/write/create permissions; +- allowed executable identities and bounded argument/environment rules; +- network protocol, host, port, method, redirect, and DNS behavior; +- secret reference IDs, account/tenant scope, and injection channel; +- artifact read/write namespace and maximum size; +- external account/resource/action scope; +- CPU, memory, process, descriptor, time, concurrency, and output limits; and +- effect class plus idempotency/reconciliation/approval requirement. + +The capability snapshot must bind graph hash/revision, node ID, input hash, +policy version, executor/provider identity, secrets by reference/version, and +approval identity/expiry. Retry and resume reuse that snapshot instead of +silently acquiring newly available ambient credentials. + +### Required denial behavior + +- denied actions produce structured failures/events; +- tool discovery never implies authorization; +- a model is not asked to invent a policy workaround; +- child/subgraph/dynamic work cannot escape the parent grant; +- safety-critical unknown policy/capability versions fail closed; and +- authorization is rechecked at the effect boundary, not only at graph compile. + +No manifest schema, policy engine, enforcement adapter, or shared denial corpus +currently satisfies this model. + +### Human approvals + +A target approval must show and bind: + +- graph revision/hash, run, node, and normalized action; +- target account/resource and proposed effect; +- input, output/effect, and relevant diff/evidence hashes; +- requested capability set and side-effect class; +- approver identity, decision, timestamp, expiry, and policy version; and +- secret-safe context sufficient to understand the action. + +Any material graph, input, target, capability, diff, or expiry change invalidates +the approval. Broad approval of future actions is a separate scoped policy +change. Current Graph Engineering has no human-gate runtime, authenticated +approval record, stale-approval rejection, or non-idempotent recovery callback. + +## 10. Target filesystem and worktree isolation — not implemented + +### Path policy + +Every file operation must begin from a fixed resolved root, reject ungranted +absolute/parent paths, resolve symlinks, verify the final target remains inside +the grant, and recheck at the operation boundary. Environment variables, `~`, +globs, command substitution, and model-produced strings cannot be security +selectors for destructive targets. + +The store's hashed identifier paths do not implement this general executor file +policy. Custom executors currently remain unrestricted. + +### Worktree provider + +Parallel writers need one provider-owned resource set per run/node attempt: + +- unique git worktree and deterministic branch identity; +- exclusive lease and ownership record; +- allowed/denied path policy; +- unique temp directory, cache, ports, and database namespace; +- bounded artifact/log output; +- no secret copying into tracked content; +- safe handling of repository hooks/config as untrusted inputs; +- cleanup only for a verified resource created by the provider; and +- quarantine/preservation after conflict or security failure. + +A worktree separates file history. It does not isolate processes, environment, +network, ports, or the kernel. + +### Merge gate + +Merge must be a separate authorized node/action, never an implicit consequence +of worker success. Its inputs include immutable worker revision/diff, target +base, tests, policy results, and approval where required. It must require a +clean status, revalidate allowed paths, run deterministic tests/security checks, +reject stale bases, and surface conflicts as structured failures. On failure, +the isolated work remains preserved and the target branch remains unchanged. + +No worktree lease, path enforcement, cleanup provider, or tested merge node is +implemented today. + +## 11. Target process and container isolation — not implemented + +A process provider must launch a node with a minimal environment, restricted +user/group, dedicated working/temp directories, explicit read-only/read-write +mounts, network disabled or allowlisted, and hard CPU/memory/process/file/output +limits. It must capture bounded stdout/stderr and have an independent kill path +for non-cooperative code. + +A container provider additionally needs image identity/provenance, no privileged +mode, no host/Docker socket, no broad credential mounts, controlled syscalls and +devices, read-only root where feasible, explicit egress, and cleanup tied to a +verified run/node resource ID. + +Isolation failures, kill failures, resource exhaustion, and cleanup failures +must be structured and must not trigger an automatic mutating retry. Neither +provider exists in the current TypeScript or Python runtime. + +## 12. Secrets, logging, and observability target — not implemented + +Secrets must be references rather than Graph IR, edge payload, prompt, +checkpoint, or artifact values. A secret resolver should inject only the +approved reference at the executor boundary, use short-lived audience-bound +credentials where possible, keep provider and tool credentials separate, and +audit only reference/version/outcome—not secret value. + +Redaction must happen before event store, log, trace, error aggregator, artifact, +or support bundle. Target controls include payload capture off by default, +field allowlists, credential/canary scanning, bounded error text, artifact +references for large data, separate privileged access, and retention/deletion +rules. + +Literal scanning is defense in depth; transformed secrets can evade it. The +primary control is withholding a secret from model/data paths entirely. + +## 13. Attack and abuse matrix + +| ID | Attack or failure | Present-alpha mitigation | Residual exposure | Target control and required evidence | +| --- | --- | --- | --- | --- | +| `A01` | Malformed, cyclic, or mutation-hostile Graph IR | Portable capture, closed envelopes, compiler diagnostics | Embedded schemas/config semantics remain opaque | Fuzz parsers/models; shared minimized fixtures; no crash or unsafe coercion | +| `A02` | Static fan-out/depth or retry explosion | Compiler fan-out/depth, concurrency, retry and total-attempt bounds | Cost, CPU/memory, duration, dynamic nodes and nested totals incomplete | Compositional hard budgets; `T18`, `Q04`, 1,000-node/resource reports | +| `A03` | Custom executor reads environment/secrets | Operator warning only | Full host-process authority | Deny-by-default capability intersection and process isolation; `T09`, `T24` | +| `A04` | Custom executor writes/deletes arbitrary paths | Store identifiers are safe, but executor paths are not mediated | Traversal, symlink escape, broad deletion, repository loss | Resolved path policy, destructive-target guards, escape corpus and OS matrix | +| `A05` | Parallel writers collide or overwrite work | Data dependencies and application serialization only | Same filesystem/process; independent graph branches can race | Leased worktrees/namespaces and structured merge gate; `T23-T24` | +| `A06` | Prompt injection requests tool/network/secret authority | No built-in mutating adapter; model text is treated as caller data by convention | Custom executor/application may obey it | Policy outside model, typed tool args, denial events, independent `T28` campaign | +| `A07` | Non-cooperative code runs after timeout/cancel | Cooperative signal and bounded new scheduling | Process/effect can continue after terminal result | Killable process/container and late-effect tests; no automatic mutating retry | +| `A08` | Crash duplicates an external mutation | Commit-before-dispatch claim, stable activity key, in-doubt non-idempotent fail | Idempotency declaration unverified; remote may ignore key | Activity ledger, reconciliation, approval/compensation, crash-after-effect matrix | +| `A09` | Two coordinators resume one run | Event-store CAS detects stale append | No lease/fence before external work; local queue is one-process only | Lease/heartbeat/fencing token, synchronized dual-resume and lease-loss tests | +| `A10` | Local attacker forges event/checkpoint history | Strict schema/sequence/content hashes and corruption failure | Unkeyed hashes can be recomputed; no auth/encryption/tenant ACL | Authenticated production stores, tenant policy, backup/restore and malicious-store suite | +| `A11` | Secret leaks through durable event payload | Private directory guidance only | Full input/output currently stored while `redacted: true` | Resolve release blocker; pre-sink redaction/protected refs and canary byte scan | +| `A12` | Secret leaks through error/log/trace/support bundle | No built-in telemetry exporter; MCP does not log graph content | Caller and future adapters may log payloads | Default-off capture, bounded allowlists, canary tests across every sink; `T26`, `SC13` | +| `A13` | Hostile MCP client submits expensive graph | Byte/node/edge limits, worker deadline/cancel, no mutations | SDK parses frame first; stdio has no authentication; thread not sandbox | Host/OS pipe and process limits, deployment identity policy, transport fuzzing | +| `A14` | Compromised MCP/tool adds or mutates authority | Current server registers a fixed read-only set | Future servers/plugins and custom executors remain untrusted | Pinned tool identities, explicit mutation enablement, capabilities/approval, response redaction | +| `A15` | Malicious GraphPatch expands authority or evades totals | Dynamic patch execution absent | Reserved event name could be misread as support | Versioned patch compiler; parent-grant intersection; malicious dry-run corpus `T15` | +| `A16` | Stale or replayed approval authorizes changed work | Approval runtime absent, so supported flow cannot approve | Applications may invent unsafe ad hoc confirmation | Durable bound approval record, expiry/invalidation and `T22` mismatch fixtures | +| `A17` | Poisoned/cross-tenant artifact consumed | ArtifactStore absent | Custom paths/services have no common integrity/ACL contract | Content address, media/size metadata, namespaces, authorization and corruption suite | +| `A18` | Model/provider cost exhaustion | Attempt/concurrency limits | No token/money reservation, provider rate/circuit policy | Atomic budget reservation, pricing version, hard pre-schedule stop and resume tests | +| `A19` | Dependency/action/plugin supply-chain compromise | Pinned Actions, read permissions, locks, audit, dependency review, CodeQL, Dependabot | No full SBOM/provenance/secret/license/publisher evidence | `SC01-SC14`, independent digest/attestation verification, clean builds | +| `A20` | Malicious repository hook/build script escapes worktree | No worktree/process provider | Runs with host authority if invoked by custom executor | Disable/review hooks, isolate process, minimal env/mounts and escape tests | +| `A21` | Router/planner selects unauthorized path | Pure route evaluator has no authority role; runtime routing absent | Future integration could conflate selection with grant | Selected route intersected with preauthorized edges/capabilities; denial and replay fixtures | +| `A22` | Multi-tenant storage disclosure or namespace confusion | Safe IDs and private-directory guidance | No tenant auth/encryption; one local host trust domain | Production ACL/tenant namespaces, encryption responsibility and cross-tenant tests | +| `A23` | Release artifact differs from reviewed source | Local package content/install rehearsals | No trusted publisher, SBOM, checksums or attestations | Candidate coordinates, source-to-artifact map, two clean builds and provenance verify | +| `A24` | False security claim masks an open control | Alpha warnings and evidence-led checklist | Marketing/release copy can still drift | Candidate-bound claim audit; `I10`; stable forbidden while mandatory row not Green | + +Every test must have a safe deterministic fake or disposable environment. Real +provider/service tests remain opt-in and must never print credentials or raw +customer data. + +## 14. R1–R3 verification and independent review + +`R1`, `R2`, and `R3` are review levels from the ownership map, not vulnerability +severity ratings. Vulnerabilities still need a separate severity/risk +disposition. + +### R1 — local, non-security-impacting change + +R1 requires the author plus one non-author reviewer. It applies only to a +package-local implementation with no public semantic, persistence, security, or +release impact. A change to parsing, path handling, capability decisions, +redaction, storage, executor launch, MCP exposure, package contents, or release +workflow cannot be classified R1 merely because the diff is small. + +Minimum evidence: reviewed revision, focused tests, lint/type checks, changed +surface inventory, and a written reason why no public/security boundary moved. + +### R2 — public semantic or boundary change + +R2 requires three identities: author, opposite native-runtime semantic reviewer, +and integration reviewer. Platform work substitutes the relevant runtime reviewer +plus integration. It applies to public APIs, cross-language semantics, +cancellation/resource bounds, persistence/recovery, providers, isolation, CLI +envelopes, and packages unless a day gate elevates the work to R3. + +Minimum evidence adds a normative contract, negative tests, shared conformance +where behavior is portable, clean package checks, commands actually run, and a +review outcome of `accepted`, `changes-requested`, or `blocked`. + +### R3 — security/release/go-no-go + +R3 requires integration, an independent security/go-no-go reviewer, and the +responsible implementation lane, plus genuine external evidence wherever the +gate requires it. Day 12 isolation, Day 16 security preflight, and Day 20 +provenance/go-no-go are R3 regardless of diff size. + +For Day 12, both runtime semantics, the platform adversarial-test owner, an +independent security reviewer, and integration risk disposition are required. +For Day 16, the implementer cannot be the only attacker/fuzzer and raw scan +evidence must be bound to the candidate. For Day 20, trusted registry authority, +artifact provenance, and independent verification cannot be simulated by an +agent or replaced with a local source test. + +### R3 evidence packet + +An R3 packet contains: + +- immutable source revision and canonical spec revision; +- exact package/artifact digests and environment/tool versions; +- threat/attack IDs addressed and residual-risk register; +- exact commands, fixture/corpus versions, seeds, raw machine reports, and + minimized reproducers; +- both-language results where the surface is shared; +- escape, denial, cleanup, cancellation, and recovery evidence; +- vulnerability findings with severity, owner, disposition, and retest; +- reviewer identities, independence statement, dates, and explicit verdicts; +- external authority/evidence references where required; and +- invalidation rules describing which downstream artifacts reopen after change. + +Silence, an agent heartbeat, test count, mock-only screenshot, or unreviewed scan +summary is not R2/R3 evidence. + +## 15. Day 12 isolation and policy exit gate + +Current state: **Open**. Accurate documentation of ambient authority is useful, +but no enforcement or isolation provider exists. Day 12 also depends on bounded +cancellation and judgment/human-gate semantics; incomplete upstream contracts +prevent a truthful Green state. + +### Required implementation + +- versioned capability manifest and deny-by-default policy engine; +- stable structured allow/deny decisions in TypeScript and Python; +- graph/parent/provider/approval capability intersection; +- authenticated, bound approval contract for high-impact/non-idempotent work; +- resolved filesystem path policy with symlink/TOCTOU defenses; +- worktree lease/ownership and safe cleanup; +- isolated temp/cache/port/database namespaces; +- killable process provider and container provider with minimal authority; +- explicit merge node with clean-status, diff, test, policy, stale-base, conflict, + and approval gates; and +- migration demo that preserves conflicted/failed work rather than merging it. + +### Required R3 evidence + +- `T09` unauthorized transform/capability expansion denials; +- `T23` worktree lease/path/test/merge conflict matrix; +- `T24` concurrent process/container port/temp/cache/database isolation; +- `T28` prompt-injection authority-expansion attempts; +- traversal, symlink, broad deletion, stale lease, orphan cleanup, hook/config, + environment, network, secret, and kill-failure attacks; +- both native runtimes returning aligned structured policy/isolation outcomes; +- implementer-independent adversarial owner and security review; and +- no write, merge, external call, or secret access after a denied decision. + +### Exit statement + +Day 12 exits only when parallel writes, ports, temporary directories, caches, +and database namespaces remain isolated under adversarial concurrency; conflicts +and policy denials are structured; cleanup cannot escape provider-owned roots; +and the merge target stays unchanged on every failure path. + +Fallback: deny shell/write/network/secret capabilities, serialize trusted work, +disable automated merge, preserve/quarantine conflicted worktrees, and keep the +feature experimental. Documentation or an external user-created container does +not close the product gate. + +## 16. Day 16 security preflight exit gate + +Current state: **Partial**. Existing CodeQL, dependency review, Dependabot, +private disclosure, npm audit, CI permissions, and package checks are meaningful. +They are not a candidate-bound complete security preflight. + +Day 16 cannot begin its final join until Day 12 isolation, Day 13 provider/tool +surface, and Day 15 deployable storage/worker topology are complete enough to +attack. Scanning an architecture that is not yet present cannot prove it safe. + +### Required implementation and campaigns + +- harden capability, adapter, store, worker, isolation, and redaction boundaries; +- resolve the `redacted: true`/raw-payload blocker; +- candidate-specific threat model and risk register covering providers, shell, + MCP, patches, stores, artifacts, worktrees, workers, Explorer, and publishing; +- parser/schema/policy/redaction fuzzing with minimized reproducers; +- prompt-injection and capability-escalation campaign; +- non-cooperative timeout/cancellation and container/process escape campaign; +- crash-after-effect, lease-loss, corrupt-store/artifact, and budget-bypass chaos; +- seeded-secret scan across repository/history/packages/source maps and every + runtime/support sink; +- npm, Python, system/container dependency scans; +- license inventory and complete third-party notices; +- source/package/deployable-artifact SBOMs; and +- proof that telemetry and prompt/response capture are off by default in clean + npm and wheel/sdist installations. + +### Required R3 evidence and threshold + +- `T09`, `T15`, `T23-T28`, `T33`, `Q04`, and `Q08` reports; +- `SC07-SC13` evidence tied to the exact candidate; +- scanner/fuzzer versions, configuration, corpora, seeds, duration, raw results, + and triaged minimized failures; +- no unaccepted high or critical vulnerability; +- all accepted mitigations retested by someone other than the implementer; and +- security and integration reviewers explicitly sign `accepted` or block the + candidate. + +Day 16 exits only when secret, dependency, license, and static-analysis scans +pass, exploit regressions pass, default privacy behavior is observed from clean +artifacts, and the reviewed risk register contains no unaccepted high/critical +item. A finding may be fixed or the feature may be removed/disabled; it cannot +be relabeled Green through documentation. + +Fallback: disable/deny the affected feature or adapter, rotate any exposed +credential outside the repository, invalidate derived artifacts, remain +prerelease, and rerun the complete affected campaign after the fix. + +## 17. Day 20 provenance and go/no-go exit gate + +Current state: **Open/External**. A public source alpha and protected CI checks +do not establish registry authority or release provenance. + +Day 20 requires Green Day 16, complete Beta evidence, an immutable RC candidate, +and all evidence collectors. Before any go/no-go review, candidate coordinates +must name source/spec revisions, npm/Python artifact digests, SBOM/checksum +manifest, CI matrix, release manager, independent reviewer, and UTC decision. + +### Required provenance evidence + +- least-privilege trusted-publishing identities and rehearsals for npm and PyPI, + with no long-lived release token; +- SPDX or CycloneDX SBOMs for source, every npm package, wheel/sdist, and + deployable site/application artifact; +- checksum manifest covering every release artifact; +- build/publish attestations binding source revision, workflow identity, and + artifact digest; +- source/tag, packages, SBOM, checksums, and attestations resolving to one + candidate with no unexplained drift; +- full secret/dependency/license/static analysis and formal threat review; +- two clean trusted-runner build manifests with reproducible or reviewed + explained variance; +- independent installation and provenance verification; and +- explicit registry ownership/authority evidence that is not inferred. + +### Mandatory go/no-go joins + +At minimum, `V1-02` security, `V1-04` provenance, `I05` external effects, `I06` +privacy, `I08` authority, `Q08`, `Q09`, and every `SC01-SC14` row must be Green. +All other mandatory release checklist rows remain conjunctive; security cannot +waive a compatibility, recovery, usability, or package failure. + +### R3 decision rule + +The release manager, responsible package lanes, independent security reviewer, +and independent go/no-go reviewer sign the exact candidate. External publishing +authority and other required external evidence must be real. Any source, spec, +dependency, package, site, workflow, or release-note change after review +invalidates affected digests and reopens dependent gates. + +Day 20 exits only when the leaf-evidence roll-up contains no Open, Partial, or +Blocked mandatory row and all release assets/provenance artifacts are ready. +Otherwise stable packages are not published. The required outcome is an +accurately labeled complete RC with an explicit blocker manifest, not a false +stable claim. + +## 18. Security critical-path priority + +This order follows exploit impact and dependency criticality, not calendar +optimism: + +| Priority | Work package | Why it precedes the next package | Completion signal | +| ---: | --- | --- | --- | +| `P0` | Correct the raw-payload/`redacted: true` contract and quarantine privacy claims | Current wire metadata can misrepresent persisted secret exposure | Spec/implementation decision plus both-language canary negative controls | +| `P0` | Freeze capability/approval policy and deny-by-default semantics | Isolation/adapters cannot safely expose operations without a common authority model | Versioned policy, structured denials, parent intersection, stale approval fixtures | +| `P0` | Implement process kill boundary and filesystem path enforcement | Cooperative cancellation does not contain hostile/non-cooperative executors | Non-cooperative/escape tests prove denied paths/effects and independent kill | +| `P0` | Worktree leases, namespaces, cleanup, and merge gate | Parallel code-writing patterns otherwise risk repository corruption | `T23-T24` conflict/escape/cleanup evidence with target branch unchanged | +| `P1` | Complete durable lease/fencing and effect reconciliation/approval | CAS alone cannot prevent stale external work or safely repeat ambiguous effects | Dual-resume/lease-loss/crash-after-effect suite and activity evidence | +| `P1` | Provider/tool/MCP mutation adapters behind capabilities | Adapter surface must inherit, not invent, authorization and redaction | Shared adapter denial/rate/cancel/fallback suite; read-only remains default | +| `P1` | Pre-sink redaction, secret references, retention and support-bundle policy | Provider/store/Explorer integration creates more payload sinks | Canary scan passes journal/artifact/log/trace/error/support bytes | +| `P1` | Production store/artifact tenant integrity and authorization | Multi-worker/deployable topology cannot use private-directory assumptions | Shared malicious/cross-tenant/storage race suite | +| `P2` | Full Day 16 fuzz/chaos/scanner/SBOM campaign | It must attack the implemented candidate surfaces, not stubs | Candidate-bound reports, zero unaccepted high/critical, R3 sign-off | +| `P2` | Day 20 trusted publishing and provenance | Provenance is meaningful only after the security candidate is frozen | `SC01-SC14`, `Q09`, independent source-to-artifact verification | + +The first four P0 packages form the Day 12 security spine. Work on launch copy, +provider breadth, Explorer payload views, or mutating MCP must not outrun them. + +### 18.1 Registry dependency audit and executable order + +This subsection is a snapshot of `codex_logs/task-registry.json` on +2026-07-26. Registry dependencies, rather than the numeric prefix in a task ID, +define executable order. In particular, `D6-ROUTER-BARRIER-023` follows the +Day 7 pipeline milestone even though its identifier starts with `D6`. + +The current executable head of the runtime chain is +`D7-PIPELINE-CONFORMANCE-013`, which is `in_progress`. The security architecture +document is also legitimate parallel work under the unblocked +`CTRL-DOCS-073`, but producing this document does **not** start or complete +`D12-ISOLATION-SPEC-044`. That task additionally requires the canonical policy +schema, public threat model, fixtures, and its declared dependency closure. + +| Execution band | Registry work | Snapshot state | Security consequence and required action | +| --- | --- | --- | --- | +| `P0-now` | `D7-PIPELINE-CONFORMANCE-013` | `in_progress` | Finish parity, cancellation/cleanup red-team work, docs, and full gates; it is the only open head that unlocks the serial runtime backbone | +| `P0-now` | `CTRL-DOCS-073` | `in_progress`, no dependency | Complete evidence-based architecture/research documents in parallel, while preserving honest implemented/target labels | +| `P0-contract` | Raw-payload/`redacted: true` correction | **No dedicated registry task** | Integration owner must create or explicitly assign a cross-language contract task before durable event/checkpoint formats expand; `D16-SECURITY-062` must validate the completed fix, not become the first place it is discovered | +| `P0-backbone` | `D6-ROUTER-BARRIER-023` -> `D7-CYCLE-SPEC-024` -> native cycles -> `D7-CYCLE-CONFORMANCE-027` -> `D8-CHAOS-OPS-030` | `planned`, transitively blocked by active pipeline task | Preserve bounded routing/cycles and cancellation semantics needed by later durable, budget, verifier, and isolation policy enforcement | +| `P0-durable` | `D9-DURABLE-EXT-SPEC-031` -> native durable extensions -> `D9-DURABLE-EXT-CONFORMANCE-034` | `planned`, depends on chaos | Freeze safe event/checkpoint payload treatment, leases, fencing, stale approvals, and effect reconciliation here; do not carry the false redaction assertion into a larger wire surface | +| `P0-policy prerequisites` | `D10-BUDGET-*` -> `D11-VERIFY-*` | `planned`, depends on durable closure | Prove authority cannot escape through budget contention, retries, verifier fan-out, citations, or stale human gates | +| `P0-isolation` | `D12-ISOLATION-SPEC-044` -> native isolation -> `D12-ISOLATION-REDTEAM-047` | `planned`, depends on verifier conformance | Implement and independently attack capabilities, approvals, process/container boundaries, worktrees, cleanup, and merge gates; documentation alone is not an exit | +| `P1-surface expansion` | `D13-ADAPTER-SPEC-048`/`D13-ADAPTERS-049` and `D14-MCP-PLUGINS-052` | `planned`, depends on isolation red-team or its policy contract | Expose model/HTTP/shell/MCP authority only after deny-by-default semantics; preserve the current read-only MCP default | +| `P1-shared state` | `D15-STORAGE-WORKERS-054` | `planned`, depends on extended durability and adapters | Add tenant authorization, shared leases, migration, artifact integrity, and worker-loss behavior before multi-worker claims | +| `P2-candidate security` | `D16-SECURITY-062` | `planned`, depends on isolation, storage workers, and MCP plugins | Execute candidate-bound redaction, fuzz, static/dependency/license, and SBOM checks; zero unaccepted high/critical findings | +| `P2-release` | `D19-RC-065` -> `D20-PROVENANCE-066` | `planned`; final task has an external authority gate | Freeze exact artifacts, then verify trusted publisher identity, attestations, checksums, and source-to-package provenance without assuming credentials | + +### 18.2 Audited security backlog gaps + +The following gaps are not interchangeable. “Mapped” means the registry has a +plausible owner and gate; it does not mean the capability exists. “Unmapped” +means integration planning must add an explicit task or amend an existing task +before implementation can be considered scheduled. + +| Rank | Gap | Current evidence | Registry disposition | Acceptance boundary | +| ---: | --- | --- | --- | --- | +| `S0` | Durable payloads are labeled `redacted: true` while raw input/output remains | Confirmed in both runtime implementations; release blocker in Section 8 | **Unmapped as a dedicated corrective task**; must be resolved at or before `D9-DURABLE-EXT-SPEC-031`, then independently revalidated by `D16-SECURITY-062` | Canary secret is absent from journal, checkpoint, artifact, stdout, stderr, log, trace, error, and support-bundle bytes under defaults; scanner positive/negative controls pass in both languages | +| `S1` | No executable capability/approval contract or authority intersection | Resource/side-effect metadata is descriptive; custom executors retain ambient authority | Mapped to `D12-ISOLATION-SPEC-044` and native D12 lanes, currently dependency-blocked | Deny-by-default, parent-child non-expansion, bounded grants, stale/replay denial, structured audit events, parity fixtures | +| `S2` | Cooperative cancellation cannot kill hostile or synchronous execution | Scheduler owns in-process tasks only | Mapped to D12 plus `D8-CHAOS-OPS-030` precursor | Independent process kill, deadline containment, descendant cleanup, no post-terminal mutation | +| `S3` | No path-safe workspace/worktree lease or merge gate | Parallel writers share host/repository unless operators isolate them | Mapped to D12 | Traversal/symlink/device escape denied; exclusive namespaces; stale lease cleanup; dirty/conflicting merge denied; target branch unchanged on failure | +| `S4` | CAS checkpoints are not distributed leases; ambiguous effects are unreconciled | Local stores provide integrity/atomicity, not ownership fencing or exactly-once effects | Split across D9 and `D15-STORAGE-WORKERS-054`; task-level responsibility must remain explicit | Dual resume has one winner; stale worker fenced; crash-after-effect does not silently duplicate; approval/reconciliation evidence retained | +| `S5` | MCP validates semantic bounds only after SDK parsing and worker threads are not sandboxes | Current server is useful and read-only, but transport is unauthenticated and parsing can precede app limits | Policy mapped to D12/D14, but pre-parse byte/framing enforcement must be named in the implementation checklist | Oversize/malformed input is rejected before unbounded allocation/work; timeout terminates work; read-only default and structured errors preserved | +| `S6` | Provider, HTTP, shell, and mutating MCP/plugin surfaces do not exist yet | No official provider/tool adapters; arbitrary custom executors remain an embedding boundary | Mapped to D13/D14 after isolation | Capability-denial, rate/deadline/cancel, fallback, secret-ref, injection, and audit suites pass with deterministic fakes by default | +| `S7` | No tenant authorization, encryption/key policy, retention deletion, or shared-worker isolation | Local private-directory assumptions only | Mapped primarily to D15, with policy validation at D16 | Cross-tenant negative suite, artifact ownership/integrity, encryption/key/retention policy, migration and worker-loss chaos | +| `S8` | Sink inventory and default privacy policy are incomplete | Telemetry/prompt capture is intended off, but durable raw payload persistence violates the stronger privacy claim | Partly D9/D12/D16; integration must keep one cross-sink acceptance matrix | One inventory covers journals, checkpoints, artifacts, logs, traces, errors, CLI/MCP output, Explorer, and support bundles; opt-in capture is explicit and bounded | +| `S9` | Supply-chain controls are partial | Pinned CI actions, lockfile installs, audit, dependency review, and CodeQL exist; final license/SBOM/attestation/trusted publishing evidence does not | Mapped to D16 and D20 | Candidate-bound scans/SBOM/checksums/attestations and independent source-to-package verification; no inferred registry authority | +| `S10` | Independent review can be claimed without candidate identity unless evidence is digest-bound | Plans define R1-R3, but future reports do not yet exist | Mapped to D12 red-team, D16, D19, and D20 | Reviewer identity/role, commands, raw outputs, fixture/candidate digests, time, exceptions, and invalidation rules are recorded | + +The urgent planning correction is `S0`: it is already exploitable as a +truthfulness and secret-persistence defect, yet the registry currently defers +the broad redaction audit to Day 16. The corrective implementation should be a +small, separately reviewable cross-language contract milestone before the Day 9 +durable schema grows. Day 16 remains the full-candidate verification gate. This +separation prevents a late audit from discovering that every intervening +fixture, migration, adapter, and storage implementation encoded the unsafe +assumption. + +The next schedule risk is the long serial path from the active pipeline gate to +Day 12. Safe acceleration means parallelizing threat analysis, fixtures, +negative-test design, and provider interfaces that do not freeze premature +authority semantics. It does not mean marking dependency-blocked D12 code as +implemented or allowing adapters, mutating MCP, shared workers, or launch claims +to bypass the isolation red-team gate. + +## 19. Current-alpha operator checklist + +Until the target controls are implemented: + +- use only reviewed Graph IR and trusted executor code; +- run under a disposable least-privileged OS account/environment; +- remove unrelated credentials and agents/sockets from the environment; +- enforce filesystem/network/process limits outside Graph Engineering; +- keep concurrency, fan-out, depth, attempts, retry, item, and timeout limits low; +- serialize executors that might touch shared state/files; +- make external mutation genuinely idempotent and reconcile ambiguous results; +- ensure asynchronous handlers honor cancellation, but assume synchronous code + cannot be stopped; +- do not put raw secrets, personal data, or credentials in graph input, node + output, prompts, errors, or application logs; +- keep local event/checkpoint directories private and remember durable journals + store input/output payloads; +- stop the old coordinator before resume and never treat CAS as a lease; +- do not expose execution directly to untrusted multi-tenant callers; and +- use the read-only MCP only through a reviewed local host configuration with + external process/pipe limits where the client is hostile. + +## 20. Change-control checklist + +Every security-relevant change must answer: + +- What actor, asset, and trust boundary changes? +- Is enforcement in deterministic code outside model/tool output? +- What is the effective authority intersection and fail-closed behavior? +- Can retry, resume, dynamic work, or a child graph widen authority? +- What happens after cancellation, kill failure, lease loss, or ambiguous effect? +- Are filesystem targets resolved and rechecked against explicit grants? +- Can two nodes collide through worktree, port, temp, cache, database, artifact, + account, or secret namespaces? +- What exact bytes reach journal, checkpoint, artifact, log, trace, error, and + support sinks under default and opt-in capture? +- Does any field/annotation claim more redaction, authentication, or isolation + than the implementation provides? +- Which shared negative, escape, canary, chaos, and recovery fixtures prove it? +- Is the review level R2 or elevated R3, and are reviewers independent? +- Which candidate artifacts and downstream gates must be invalidated on change? + +If the evidence is incomplete, the feature stays disabled, deny-by-default, or +prerelease. Stable-v1 eligibility is a conjunction of accepted controls, not a +confidence score or a documentation assertion. diff --git a/codex_plans/delivery/agent-ownership-map.md b/codex_plans/delivery/agent-ownership-map.md new file mode 100644 index 0000000..7436271 --- /dev/null +++ b/codex_plans/delivery/agent-ownership-map.md @@ -0,0 +1,410 @@ +# Graph Engineering agent ownership and review map + +- Authority: [Graph Engineering 21-Day Master Plan](../Graph-Engineering-21-Day-Master-Plan.md) +- Dependency model: [task dependency graph](task-dependency-graph.md) +- Current gap authority: [master-plan coverage matrix](master-plan-coverage-matrix.md) +- Stable-release gates: [release checklist](release-checklist.md) +- Live assignment source: [task registry](../../codex_logs/task-registry.json) +- Repository boundary rules: [AGENTS.md](../../AGENTS.md) +- Snapshot date: 2026-07-26 + +This document assigns accountable roles, review roles, and file boundaries for +every lane and every day of the 21-day plan. It is an execution control, not a +completion report. A role listed here is not an active assignment until the +task registry names an agent and dependency state. A deliverable is not complete +until its exit evidence is accepted under the release checklist. + +The four-lane limit is fixed: integration, TypeScript runtime, Python runtime, +and platform/quality/growth. Review work is time-sliced into the two daily +integration windows; reviewer labels do not create hidden fifth or sixth +implementation lanes. + +## 1. Role vocabulary and accountability + +| Code | Accountable role | Primary responsibilities | May merge or declare gate state? | +|---|---|---|---| +| `INT` | Main/integration agent | Canonical protocol, architecture decisions, shared fixtures, dependency ordering, cross-language joins, root configuration, risk acceptance, release decision | May integrate after required reviews; is the only role that may record a cross-lane join or release decision | +| `TSR` | TypeScript runtime agent | Native TypeScript compiler/runtime/SDK, Node adapters, npm package behavior, focused tests and package documentation | May hand off a reviewed package change; cannot self-approve a shared semantic or release gate | +| `PYR` | Python runtime agent | Native Python compiler/runtime/SDK, Python adapters and CLI/library behavior, PyPI package behavior, focused tests and package documentation | May hand off a reviewed package change; cannot self-approve a shared semantic or release gate | +| `PQG` | Platform/quality/growth agent | CLI, MCP, Explorer/site, examples, patterns, conformance runners, matrices, docs, security/QA assets, tester operations and organic launch assets | May hand off platform artifacts; cannot turn a mock or content asset into runtime evidence | +| `SRV` | Security/release reviewer duty | Threat, capability, supply-chain, provenance, package and claim review; normally performed by `INT` plus the non-author lane during an integration window | Advisory until evidence is signed; never bypasses a failed technical gate | +| `EXT` | External tester or independent specialist | Usability reports, consented adopter evidence, legal/security review where required, independent go/no-go review | Supplies external evidence only; cannot be simulated by a maintainer agent | + +Agent identifiers are ephemeral. Registry tasks bind an identifier to one of +these roles for one bounded work package. Reassignment must preserve the prior +owner, reason, last accepted evidence, uncommitted-file manifest, and new owner. + +### Accountability rules + +1. One work package has one primary owner. “Main + agent” means `INT` owns the + contract or join while the named lane owns only its bounded implementation. +2. The primary owner writes code and focused tests, assembles the handoff + packet, and reports exclusions. A reviewer does not quietly finish missing + implementation while claiming to have reviewed it. +3. `INT` owns the final merge, shared-fixture interpretation, status change, + release label, and any waiver explicitly allowed by the master plan. +4. `PQG` owns evidence collection mechanics, but the producing lane owns the + truth of the measured behavior. A dashboard cannot promote an unreviewed + result. +5. `EXT` evidence is required for external-usability and authentic-adoption + gates. An agent-authored report can prepare the method but cannot count as an + external report. + +## 2. Review levels and two-person controls + +“Reviewed” always means a distinct identity from the author. The minimum +two-person control is the author plus one independent reviewer. High-risk joins +require two independent reviewers in addition to the author. + +| Level | Minimum identities | Applies to | Required reviewers | +|---|---:|---|---| +| `R1` | 2 total | Package-local implementation with no public semantic, security, persistence, or release impact | Author plus one non-author lane reviewer; `INT` still integrates if root/shared files change | +| `R2` | 3 total | Public API, shared semantics, cross-language behavior, cancellation/resource bounds, persistence/recovery, providers, isolation, CLI envelopes, packages | Author, opposite native-runtime semantic reviewer, and `INT` integration reviewer; for platform work use `INT` plus the relevant native lane | +| `R3` | 3 total plus required external evidence | Security go/no-go, stable release, provenance, usability threshold, public case/adopter claims | `INT`, independent `SRV` or go/no-go reviewer, and the responsible lane; add `EXT` evidence where the gate requires it | + +Standard review rotation: + +| Authored surface | Semantic reviewer | Integration/risk reviewer | Prohibited self-review shortcut | +|---|---|---|---| +| `spec/**` or shared fixture | `TSR` and `PYR` both review native implementability | `INT` records the decision; `PQG` checks fixture/tool usability when relevant | A contract author cannot use one passing runtime as the definition of the contract | +| TypeScript runtime/package | `PYR` checks portable behavior against spec and fixtures | `INT` checks scope, evidence and downstream impact | A TS unit test alone cannot close parity | +| Python runtime/package | `TSR` checks portable behavior against spec and fixtures | `INT` checks scope, evidence and downstream impact | A Python unit test alone cannot close parity | +| CLI/MCP/Explorer/tool/docs/example | Relevant `TSR` or `PYR` owner checks the consumed API | `INT` checks claims, safety and public envelope | A screenshot, mock, or docs build cannot close runtime behavior | +| Security/isolation/provider/store | Opposite runtime reviewer plus `PQG` adversarial test owner | `INT`/`SRV` signs risk disposition | The implementer cannot be the only exploit or fault-injection author | +| Package/release/provenance | Package lane checks install artifact | `INT` plus independent `SRV`/go-no-go reviewer | Local source tests cannot substitute for packed clean-install and provenance evidence | + +An `R2` or `R3` reviewer must record one of `accepted`, `changes-requested`, or +`blocked`, cite the reviewed revision, and list commands actually run. Silence, +a heartbeat, or “looks good” is not review evidence. + +## 3. File ownership and write leases + +The table is the default ownership map. A task-specific handoff may narrow or +temporarily transfer a path, but it must be explicit before the first write. + +| Path or artifact class | Default writer | Required reviewer(s) | Parallel safety boundary | +|---|---|---|---| +| `spec/**`, especially schemas and `spec/conformance/**` | `INT` | Both `TSR` and `PYR`; `PQG` for runner compatibility | Native lanes read the frozen revision and never patch a fixture to make only their implementation pass | +| Root `package.json`, workspace/lockfiles, root `tsconfig*`, root release metadata | `INT` | Affected package lane plus `PQG` for package/install jobs | Dependency requests are handed to `INT`; no concurrent lockfile writers | +| `.github/**`, root `README.md`, `SECURITY.md`, governance and root `scripts/**` | `INT` | `PQG` plus `SRV` for security/release changes | `PQG` drafts in owned files or a patch handoff; public claims are merged only after implementation evidence | +| `packages/core/**`, `packages/runtime/**` | `TSR` | `PYR` semantic review and `INT` integration | One active TS package claim; do not run rewriting formatters across other packages | +| `packages/primitives/**`, `packages/persistence/**` | `TSR` when assigned | `PYR` parity review and `INT` integration | Contract revision is frozen first; package-local changes remain separate from core/runtime changes when possible | +| Future TS provider, storage, worker, policy or isolation packages | `TSR` after an explicit path claim | `PYR`, `PQG` adversarial owner, `INT` | New package name and public boundary are approved before scaffolding; no shared root dependency edit by the lane | +| `python/**` | `PYR` | `TSR` semantic review and `INT` integration | Python is a native runtime, not a TS client; one active Python module family per work package | +| `packages/cli/**`, `packages/mcp-server/**`, `packages/patterns/**` | `PQG` when assigned | Relevant native lane plus `INT` | Platform may scaffold against released interfaces, but mock-only behavior stays labeled and cannot imply runtime lowering | +| `apps/**`, `docs/**`, `examples/**` | `PQG` | `INT` claim review; relevant native lane for executable examples | Example code is tested against packed/public APIs; generated screenshots do not authorize API changes | +| `tools/**`, including conformance report emitters and progress scanner | `PQG` unless `INT` explicitly assigns one file | `INT`; both runtime lanes for cross-language report semantics | Fixture authority remains in `spec/**`; report tools normalize only contract-approved diagnostic differences | +| `codex_plans/**`, `codex_logs/**` and task registry | `INT` | A non-author lane for high-risk control documents | Logs are append-only in meaning; assigned agents may edit only the exact delegated control file | +| Build output (`dist/**`), caches, virtual environments, `node_modules/**` | No manual owner | Generated-artifact/package checks | Never hand-edit or treat generated files as source evidence; clean artifacts before release comparisons | + +### Write-lease rules + +1. Before editing, the registry task names the exact path set, contract revision, + branch/worktree, primary owner, reviewers, dependencies and expected tests. +2. A lane may hold one active write lease. Read-only audit work may run in + parallel but must not mutate another lane’s files. +3. Shared paths are serialized by `INT`. If two tasks need the same file, the + later task waits or receives an explicit handoff; “small edit” is not an + exception. +4. A lane does not amend, reset, discard or reformat another lane’s uncommitted + work. Overlap is reported with the exact files and stopped before editing. +5. Package dependency additions are proposed with package, version range, + license, reason and affected lockfiles. `INT` applies the root/lockfile + change in the integration window. +6. Generated protocol code, if introduced, is regenerated by one integration + task from a named spec revision; native lanes do not independently regenerate + and race. +7. External writes—publishing, posting, inviting, package ownership, hosted + deployment—need explicit authority and a dry-run or preview. A terminal + instruction does not broaden authority. + +## 4. Day 1–21 lane map + +Each row names the primary role, owned source area, required review and accepted +evidence. “Current gap” is a 2026-07-26 snapshot from the coverage matrix; it +must be rechecked against the live registry before assignment. `Partial` never +means the day gate passed. + +### Wave 0–1: authority, canonical data and compiler + +| Day | Integration lane (`INT`) | TypeScript lane (`TSR`) | Python lane (`PYR`) | Platform/quality/growth lane (`PQG`) | Review and join evidence | Current gap | +|---|---|---|---|---|---|---| +| **1** | Materialize plan/control set; freeze initial IR/event namespace and ADRs; record repository, registry and authority risks. Own `spec/**`, root config, `codex_plans/**`, `codex_logs/**`. | Bootstrap Node 20+ workspace and strict package foundations in `packages/core/**` and `packages/runtime/**`. | Bootstrap Python 3.11+ package, typing and test foundations in `python/**`. | Establish CI/governance, progress scanner, registry audit and onboarding shell in `.github/**` drafts, `tools/**`, `docs/**`, `packages/cli/**`. | `R2`: both native lanes review contract implementability; `PQG` reviews operational controls; `INT` accepts only with plan/risk/owner manifest, clean bootstrap tests and recorded external blockers. | **Partial.** Public repo, CI, governance, schemas and scanner exist. Several planned architecture/growth controls remain open; historical registry tasks lack explicit reviewer evidence; this ownership map was itself missing. | +| **2** | Review/freeze canonical serialization, protocol revision and hash fixtures. Own shared canonical corpus and any freeze ADR. | Implement general TS builders, schema types and stable content/revision hashes. Own `packages/core/**`. | Implement Python builders/Pydantic models with the same canonical projection. Own `python/**`. | Maintain JSON Schema, negative fixture tooling and CLI contract without changing semantics. Own `tools/**` and `packages/cli/**`; fixture edits go through `INT`. | `R2`: TS and Python cross-review; `INT` compares canonical bytes/hashes (`X01`); evidence includes fixture IDs, exact commands, both runtime revisions and mutation/negative tests. | **Partial.** Models, canonical JSON and shared hashes exist; general builders, YAML loader, node/edge/schema hashes, port/schema, concurrent-state, budget and capability validation remain open. | +| **3** | Freeze ordered diagnostics, stable codes, JSON envelope and exit-code semantics. | Implement compiler/DAG validation and TS public compiler API. | Implement compiler/DAG validation and Python public compiler API. | Implement `init`, `validate`, `compile`, `plan`, Quickstart v0 and machine-readable CLI tests. | `R2`: native lanes review each other against invalid fixtures (`X02`); `INT` reviews CLI envelope; evidence includes all negative fixture verdicts, stdout/stderr/exit tests and packed example. | **Partial.** Shared DAG-invalid fixtures and TS CLI exist; Python CLI/alias, exhaustive diagnostics and complete machine-envelope/exit-code reference remain open. | + +### Wave 2: deterministic execution, primitives and honest Alpha 1 + +| Day | Integration lane (`INT`) | TypeScript lane (`TSR`) | Python lane (`PYR`) | Platform/quality/growth lane (`PQG`) | Review and join evidence | Current gap | +|---|---|---|---|---|---|---| +| **4** | Integrate chain/diamond semantics, event constraints and terminal envelopes. | Implement deterministic ready-queue scheduler and bounded fan-out/fan-in in `packages/runtime/**`. | Implement equivalent native scheduler in `python/**`. | Build trace-view scaffold and deterministic concurrency tests from real event output. | `R2`: opposite runtime reviews scheduling; `INT` runs shared diamond/event-order conformance (`X05`); evidence includes bounded concurrency probes, deterministic mock traces and mutation isolation. | **Partial.** Native DAG/diamond schedulers are present. Trace viewer, nested subgraphs/namespaces, explicit reducers and executable stream/artifact edges remain open. | +| **5** | Freeze standalone pipeline and scheduler barrier semantics; prevent docs from conflating standalone streams with Graph IR stream edges. | Implement bounded pipeline, barrier integration and backpressure; own runtime/primitives package claim. | Implement native bounded pipeline and barrier parity in `python/**`. | Build provider-free research demo, slow-consumer/no-barrier probes and reproducible benchmark harness. | `R2`: TS/Python cross-review cancellation, queue bounds and wire projections; `INT` owns fixture; evidence includes fast-item gate, slow-consumer probe, stop/drop/dead-letter, timeout/retry, cleanup and repeated deterministic reports (`X04`). | **Partial, active.** Pure barrier/router evaluators and standalone pipelines exist locally. Merge/review gates, scheduler deadlines/quorum, conditional edges, route replay/confidence escalation and stream-edge activation remain open. | +| **6** | Freeze state machine, terminal/run failure envelopes, quorum and unknown semantics. | Implement runtime router, failure propagation and quorum behavior. | Implement native router/failure/quorum parity. | Build diff-review flow and deterministic failure-injection matrix. | `R2`: both runtimes compare terminal states/codes (`X03`, `X06`); `INT` checks no null substitution; evidence covers every route/default/quorum/failure policy and unknown escalation. | **Partial.** Structured scheduler/pipeline failures and pure routing exist; full node/edge terminal set, runtime quorum/abstention, conditional routing, human/unknown escalation and injection matrix remain open. | +| **7** | Integrate bounded-cycle contract and produce an honest Alpha 1 scope/exclusion record. | Implement `untilDry`, bounded `while`, evaluator-optimizer and dynamic-patch limits in TS. | Implement identical cycle/convergence behavior in Python. | Build discovery demo, provider-free examples and evidence-backed build-in-public assets. | `R2`: native cross-review plus `INT` seen-set/budget review; evidence includes global-seen dedupe, every hard stop, explicit exit reasons, malicious/unbounded rejection and clean package install. Tag evidence cannot replace missing cycle evidence. | **Partial.** Source `v0.1.0-alpha.1` and a static graph constructor exist; executable cycles, semantic convergence, global seen set, hard duration/cost/node/fan-out limits and replayable exits are open. | + +### Wave 3: retry, recovery, budgets and verification + +| Day | Integration lane (`INT`) | TypeScript lane (`TSR`) | Python lane (`PYR`) | Platform/quality/growth lane (`PQG`) | Review and join evidence | Current gap | +|---|---|---|---|---|---|---| +| **8** | Review attempt accounting and cancellation precedence across runtime, source, provider and tool boundaries. | Close TS retry/timeout/cancel races, propagated abort and cleanup. | Close Python retry/timeout/cancel races, propagated cancellation and cleanup. | Build chaos matrix and `status/watch/inspect/logs/pause/resume/cancel/retry` UX against real runtime state. | `R2`: opposite runtime red-team plus `INT`; evidence includes pre-start, queued, running, retry-delay, downstream-admission, late-outcome and no-task/listener-leak cases, then randomized bounded runs (`X07`). | **Partial.** Strong native retry/cancel suites exist, including pipeline adversarial work; full chaos matrix, operational commands and proof across all future executors/providers remain open. | +| **9** | Freeze truthful redaction plus complete Event/Checkpoint/Artifact/Lock contracts, recovery state machine and non-idempotent approval rule. | Correct the TS `redacted` wire/payload boundary, then implement leases/CAS, acceleration, SQLite, artifacts, replay/fork and approvals. | Implement native Python redaction and recovery parity. | Build canary-secret, crash-window/dual-resume and run-history/time-travel evidence. | `R3` for `D9-REDACTION-039`, then `R2` recovery review; `SRV` reviews secret and external-effect boundaries. Evidence includes canary absence across every sink, truthful flags, every crash window, dual-resume, replay/fork, stale approval and confirmation (`T19-T22`, `T26`, `T30`). | **Partial, critical corrective open.** Immutable local-DAG start/resume exists, but events currently persist raw input/output while claiming `redacted: true`; redaction, LockManager/leases, acceleration, stores, replay/fork, approvals and complete races remain open. | +| **10** | Freeze atomic budget reservation, pricing snapshot and deterministic model-routing contract. | Implement TS cost/token/time/node/attempt budgets and model router. | Implement Python budget/model parity. | Build `graph cost`, cost UI, pricing snapshot updater and offline mock scenarios. | `R2`: native parity plus `INT` recovery/budget review; evidence proves a hard limit stops before new scheduling and survives resume, unknown cost fails safely, and pricing versions are retained. | **Open.** Only narrow graph concurrency/attempt bounds exist; model tiers, usage, pricing, atomic reservations, UI/CLI and hard money/token/time/node stops are unimplemented. | +| **11** | Freeze verifier/vote/rubric/evidence/abstention/human-gate semantics. | Implement TS reflection, adversarial refutation, diverse judges and citation verification. | Implement native Python verifier/judge parity. | Build cited-report pattern, verifier fixtures, retained-vote inspection and authentic demo. | `R2`: opposite runtime and `INT`; `PQG` supplies adversarial evidence. Evidence covers pass/reject/abstain, insufficient quorum, tie-break version, isolated maker/verifier contexts and original evidence retention (`I09`, `T16`). | **Open.** A declarative verified-fanout constructor is not runtime verification; panels, citations, reflection, votes, rubrics and unknown/human gates remain open. | + +### Wave 4: isolation, adapters and public API freeze + +| Day | Integration lane (`INT`) | TypeScript lane (`TSR`) | Python lane (`PYR`) | Platform/quality/growth lane (`PQG`) | Review and join evidence | Current gap | +|---|---|---|---|---|---|---| +| **12** | Freeze capability manifest, approval, isolation, worktree lease and merge-node contract; conduct threat review. | Implement TS worktree/process/container providers, path policy and structured merge conflicts. | Implement equivalent Python isolation providers and policy enforcement. | Build migration demo, escape/conflict/prompt-injection suite and cleanup tooling. | `R3`: both native semantics, `PQG` adversarial owner, independent `SRV`, `INT` risk disposition. Evidence covers tool/fs/network/secret denial, lease ownership, ports/temp/cache/db namespaces, preserved conflict work and cleanup (`T09`, `T23-T24`, `T28`). | **Open.** Current documentation accurately admits ambient authority; enforceable capabilities, worktrees, merge gate, process/container isolation and escape tests do not yet exist. | +| **13** | Freeze shared adapter contract and integrate honestly scoped Alpha 2. | Implement TS mock/OpenAI/Anthropic/Gemini/compatible-local/HTTP/shell/MCP adapters. | Implement Python adapters to the same discovery/stream/tools/usage/retry/rate/cancel contract. | Implement doctor/Graph Ready score/badge/visualize improvements and opt-in live-test harness. | `R2`, with `R3` for shell/MCP: native lanes cross-review; `INT` and `SRV` review capabilities. Evidence uses deterministic mock in normal CI, opt-in real providers, fallback/circuit/rate/cancel tests and package install. | **Open.** Deterministic local executors exist, but official adapters, shared adapter suite, fallback/circuit behavior and complete doctor/score/badge surface are absent. | +| **14** | Perform public API/IR compatibility audit and record freeze or explicit exclusions. | Freeze TS runtime/MCP extension interfaces and compatibility tests. | Freeze Python plugin extension interfaces, discovery and compatibility tests. | Deliver read-only-default MCP policy and ten cross-language pattern skeletons. | `R2`: both runtimes and `INT` sign API diff; `SRV` signs MCP mutation boundary. Evidence includes public export manifests, semver/API report, plugin/MCP denial tests and ten skeleton manifests. | **Open.** Alpha MCP is read-only validation/planning and four TS constructors exist; plugin SDK, runtime mutation policy, complete public freeze and ten YAML/JSON/TS/Python skeletons remain open. | + +### Wave 5: scale, security and tester-backed Beta + +| Day | Integration lane (`INT`) | TypeScript lane (`TSR`) | Python lane (`PYR`) | Platform/quality/growth lane (`PQG`) | Review and join evidence | Current gap | +|---|---|---|---|---|---|---| +| **15** | Review performance methodology, storage/worker contract and approved baselines. | Implement TS PostgreSQL, S3-compatible artifacts, LockManager/worker mode and OTel hooks. | Implement Python storage/worker/telemetry parity. | Build Explorer/site/video, critical-path/utilization views and clean first-run study. | `R2`: storage parity plus `INT`; `PQG` verifies benchmark environment. Evidence includes shared store suite, 1,000-node/resource runs, worker races, before/after baselines and under-five-minute first run. | **Open.** Local events/checkpoints exist; production stores, local ArtifactStore default, distributed workers, OTel, Explorer/time travel and accepted performance baseline are absent. | +| **16** | Run security preflight, require closed `D9-REDACTION-039`, own risk register and reject unaccepted high/critical findings. | Independently harden/retest TS redaction, policy and adapter/isolation boundaries. | Independently harden/retest Python redaction, policy and adapter/isolation boundaries. | Own canary scans, threat model, secret/dependency/license/static scans, fuzz/property/chaos and SBOM generation. | `R3`: implementer cannot be sole attacker; `SRV` and `INT` sign. Evidence includes scanner versions/raw reports, canary bytes absent from every sink, exploit regressions, default-off capture, SBOM/license inventory and zero unaccepted high/critical findings. | **Partial.** CodeQL/dependency review/Dependabot/private reporting and the security ledger exist; corrective redaction, runtime policy, threat model, secret/license scans, fuzz/chaos, SBOM and escape campaigns remain open. | +| **17** | Integrate Beta candidate, triage P0/P1 defects and bind all evidence to one immutable revision. | Burn down TS release blockers and produce npm beta artifact. | Burn down Python blockers and produce wheel/sdist beta artifacts. | Complete API docs, external tester protocol, feedback intake and beta onboarding. | `R3`: `INT` plus independent go/no-go reviewer; `EXT` supplies at least five reports. Evidence includes package digests, no-P0/P1 query, anonymized cohort, timing method and at least 80% five-minute success. | **Open/External.** Public alpha recruitment surface exists; beta artifacts, full docs, accepted tester reports, timing cohort and feedback triage are missing. | + +### Wave 6: compatibility, RC, provenance and release/support + +| Day | Integration lane (`INT`) | TypeScript lane (`TSR`) | Python lane (`PYR`) | Platform/quality/growth lane (`PQG`) | Review and join evidence | Current gap | +|---|---|---|---|---|---|---| +| **18** | Own complete compatibility audit and block RC on any `X01-X10` divergence. | Fix TS parity/platform defects without reopening frozen features. | Fix Python parity/platform defects without reopening frozen features. | Run reproducible benchmarks, 100 randomized failures, OS/version matrices, ten pattern E2E and launch-claim audit. | `R2`: native cross-review and `INT`; evidence includes Linux/macOS/Windows, Node 20/22, Python 3.11-3.13, 100-way/1,000-node tests, coverage and no unexplained >10% regression. | **Open.** Linux version CI and many tests exist; macOS/Windows, accepted coverage report, scale/resource baseline, 100 fault runs, full parity audit and ten pattern E2E are absent. | +| **19** | Freeze `1.0.0-rc.1`, permit only reviewed release-blocker fixes and invalidate artifacts after any change. | Run clean npm install/upgrade and API compatibility matrix. | Run clean wheel/sdist install/upgrade and migration matrix. | Run documentation/link/code tests and assemble release/support matrix. | `R3`: package lanes, `INT`, independent release reviewer. Evidence includes clean external projects/environments, artifact digests, migration guide, doc tests and zero P0/P1. | **Open.** Local npm tarball and Python build rehearsals exist; clean OS install/upgrade matrix, migration evidence, full docs checks and signed RC candidate are absent. | +| **20** | Own provenance/go-no-go roll-up, candidate coordinates, package identity and fallback decision. | Rehearse least-privilege npm trusted publishing and verify provenance. | Rehearse least-privilege PyPI trusted publishing and verify provenance. | Produce SBOM/checksums/attestations, site/assets/community readiness and claim audit. | `R3`: independent security and go/no-go reviewers. Evidence must link every mandatory checklist leaf, two clean build manifests, package digests, identity configuration and explicit Open/Partial/Blocked list. | **Open/External.** Source release/protected checks exist; trusted registry identity, attestations, checksums, SBOM, hosted assets and formal candidate-bound go/no-go record remain missing. | +| **21** | Choose stable only if every conjunctive v1 gate is Green; otherwise release/support the complete, accurately labeled RC. Own incident and next-review decision. | Publish/support npm only with explicit authority; deprecate/forward-fix rather than rewrite. | Publish/support PyPI only with explicit authority; yank/deprecate only under policy and publish a new fixed version. | Coordinate GitHub/site/content/community launch, metrics, support rota and transparent limitations. | `R3`: release manager, independent go/no-go and security reviewer; `EXT` evidence remains linked. Evidence includes signed decision, immutable source/artifact identities, support/incident runbooks, current links and post-release monitoring. | **Open/External.** Public alpha exists. Stable v1, packages/site, full asset set, external usability/adoption evidence and support operations are not complete. A full RC is the required fallback if any gate stays non-Green. | + +## 5. Cross-cutting capability ownership + +Day rows do not replace capability closure. These owners remain accountable +across days and must join the release checklist. + +| Capability | Primary assembly owner | Required contributors/reviewers | File domain | Closure evidence and present gap | +|---|---|---|---|---| +| `S01` Graph IR/compiler | `INT` | `TSR`, `PYR`, `PQG` fixture runner | `spec/**`, native model/compiler paths | Canonical hashes and DAG subset exist; full builders/YAML/node kinds/typed ports/nested graphs/policy diagnostics remain Partial. | +| `S02` execution primitives | `INT` | Both native lanes; `PQG` adversarial suite | runtime/primitives, `python/**`, fixtures | DAG and standalone pipeline slices exist; dynamic patches, integrated streams/barriers/routers, subgraphs, humans and cycles remain open/partial. | +| `S03` durable execution | `INT` | `TSR`, `PYR`, `PQG` crash/canary harness, `SRV` secret and side-effect review | spec, persistence/runtime, Python stores | Local immutable-DAG start/resume exists; `D9-REDACTION-039` and complete leases/stores/replay/fork/approval remain open. | +| `S04` providers/tools | `INT` contract; native lanes implement | `PQG` conformance/security; `SRV` for shell/MCP | future TS adapter packages, `python/**`, tools | Deterministic mock only; official adapter surface is Open. | +| `S05` policy/isolation | `INT` contract | Both native lanes; `PQG` red-team; `SRV` | future isolation/policy packages, persistence/runtime, Python, docs | Honest boundary docs exist; truthful redaction, enforceable deny-by-default and isolation are Open. | +| `S06` observability/Explorer | `PQG` assembly | Native event/OTel producers; `INT` claim review; `SRV` redaction review | `apps/**`, docs/tools, native telemetry | Event history/Mermaid/DOT exist; truthful sink redaction, OTel/live Explorer/time travel are Open. | +| `S07` CLI/MCP | `PQG` | Relevant native lane; `INT`; `SRV` for mutation | CLI/MCP packages, Python CLI, docs | TS alpha command subset/read-only MCP exists; full dual-language operational surface is Partial. | +| `S08` production storage/workers | `INT` contract | Native lanes implement; `PQG` chaos | storage/worker packages and Python | Local JSONL/file slice exists; SQLite default, PostgreSQL/S3/workers are Open. | +| `S09` education/product parity | `PQG` | Both native lanes verify examples; `INT` claims | docs/examples/patterns/apps | Quickstart/four TS constructors/two examples exist; course, ten bundles, picker and galleries are Partial/Open. | +| `S10` governance/distribution | `INT` | `PQG`, package lanes, `SRV`, `EXT` where needed | root governance, workflows, release assets | Public MIT alpha/governance exist; trusted packages, provenance, external evidence and full support plan remain Partial/Open. | + +## 6. Ten-pattern bundle ownership + +`PQG` owns bundle assembly and user-facing coherence. `TSR` and `PYR` own their +native executable implementations. `INT` owns shared pattern semantics, +fixtures and the decision that the pattern bundle gate (`PB`) passed. Every row +requires YAML and JSON, TS and Python, deterministic mock E2E, expected events, +optional provider setup, diagram, budgets, permissions, failure/resume proof, +tests and Claude Code/Codex/MCP/shell guides. + +| Pattern | Capability dependency owner(s) | Review pair | Current gap / next ownership action | +|---|---|---|---| +| `P01` Multi-source research diamond | `TSR`/`PYR` runtime; `PQG` bundle; `INT` reduction contract | Opposite runtime + `INT` | TS constructor/showcase is Partial; assign Python/YAML/JSON, resume, budgets, permissions and full PB evidence. | +| `P02` Cited deep research | `INT` verifier contract; native lanes; `PQG` citations/guides | Native cross-review + `INT` source-quality review | Open pending Day 11 verifier and Day 13 adapters. | +| `P03` Route-auth security sweep | `INT` router/policy; native lanes; `PQG` security harness | `SRV` + `INT` + opposite runtime | Open pending conditional routing, capabilities, isolation and security gate. | +| `P04` Diff-risk router/judge | `INT` quorum/verifier; native lanes; `PQG` demo | Opposite runtime + `INT` | Router constructor is Partial; runtime panels, retained votes/abstention and resume bundle are open. | +| `P05` Loop-until-dry discovery | `INT` cycle/budget; native lanes; `PQG` demo | Opposite runtime + `INT` | Static constructor is Partial; global seen set, convergence, hard exits and verifier are open. | +| `P06` File migration/worktrees | `INT` isolation/merge; native lanes; `PQG` migration UX | `SRV` + opposite runtime + `INT` | Open pending worktree leases, path policy, test gate, structured conflicts and recovery. | +| `P07` CI failure sweeper | Native retry/process/adapters; `PQG` fake CI bundle | Opposite runtime + `INT` | Open pending durable retry/process isolation/shell adapter and approval semantics. | +| `P08` Dependency update sweeper | Router/recovery/isolation/adapters; `PQG` dependency fixtures | `SRV` + `INT` | Open pending isolation, HTTP/shell and dependency/license policy. | +| `P09` PR babysitter | Router/durable waits/human gates/MCP; `PQG` fake PR harness | `SRV` + `INT` | Open pending stale approvals, idempotent writes, adapter/MCP policy and resume trace. | +| `P10` Scheduled ecosystem scan | Cycle/budget/recovery/provider/worker owners; `PQG` schedule bundle | Opposite runtime + `INT` | Open pending bounded schedules, providers, production worker/storage and partial-outage recovery. | + +Ten directories on Day 14 satisfy only skeleton coverage. `PB` remains open for +each row until the complete bundle and cross-language E2E evidence are reviewed. + +## 7. Handoff and integration protocol + +### 7.1 Assignment packet + +Before work starts, `INT` records: + +1. task ID, objective, explicit non-goals and master-plan/gate references; +2. primary role/agent, `R1`/`R2`/`R3` reviewers and merge owner; +3. exact owned path set, branch/worktree and base revision; +4. frozen spec/fixture revision and any permitted local interface assumptions; +5. hard dependencies, scaffold-only dependencies and blocked external inputs; +6. expected artifacts, focused tests, shared conformance, docs and evidence IDs; +7. attempt/time/cost/fan-out bounds for agent or generated work; and +8. next integration window and heartbeat deadline. + +No agent starts by broadening its scope to “whatever is needed.” A missing +contract, permission or shared-file change returns to `INT` as a dependency. + +### 7.2 Implementation packet + +The primary owner hands off: + +- base and head revisions plus a complete changed/untracked file manifest; +- public behavior added, deliberately excluded behavior and compatibility risk; +- exact commands, tool versions, test counts/results and generated report paths; +- shared fixture IDs and normalized output when portable behavior changed; +- cancellation, cleanup, failure, resource-bound, mutation and negative-test + coverage appropriate to the change; +- docs/example impact, security/capability impact and migration requirement; +- unresolved questions, flaky or opt-in tests and any external authority needed; +- confirmation that no unrelated user/agent changes were discarded or + reformatted; and +- a suggested reviewer reproduction sequence that starts from a clean checkout + or packed artifact when applicable. + +“Tests pass” without the exact command and revision is not a handoff packet. + +### 7.3 Review and merge sequence + +1. The semantic reviewer reads the frozen contract and changed public behavior, + then runs focused negative/adversarial tests. They do not infer correctness + from the author’s summary. +2. The opposite runtime reviewer compares portable projections and reduces any + mismatch to a shared fixture. Native implementation details may differ; + contract fields may not. +3. `INT` verifies path ownership, dependency state, evidence completeness, + backwards compatibility, docs claims and downstream reopen impact. +4. `PQG` runs relevant conformance, docs, package, matrix, benchmark or security + collectors only after the semantic review is satisfied. +5. `INT` integrates in the scheduled window, resolves no semantic conflict by + guesswork, and reruns affected joins from the integration revision. +6. The registry/log receives reviewed and merged evidence. “Completed” is used + only when the whole task acceptance condition is met; otherwise the task is + `in_progress`, `waiting`, `blocked` or a narrower successor is opened. + +### 7.4 Conflict, stale work and reassignment + +- On overlapping edits, both writers stop. `INT` identifies the authoritative + owner, captures both manifests and chooses ordered application or a new + worktree. No reset, checkout, overwrite or mass formatter resolves ownership. +- At 60 minutes without evidence the scanner warns; at 120 minutes without a + heartbeat/artifact/test/commit it marks stale. Waiting on a registered hard + dependency is not slowness. +- Reassignment records the old owner, blocker/staleness evidence, accepted + artifacts, unsafe partial work and new lease. The new owner does not claim + authorship or silently discard the prior diff. +- The same blocker in two scans escalates to `INT`. A blocker stays open until + the missing authority/state changes; scanner liveness cannot clear it. + +## 8. Evidence contract by lane + +Every accepted evidence record contains source revision, spec revision/hash, +exact command, environment matrix, result, immutable report/artifact path, +date, independent reviewer and explicit exclusions. + +| Lane | Minimum implementation evidence | Additional release evidence | +|---|---|---| +| `INT` | Reviewed contract/ADR, fixture manifest, dependency/gate mapping, both native conformance reports, downstream reopen analysis | Candidate coordinates, complete leaf-gate roll-up, signed go/no-go/fallback decision, package/image/SBOM/checksum/attestation identities | +| `TSR` | Focused Vitest, typecheck/lint/build, resource and cancellation tests, public export/API diff, shared conformance | Packed tarball clean install on Node 20/22 and supported OSes, upgrade, npm trusted-publishing rehearsal and provenance | +| `PYR` | Focused pytest, Ruff, strict mypy, build, resource/cancellation/cleanup tests, public export diff, shared conformance | Wheel and sdist clean installs on Python 3.11/3.12/3.13 and supported OSes, upgrade, PyPI trusted-publishing rehearsal and provenance | +| `PQG` | CLI subprocess envelopes, MCP denial/defaults, docs links/code samples, deterministic report repetition, UI smoke/accessibility, benchmark method and raw output | OS/version matrices, threat/fuzz/chaos reports, tester cohort, consented external assets, site/package link audit, launch/support manifests | +| `SRV`/`EXT` | Named scope, method, date, reviewed immutable revision, findings and disposition | Independent sign-off, consent/redaction where relevant; no credentials, raw prompts or user data in committed evidence | + +Evidence must demonstrate the property, not merely artifact presence. Examples: + +- a schema file is not canonical parity without both native projections; +- a task heartbeat is not completion; +- aggregate test count is not coverage, chaos or portability evidence; +- a mock UI is not runtime functionality; +- a package build is not a clean packed install or trusted publication; +- a maintainer-run Quickstart is not an external usability report; and +- stars are observed organic outcomes, never a technical gate or manufactured + deliverable. + +## 9. Current ownership and evidence gaps + +These gaps must remain visible until superseded by accepted evidence: + +1. **Registry-to-plan coverage:** the historical registry strongly covers the + alpha slice and active pipeline work, but Days 8–21 and many control IDs need + explicit primary/reviewer/path/evidence assignments. Every coverage-matrix + control must be reconciled; a listed control is not proof that a live task + exists. +2. **Reviewer provenance:** many existing registry entries name an implementer + but no independent reviewer field or immutable review artifact. Prior code + can remain useful, but affected public gates stay Partial until review is + reconstructed against a named revision. +3. **Shared-contract serialization:** `INT` must remain the sole writer for + specs/fixtures while native lanes implement in parallel. Current pipeline + work still requires integrated review and final shared-gate evidence before + it becomes Green. +4. **Unowned future package paths:** provider, isolation, policy, storage, + worker and Explorer packages do not all exist. Their exact path and writer + must be assigned before scaffolding to prevent overlapping “platform” and + “runtime” interpretations. +5. **Required control documents:** competitor matrix, four architecture + documents and three growth documents remain open according to the coverage + matrix. Their future owners are `INT` for architecture/decision authority + and `PQG` for research/growth drafts, with explicit single-file delegation. +6. **Cross-language completeness:** Python CLI, builders/YAML, complete + router/barrier integration, cycles, budgets, verification, isolation, + adapters, production stores and plugins remain unclosed. Neither native lane + may be silently downgraded. +7. **Platform truthfulness:** trace viewer, Explorer, complete CLI/MCP, ten PB + pattern bundles, course, site and galleries must consume real reviewed + behavior or remain clearly mock/scaffold-only. +8. **Quantitative gates:** consolidated coverage, macOS/Windows matrices, + accepted performance baselines, randomized failure campaign, complete + security/license/secret evidence and pattern E2E are missing. +9. **External gates:** registry publishing identities, at least five external + usability reports, 80% five-minute success, authentic adopter consent and + hosted launch/support evidence require outside state or authority. They + cannot be auto-completed by an agent. +10. **Release and popularity claims:** stable v1 remains blocked until every + mandatory checklist row is Green. The 6,000+ Day-21 star number is an + organic breakout target and 9,416+ is a moving benchmark—not an ownership + task, guarantee or waiver for technical quality. + +## 10. Post-audit primary ownership and write leases + +The following lanes close the 16 omissions found by the full-plan audit. Each +row has one primary writer; named reviewers do not share that write lease. + +| Task(s) | Primary | Exclusive write lease | Required independent review | +|---|---|---|---| +| `CTRL-RELEASE-MAP-074` | `INT` | `release-task-map.json`, mapping checker/tests | `PQG` verifies 178/178, blocking classification and negative fixtures. | +| `CTRL-EVIDENCE-BACKFILL-075` | `INT` | revalidation schema/overlay/checker | Release reviewer verifies candidate binding and exclusions. | +| `D17-USABILITY-076` | `PQG` | study method and redacted evidence manifests | `EXT` supplies authentic reports; privacy owner and `INT` review aggregation. | +| `D9-APPROVAL-077` | `INT` | approval spec and shared fixtures | `SRV` reviews authority/staleness; native implementers consume but do not edit fixtures. | +| `D14-NPM-DIST-078` | `TSR` | canonical distribution package and checker | `PQG` clean-installs; registry owner is an external gate only. | +| `D16-PRIVACY-079` | `PQG` | privacy/consent/retention docs and schemas | Human data owner plus `SRV` approve before collection. | +| `D18-SUPPORT-READINESS-080` | `INT` | support/incident runbooks and readiness manifest | `PQG` tabletop; `SRV` support-bundle review; roster owners acknowledge. | +| `D13-TS-ADAPTERS-081` | `TSR` | TypeScript adapter package/tests | `INT/PQG` consume results at `049`; no fixture write. | +| `D13-PY-ADAPTERS-082` | `PYR` | Python adapter package/tests | `INT/PQG` consume results at `049`; no fixture write. | +| `D18-EDUCATION-ASSETS-083` | `PQG` | course/case/demo manifests and assets | Domain maintainers run snippets; external stories require consent. | +| `D8-RUNTIME-CHAOS-084` | `PQG` | runtime chaos corpus/report | `TSR/PYR` review native leak or accounting findings independently. | +| `D9-OPS-CONTROL-085` | `PQG` | operational CLI acceptance/docs | Platform implementers supply commands; `INT` owns envelope join. | +| `CTRL-RELEASE-ROLLUP-086` | `INT` | roll-up engine, blocker manifest, decision record | Distinct R3 release/security reviewers sign; publisher acts only afterward. | +| `D9-TS-REDACTION-087` | `TSR` | TS redaction/persistence/runtime tests | `SRV/PQG` attack sinks at `089`. | +| `D9-PY-REDACTION-088` | `PYR` | Python redaction/persistence/runtime tests | `SRV/PQG` attack sinks at `089`. | +| `D9-REDACTION-CONFORMANCE-089` | `INT` | shared redaction fixtures and parity report | `PQG` owns canary scanner; `SRV` provides independent R3 disposition. | + +Current active leases are disjoint: `INT` owns shared D2 protocol/fixtures and +registry integration, the TypeScript lane owns only TypeScript authoring files, +the Python lane owns only Python authoring files, and the redaction protocol lane +owns only `spec/redaction-semantics.md`. No active lane may modify another +lane's shared expected-output file. + +## 11. Assignment readiness checklist + +`INT` may dispatch a new work package only when every answer is yes: + +- Is its master-plan day, dependency node, capability/pattern row and release + gate identified? +- Are hard dependencies accepted, or is the work explicitly scaffold-only? +- Is one primary owner named with a non-overlapping path lease? +- Are the semantic and integration reviewers named at the correct review level? +- Is the canonical spec/fixture revision frozen and readable? +- Are exact positive, negative, cancellation, cleanup, resource and parity + tests specified in proportion to risk? +- Are docs, examples, package and security impacts assigned rather than left as + “later” work? +- Is the evidence destination and integration window known? +- Are external permissions and non-goals explicit? +- Will a failed gate remain failed rather than being hidden by a mock, null, + retry, relaxed bound, unsupported claim or premature release label? + +If any answer is no, `INT` records the dependency or blocker and assigns only +independent work that does not consume the missing contract. diff --git a/codex_plans/delivery/d2-builder-yaml-implementation-brief.md b/codex_plans/delivery/d2-builder-yaml-implementation-brief.md new file mode 100644 index 0000000..968f55b --- /dev/null +++ b/codex_plans/delivery/d2-builder-yaml-implementation-brief.md @@ -0,0 +1,648 @@ +# D2 通用 Builder、Safe YAML、Typed Port 与初始 Revision 施工简报 + +- 状态:**Ready for contract freeze; implementation not started** +- 日期:2026-07-26(America/Vancouver) +- 对应任务:`D2-BUILDERS-YAML-020` +- 权威计划:`codex_plans/Graph-Engineering-21-Day-Master-Plan.md` +- 架构账本:`codex_plans/architecture/graph-ir-and-schema.md` +- 审计基线:`HEAD=d4de336`,分支 `feat/pipeline-runtime`,dirty worktree +- 本文作用:把 D2 剩余范围拆成可并行施工、可共享验收、不会过度宣称的契约。 + +本文是在完整阅读 355 行主计划后,对当前 TypeScript、Python、Graph IR、 +CLI、共享 conformance 与发布控制做的只读审计。除本文外,本轮不修改实现、 +schema、fixture、registry 或日志。 + +## 1. 结论与 P0 顺序 + +当前 D2 只能标为 **Partial / in_progress**。现有安全整数范围内的整图 canonical +JSON/hash、静态 DAG 编译、TS/Python GraphSpec 投影和 explicit-null 拒绝已经有 +共享证据;下面四项仍未实现: + +1. 任意 GraphSpec 的 TypeScript/Python 通用 builder; +2. 有明确安全子集与重复键策略的 YAML authoring; +3. 可证明、跨语言一致的 typed-port 编译诊断; +4. node/edge/schema 组件哈希和 revision-1 身份清单。 + +施工顺序必须是: + +1. **P0-A:主 Agent 冻结本文第 3-6 节的规范选择和诊断码。** 未冻结前,TS、 + Python 不得各自发明 YAML 或端口规则。 +2. **P0-B:先落共享正负 fixtures 与 expected 投影,再并行写双语言实现。** +3. **P0-C:TS builder/identity/port 与 Python builder/identity/port 并行;CLI/YAML + 集成只消费冻结 API。** +4. **P0-D:共享 conformance join 通过后,才补文档、pack/install 和不可变候选证据。** + +`GraphPatched`、revision 2+、运行中修改图、patch 授权/预算、durable fold 均属于 +`D7-CYCLE-*` 与后续 durable 任务,**不属于 D2**。D2 只建立 initial revision 1 +身份和对不支持 revision 的 fail-closed 诊断,不能宣称动态 GraphPatch 已实现。 + +## 2. 当前可复现基线 + +### 2.1 已实现且必须保留 + +- `spec/graph.schema.json` 是唯一 Graph IR 语义源;CLI/MCP 内的 schema 是发布副本。 +- `packages/core/src/canonical.ts` 与 Python canonical/portable JSON 边界产生一致的 + diamond hash,并拒绝非有限、非安全、hostile 或不可移植输入。 +- `compileGraph` / `try_compile_graph` 对 identity、endpoint、entrypoint、reachability、 + cycle、maxFanOut 和 maxDepth 给出稳定诊断。 +- node/edge 声明数组顺序是语义顺序和 hash 输入;不能由 builder 擅自排序。 +- 4 个 explicit-null 共享 fixtures 已存在;Python 还覆盖 7 个模型的 27 个已知 + optional 字段。`config: null` 与未知 policy extension 中的 null 仍合法。 +- CLI 的 `validate/compile/plan/visualize` 目前只通过 `JSON.parse` 读入 JSON。 +- `@graph-engineering/patterns` 只有四个专用构造器,不等于通用 builder。 +- durable runtime 当前固定 `graphRevision=1`;事件枚举中的 `GraphPatched` 只是保留词。 + +### 2.2 本轮只读验证 + +以下命令在 dirty worktree 上通过,只是施工前基线,不是 release candidate 证据: + +```bash +corepack pnpm --filter @graph-engineering/core test +# 2 files, 41 tests passed + +uv run --project python pytest -q \ + python/tests/test_models.py python/tests/test_compiler.py +# 69 passed + +corepack pnpm test:conformance +# 12 graph fixtures;ready queue、invalid/cancel、barrier 8、router 12、 +# persistence、durable、双向 terminal history 2、pipeline 8 全部通过 +``` + +### 2.3 架构账本需要同步的事实 + +`graph-ir-and-schema.md` 中“Python 接受 optional explicit null”的描述已被当前 +工作树修复。实施 D2 时应把该项改为 Green,但不得因此把 builders、YAML、typed +ports 或 revision identity 推断为已完成。 + +## 3. 必须先冻结的公共契约 + +### 3.1 源与输出边界 + +所有 authoring 路径必须汇入同一管线: + +```text +JSON text ─ strict source decode ─┐ +YAML text ─ safe source decode ──┼─ portable JSON snapshot ─ GraphSpec compile +TS builder ─ detached calls ─────┤ ├─ canonicalGraph +Python builder ─ detached calls ─┘ ├─ graphHash + └─ revision-1 identity +``` + +不可出现“YAML compiler”“builder compiler”和现有 core compiler 三套语义。source +decoder 和 builder 只生产 Graph IR;最终有效性、拓扑、端口、hash 与诊断均由 core +compiler 决定。 + +### 3.2 D2 新规范文件 + +主 Agent 先新增并审阅: + +- `spec/authoring-semantics.md`:builder 顺序、snapshot、safe YAML、source error; +- `spec/compiled-identity.schema.json`:revision-1 组件身份清单; +- `spec/README.md`:把两者加入规范索引并明确 GraphPatch 排除项; +- 一个 ADR:采用“strict-exact typed ports”而不是未经证明的通用 JSON Schema + assignability。 + +规范冻结应产生一条 ADR/decision 日志,记录 API version、诊断码、YAML 限制、 +domain-separated hash preimage 与 GraphPatch 非目标。任何一边实现不允许先于该记录。 + +## 4. 通用 Builder 契约 + +### 4.1 建议公共 API + +TypeScript: + +```ts +const built = graphBuilder({ metadata, inputSchema, outputSchema, policies }) + .addNode(split) + .addNode(merge) + .addEdge(edge) + .addEntrypoint("split") + .addOutput("result", { node: "merge" }) + .build(); + +built.graph; +built.canonicalGraph; +built.graphHash; +built.identity; +``` + +Python 使用同一操作模型与 snake_case 名称: + +```python +built = ( + graph_builder(metadata=metadata, input_schema=input_schema, output_schema=output_schema) + .add_node(split) + .add_node(merge) + .add_edge(edge) + .add_entrypoint("split") + .add_output("result", {"node": "merge"}) + .build() +) +``` + +两边的 `BuiltGraph` 公开字段语义相同:GraphSpec snapshot、canonical text、整图 +hash、initial identity。命名可以语言惯用,但导出的 JSON 结果必须完全一致。 + +### 4.2 Builder 不变量 + +1. constructor 必须显式接收 metadata、inputSchema、outputSchema;不得生成 graph + name/version,不得根据节点推断 schema。 +2. entrypoints 和 outputs 必须显式加入;不得按首节点、零入度或 sink 推断。 +3. node/edge 按 `add*` 调用顺序保留;不得按 ID 排序。该顺序影响调度和 graphHash。 +4. output map 的 key 重复、entrypoint 重复、node ID 重复、edge ID 重复在 builder + 层立即成为结构化错误;不得覆盖前值。 +5. 每次 `add*` 时立即做 portable detached snapshot。调用者随后修改原对象不得改变 + builder;getters/proxies/custom mappings 不得被执行或泄漏内部异常。 +6. optional 字段的“缺失”和 `null` 不得合并。已知 non-null optional 字段出现 null + 时失败;required `config: null` 原样保留。 +7. `build()` 只能成功一次并 seal builder;后续 mutation 返回/抛出 + `GE_BUILDER_SEALED`,不能悄悄产生第二个身份不同的图。 +8. `build()` 必须调用 canonical compiler;失败携带完整 compiler diagnostics,不能 + 返回 null、部分 graph 或“best effort”结果。 +9. 成功的 `BuiltGraph` 身份来自 compiler 的单次 detached capture。graph、canonical + text、graphHash、component hashes 必须绑定同一 snapshot。 +10. TS 返回深冻结 plain GraphSpec。Python 必须保证 `BuiltGraph` 内部 canonical + snapshot 不被嵌套 dict/list 修改;若 Pydantic model 不能深冻结,则 `graph` 属性 + 返回重新验证的副本,而身份字段永远从私有 canonical bytes 派生。 +11. 不自动添加 capability、pattern、typed-port 或 revision 字段。strict typed port + 只由显式 policy helper 开启。 +12. 不生成 node/edge ID。自动 ID 会把调用顺序、并发和语言实现差异写入 hash。 + +### 4.3 Builder 错误投影 + +两边异常/结果至少归一化为: + +```json +{ + "code": "GE_BUILDER_DUPLICATE_NODE", + "message": "...", + "path": "#/nodes/1/id", + "diagnostics": [] +} +``` + +固定 builder code: + +| Code | 触发 | +|---|---| +| `GE_BUILDER_INVALID_INPUT` | 非 portable、非法 null、非法 metadata/schema/endpoint | +| `GE_BUILDER_DUPLICATE_NODE` | 重复 node ID | +| `GE_BUILDER_DUPLICATE_EDGE` | 重复 edge ID | +| `GE_BUILDER_DUPLICATE_ENTRYPOINT` | 重复 entrypoint | +| `GE_BUILDER_DUPLICATE_OUTPUT` | 重复 public output name | +| `GE_BUILDER_MISSING_REQUIRED` | build 时缺 entrypoint/output 或必需 envelope | +| `GE_BUILDER_CORE_REJECTED` | canonical compiler 拒绝;附原始 diagnostics | +| `GE_BUILDER_SEALED` | build 后继续写 | + +## 5. Safe YAML v1alpha1 + +### 5.1 输入格式和 CLI + +- 文件扩展名 `.json`、`.yaml`、`.yml` 可在 `auto` 模式选择 decoder。 +- stdin `-` 不做内容嗅探;默认保持 JSON,YAML 必须显式 + `--input-format yaml`。 +- 现有 `visualize --format mermaid|dot` 保留;输入格式必须使用独立的 + `--input-format json|yaml|auto`,不能复用 `--format`。 +- Node 读取 bytes 后用 fatal UTF-8 decoder;Python 使用 strict UTF-8。非法字节是 + source error,不能被 replacement character 悄悄替换。 +- source decode error 使用 CLI input exit code 2;成功解析但 Graph IR 无效使用 + compiler exit code 1。 + +### 5.2 允许的 YAML 子集 + +允许 YAML 1.2 的单文档、JSON-compatible 数据模型:mapping、sequence、string、 +finite number、boolean、null。comments、flow/block collection 与普通 block string +可用。mapping key 必须是 string;sequence 顺序严格保留。 + +必须拒绝: + +- 第二个 document 或 document stream; +- duplicate mapping keys(任何深度、在构造 object 前检测); +- anchors、aliases、merge key `<<`; +- explicit/custom tags、directives、schema 扩展; +- complex/non-string keys; +- timestamp/date/binary/set/ordered-map 等非 JSON 类型; +- `.nan`、`.inf`、非有限数、超出 JS safe integer 的整数; +- parser-specific YAML 1.1 coercion;`yes/no/on/off` 不能跨语言变成不同 boolean; +- cyclic/shared alias graph、超过 1 MiB source、100 层 nesting 或 100,000 AST nodes; +- parser warning 被忽略、unknown token 被恢复、partial document 被返回。 + +建议 TS 使用 `yaml` major 2 的 document AST,Python 使用具备 YAML 1.2 AST/event +能力的 `ruamel.yaml`;两边都先检查 AST,再构造 null-prototype/plain JSON,最后走 +portable snapshot。依赖版本由 lockfile 固定并经过 production audit,不能依赖 +`safeLoad` 名字就假设重复键、alias 或 YAML 1.1 coercion 已安全。 + +### 5.3 YAML source error + +统一字段为 `code/format/message/path/line/column`,行列使用 1-based。固定 code: + +| Code | 触发 | +|---|---| +| `GE_SOURCE_INVALID_UTF8` | 非法 UTF-8 | +| `GE_SOURCE_TOO_LARGE` | 超过 byte/node/depth bound | +| `GE_SOURCE_SYNTAX` | parser syntax error 或 trailing content | +| `GE_SOURCE_MULTIPLE_DOCUMENTS` | 多文档 | +| `GE_SOURCE_DUPLICATE_KEY` | 重复 key | +| `GE_SOURCE_UNSAFE_YAML_FEATURE` | tag/anchor/alias/merge/directive | +| `GE_SOURCE_NON_JSON_VALUE` | timestamp、complex key、非有限/不安全数字等 | + +错误中不得包含整个 source、secret value、parser stack 或绝对用户路径。 + +## 6. Typed Port、组件哈希与 Revision-1 契约 + +### 6.1 为什么必须 opt-in 且保守 + +当前 diamond 等 graph 使用 `{ "type": "object" }`,没有 properties;若直接把完整 +typed-port 检查改成默认,会破坏已发布 alpha。通用 JSON Schema assignability 又不是 +简单深比较,未经规范的“兼容”会产生假安全。 + +D2 推荐新增 versioned policy extension: + +```json +{ + "policies": { + "graphengineering.reacher-z.github.io/typed-ports": { + "apiVersion": "graphengineering.reacher-z.github.io/typed-ports/v1alpha1", + "mode": "strict-exact" + } + } +} +``` + +未启用时维持现有 named-port runtime 语义,不宣称静态类型证明。启用后 fail closed。 +Builder 提供显式 `enableStrictTypedPorts()` / `enable_strict_typed_ports()` helper, +只生成上述 policy,不改变 node/edge。 + +### 6.2 strict-exact 算法 + +1. entrypoint node `inputSchema` 必须与 graph `inputSchema` canonical-identical。 +2. `edge.from.port` 存在时,producer `outputSchema` 必须是 object schema,包含 + `properties[port]`,并把 port 列入 `required`。 +3. 没有 `from.port` 时,传输 schema 是 producer 的完整 `outputSchema`。 +4. target binding key 是 `edge.to.port`,若省略则是 source node ID。consumer + `inputSchema.properties[bindingKey]` 必须存在并在 `required` 中。 +5. source transfer schema 与 target property schema 必须 canonical-identical。 + “语义近似”“integer 可赋给 number”等 widening 不在 v1alpha1;后续版本另写算法。 +6. 若 edge 自带 `schema`,它必须与 source/target 两边 canonical-identical。 +7. 同一 target 的两个 incoming edges 解析到相同 binding key,编译时失败。 +8. public output endpoint 有 port 时,node output property 必须存在且 required;选中 + schema 必须与 `graph.outputSchema.properties[outputName]` identical,且 public + output name 必须 required。 +9. public output endpoint 无 port 时,node 完整 outputSchema 与 graph output property + schema比较。 +10. strict-exact 只接受 `value` 或省略 mode。`stream`、`artifact-ref` 当前没有 IR + lowering,启用 strict typed ports 时必须明确失败,不能假装 value-compatible。 +11. 参与比较的 root object/port schemas 必须通过 Draft 2020-12 meta-validation; + strict-exact v1 拒绝 external `$ref`/`$dynamicRef`,避免两语言 resolution 差异。 +12. 所有比较基于 detached canonical schema bytes;object key 插入顺序不影响结果。 + +### 6.3 新 compiler diagnostics + +| Code | 稳定含义 | +|---|---| +| `GE1201_MISSING_SOURCE_PORT` | source/output endpoint port 未在 outputSchema 声明/required | +| `GE1202_MISSING_TARGET_PORT` | target binding key 未在 inputSchema 声明/required | +| `GE1203_PORT_SCHEMA_MISMATCH` | source、edge、target schema 不完全相同 | +| `GE1204_DUPLICATE_TARGET_BINDING` | 两条 edge 写同一 target input key | +| `GE1205_INVALID_PORT_SCHEMA` | strict profile schema 非 Draft 2020-12 或含不支持 ref | +| `GE1206_OUTPUT_SCHEMA_MISMATCH` | public output binding 与 graph outputSchema 不匹配 | +| `GE1207_ENTRYPOINT_SCHEMA_MISMATCH` | graph input 与 entrypoint input schema 不匹配 | +| `GE1208_UNSUPPORTED_TYPED_EDGE_MODE` | strict profile 使用 stream/artifact-ref | +| `GE1301_UNSUPPORTED_GRAPH_REVISION` | initial compiler/identity verifier 收到 revision != 1 | +| `GE1302_GRAPH_IDENTITY_MISMATCH` | identity manifest 的 graphHash 与 graph 不一致 | +| `GE1303_COMPONENT_IDENTITY_MISMATCH` | node/edge/schema hash 或顺序与 graph 不一致 | + +诊断顺序固定:envelope/portable JSON → identity/reference → DAG → policies → +typed-port schema/profile → component/revision verification。TS/Python conformance 只比较 +稳定 code、path、node/edge/output 标识,不比较 parser 或 validator 的英文消息。 + +### 6.4 Domain-separated component hashes + +整图 `graphHash` 算法不变。新组件 hash 使用 UTF-8 domain separation,避免相同 JSON +在 node/edge/schema 角色间混淆: + +```text +SHA256("graph-engineering/component/v1alpha1\0" + KIND + "\0" + canonical(value)) +``` + +`KIND` 只能是 `node`、`edge`、`schema`。node/edge hash 覆盖其完整声明(包括 ID); +schema hash 覆盖 schema object 本身,因此同一 schema 在不同 owner 可复用同一 hash。 + +`CompiledGraphIdentity` 至少包含: + +```json +{ + "apiVersion": "graphengineering.reacher-z.github.io/compiled-identity/v1alpha1", + "kind": "CompiledGraphIdentity", + "graphRevision": 1, + "graphHash": "<64 lowercase hex>", + "nodes": [{ "id": "split", "index": 0, "contentHash": "...", "inputSchemaHash": "...", "outputSchemaHash": "..." }], + "edges": [{ "id": "split-left", "index": 0, "contentHash": "...", "schemaHash": null }], + "graphSchemas": { "input": "...", "output": "...", "state": null }, + "revisionHash": "" +} +``` + +node/edge arrays保留声明顺序并带 index。`revisionHash` 的 preimage 使用独立 +`graph-engineering/revision/v1alpha1\0` domain,覆盖除自身外的完整 canonical +identity。GraphSpec 本身仍没有 `graphRevision` 字段;往 GraphSpec 塞该字段继续是 +`GE1007_INVALID_GRAPH`。 + +identity verifier 只接受 revision 1,并重算全部 hash。它不创建 revision 2, +不 emit `GraphPatched`,不改 durable history。未来 GraphPatch 必须新规范定义 parent +revisionHash、patchHash、授权和预算链路。 + +## 7. 共享 Fixtures(主 Agent 独占写) + +### 7.1 Authoring/hash 正向 corpus + +新增 `spec/conformance/authoring.case.json`,每个 case 指向明确文件并记录完整预期: + +| Case | 输入/动作 | 必须相同的输出 | +|---|---|---| +| `equivalent-diamond` | JSON、YAML、TS builder、Python builder | exact GraphSpec、canonicalGraph、graphHash、identity | +| `typed-port-diamond` | strict-exact graph 的四种 authoring 路径 | diagnostics=[]、所有 component/revision hashes | +| `declaration-order` | 非字典序 node/edge 调用和 YAML sequence | 数组顺序、topological tie-break、hash | +| `explicit-null-config` | node `config:null` | null 保留且四路径 hash 相同 | +| `unicode-and-keys` | Unicode key、lone surrogate JSON vector、非 ASCII labels | canonical bytes/hash 一致 | +| `safe-integer-edges` | `±(2^53-1)` 与 timer ceiling | accepted bytes/hash 一致 | + +推荐 fixture 文件: + +```text +spec/conformance/authoring/ + equivalent.graph.json + equivalent.graph.yaml + typed-ports.graph.json + typed-ports.graph.yaml + declaration-order.graph.yaml + yaml-scalars.case.json + component-identity.expected.json +``` + +`component-identity.expected.json` 必须写死整图、每个 node、每个 edge、每类 schema、 +revisionHash,不能运行时只比较“两边恰好一样”。 + +### 7.2 YAML 负向 corpus + +`yaml-invalid.case.json` 引用单缺陷 source,并固定 source code 与位置类别: + +- duplicate root key;duplicate nested node key; +- anchor;alias;merge key;custom tag;directive;multi-document; +- mapping 作 key;timestamp/non-JSON scalar;NaN/Infinity;unsafe integer; +- root sequence;超过 depth/node/byte limit;trailing invalid token; +- explicit null 放入 metadata.description、stateSchema、output.port、node.retry。 + +最后四项 parse 成 JSON 后必须继续得到现有 `GE1007_INVALID_GRAPH`,不能在 YAML 层 +擅自改成成功或丢字段。 + +### 7.3 Typed-port/revision 负向 graph fixtures + +每个 fixture 只引入一个缺陷并加入 `expected.json`: + +```text +invalid-typed-missing-source-port.graph.json -> GE1201 +invalid-typed-missing-target-port.graph.json -> GE1202 +invalid-typed-schema-mismatch.graph.json -> GE1203 +invalid-typed-duplicate-binding.graph.json -> GE1204 +invalid-typed-schema-profile.graph.json -> GE1205 +invalid-typed-output-schema.graph.json -> GE1206 +invalid-typed-entrypoint-schema.graph.json -> GE1207 +invalid-typed-stream-mode.graph.json -> GE1208 +``` + +Revision cases放在 identity case 文件,不伪装 GraphSpec 字段:revision 0、2、unsafe +integer、graphHash mutation、node hash mutation、edge order mutation、schema hash mutation; +分别固定 GE1301/GE1302/GE1303。 + +### 7.4 Conformance reporter + +新增 `tools/conformance/python_authoring_report.py`,并在 `run.mjs` 中生成 TS report。 +标准 report: + +```json +{ + "case": "equivalent-diamond", + "sourceFormat": "yaml", + "valid": true, + "canonicalGraph": "...", + "graphHash": "...", + "identity": {}, + "diagnosticCodes": [], + "sourceError": null +} +``` + +协调器必须同时比较 expected、TS、Python 三方;只比较 TS==Python 会让同样的 bug +误判为 conformance。 + +## 8. 文件级并行施工边界 + +### 8.1 Main/integration(唯一 shared writer) + +- `spec/authoring-semantics.md` +- `spec/compiled-identity.schema.json` +- `spec/conformance/authoring/**` +- typed-port graph fixtures 与 `expected.json` +- `spec/README.md`、ADR/decision、最终 `tools/conformance/run.mjs` join +- 诊断码、hash vectors、policy extension 的最终签核 + +### 8.2 TypeScript lane + +- `packages/core/src/builder.ts` +- `packages/core/src/source.ts` +- `packages/core/src/component-identity.ts` +- `packages/core/src/typed-ports.ts` +- `types.ts`、`compiler.ts`、`index.ts` 的受控扩展 +- `packages/core/test/{builder,source,component-identity,typed-ports}.test.ts` +- `packages/core/package.json` 与 README + +TS lane 不写 `spec/`、Python、CLI 或 conformance coordinator。现有 patterns 中的 +portable snapshot/deep-freeze 逻辑应抽成 core 内部 helper 后由 patterns 消费或保持 +独立;不得复制出第三套稍有不同的安全边界。 + +### 8.3 Python lane + +- `python/src/graph_engineering/{builder,source,component_identity,typed_ports}.py` +- `models.py`、`compiler.py`、`__init__.py` 的受控扩展 +- `python/tests/test_{builder,source,component_identity,typed_ports}.py` +- `python/pyproject.toml` 与 README + +Python lane 不写 `spec/` 或 TS。Pydantic 的 shallow frozen 不能被误当成 nested +immutability;测试必须直接 mutation returned nested config/schema 并证明 identity +不漂移。 + +### 8.4 Platform/CLI lane + +- `packages/cli/src/cli.ts` 或独立 `source-loader.ts` +- CLI help/README/tests;`.yaml/.yml` 与 `--input-format` 行为 +- `scripts/validate-fixtures.mjs` 增加 YAML/case 引用完整性 +- schema bundle byte-equality guard;packed-install smoke 加 YAML compile +- Python reporter 可以由 platform lane 新建,main 只做最终 join + +CLI lane 不重新实现 parser policy;只调用 core source decoder。MCP 结构化 Graph +输入不自动获得 YAML 文本入口,除非另有明确 API/权限设计。 + +### 8.5 集成顺序 + +```text +contract + golden fixtures + ├── TS builder/source/identity/ports ─┐ + ├── Python builder/source/identity ──┼── shared authoring conformance + └── CLI input-format integration ────┘ + ├── docs/package/install gates + └── independent review/evidence +``` + +TS 与 Python 可以最大并行;两边在 fixtures/diagnostics 未冻结前不得修改 shared +expected。CLI 可以在 source API 类型冻结后并行。最终 conformance、registry evidence +和 commit 由 main 串行完成。 + +## 9. 测试矩阵 + +### 9.1 Builder + +- 四路径 exact document/canonical/hash/identity parity; +- insertion/declaration order;重复 IDs/outputs/entrypoints;缺必需字段; +- hostile getter/proxy/mapping 零执行;cyclic、sparse、class、Date、bigint/unsafe int; +- caller mutation before/after build;result nested mutation;build 后 sealed; +- optional absence/null;required config null;unknown policy extension null; +- reserved output names(`__proto__` 等)不污染原型且不被静默丢弃; +- compiler diagnostics 原样保留,无 silent null。 + +### 9.2 Source/YAML + +- `.json/.yaml/.yml` auto;stdin explicit format;unknown extension; +- fatal UTF-8;1 MiB/100-depth/100k-node bounds; +- duplicate keys at every nesting;anchors/aliases/tags/merge/multi-doc; +- YAML 1.2 scalar traps;finite/safe numeric boundaries; +- comments/block strings/Unicode;line/column 1-based;错误不泄漏 source; +- valid YAML 与 JSON exact canonical/hash;invalid Graph YAML 仍走 GE1007。 + +### 9.3 Typed ports + +- from.port、implicit target key、explicit target port、public output port; +- required/property 缺失;duplicate binding;edge.schema 三方一致/不一致; +- object key order不同但 schema canonical identical; +- external ref、invalid Draft 2020-12、stream/artifact; +- legacy non-opt-in diamond 仍通过且不产生虚假的 typed claim; +- TS/Python code/path/node/edge/output order完全一致。 + +### 9.4 Identity/revision + +- golden domain-separated component hashes;重复 schema hash 复用; +- node/edge declaration order与 index;metadata-only change 只改变 graph/revision hash, + 不改变无关 node/edge content hash; +- node/edge/schema 单字段修改只改变预期 component 与 graph/revision hash; +- revision 1 default/explicit identical;revision 0/2/unsafe fail GE1301; +- manifest graph/component/order mutation fail GE1302/GE1303; +- GraphSpec 添加 graphRevision 继续 GE1007;durable revision-1 tests 不回归。 + +## 10. 验收命令 + +### 10.1 每个 lane 的 focused gate + +```bash +corepack pnpm --filter @graph-engineering/core test +corepack pnpm --filter @graph-engineering/core typecheck + +uv run --project python pytest -q \ + python/tests/test_builder.py \ + python/tests/test_source.py \ + python/tests/test_component_identity.py \ + python/tests/test_typed_ports.py \ + python/tests/test_models.py \ + python/tests/test_compiler.py +uv run --project python ruff check python/src python/tests \ + tools/conformance/python_authoring_report.py +uv run --project python mypy python/src/graph_engineering + +corepack pnpm --filter @graph-engineering/cli test +``` + +### 10.2 Shared closure gate + +```bash +corepack pnpm validate:fixtures +corepack pnpm test:conformance +corepack pnpm check:docs +``` + +报告必须明确打印 authoring cases、YAML negative cases、typed-port graph fixtures、 +component hash vectors 与 revision negative cases 数量;不能只打印笼统“passed”。 + +### 10.3 完整仓库与发布 rehearsal + +```bash +corepack pnpm build +corepack pnpm typecheck +corepack pnpm lint +corepack pnpm test +corepack pnpm check:packages +corepack pnpm check:packed-install +corepack pnpm audit:prod + +uv run --project python pytest -q +uv run --project python ruff check python/src python/tests tools/conformance +uv run --project python mypy python/src/graph_engineering +uv build --project python +python3 scripts/check-python-artifacts.py + +git diff --check +``` + +packed install 必须从实际 tarball/wheel 导入 builder/source/identity API,并编译同一 +YAML fixture;源码工作树 import 不算发布证据。新增 YAML/schema validator 依赖必须 +出现在 tarball/wheel metadata 和 production audit 中。 + +## 11. Registry 与 completion evidence + +`D2-BUILDERS-YAML-020` 的四条 expected test 应按下列证据关闭: + +| Registry requirement | 最低完成证据 | +|---|---| +| builder hash parity | 四路径 golden case;TS/Python focused tests;packed imports | +| YAML/JSON equivalence | safe profile 正负 corpus;CLI file/stdin;三方 expected join | +| revision and typed-port diagnostics | GE1201-1208、GE1301-1303 fixtures;identity golden hashes | +| optional IR fields reject explicit null | 现有 4 shared fixtures + Python 27-field matrix + candidate conformance | + +完成记录必须绑定:candidate commit SHA、spec/fixture hash、精确命令、平台/版本、 +结果、不可变报告路径、独立 reviewer 与明确排除项。dirty worktree 的本地通过不能 +直接把 task 标 completed。 + +建议完成产物: + +```text +codex_logs/release-evidence/d2-authoring// + manifest.json + ts-core.txt + python-core.txt + cli.txt + conformance.txt + package-install.txt + dependency-audit.txt + review.json +``` + +## 12. Definition of Done + +D2 只有全部勾选才可从 `in_progress` 变为 `completed`: + +- [ ] authoring、safe YAML、typed-port strict-exact、identity/revision-1 规范已审批; +- [ ] TS/Python 通用 builder 能构造任意当前 v1alpha1 GraphSpec; +- [ ] builder 无推断 root/output/ID、无覆盖重复值、无 mutation/hostile-input 漏洞; +- [ ] YAML 只接受冻结安全子集,所有危险结构 fail closed; +- [ ] JSON/YAML/TS/Python 四路径产出 exact canonicalGraph/graphHash; +- [ ] strict typed ports 产生 GE1201-1208,legacy graph 不回归也不虚假宣称; +- [ ] node/edge/schema/revision hashes 有 domain separation 与写死 golden vectors; +- [ ] initial identity verifier 对 revision/hash/order mutation fail closed; +- [ ] GraphPatch、revision 2+ 与 stream/artifact runtime 仍明确标为未实现; +- [ ] fixture validator、双语言 conformance、focused/full/package gates 全绿; +- [ ] CLI help/README、core/Python docs、architecture ledger 与 limitations 同步; +- [ ] candidate-bound evidence 和独立 review 已写入 registry/log; +- [ ] 没有把本地 green、enum、schema vocabulary 或文档当作运行时能力证明。 + +完成 D2 后可以解除 `D3-PY-CLI-021` 的 authoring 依赖,并为 D4 subgraph、D7 +GraphPatch、D14 pattern skeleton 提供稳定入口;它本身不解除这些后续任务的任何 +运行时、durability、安全或 release gate。 diff --git a/codex_plans/delivery/d9-redaction-implementation-brief.md b/codex_plans/delivery/d9-redaction-implementation-brief.md new file mode 100644 index 0000000..3ff1690 --- /dev/null +++ b/codex_plans/delivery/d9-redaction-implementation-brief.md @@ -0,0 +1,569 @@ +# D9 durable redaction implementation brief + +Status: **implementation brief / release-blocking contract proposal** +Task: `D9-REDACTION-039` +Prepared: 2026-07-26 +Scope: TypeScript and Python durable events, checkpoints, future trace/export +sinks, legacy histories, and cross-language acceptance +Implementation status: **not implemented by this document** + +This brief is subordinate to the canonical specifications under `spec/`. It +records the implementation decision that should be frozen in +`spec/redaction-semantics.md` before either native runtime is changed. It does +not mark `D9-REDACTION-039`, `I06`, `T26`, `Q08`, `SC07`, or `SC13` complete. + +## 1. Required decision + +The current `redacted` signal must not be repaired by merely documenting it. +The implementation should land in two explicit stages: + +1. **Truth hotfix.** Existing v1alpha1 events that contain inline application + values must be written with `redacted: false`; the JSON Schema default of + `true` must be removed. Durable resume must reject legacy histories that say + `redacted: true` while using the known raw v1alpha1 payload shapes. This + immediately removes the false safety claim but does not complete D9. +2. **Protected durable payload contract.** A new recovery contract stores + authoritative inputs, outputs, result snapshots, and optional diagnostic + evidence behind authenticated encrypted `ProtectedValueRef` values. The + journal and checkpoints contain references, policy identity, and keyed + semantic identities—not plaintext application values. Default + log/trace/error/prompt/tool/support capture remains off or metadata-only. + D9 closes only after this path, both native implementations, legacy handling, + packaged canary scans, and independent security review are green. + +Encryption/protection and redaction are different facts. Ciphertext or a +protected reference is **not** described as redacted. An event is `redacted: +true` only when a defined irreversible redaction transform was actually applied +to material that otherwise would have appeared in that event's persisted +`data`. This narrow definition prevents the boolean from becoming a blanket +claim that every byte in every sink is secret-safe. + +## 2. Read-only audit findings + +### 2.1 Wire and schema mismatch + +| Evidence | Actual behavior | Security consequence | +| --- | --- | --- | +| `spec/event.schema.json:7-39` | `redacted` is optional and has JSON Schema `default: true`; `data` is an unrestricted object. | Absence can be interpreted as true by a default-applying consumer even though no transform exists. | +| `packages/persistence/src/events.ts:35-52,103-154` | TypeScript models `redacted?: boolean` and validates only its type. It does not apply or verify redaction. | The flag is an unaudited caller assertion. | +| `python/src/graph_engineering/events.py:55-85` | Python gives `GraphEvent.redacted` a runtime default of `True`; validation checks shape, not payload treatment. | A Python-created event can claim redaction without a transform or receipt. | +| `packages/runtime/src/durable.ts:397-414` | Every scheduler event receives `redacted: true` while `data` is copied unchanged and `payloadHash` hashes those unchanged bytes. | Every TS durable event carries a false signal. | +| `python/src/graph_engineering/durable.py:217-236` | Every Python scheduler event likewise receives `"redacted": True` with the unchanged draft data. | The defect has native parity rather than native safety. | +| `spec/conformance/run-created.event.json:1-13` | The shared fixture explicitly expects `redacted: true`. | Conformance currently preserves the defect. | + +### 2.2 Plaintext durable values are broader than two fields + +The current security plan calls out `RunCreated.input` and +`NodeSucceeded.output`. The implementation audit finds five additional copies +or channels that the correction must cover. + +| Event/data location | Concrete evidence | Plaintext or sensitive derivative persisted today | +| --- | --- | --- | +| `RunCreated.data.input` | TS `packages/runtime/src/durable.ts:1935-1956`; Python `python/src/graph_engineering/durable.py:1916-1941` | Complete tagged original graph input. | +| `NodeScheduled.data.input` | TS `packages/runtime/src/durable.ts:493-525`; Python `python/src/graph_engineering/durable.py:335-368` | Complete tagged bound input for every attempted node, including values derived from upstream output. | +| `NodeSucceeded.data.output` | TS `packages/runtime/src/durable.ts:587-616`; Python `python/src/graph_engineering/durable.py:441-474` | Complete tagged validated node output. | +| `NodeAttemptFailed.data.failure` | TS `packages/runtime/src/durable.ts:533-584` and `:209-221`; Python `python/src/graph_engineering/durable.py:375-439` and `:110-137` | Human error message and host cause name; thrown errors may contain prompts, tool responses, paths, tokens, or user data. | +| `NodeSettledWithoutAttempt.data.result` | TS `packages/runtime/src/durable.ts:619-628` plus `:244-253`; Python `python/src/graph_engineering/durable.py:476-490` plus `:140-153` | Tagged node result, including bound input and failure message. | +| terminal `Run*.data.result` | TS `packages/runtime/src/durable.ts:631-640` plus `:256-267`; Python `python/src/graph_engineering/durable.py:492-503` plus `:156-169` | A second complete snapshot of node inputs, node outputs, failures, and graph output. | +| hash/activity fields | `spec/durable-recovery-semantics.md:79-82,168-182` | Unkeyed input/output hashes and activity keys can disclose equality and permit dictionary guesses for low-entropy secrets. | + +The recovery fold proves that these are not inert annotations. It decodes and +uses raw `RunCreated.input`, `NodeScheduled.input`, and `NodeSucceeded.output` +to reconstruct scheduling state (TS `packages/runtime/src/durable.ts:1363-1397, +1551-1594,1657-1689`; Python +`python/src/graph_engineering/durable.py:1315-1358,1517-1564,1625-1657`). A +redaction transform cannot replace these authoritative values inline without +changing replay behavior. + +### 2.3 Stores persist exactly what they receive + +| Sink | Concrete evidence | Current treatment | +| --- | --- | --- | +| TS JSONL events | `packages/persistence/src/jsonl-event-store.ts:107-137` | Canonicalizes the whole event and appends it as UTF-8 JSONL; no filter or redactor. | +| TS memory events | `packages/persistence/src/memory-event-store.ts:8-31` | Deep-clones the complete event into process memory. | +| Python JSONL events | `python/src/graph_engineering/persistence/event_store.py:221-247` | Canonicalizes the whole Pydantic event and writes raw UTF-8 JSONL; no filter or redactor. | +| Python memory events | `python/src/graph_engineering/persistence/event_store.py:107-145` | Deep-copies the complete event. | +| TS checkpoints | `packages/persistence/src/file-checkpoint-store.ts:215-254` | Deep-clones arbitrary `state`, includes it in `contentHash`, and writes it in full. There is no redaction/disposition field. | +| Python checkpoints | `python/src/graph_engineering/persistence/checkpoint_store.py:214-250` | Includes arbitrary checkpoint `state` in the canonical body/hash and writes it in full. There is no redaction/disposition field. | + +Checkpoint state is currently a standalone caller-controlled storage primitive; +the durable scheduler does not use it. That limits the current scheduler leak +surface, but it does not make checkpoint bytes safe. A future scheduler +checkpoint would duplicate inputs and outputs unless the protected-reference +contract is frozen first. + +### 2.4 Trace boundary + +- `traceId`, `spanId`, and `parentSpanId` are optional event fields in + `spec/event.schema.json:30-32`, TypeScript + `packages/persistence/src/events.ts:43-45`, and Python + `python/src/graph_engineering/events.py:76-78`. A generic JSONL store preserves + them exactly. +- The durable writers do not currently populate those fields. +- The repository currently has no OpenTelemetry exporter, prompt/response + capture implementation, or support-bundle implementation. This is an absent + surface, not evidence that future trace capture is redacted. +- The shared event fixture places `traceId` beside the false `redacted: true` + signal (`spec/conformance/run-created.event.json:9-10`), so it must be + corrected with the wire contract. + +## 3. Normative wire truth contract + +### 3.1 Required event facts + +The next event envelope revision should make both of these fields required: + +```json +{ + "redacted": false, + "payloadDisposition": "protected-ref" +} +``` + +`payloadDisposition` is one of: + +- `metadata-only`: the event was designed to contain only an allowlisted + metadata schema; no application value was sourced for inline capture; +- `protected-ref`: authoritative application values are represented only by + validated `ProtectedValueRef` objects; +- `redacted`: one or more application-derived fields were irreversibly + transformed under the recorded policy before this event was created; +- `inline-unredacted`: application values are present in plaintext after an + explicit high-risk opt-in. + +The truth table is conjunctive: + +| Persisted condition | `payloadDisposition` | `redacted` | Allowed by default? | +| --- | --- | ---: | ---: | +| No application payload field exists | `metadata-only` | `false` | Yes | +| Authenticated encrypted reference exists; plaintext does not | `protected-ref` | `false` | Yes | +| Defined transform replaced/removed application-derived material and a valid receipt is present | `redacted` | `true` | Only for observational data, not scheduler authority | +| Any plaintext application value remains | `inline-unredacted` | `false` | No; explicit risk authorization required | +| Ciphertext/ref labeled redacted | invalid | `true` | Never; encryption is not redaction | +| `redacted: true` with no receipt/policy hash | invalid | `true` | Never | + +For `payloadDisposition: redacted`, the event must include a `redactionReceipt` +containing only `policyHash`, transform version, transformed JSON Pointer paths, +replacement mode, and count. It must not contain the removed values or their +unkeyed hashes. Event validation checks that the receipt is structurally valid; +the sink guard checks that it matches the deterministic transform result. + +### 3.2 v1alpha1 truth hotfix + +Before the new protected contract lands: + +- remove `"default": true` from `spec/event.schema.json`; +- require durable writers to emit `redacted: false` for every current inline + v1alpha1 event; +- update the event fixture from `true` to `false`; +- make TS and Python defaults aligned: absence stays absence and is never + interpreted as true; +- have the durable semantic fold reject a known v1alpha1 raw payload shape with + `redacted: true` or an absent flag as `LEGACY_REDACTION_MISMATCH` before any + executor invocation; and +- retain the raw `payloadHash` rule for the hotfix, because it truthfully hashes + the bytes actually stored. + +The hotfix is a truthful but still unsafe inline mode. Documentation and release +checks must continue to call D9 Open until the protected contract and canary +matrix pass. + +## 4. Capture policy + +One immutable policy is bound at run creation and reused on resume: + +```json +{ + "apiVersion": "graphengineering.reacher-z.github.io/capture-policy/v1alpha1", + "durableValues": "protected", + "checkpointValues": "protected", + "events": "metadata-or-protected", + "errors": "codes-and-sanitized-message", + "logs": "metadata-only", + "traces": "metadata-only", + "prompts": "off", + "responses": "off", + "tools": "off", + "supportBundles": "off", + "maxDiagnosticUtf8Bytes": 1024, + "redactionTransform": "json-pointer-rules/v1alpha1", + "keyRef": "operator-owned-key-reference" +} +``` + +### Default behavior + +- Non-durable runtime payload capture, prompt/response capture, tool body + capture, traces, and support bundles remain off. +- Durable and checkpoint application values use `protected` mode. +- If no `ProtectedPayloadStore`/key authority is configured, a durable start or + checkpoint save that would persist an application value fails **before the + first event, checkpoint, log, or error payload is written** with + `PAYLOAD_PROTECTION_REQUIRED`. +- Default errors persist only a stable code, node/attempt identity, retryability, + and a bounded sanitizer-produced message. Raw exception strings and tool or + provider response bodies are not persisted. +- No API silently falls back from protected to inline capture. + +This fail-closed default is intentionally breaking for the alpha durable API. +Generating an encryption key beside ciphertext automatically would make the +byte scan look green while providing little confidentiality, so the runtime +must not do that. + +### Explicit modes + +- `protected`: stable-v1-supported authoritative mode. Requires a configured + key provider and protected store. +- `metadata-only`: valid for observational sinks. It is invalid for a durable + value that recovery must reconstruct. +- `redacted`: valid for derived logs/traces/errors after deterministic + transformation. A redacted derivative never feeds scheduling, replay, routing, + approval, or hash identity. +- `inline-unredacted`: compatibility/debug escape hatch only. It requires an + explicit policy grant and risk acknowledgement, writes `redacted: false`, is + disabled by stable production policy, and cannot satisfy D9 canary/release + evidence. + +Policy selection is deterministic code outside model/tool output. A graph, +node, planner, tool, provider, resumed worker, or child graph may request less +capture but cannot expand capture or change `keyRef`. The canonical policy hash +is recorded in `RunCreated` and bound into every protected value's associated +data. Resume under another policy fails closed. + +## 5. Protected payload design + +### 5.1 `ProtectedValueRef` + +Authoritative values are encoded as Tagged Durable JSON, then protected using +AEAD. The journal/checkpoint representation is a closed object such as: + +```json +{ + "apiVersion": "graphengineering.reacher-z.github.io/protected-value/v1alpha1", + "ref": "pv_01J...", + "codec": "durable-json/v1alpha1", + "ciphertextHash": "<64 lowercase hex>", + "valueMac": "<64 lowercase hex>", + "keyRefHash": "<64 lowercase hex>", + "aadHash": "<64 lowercase hex>" +} +``` + +The protected blob uses a random production nonce and an authenticated cipher +available in both runtimes (AES-256-GCM is the baseline). The encryption key is +obtained from an operator-owned `KeyProvider`; it never appears in Graph IR, +events, checkpoints, artifacts, logs, traces, errors, or committed evidence. +File blobs are private and atomically published. Decryption requires both store +read authority and key authority. + +Associated data binds at least contract version, run ID, graph revision, event +type, node/edge/attempt identity when present, logical field JSON Pointer, +capture-policy hash, codec, and `valueMac`. Copying a blob/ref to another +run/field therefore fails authentication. + +`ciphertextHash` is SHA-256 over the persisted blob and is an integrity/address +fact. `valueMac` is HMAC-SHA-256 over context plus canonical Tagged Durable JSON +using a run-scoped identity key derived by the key provider. An unkeyed hash of +a low-entropy secret must not be exposed as its durable identity. + +### 5.2 Event payload mapping + +| Current field | Protected contract | +| --- | --- | +| `RunCreated.input` / `inputHash` | `inputRef` / `inputMac`; also bind capture-policy hash, protected-store contract, and key reference hash. | +| `NodeScheduled.input` / `inputHash` | `inputRef` / `inputMac`; a ref may be reused only when its AAD permits this exact logical field, otherwise create a new protected blob. | +| `NodeStarted.inputHash` | `inputMac`; no value or ref is needed. | +| `NodeSucceeded.output` / `outputHash` | `outputRef` / `outputMac`. | +| `EdgeEmitted.outputHash` | `outputMac`; consumers still require the authorized output ref from the producer projection. | +| `NodeAttemptFailed.failure.message` | Stable sanitized message inline; optional raw evidence may be stored only as a protected ref under explicit policy. | +| `NodeSettledWithoutAttempt.result` | `resultRef` / `resultMac`; stable outcome metadata may remain inline. | +| terminal `Run*.result` | `resultRef` / `resultMac`; no duplicate plaintext node/result data. | +| scheduler checkpoints | Metadata projection plus protected refs/MACs only; `contentHash` covers the persisted checkpoint body, never plaintext. | + +A narrow `ProtectedPayloadStore` is a D9 security primitive, not a claim that +the complete Day-9 `ArtifactStore` or Day-15 production storage work is done. +The future ArtifactStore may implement this interface, but D9 should not wait +for or silently claim the broader artifact milestone. + +## 6. Hash, activity-key, and replay rules + +1. Snapshot and validate the logical value first. +2. Encode canonical Tagged Durable JSON. +3. Compute its context-bound `valueMac` before any redaction or encryption. +4. Protect the bytes and persist only `ProtectedValueRef` plus allowed metadata. +5. Compute event `payloadHash` over the exact persisted `data` containing refs. +6. On recovery, authorize read, authenticate/decrypt, decode, recompute + `valueMac`, and only then expose the logical value to the fold. + +Consequences: + +- `payloadHash` changes when inline fields become refs. It remains a byte-level + event integrity check and is never used as semantic replay identity. +- The v1alpha2 activity key is derived from the original logical input identity, + not from a redacted derivative: + + ```text + HMAC(runIdentityKey, + tagged(["activity/v1alpha2", runId, graphRevision, nodeId, inputMac])) + ``` + +- The same node input in the same run keeps one activity/idempotency key across + retries and resume. Different runs do not expose a correlatable unkeyed input + digest. +- A policy change, missing key, missing blob, authentication failure, MAC + mismatch, or unauthorized ref fails before executor invocation. The fold must + never substitute null, a redaction token, or an empty value. +- Observational redaction is one-way and cannot be read back into the scheduler. + Redaction therefore cannot alter routing, budget, approval, output, replay, or + fork identity and cannot authorize hidden work. +- Key rotation uses envelope-key rewrapping outside immutable events. It must not + rewrite event bytes, change refs, or change `valueMac`/activity identity. + +## 7. Sink-before-write pipeline + +Every sink adapter must receive output only from one shared deterministic guard: + +```text +snapshot + portable validation + -> classify field and sink + -> compute keyed semantic identity (authoritative values only) + -> apply immutable capture policy + -> redact derivative OR protect authoritative bytes + -> validate disposition/receipt/ref + -> canary/credential defense-in-depth scan + -> canonicalize and hash persisted representation + -> write/export +``` + +There is no write-then-scrub path. A transform, protector, scanner, serializer, +or sink error is structured, contains no offending value, and causes no partial +fallback write. Batch event commits fail as a unit before executor/dependent +release. Checkpoint temporary files must contain only the already-protected +representation. + +The guard applies to journal, checkpoint, artifact/protected blob, stdout, +stderr, application/runtime log, trace exporter, error aggregator, CLI/MCP +diagnostic, Explorer response, and support bundle. Adapters cannot opt out by +calling a lower-level writer with raw values. Low-level storage interfaces may +remain available for embedding, but must be named/typed as unsafe and cannot be +used by default runtime paths or release evidence. + +Trace IDs should be constrained to their protocol-safe format rather than +arbitrary user text. Trace/span attributes use a fixed metadata allowlist; +prompt, response, tool body, input, output, exception text, environment, and +secret values are absent by default. + +## 8. Legacy histories and migration + +Known legacy condition: + +```text +scheduler-recovery/v1alpha1 +AND redacted is true or defaulted/absent +AND an inline input/output/result/failure payload shape is present +``` + +Default behavior is `LEGACY_REDACTION_MISMATCH` before `RunResumed` and before +any executor invocation. Terminal history may be inspected only through an +explicit unsafe read/export API that returns a prominent structured warning and +never continues work. + +The project must **not** silently flip the flag, recompute hashes, or rewrite the +JSONL in place. That would violate append-only audit expectations and conceal +which bytes were exposed. It also must not copy a nonterminal history under a +new run ID while retaining old activity keys, because activity identity includes +the run ID and external effects may already be in doubt. + +Supported migration outcomes are: + +1. **Quarantine:** preserve the original file read-only, restrict permissions, + record its digest and unsafe classification, and block resume. +2. **Sealed archive:** with explicit operator authority, encrypt the complete + legacy bytes into a protected archive and emit a metadata-only migration + manifest containing source digest, destination ciphertext digest, tool + version, time, and reviewer—never raw payloads. +3. **New run/replay-fork:** start a v1alpha2 run with a new run ID and protected + inputs. Nonterminal or externally effectful histories require reconciliation + and the later approval/replay contract; no automatic conversion is allowed. + +An already created v1alpha1 history with `redacted: false` is truthful but still +inline unsafe. It may be read only under explicit legacy-inline authorization; +it is not accepted as protected D9 evidence. + +## 9. Stable cross-language failure codes + +| Code | Required trigger | Executor/write rule | +| --- | --- | --- | +| `REDACTION_POLICY_REQUIRED` | No capture policy can be resolved | No sink write; no executor | +| `REDACTION_POLICY_INVALID` | Unknown version/mode, invalid selector, inconsistent disposition | No sink write; no executor | +| `CAPTURE_POLICY_MISMATCH` | Resume policy hash differs from `RunCreated` | No append; no executor | +| `INLINE_CAPTURE_NOT_AUTHORIZED` | Inline mode lacks explicit policy/acknowledgement or production policy denies it | No sink write | +| `PAYLOAD_PROTECTION_REQUIRED` | Authoritative value needs persistence but no protected store/key is configured | No sink write; no executor | +| `PAYLOAD_PROTECTION_FAILED` | Encode/encrypt/atomic publish fails | No event/checkpoint reference is committed | +| `PROTECTED_PAYLOAD_NOT_FOUND` | Referenced blob is missing | No executor; do not replace with null | +| `PROTECTED_PAYLOAD_UNAUTHORIZED` | Store/key authority denies access | No executor; no sensitive detail | +| `PROTECTED_PAYLOAD_CORRUPT` | Ciphertext hash, AEAD authentication, codec, AAD, or value MAC fails | No executor; preserve evidence | +| `REDACTION_RECEIPT_INVALID` | `redacted: true` does not match a valid deterministic transform receipt | Reject before persistence or as corrupt history | +| `LEGACY_REDACTION_MISMATCH` | Known raw legacy shape claims/defaults to redacted | No resume/executor; quarantine path only | +| `SECRET_CANARY_DETECTED` | Defense-in-depth pre-sink scanner finds seeded/credential material in a disallowed representation | Reject write; report only canary ID and sink, never value | + +TypeScript and Python exception class names may differ, but codes, phase, +redacted-safe detail keys, and no-write/no-executor behavior are portable. +Persisted-history violations should retain the corruption/invalid-history causal +chain without echoing offending bytes. + +## 10. Shared fixture set + +Add these canonical fixtures under `spec/conformance/`: + +| Fixture | Purpose | +| --- | --- | +| `redaction-wire-truth.case.json` | Every truth-table row, missing/invalid receipt, encryption-not-redaction, and exact TS/Python envelope parity. | +| `redaction-policy.case.json` | Default policy, explicit protected/metadata/redacted/inline modes, forbidden expansion, immutable policy hash, and invalid policies. | +| `protected-value.case.json` | Tagged values, fixed test key/nonce only for deterministic fixture vectors, AAD binding, ciphertext hash, value MAC, Unicode/float/null values, and malformed refs. | +| `durable-protected-resume.case.json` | Success, retry, interruption, terminal resume, missing/denied/corrupt ref, stable activity key, and zero executor calls on protection failure. | +| `checkpoint-protected.case.json` | Metadata/ref-only state, content hash, missing/stale/corrupt ref behavior, and proof that no plaintext enters temp/final bytes. | +| `redaction-legacy.case.json` | Misleading true, absent/defaulted flag, truthful-but-inline false, terminal unsafe inspection, nonterminal rejection, and quarantine manifest. | +| `redaction-canary.case.json` | Synthetic unique canaries for every source category and obvious encoded variants. | + +Production nonce generation is random and must not be injectable by untrusted +callers. Deterministic key/nonce vectors exist only in test providers clearly +labeled non-production. Normal conformance compares semantic results and +failure codes; exact ciphertext comparison is limited to those fixed vectors. + +## 11. Canary sink matrix + +Each language runs clean packaged success, retry, timeout, cancellation, +failure, crash/resume, terminal-resume, and protection-failure scenarios. Seed a +different canary in graph input, mock model prompt/input, executor output, thrown +error, mock tool response, artifact/protected value, and support/log metadata. + +| Sink scanned as raw bytes | Default expected result | Explicit protected/capture check | +| --- | --- | --- | +| Event journal, including temp/torn tails | No raw or obvious encoded canary | Only refs/MACs/allowed metadata; flags/disposition exact | +| Checkpoint temp and final files | No raw or obvious encoded canary | Only protected refs; content hash valid | +| Protected payload/artifact blobs | No plaintext or obvious encoded canary | Authorized decrypt returns exact original; wrong key/AAD fails | +| stdout | No canary | Raw capture requires a separate explicitly authorized test and is never release-default evidence | +| stderr | No canary | Same | +| Runtime/application JSONL logs | No canary | Bounded allowlisted/redacted attributes only | +| Trace/export/network bytes | No canary and no exporter/network activity by default | Enabled trace has metadata/redaction receipt only | +| Returned/serialized error reports | No canary | Stable code, safe IDs/hashes, bounded sanitized message | +| CLI/MCP machine JSON and diagnostics | No canary | Safe structured envelope only | +| Explorer/API responses and persisted cache | No canary | Privileged protected fetch is separately authorized/audited | +| Support bundle/archive | No bundle by default, otherwise no canary | Explicit consent plus allowlist/redaction receipt | +| Migration manifest and release evidence | No canary | Digests, versions, disposition, reviewer only | + +The scanner checks literal UTF-8 plus JSON escaping, URL encoding, base64, +hex, UTF-16 LE/BE, and compressed archive members. It records scanner +version/config, candidate and package digests, scenario, sink, canary ID, +result, and false-positive disposition. It never records canary values in the +committed report. + +Every campaign includes: + +- a positive negative-control file containing an intentionally seeded unsafe + canary and proof that the scanner fails; +- a clean control proving the scanner does not fail every file; +- a transformed-secret case to document literal scanner limits; +- independent review of raw machine reports; and +- deletion/quarantine verification for temporary files after failures. + +## 12. Complete acceptance matrix + +| Area | Required acceptance evidence | +| --- | --- | +| Canonical contract | `spec/redaction-semantics.md`, revised event/persistence/durable/checkpoint specs, closed schemas, examples, truth table, policy and migration rules reviewed before implementation. | +| TS wire/store | Event validator rejects false combinations; writer never defaults true; store path receives guarded representation only; focused mutation, partial-write, hostile getter/error, and concurrency tests. | +| Python wire/store | Pydantic model has no implicit true; identical combinations/codes; guarded JSONL/checkpoint writes and focused adversarial tests. | +| Protected store | AEAD/AAD/key/ref validation, atomic publication, permission tests, wrong key/run/field, truncation/tamper, missing/denied value, no key logging, and cleanup. | +| Recovery | Committed success reuse, retry, crash windows, terminal idempotence, activity-key stability, no null substitution, and zero executor calls on policy/ref failures. | +| Hash parity | Same policy hash, value MAC, AAD hash, fixed-vector ciphertext hash, payload hash, and activity key under shared test keys. | +| Redaction semantics | Every event type and sink matches the truth table; `redacted: true` requires a receipt; protected refs are never mislabeled redacted. | +| Checkpoints | No raw application state in temp/final bytes; stale/corrupt/ahead/missing refs cannot authorize work; event authority preserved. | +| Trace/default privacy | Clean npm and wheel/sdist install proves no trace/prompt/response/tool/support capture or network export by default. | +| Legacy | Misleading histories fail before resume; unsafe read is explicit; quarantine/sealed archive manifests are safe; no silent byte rewrite or activity-key reuse. | +| Canary campaign | Every source x lifecycle x sink row passes in both packaged runtimes; encoded forms and negative/clean controls pass. | +| Packaging/docs | npm tarballs and wheel/sdist include required schemas/modules/docs and no keys, reports with canary values, raw fixtures, or accidental captures. | +| Independent R3 review | Exact source/package digests, commands, tool versions, raw reports, residual risks, reviewer independence, and explicit accepted/blocked verdict. | + +No single unit-test count, source-only scan, or in-memory fake closes this task. +Candidate-bound clean-package evidence and an implementer-independent security +review are mandatory. + +## 13. Concrete implementation map and ownership + +Main/integration owns canonical files: + +- new `spec/redaction-semantics.md` and protected-value schema; +- versioned event/checkpoint envelope decision; +- updates to `spec/durable-recovery-semantics.md`, + `spec/persistence-semantics.md`, and shared fixtures; +- policy/hash/error/migration vocabulary; and +- the cross-language conformance join. + +TypeScript lane owns: + +- `packages/persistence/src/events.ts`, event/checkpoint validators and guarded + stores; +- a narrow protected payload/key-provider interface and local encrypted adapter; +- `packages/runtime/src/durable.ts` and `durable-types.ts` integration; +- focused package tests and API documentation. + +Python lane owns: + +- `python/src/graph_engineering/events.py` and persistence validators/stores; +- native protected payload/key-provider implementation; +- `python/src/graph_engineering/durable.py` integration; +- focused native tests and API documentation. + +Platform/quality owns the packaged byte scanner, sink inventory, negative +control, raw evidence manifest, and clean-install runs. The independent security +reviewer owns attack review and acceptance, not implementation. + +Recommended executable split, matching the audited backlog: + +```text +D9-REDACTION-039 canonical contract + migration decision + -> D9-TS-REDACTION-087 TS implementation + -> D9-PY-REDACTION-088 Python implementation + -> D9-REDACTION-CONFORMANCE-089 shared join + canary + R3 review +``` + +Hard dependencies/reopening rules: + +- Keep `D6-DURABLE-CONFORMANCE-011` as the upstream frozen recovery baseline. +- Make `D9-DURABLE-EXT-SPEC-031` depend on the accepted redaction conformance + join, not merely creation of the contract task; leases/replay/artifacts must + not grow the unsafe schema. +- `D9-APPROVAL-077`, future trace/Explorer work, mutating adapters/MCP, and + production storage consume the protected/redacted contracts and may not + redefine them. +- `D16-SECURITY-062` independently reruns the complete canary campaign on the + frozen candidate; D9 evidence does not substitute for Day-16 candidate review. +- Any change to event/checkpoint shape, capture policy, redaction transform, + key/AAD derivation, codec, hash/MAC, activity key, sink inventory, package + contents, or migration tool invalidates the affected D9/D16 evidence. + +## 14. Definition of done for `D9-REDACTION-039` + +The task is complete only when all of the following are true: + +1. No default or durable writer can assert `redacted: true` without performing + and proving the defined transform. +2. Default packaged durable execution never persists plaintext application + values; missing protection fails before a sink write. +3. Authoritative recovery obtains exact values only through authenticated, + authorized protected refs and never through redacted derivatives. +4. Hash/MAC/activity/replay behavior is specified and identical in TS/Python. +5. Existing misleading histories are rejected before executor invocation and + have an explicit quarantine/archive/new-run path. +6. The complete canary source/lifecycle/sink matrix and its negative control pass + for clean npm and Python artifacts. +7. Shared conformance, focused native suites, full workspace gates, package + checks, and independent R3 review are green for one immutable revision. +8. Release language says exactly what is protected, redacted, disabled, and + still outside the threat model; no stable claim relies on the boolean alone. + +Until then, current JSONL/checkpoint directories must be treated as containing +raw application data, kept private, and excluded from trace galleries, support +bundles, and release evidence. diff --git a/codex_plans/delivery/full-plan-gap-audit.md b/codex_plans/delivery/full-plan-gap-audit.md new file mode 100644 index 0000000..1cad815 --- /dev/null +++ b/codex_plans/delivery/full-plan-gap-audit.md @@ -0,0 +1,708 @@ +# Graph Engineering 全量计划缺口审计 + +- 审计日期:2026-07-26(America/Vancouver) +- 审计基线:分支 `feat/pipeline-runtime`,`HEAD=d4de336eade9e468a219a595c45b6db8d20d74de` +- 权威计划:`codex_plans/Graph-Engineering-21-Day-Master-Plan.md` +- 控制面:`codex_logs/task-registry.json`、`master-plan-coverage-matrix.md`、 + `task-dependency-graph.md`、`release-checklist.md`、`agent-ownership-map.md` +- 本轮唯一写入:本文件;未修改 registry 或其他共享控制文档 + +## 1. 结论 + +当前只能准确称为**有大量可用本地切片的 source alpha**。稳定 v1 是明确 +no-go;完整 Beta 和完整 RC 也尚不成立。理由不是“文件不够多”,而是: + +1. 178 个 release-checklist 叶子全部仍为 `Open`,candidate coordinates 为空, + 且 178 个 `REL-*` ID 没有一个被机器映射到 live registry。 +2. 审计期间 registry 从 90 项变为 91 项;最新状态为 35 `completed`、3 + `in_progress`、53 `planned`。新增的 critical `D9-REDACTION-039` 只证明风险 + 已登记,不证明 redaction 已修复或验收。 +3. 审计中捕获的 explicit-null `I01/X02/T05` 分歧已被本地修复,最新共享 + conformance 通过;但 `D2-BUILDERS-YAML-020` 仍在进行,且没有 immutable + candidate/reviewer evidence,故该 P0 只能标为“本地修复待验收”。 +4. durable 事件在保存完整 `data` 的同时写入 `redacted: true`;该 false signal + 已由 `D9-REDACTION-039` 登记为 critical,但实现、迁移、双语言 canary 扫描和 + 独立安全复核仍未完成。 +5. pipeline 的无限 stage iterable 同步阻塞在审计中被识别;当前工作树已出现 + 双语言 `maxStages/max_stages=2048` 本地修复和恶意 iterator 测试,但 Python + implementation task 虽已 completed,跨语言 join 仍在进行。最新 conformance + 已本地通过,但未绑定 candidate/reviewer。因此该 P0 仍是“本地修复待验收”。 +6. 早期 30 个 completed 任务没有任何测试/完成证据;全部 35 个 completed + 任务都没有 `completed_at`。迁移截止时间可以解释历史数据格式,不能让这些 + 状态成为候选版本 release evidence。 +7. 发布依赖闭包允许 provenance/go-no-go、RC、文档或 pattern skeleton 在完整 + acceptance、十个完整 pattern、支持准备或最终 provenance 叶子之前被标记完成。 +8. master plan 的主要 Day 10–15 能力、Day 17–21 外部/发布成果仍为 Open; + coverage、三 OS、chaos、供应链、外部可用性、完整 CLI、全部 patterns 和支持 + 操作均没有候选版本证据。 + +即使所有本地单测通过,也不能覆盖上述 conformance、安全、外部权限、矩阵、 +provenance 或用户证据缺口。 + +## 2. 审计口径与实测快照 + +本审计完整读取了主计划,并逐项对照 91 项 live registry、coverage matrix、 +dependency graph 和 178 项 release checklist。审计开始时 registry 为 90 项; +`D9-REDACTION-039` 在审计进行中加入,故以下结果以最新 91 项为准,并显式记录 +控制文档的同步欠账。 + +证据判定规则: + +- `planned`、目录、schema、enum、README 描述、scanner heartbeat 都不是完成证据。 +- 本地 dirty worktree 上一次成功命令只证明该快照的局部行为,不是 immutable + candidate、受保护 CI、独立 review、packed install 或发布 provenance。 +- `completed` 只有在 source/spec revision、精确命令、结果、不可变报告、日期、 + reviewer、排除项齐全时,才可被 release gate 消费。 +- 外部测试、registry/hosting 权限、独立签核、真实 provider 与 consent 不能由 + agent 或 mock 伪造。 + +本轮实测环境是 Linux x86_64、Node 22.23.1、pnpm 10.13.1、Python 3.14.0; +最新工作树有 45 个 modified/untracked 路径,因此以下结果不是 release evidence: + +| 命令 | 结果 | 能证明什么 / 不能证明什么 | +|---|---|---| +| `corepack pnpm check:docs` | **最终复验失败** | 新增的 `content-calendar.md:7` 与 `launch-plan.md:96` 均链接到尚不存在的 `metrics-and-experiments.md`;较早一次在这些并行文件出现前曾通过 161 links。 | +| `corepack pnpm validate:fixtures` | 通过,24 JSON fixtures、1 graph hash、1 checkpoint hash、14 Durable JSON vectors | 证明 fixture 结构/静态 hash 检查;不能替代 native conformance。 | +| `corepack pnpm test` | 通过全部 workspace package tests | 证明当前本地 TS 切片;无 coverage、OS matrix、packed candidate 或 immutable report。 | +| `uv run --project python pytest -q` | 最新复验 584 passed、2 subtests passed | 原始数量超过 Q02,但没有 candidate-bound 分类报告、coverage 或支持版本/OS矩阵。 | +| `corepack pnpm test:conformance` | **最新复验通过** | 12 graph fixtures、runtime、persistence、durable、barrier/router 与 8 pipeline cases 均通过;仍非 candidate-bound evidence。 | +| TS/Python malicious-stage focused tests | 通过 | TS targeted 1 passed/114 skipped(full runtime 115);Python `max_stages` selector 10 passed。证明本地 limit+1 行为,不关闭 release join。 | +| `corepack pnpm check:packages` | 顺序复验通过 | 7 个 npm manifests/tarballs 通过;不包含 canonical unscoped distribution、cross-OS clean install 或 trusted publish。 | +| `python3 scripts/check-python-artifacts.py` | 通过 | 本地 wheel/sdist 结构通过;不证明 supported-version/OS clean install、upgrade 或 PyPI provenance。 | + +Registry 结构检查结果:91 个唯一 ID、无 dangling dependency、无图论环;但 +`updated_at=2026-07-26T16:45:30Z` 早于 17:45 新增的 redaction task。D2 状态在 +审计中已从 planned/started 不一致修正为 `in_progress`。 + +## 3. Day 1–21 逐日交叉核验 + +| Day | Live 判定 | 对应 registry | 仓库/验收事实 | +|---:|---|---|---| +| 1 | Partial | `D1-*`, `CTRL-PLAN-COVERAGE-001`, `CTRL-EVIDENCE-002`, `CTRL-DOCS-073` | 治理、schema、CI、scanner 存在;历史完成证据、growth 文档和候选 review 未闭合。新 architecture/research 文件虽已出现,仍是 untracked/未验收。 | +| 2 | Partial,P0 本地修复待验收 | `D2-BUILDERS-YAML-020` | 基础 canonical/hash 存在,explicit-null 分歧已本地修复且 conformance 通过;builders/YAML/typed ports 与 immutable review 仍未完成。 | +| 3 | Partial | `D3-CLI-002`, `D3-PY-CLI-021`, `D14-API-FREEZE-050` | TS 只有 `validate/plan/compile/visualize/doctor/init`;Python CLI、完整 envelopes/exit reference 和其余命令不存在。 | +| 4 | Partial | `D4-TRACE-SUBGRAPH-022`, `D15-EXPLORER-060` | 双语言 deterministic DAG 有实现;nested subgraph、reducer、stream/artifact edge activation、trace viewer 未完成。 | +| 5 | Partial,P0 待验收 | `D7-PIPELINE-*`, `D6-ROUTER-BARRIER-023` | standalone pipeline 与纯 barrier/router 存在。`maxStages` 本地修复已出现,但 join 未闭合;scheduler-integrated stream/router/barrier/deadline/replay 未完成。 | +| 6 | Partial | `D6-ROUTER-BARRIER-023`, `D11-VERIFY-*` | 结构化基础失败、retry/cancel 存在;完整 terminal set、runtime quorum/abstain/unknown/human gate 未完成。 | +| 7 | Partial | `D7-CYCLE-*` | 只有静态 finite constructor;runtime cycles、global seen set、semantic convergence、全维度 hard stop/replay exit 未完成。 | +| 8 | Partial | `D8-CHAOS-OPS-030` | runtime/pipeline retry/cancel 测试强;全 provider/tool 边界、chaos 和 operational CLI 未完成。 | +| 9 | Partial,critical redaction open | `D6-DURABLE-*`, `D9-REDACTION-039`, `D9-DURABLE-EXT-*` | local event-sourced DAG resume 存在;redaction flag 不真实,且 SQLite/Artifact/Lock/lease/replay/fork/approval/dual resume 未闭合。 | +| 10 | Open | `D10-*` | 只有 attempts/concurrency 狭窄边界;token/money/time/node budgets、reservation、models/pricing/cost UI 未完成。 | +| 11 | Open | `D11-*` | verified-fanout 只是 declarative constructor;votes/rubric/citation/panel/unknown gate 未实现。 | +| 12 | Open | `D12-*` | security architecture 文件出现不等于 enforcement;deny-by-default capabilities、worktree/process/container isolation 和 red team 未完成。 | +| 13 | Open | `D13-*` | 无 official adapters;mock/provider/tool contract、rate/circuit/fallback/cancel 和 score/badge 未完成。 | +| 14 | Open | `D14-*` | read-only alpha MCP 与四个 TS constructors 存在;API freeze、plugin SDK、mutation policy、十个跨语言 skeleton 未完成。 | +| 15 | Open | `D15-*` | 只有 memory/JSONL/file local stores;SQLite default、Postgres/S3、workers、OTel、Explorer、benchmark baseline 未完成。 | +| 16 | Partial | `D16-SECURITY-062`, `D9-REDACTION-039` | CodeQL/dependency review/Dependabot 存在;critical redaction、secret/license scans、SBOM、fuzz/chaos、policy/escape 与 signed disposition 未完成。 | +| 17 | Open / External | `D17-BETA-063` | 没有 immutable beta、五份外部报告、80%/5min、zero P0/P1 或 feedback retest。 | +| 18 | Open | `D18-COMPAT-BENCH-064`, `CTRL-ACCEPTANCE-070` | CI 仅 Linux;无 macOS/Windows、1k node、100 randomized faults、accepted coverage/benchmark/parity report。 | +| 19 | Open | `D19-RC-065` | 本地 pack/build 不等于 clean OS install/upgrade、signed RC、migration、candidate-bound docs。 | +| 20 | Open / External | `D20-PROVENANCE-066` | 无 trusted npm/PyPI identity evidence、SBOM/checksum/attestation、一致 artifact manifest 或可执行 go/no-go roll-up。 | +| 21 | Open / External | `D21-RELEASE-067`, `CTRL-*` | packages/site/content/support/incident/monitoring 均未闭合;任何 stable 标签均不被证据支持,完整 RC 也仍缺资产。 | + +## 4. P0:立即阻塞集成或发布 + +### P0-01 — durable `redacted: true` 是 false security signal + +- 任务:`D9-REDACTION-039`(planned, critical),并已被 + `D9-DURABLE-EXT-SPEC-031` 与 `D16-SECURITY-062` 依赖。 +- 证据:`packages/runtime/src/durable.ts` 无条件写 `redacted: true`; + `python/src/graph_engineering/durable.py` 同样写 `"redacted": True`;完整 `data` + 仍由 event stores 写盘。`spec/event.schema.json` 默认值、 + `spec/conformance/run-created.event.json` 也强化了该误导信号。 +- 影响:`I06`, `T26`, `V1-02`, `Q08`, `SC07`, `SC12`, `SUP06`;日志、trace、 + checkpoint、artifact、CLI/support bundle 的用户都可能错误相信 payload 已脱敏。 +- 控制缺口:当前 task owner 同时写 `main + TypeScript + Python + independent + security reviewer`,并横跨 spec、两种实现与安全 join;这不满足 ownership map 的 + one-primary-owner/R2-R3 非自审规则,应按 9.1/9.2 拆 lane 与 conformance join。 +- 机器验收: + +```bash +corepack pnpm test:redaction +uv run --project python pytest -q python/tests/test_redaction.py +corepack pnpm test:conformance +node scripts/check-secret-canaries.mjs --manifest codex_logs/release-evidence/redaction/manifest.json +``` + +前三个新 redaction 入口/测试和最后一个 sink-byte canary 检查必须随任务落地;要求 +journal、checkpoint、artifact、stdout、stderr、log、trace、error、support bundle +均无 canary,flag 与真实 wire bytes 一致,legacy 误标历史被明确拒绝或迁移,且 +redaction 不改变 replay identity 或暗中授权工作。 + +### P0-02 — pipeline 无限 stage iterable:本地修复存在,release closure 不存在 + +- 任务:`D7-PIPELINE-SPEC-012`, `D7-TS-PIPELINE-012`, + `D7-PY-PIPELINE-012`, `D7-PIPELINE-CONFORMANCE-013`。 +- 初始证据:TS `normalizeStages` 曾对任意 `Iterable` 使用 `[...stages]`,恶意/无限 + iterator 会在 `runPipeline` 同步永久阻塞;旧 spec 只有“finite”前置声明而没有 + 可执行上限。Python 的自定义 `Sequence` 也可通过永不 `StopIteration` 的 + `__getitem__` 无限推进。 +- 当前修复证据:`spec/pipeline-semantics.md` 已规定 portable + `maxStages=2048`;TS/Python 实现都只拉取 `limit+1`;两边已有无限 iterator/ + sequence 测试。Python implementation task 已 completed,conformance join 仍 + `in_progress`;最新 full conformance 本地通过,但工作树未绑定 candidate/reviewer。 +- 机器验收: + +```bash +timeout 10s corepack pnpm --filter @graph-engineering/runtime exec vitest run -t 'bounds an infinite stage iterable' +timeout 10s uv run --project python pytest -q python/tests/test_pipeline.py -k 'infinite_stage_sequence or max_stages' +corepack pnpm test:conformance +``` + +只有三条在同一 immutable revision 通过、共享 fixture 固定默认/最大值与错误投影、 +对 overflow element 零属性访问、iterator 正常关闭、source 零推进后,P0 才可关闭。 + +### P0-03 — explicit-null conformance 分歧已本地修复,候选闭包仍未完成 + +- 任务:`D2-BUILDERS-YAML-020`, `D1-SPEC-001`, `CTRL-ACCEPTANCE-070`。 +- 初始证据:`spec/conformance/invalid-null-metadata-description.graph.json`、 + `spec/conformance/expected.json`、`python/src/graph_engineering/models.py`、 + `tools/conformance/run.mjs`;本轮 `pnpm test:conformance` 报 Python validity + `true !== false`。 +- 当前证据:D2 已改为 `in_progress`,登记 27-field explicit-null parity 修复;最新 + `pnpm test:conformance` 通过 12 graph fixtures 及全部现有 runtime/persistence/ + pipeline joins。该结果来自 dirty worktree,D2 的 builders/YAML/typed-port 完整 + scope、completion evidence 与独立 review 仍为空。 +- 影响:修复若未绑定 immutable candidate,`I01/I02`, `X02`, `T05` 仍不能从 + release Open 变 Green;`validate:fixtures` 单独通过也不能替代 native conformance。 +- 机器验收: + +```bash +corepack pnpm validate:fixtures +uv run --project python pytest -q python/tests/test_models.py python/tests/test_compiler.py +corepack pnpm --filter @graph-engineering/core test +corepack pnpm test:conformance +``` + +所有 optional known fields 的 explicit null 必须在两种语言得到相同稳定 code/path; +现有 `in_progress` 状态正确,只有 broader builder/YAML acceptance、immutable report +与 reviewer 齐全后才能 completed。 + +### P0-04 — 178 个 release leaf 没有 live-task 映射或 fail-closed roll-up + +- 任务:`CTRL-ACCEPTANCE-070`, `D20-PROVENANCE-066`, `D21-RELEASE-067`。 +- 证据:`release-checklist.md` 明确要求 `REL-*` 映射 live registry;当前 178 个 + unique `REL-*` 中 registry 直接命中为 0,也不存在机器可读映射 manifest。 + candidate coordinates 全空,所有 leaf 为 Open。 +- 额外风险:`GR03-GR05` 是 non-blocking tracking,但 checklist 没有独立机器字段; + 一个朴素“全部 178 必须 Green”的 roll-up 会错误阻塞,反之又可能漏掉 `GR01`, + `GR02`, `GR06`, `GR07` 的 mandatory conduct/assets。 +- 机器验收: + +```bash +node scripts/check-release-task-map.mjs \ + --checklist codex_plans/delivery/release-checklist.md \ + --registry codex_logs/task-registry.json \ + --map codex_plans/delivery/release-task-map.json +node --test scripts/tests/release-rollup.test.mjs +node scripts/release-rollup.mjs --candidate "$CANDIDATE_SHA" --fail-on-open-blocking +``` + +必须覆盖 178/178、只引用存在的 registry ID、显式 `blocking` boolean、检测 dangling/ +duplicate/cycle,并用 Open/Partial/Blocked/Green 及 non-blocking growth negative fixtures +证明 fail closed。 + +### P0-05 — acceptance、provenance 与 go/no-go 的依赖闭包不成立 + +- 任务:`CTRL-ACCEPTANCE-070`, `D19-RC-065`, `D20-PROVENANCE-066`, + `D21-RELEASE-067`, `CTRL-PATTERNS-071`, `CTRL-DOCS-073`, + `CTRL-GROWTH-072`。 +- 证据:`CTRL-ACCEPTANCE-070` 声称覆盖全部 mandatory thresholds,却不依赖 + D20,因此不能验收其自身需要的 `Q09/SC01-SC14` provenance;D20 又可在 + acceptance/pattern/docs/growth/support 之前完成并自称 go/no-go。D19 不依赖十个 + 完整 pattern;`CTRL-DOCS-073` 零依赖,理论上可在 API/Explorer/security 尚未 + 冻结时完成。 +- 影响:registry 是 DAG,但语义上存在“消费者可先于证据生产者变 Green”的假闭包。 +- 机器验收: + +```bash +node scripts/check-task-graph.mjs --registry codex_logs/task-registry.json \ + --rules codex_plans/delivery/release-dependency-rules.json +node --test scripts/tests/release-invalidation.test.mjs +node scripts/release-rollup.mjs --candidate "$CANDIDATE_SHA" --explain-blockers +``` + +最终 roll-up 必须在 provenance 之后,D21 只能消费该 roll-up;任何 contract、 +security、pattern、docs、package 或 provenance gate 重开都必须使下游 candidate +失效。 + +### P0-06 — 历史 completed 状态不能被 release gate 消费 + +- 任务:30 个无证据的早期 completed 任务、全部 35 个无 `completed_at` 的 + completed 任务、`CTRL-EVIDENCE-002`。 +- 证据:`codex_logs/task-registry.json`。典型冲突是 `D1-BRAND-001` 的 + expected test 包含 registry availability/authenticated rehearsal,但 next action 又明确 + publication deferred;`D5-SECURITY-RELEASE-008` 也只有状态,没有 candidate-bound + scan/review artifact。 +- 影响:旧格式迁移豁免只能保留历史,不能为 release checklist 生成合格证据。 +- 机器验收: + +```bash +node scripts/check-task-registry.mjs --strict --candidate "$CANDIDATE_SHA" +node scripts/check-evidence-closure.mjs --registry codex_logs/task-registry.json \ + --root CTRL-RELEASE-ROLLUP-086 --candidate "$CANDIDATE_SHA" +``` + +允许保留旧 `completed` 原值,但必须增加 append-only candidate revalidation overlay, +包含 revision、spec hash、命令、环境、结果、immutable report、reviewer、排除项; +没有 overlay 的历史任务对发布闭包权重为零。 + +### P0-07 — canonical npm `graph-engineering` distribution 没有实现任务 + +- 任务:当前无精确实现任务;相关但不足的是 `D1-BRAND-001`, `D19-RC-065`, + `D20-PROVENANCE-066`。 +- 证据:root `package.json` 是 `private: true` 的 workspace;现有 publishable npm + packages 都是 `@graph-engineering/*`,CLI binaries 位于 scoped + `@graph-engineering/cli`。release `PKG02/PKG05/PKG13` 明确要求 canonical + `graph-engineering` tarball 与 `graph`/`grapheng`。 +- 影响:即使所有 scoped packages 通过,也不能满足主计划的 canonical npm 安装路径。 +- 机器验收: + +```bash +corepack pnpm check:packages +corepack pnpm check:packed-install +node scripts/check-canonical-distribution.mjs --name graph-engineering \ + --bins graph,grapheng --nodes 20,22 +``` + +必须先记录 namespace/alias 与 package-boundary ADR;不得为占名发布空包。 + +## 5. P1:必须在 RC 前关闭的计划/依赖/证据缺口 + +### P1-01 — day spine 过度串行,且数个 task 混合了互相矛盾的前置条件 + +| 现有任务 | 问题 | 精确修正方向 | +|---|---|---| +| `D4-TRACE-SUBGRAPH-022` | 只依赖 builders,却会修改 runtime、checkpoint namespace 和 edge execution。 | 增加双语言 runtime 与 durable spec 前置;contract 可先行,checkpoint integration 另设 join。 | +| `D6-ROUTER-BARRIER-023` | 同一任务既含 scheduler routing/barrier,又含 durable route replay;后者实际依赖 D9。 | D6 只做 runtime conditional route/deadline/quorum,并依赖现有 router/barrier primitives;把 zero-rejudge replay 移到 D9 conformance。 | +| `D7-CYCLE-SPEC-024` | contract 被完整 D6 implementation 阻塞。 | spec 只依赖 canonical IR/state/failure contract;native lowering 再依赖 D6,允许提前冻结 bounds。 | +| `D8-CHAOS-OPS-030` | 混合 runtime chaos 与 status/watch/pause/resume;后半依赖 durable control,而 D9 又依赖整个 D8。 | 拆成 runtime fault/cancel campaign 与 D9 operational control 两项;原 030 仅作 join。 | +| `D9-DURABLE-EXT-SPEC-031` | 被完整 D8 ops 阻塞,延迟 lease/replay/approval contract。 | 只依赖 basic durable、redaction 和 cancellation semantics;CLI ops 在实现后消费它。 | +| `D10-BUDGET-SPEC-035` | portable budget contract 被全部 D9 conformance 阻塞。 | pricing/unit/reservation spec 可提前;只有 crash-safe reservation integration 依赖 D9。 | +| `D11-VERIFY-SPEC-040` | verifier semantics 被整个 budget implementation 阻塞。 | vote/rubric/unknown contract 依赖 failure/approval contract;model-cost execution 再依赖 D10。 | +| `D12-ISOLATION-SPEC-044` | threat/capability contract 等到 verifier 全完成,安全设计过晚。 | threat model/spec 提前依赖 cancellation、approval/redaction;implementation/human gate 分别 join D9/D11。 | +| `D18-COMPAT-BENCH-064` | 技术矩阵完全被外部 tester 时间阻塞。 | 拆分 Beta artifact 与 external usability;D18 依赖 immutable Beta artifact,不依赖报告收集完成。最终 release roll-up 再 join usability。 | + +机器验收:`node scripts/check-task-graph.mjs --rules +codex_plans/delivery/release-dependency-rules.json`,并为每个拆分任务验证唯一 primary +owner、无循环、每个 expected test 的生产者位于消费者之前。 + +### P1-02 — adapters 缺少双语言独立 lane 与独立 conformance join + +`D13-ADAPTERS-049` 同时由 TS + Python lanes 编写、还自带 conformance,违反 +ownership map 的 one primary owner 与 non-self-review。应新增 TS、Python 两项, +将 049 收窄为 main + independent reviewer 的 join。Mock normal CI 与 vendor live +opt-in 必须分开,live credential 缺失不能使 mock gate 失败,也不能被 mock 冒充。 + +验收: + +```bash +corepack pnpm test:adapters +uv run --project python pytest -q python/tests/adapters +corepack pnpm test:adapter-conformance +``` + +### P1-03 — 十个 pattern 的 live dependencies 与 dependency graph 自己的 P01–P10 表不一致 + +Registry 除 P05 外几乎全部只依赖 skeleton;因此可在真实能力不存在时标记 pattern +完成。至少增加: + +| Pattern task | 必需依赖 | +|---|---| +| `PATTERN-01-RESEARCH` | pipeline/barrier、D9 durable ext、D10 budget | +| `PATTERN-02-CITED` | P01、D11 verifier、D13 adapter join | +| `PATTERN-03-AUTH` | D6 routing、D12 isolation、D16 security | +| `PATTERN-04-DIFF` | D6 quorum、D11 verifier、D12 isolation | +| `PATTERN-05-UNTIL-DRY` | D7 cycles、D10 budget、D11 verifier | +| `PATTERN-06-MIGRATION` | D9 resume、D12 worktree/merge、D16 escape tests | +| `PATTERN-07-CI` | D8 runtime chaos、D9 recovery、D12 process isolation、D13 shell adapter、approval spec | +| `PATTERN-08-DEPS` | D6 routing、D9 recovery、D12 isolation、D13 HTTP/shell、D16 license/security | +| `PATTERN-09-PR` | D6 quorum、D9 waits/approvals、D11 gates、D13/D14 MCP | +| `PATTERN-10-ECOSYSTEM` | D7 cycles、D9 recovery、D10 budget、D13 providers、D15 workers/storage | + +验收:`node scripts/check-pattern-bundles.mjs --all --negative-fixture +scripts/fixtures/pattern-bundle-missing-resume.json` 后运行十个 TS/Python/YAML/JSON +mock E2E、failure/resume 和 capability denial。 + +### P1-04 — API freeze、storage、Explorer、security 与 RC 的关键 dependency 缺边 + +- `D14-API-FREEZE-050` 缺 `D4-TRACE-SUBGRAPH-022`,可在 subgraph/edge public + surface 前冻结。 +- `D14-PATTERN-SKELETONS-053` 缺 `D2-BUILDERS-YAML-020`,却要求 YAML/JSON。 +- `D15-STORAGE-WORKERS-054` 缺 `D14-API-FREEZE-050`。 +- `D15-EXPLORER-060` 应显式依赖 API freeze、operational state、replay/fork 和 + redaction envelope;不能只靠间接长链。 +- `D15-PERFORMANCE-061` 缺 API/pattern surface 和 baseline enforcement producer; + “first run under five minutes”应由外部 usability task 最终验收。 +- `D16-SECURITY-062` 还应依赖 Explorer/observability attack surface;新增 + `D9-REDACTION-039` 依赖是正确的但尚未同步 dependency 文档。 +- `D19-RC-065` 缺 `CTRL-PATTERNS-071`、完整 education assets 与 support preflight。 + +证据路径是 `codex_logs/task-registry.json` 各 `depends_on` 与 +`task-dependency-graph.md` 的 D14–D20 rows。机器验收同 P1-01 的 +`check-task-graph.mjs`,并以一个故意移除 API/pattern/security 前置的 negative DAG +证明 checker 会失败。 + +### P1-05 — I01–I10 跨切面状态 + +| Gate | 当前判定 | 主要任务/证据 | +|---|---|---| +| I01 canonical spec | Partial / local Green | `D2-BUILDERS-YAML-020`; latest conformance passes,candidate review open。 | +| I02 structured failures/no null | Partial | basic compiler/runtime/pipeline 有结构化失败;全 node/provider/tool/store 未闭合。 | +| I03 transforms vs model judgment | Partial | 文档/类型存在;model/verifier/policy runtime 未实现。 | +| I04 no implicit cycles/unbounded retry/fanout | Partial | DAG cycle、attempt/fanout 局部边界;dynamic patch/cycle/provider/worker 未闭合;pipeline cap 待 join。 | +| I05 at-least-once + idempotency/approval | Open/Partial | basic durable activity key;完整 approval/reconciliation/tool effects 未实现。 | +| I06 telemetry/capture default-off + redaction | **Red** | `D9-REDACTION-039`; false redacted flag,且 OTel/support bundle 未实现。 | +| I07 malicious dynamic patches bounded | Open | `D7-CYCLE-*`, `D12-*`, `D10-*`。 | +| I08 no authority expansion | Open | `D12-*`, `D14-MCP-PLUGINS-052`。 | +| I09 insufficient verifier quorum unknown/human | Open | `D11-*`。 | +| I10 evidence-limited release claims | Open | candidate/claim audit、external evidence 和 final sign-off 不存在。 | + +### P1-06 — X01–X10 cross-language equality 状态 + +| Gate | 当前判定 | 缺口/命令 | +|---|---|---| +| X01 canonical bytes/hashes | Local subset present, release Open | `pnpm test:conformance` 必须在 candidate 通过。 | +| X02 compile verdict/codes | Local repair passes;release Open | explicit-null regression now passes full conformance;immutable report/reviewer open。 | +| X03 route + replay | Partial | pure route parity;runtime selection/replay open。 | +| X04 barrier/quorum | Partial | all/minimum/percentage evaluator;deadline/quorum runtime open。 | +| X05 event ordering | Partial | basic DAG/durable events;full terminal/replay/worker ordering open。 | +| X06 terminal/failure | Partial | basic scheduler/pipeline;all policies/providers/tools open。 | +| X07 retry/timeout/cancel accounting | Partial strong local | future provider/tool/cycle/worker and candidate report open。 | +| X08 resume/replay/fork | Partial | local cross-language resume;replay/fork/lease open。 | +| X09 CLI JSON/exit | Partial | TS subset only;Python/full operational CLI open。 | +| X10 adapter/storage | Partial | basic event/checkpoint stores;official adapters、SQLite/Postgres/S3/Artifact/Lock open。 | + +### P1-07 — T01–T33 mandatory scenario 状态 + +| Gate | 当前判定 | 主要 task/control | +|---|---|---| +| T01 missing reference | Local implementation present;release Open | core/compiler conformance, `CTRL-ACCEPTANCE-070` | +| T02 duplicate identity | Local implementation present;release Open | core/compiler conformance | +| T03 unreachable | Local implementation present;release Open | core/compiler conformance | +| T04 invalid ports | Partial | `D2-BUILDERS-YAML-020` | +| T05 invalid schemas | Local repair passes;broader scope/release Open | `D2-BUILDERS-YAML-020` | +| T06 implicit cycle | Local DAG reject present;release Open | core/compiler | +| T07 incomplete router | Open | `D6-ROUTER-BARRIER-023` | +| T08 unbounded loops | Partial/static only | `D7-CYCLE-*` | +| T09 unauthorized transform/authority | Open | `D12-*`, `D16-SECURITY-062` | +| T10 100-way bounded runtime | Partial constructor boundary only | `D18-COMPAT-BENCH-064` | +| T11 all failure policies | Partial | pipeline/scheduler slices;quorum/partial/full matrix open,`D8`, `D11` | +| T12 real backpressure | Local pass,P0 cap join open | `D7-PIPELINE-CONFORMANCE-013` | +| T13 barrier deadline stats | Open/Partial evaluator | `D6-ROUTER-BARRIER-023` | +| T14 router replay no rejudge | Open | `D6` + `D9-DURABLE-EXT-CONFORMANCE-034` | +| T15 malicious patch/dry run | Open | `D7-CYCLE-*`, `D12`, `D16` | +| T16 verifier pass/reject/abstain | Open | `D11-*` | +| T17 global seen-set convergence | Open | `D7-CYCLE-*` | +| T18 every hard budget dimension | Partial attempts/fanout only | `D7`, `D10` | +| T19 every crash window | Partial local recovery only | `D9-DURABLE-EXT-CONFORMANCE-034` | +| T20 dual resume race | Open | `D9-DURABLE-EXT-CONFORMANCE-034`, `D15` | +| T21 replay/fork lineage | Open | `D9-*` | +| T22 stale approvals | Open | approval task + `D9`, `D11` | +| T23 worktree conflicts | Open | `D12-*` | +| T24 namespace isolation | Open | `D12-*`, `D16` | +| T25 provider fallback/circuit | Open | `D13-*` | +| T26 secret redaction | **Red / critical** | `D9-REDACTION-039`, `D16` | +| T27 cancel runtime/provider/tool | Partial native runtime/pipeline | `D8`, `D13` | +| T28 prompt injection/authority | Open | `D12`, `D16` | +| T29 complete CLI/MCP | Partial six TS commands/read-only MCP | `D3-PY-CLI-021`, `D8`, `D14`, `D19` | +| T30 Event/Checkpoint/Artifact/Lock + stores | Partial memory/JSONL/file | `D9`, `D15`, `D18` | +| T31 all 10 patterns x 3 forms | Open | pattern tasks + `CTRL-PATTERNS-071` | +| T32 1,000-node resource bound | Open | `D18-COMPAT-BENCH-064` | +| T33 kill/network/store/artifact chaos | Open | `D15`, `D16`, `D18` | + +### P1-08 — Q01–Q11 quantitative gate 状态 + +| Gate | 当前判定 | 最小机器验收 | +|---|---|---| +| Q01 90% statements/85% branches per subsystem | Open | `pnpm coverage:release && uv run --project python coverage run -m pytest && node scripts/check-coverage-thresholds.mjs` | +| Q02 >=250 cases/language | Local raw counts pass;candidate report Open | `node scripts/test-inventory.mjs --exclude-skipped --candidate "$CANDIDATE_SHA"` | +| Q03 shared adapter/storage suite | Open/Partial basic persistence | `pnpm test:adapter-conformance && pnpm test:storage-conformance` | +| Q04 100 randomized failures | Open | `pnpm test:chaos -- --runs 100 --write-seeds codex_logs/release-evidence/chaos/seeds.json` | +| Q05 Linux/macOS/Windows matrix | Partial Linux only | `node scripts/check-ci-matrix.mjs .github/workflows/ci.yml` 加 immutable CI run URLs | +| Q06 mock default/live opt-in | Partial mock only | `node scripts/check-provider-ci-policy.mjs` 加 opt-in live reports | +| Q07 >10% perf gate | Open | `node scripts/check-bench-regression.mjs --threshold 0.10 --require-baseline-adr` | +| Q08 no high/critical + all scans | **Red/Open** | redaction、secret/license/SBOM/fuzz 未过;`pnpm security:release` | +| Q09 trusted publishing/provenance | Open/External | `node scripts/verify-release-provenance.mjs --manifest ...` | +| Q10 <=3 commands + 80% <=5min | Partial/External | `node scripts/check-usability-evidence.mjs --min-rate .8 --max-seconds 300` | +| Q11 zero P0/P1 + >=5 reports | Open/External | 同上加 immutable issue query 与 reviewer sign-off | + +这里列出的新 script names 是所对应任务应新增的验收器;当前缺失本身就是 gap, +不能把“命令不存在”解释成免验收。 + +### P1-09 — security/supply-chain 只有基础 CI,没有 release-grade evidence + +任务:`D9-REDACTION-039`, `D12-ISOLATION-REDTEAM-047`, `D16-SECURITY-062`, +`D20-PROVENANCE-066`。证据路径:`.github/workflows/{codeql,dependency-review}.yml`、 +`SECURITY.md`,以及当前不存在的 `tests/security/`, `sbom/`, release reports。 + +当前存在 CodeQL、dependency review、Dependabot、`pnpm audit:prod` 与 package +content checks;不存在 accepted threat model review、secret scan、license inventory/ +third-party notices、SBOM、fuzz/property/chaos、seeded-secret control、clean trusted +runner reproducibility、checksum/attestation 与 high/critical disposition。`D16` 单任务 +覆盖面过大,且 external reviewer/法律责任/凭据门槛未显式登记。 + +机器验收:`corepack pnpm security:release && node +scripts/verify-release-provenance.mjs --candidate "$CANDIDATE_SHA"`,报告必须把每个 +finding 映射为 accepted fixed/waived-by-authorized-reviewer;high/critical 不允许无声 +waiver。 + +### P1-10 — privacy、external study 与 support readiness 粒度不足 + +任务:`D17-BETA-063`, `CTRL-GROWTH-072`, `D21-RELEASE-067`,以及建议新增的 +`D16-PRIVACY-079`, `D17-USABILITY-076`, `D18-SUPPORT-READINESS-080`。证据路径: +release `UX09/DOC12/SUP01-SUP08` 与当前缺失的 usability/support manifests。 + +`D17-BETA-063` 提到 consent-safe script,但没有独立 retention、withdrawal、PII +redaction、gallery consent manifest 验收;`D21-RELEASE-067` 把 roster、incident、 +rollback、deprecate/yank、support-bundle redaction 全放在发布当天。release checklist +的 `UX09`, `DOC12`, `SUP01-SUP08` 需要可在 publish 之前失败的独立任务。 + +机器验收:`node scripts/check-usability-evidence.mjs --require-consent && node +scripts/check-support-readiness.mjs --candidate "$CANDIDATE_SHA" --tabletop-required`。 + +## 6. D9-REDACTION-039 控制文档同步清单 + +新增 task 的依赖边已经进入 registry,但以下文档尚无 `D9-REDACTION-039` 字样, +因此目前控制面会漏报或错误归属风险: + +| 文档 | 需要同步的精确位置 | +|---|---| +| `master-plan-coverage-matrix.md` | Day 9 行加入 false-signal/current critical gap 与 control;Day 16 control 加 D9;`Durable state`、`Security/isolation` capability 行;mandatory ledger 的 redaction/T26 对应行;Q08 证据说明。文档存在状态另见 P2-01。 | +| `task-dependency-graph.md` | D09 required deliverables/entry/exit;D16 hard entry;S03 durable、S05 policy/isolation、S06 observability;I06;T26;security escape/recovery reopening rule。明确 D9 durable-ext 与 D16 均硬依赖 redaction task。 | +| `release-checklist.md` | 将 `REL-V1-02`, `REL-I06`, `REL-T26`, `REL-SC07`, `REL-SC12`, `REL-SUP06` 映射到 D9 task;扩展 T26 为“wire flag truth + sink-before-write + legacy migration”,仍保持 Open,不能因 task 已登记而 Green。 | +| `agent-ownership-map.md` | Day 9 加 redaction contract 与 TS/Python/SRV R3 review;Day 16 加 hard dependency;S03/S05/S06 closure;current ownership gaps;明确 `spec/redaction-semantics.md` 由 INT 写、双 runtime 实现、PQG canary、独立 SRV 签核。 | + +同步验收: + +```bash +for f in \ + codex_plans/delivery/master-plan-coverage-matrix.md \ + codex_plans/delivery/task-dependency-graph.md \ + codex_plans/delivery/release-checklist.md \ + codex_plans/delivery/agent-ownership-map.md; do + rg -q 'D9-REDACTION-039' "$f" || exit 1 +done +corepack pnpm check:docs +``` + +## 7. 外部权限与不可伪造门槛 + +Registry 目前只有 5 项带 `external_gate`;下列门槛需要补充到对应 task。准备 mock、 +fixture、dry-run 与 consent template 可以在仓库内完成,实际权限/人类证据不得推断。 + +| 外部门槛 | 影响 task/gate | 所需证据 | 当前登记情况 | +|---|---|---|---| +| 五名以上真实外部 tester、80%/5min、retest | `D17`, Q10/Q11, UX01-10 | consented/redacted reports、timing method、cohort、disposition、independent review | 已粗略登记,但需从 Beta artifact 拆出独立外部任务。 | +| OpenAI/Anthropic/Gemini/compatible live accounts and keys | `D13`, Q06, T25 | opt-in/nightly run IDs、rate/fallback/cancel evidence;零 secret 入库 | 未登记 external gate。 | +| npm/PyPI namespace ownership 与 OIDC trusted publisher admin | `D14 npm dist`, `D20`, SC01/02, PKG13 | 当前 lookup、owner/role、least-privilege identity、rehearsal | D20 只做聚合描述;需分 npm/PyPI 身份。 | +| GitHub repo admin:branch/tag/release/environment protection | `D19-D21`, SC05/14, RC02/06/07 | protection snapshot、signed tag/release、immutable run/artifact URLs | 未在 D19 明示。 | +| Hosting/domain/CDN 与 channel posting accounts | `CTRL-GROWTH`, `D20-D21`, DOC03/11-16 | deploy preview、DNS/hosting authority、current links、posting owner | D20 泛化为 hosting;growth 只登记 star 非保证。 | +| macOS/Windows runners 与 Actions capacity | `D18`, Q05 | 全 matrix immutable run,不能 allowed-failure | 未登记。 | +| Postgres/S3-compatible integration infra/credentials | `D15`, T30/T33 | disposable namespace、cleanup、race/chaos report;本地容器可作为无外权替代 | 未登记,需明确 local-vs-hosted fallback。 | +| 独立 security/go-no-go/release reviewer,必要时 legal/license reviewer | `D12`, `D16`, `D19-D21`, Q08 | distinct identity、scope、revision、findings/disposition、signature/date | owner 文本有 reviewer,external gate/availability 未登记。 | +| case study/adopter/gallery consent 与撤回/保留责任 | `D17`, docs/growth, UX09/DOC08/DOC12 | per-entry consent、redaction、retention/withdrawal、authentic source | 未独立登记。 | +| launch/support roster acknowledgement 与 registry yank/deprecate authority | support task, `D21`, SUP01-08 | UTC roster、escalation、dry-run、owner roles、non-destructive procedure | 仅埋在 D21 expected test。 | + +机器只能验收证据引用的完整性,不能制造权限本身: + +```bash +node scripts/check-external-authority-manifest.mjs \ + --candidate "$CANDIDATE_SHA" \ + --require-consent --require-expiry --require-reviewer --reject-secrets +``` + +缺失/过期/不可验证的权限必须输出 `Blocked`,而不是 `Green` 或空值。 + +## 8. P2:控制面精度与文档维护缺口 + +### P2-01 — coverage matrix 的 required-document snapshot 已过时 + +任务:`CTRL-DOCS-073`, `CTRL-GROWTH-072`, `CTRL-PLAN-COVERAGE-001`。证据路径: +`master-plan-coverage-matrix.md` 的 required-document table 与 +`codex_plans/{research,architecture,growth}` 实际目录。 + +Matrix 仍把 competitor matrix 以及 graph IR/runtime/persistence/security 四份 +architecture 文档写为 `Open`。当前这些路径已出现,但均为 untracked/未完成 review; +正确更新应是 `Present, unreviewed` 或 `Present, acceptance Open`,不能直接 Green。 +growth 的 launch plan 与 content calendar 已出现,但 metrics/experiments 仍不存在, +并造成两条 broken links。验收应同时检查 presence、git identity、domain review 与 +status-claim audit。 + +机器验收:`node scripts/check-plan-document-inventory.mjs && corepack pnpm check:docs`。 + +### P2-02 — registry 元数据没有原子更新 + +任务:`CTRL-EVIDENCE-002`, `CTRL-PLAN-COVERAGE-001`, `D9-REDACTION-039`。证据 +路径:`codex_logs/task-registry.json` 根时间戳及每项 status/timestamps/evidence。 + +- registry `updated_at` 早于新 `D9-REDACTION-039.assigned_at`。 +- completed tasks 全无 `completed_at`。 +- 新 redaction task 已由下游依赖,但四份控制文档未同步。 + +应由 `scripts/check-task-registry.mjs --strict` 在 CI 拒绝上述状态,而不是依靠人工 +发现。 + +### P2-03 — expected artifacts 过多使用目录或未来路径,不能唯一定位证据 + +任务:所有未来 implementation/release tasks,治理 owner 为 `CTRL-EVIDENCE-002`。 +证据路径:`codex_logs/task-registry.json` 的 `expected_artifacts` 与 +`release-checklist.md` 的 Evidence slots。 + +例如 `spec`、`packages/runtime`、`docs`、`.github/workflows`、`codex_logs/release-evidence` +都太宽;文件存在无法说明哪一 revision/command/reviewer 满足哪一 requirement。 +后续 task 应给出 report manifest 路径、schema 与 content digest。 + +机器验收:`node scripts/check-evidence-schema.mjs --reject-directory-only-artifacts +--require-digests --require-reviewer`。 + +### P2-04 — docs check 只验证链接 + +任务:`CTRL-DOCS-073`, `D14-API-FREEZE-050`, `D18-EDUCATION-ASSETS-083`(建议 +新增)。证据路径:`scripts/check-doc-links.mjs`、`docs/`、`codex_plans/growth/`。 + +现有 `scripts/check-doc-links.mjs` 当前还因缺失 growth metrics 文件而失败;即使修好, +它也不执行 snippets、不比对 CLI help/API exports、不检查 bilingual/version/claims, +也不验证 hosted assets。`CTRL-DOCS-073` 的 +expected tests 需要拆为 links、snippets、API/CLI diff、status audit、bilingual、 +deployed-link 六个机器报告。 + +机器验收:`corepack pnpm check:docs && node scripts/check-doc-snippets.mjs && node +scripts/check-doc-claims.mjs --candidate "$CANDIDATE_SHA"`。 + +### P2-05 — 本地版本与 release matrix 不能混用 + +任务:`D18-COMPAT-BENCH-064`, `CTRL-ACCEPTANCE-070`。证据路径: +`.github/workflows/ci.yml` 与未来 `codex_logs/release-evidence/matrix/`。 + +本轮 Python 是 3.14,而承诺矩阵是 3.11/3.12/3.13;本地 Node 22/Linux 成功不能 +证明 Node 20、macOS、Windows。每个 report 必须记录 OS image、runtime、package +digest 和 candidate SHA。 + +机器验收:`node scripts/check-ci-matrix.mjs .github/workflows/ci.yml --require-os +linux,macos,windows --node 20,22 --python 3.11,3.12,3.13` 加每个 cell 的 immutable +run URL/digest manifest。 + +## 9. 精确 registry patch 建议(本审计不直接应用) + +以下是 selector-based semantic patch;新增 ID 在当前 91 项中均未占用。每个新增 +task 都应设置 `status: planned`、`started_at: null`、`evidence_required: true`,并遵守 +单 primary owner。`assigned_at` 与 `last_heartbeat` 必须取实际 apply UTC, +`blocker: null`;不得回填本审计时间。Work packages 建议为:074/075/086/089 +属于 `WP11-WP12`,076/079/080/083 属于 `WP10-WP12`,077/087/088 属于 +`WP3-WP9-WP11`,078 属于 `WP10-WP11`,081/082 属于 `WP7-WP11`,084/085 +属于 `WP4-WP11`。每项 `next_action` 必须是表中第一个尚未满足的 expected test, +而不是泛化“继续实现”。 + +### 9.1 必须新增的任务 + +| 新 ID | Title / owner / risk | depends_on | expected_artifacts | expected_tests / external_gate | +|---|---|---|---|---| +| `CTRL-RELEASE-MAP-074` | Machine-map every release leaf / INT primary,PQG review / critical | `CTRL-PLAN-COVERAGE-001` | `release-task-map.json`, checker + negative tests | 178/178 unique mapping、blocking classification、existing IDs、no dangling/cycle;无外部门槛 | +| `CTRL-EVIDENCE-BACKFILL-075` | Candidate revalidation overlay for historical completed tasks / INT primary,independent review / critical | `CTRL-EVIDENCE-002` | `codex_logs/release-evidence/task-revalidation.json` | every release-closure ancestor has revision/command/result/artifact/reviewer/exclusions | +| `D17-USABILITY-076` | External Beta usability and consent evidence / PQG primary,EXT evidence / critical | `D17-BETA-063`, `D16-PRIVACY-079` | `codex_logs/usability`, method/consent schemas | >=5 reports、>=80% <=300s、retest、zero P0/P1;external testers + elapsed time | +| `D9-APPROVAL-077` | Canonical approval/idempotency authority contract / INT / critical | `D6-DURABLE-SPEC-010`, `D9-REDACTION-039` | `spec/approval-semantics.md`, shared fixtures | approve/reject/revoke/expire、graph/run/revision binding、stale reject、idempotency key、redacted audit | +| `D14-NPM-DIST-078` | Canonical unscoped npm distribution / TS+PQG, TS primary / critical | `D14-API-FREEZE-050`, `D3-CLI-002` | explicit package path + distribution ADR | tarball clean install Node20/22、no workspace refs、`graph`/`grapheng`、nonempty real package;npm namespace authority only for live rehearsal | +| `D16-PRIVACY-079` | Telemetry/usability/gallery consent, retention and redaction / PQG primary,SRV review / high | `D9-REDACTION-039`, `D12-ISOLATION-REDTEAM-047` | `docs/PRIVACY.md`, evidence schemas | default-off、PII canary、minimal retention、withdrawal/deletion、gallery consent;human data owner/consent | +| `D18-SUPPORT-READINESS-080` | Pre-publish support and incident readiness / INT primary,PQG+SRV review / critical | `D16-SECURITY-062`, `D18-COMPAT-BENCH-064` | runbooks、roster、support-bundle tests | SUP01-08、rollback/deprecate/yank/forward-fix、incident tabletop;roster/registry authority | +| `D13-TS-ADAPTERS-081` | TS adapters / TS / high | `D13-ADAPTER-SPEC-048` | `packages/adapters` | mock/provider/http/shell/MCP focused suite;live vendor credentials opt-in | +| `D13-PY-ADAPTERS-082` | Python adapters / PY / high | `D13-ADAPTER-SPEC-048` | `python/src/graph_engineering/adapters` | same native suite;live vendor credentials opt-in | +| `D18-EDUCATION-ASSETS-083` | Course/case studies/demo/bilingual executable assets / PQG / high | `CTRL-PATTERNS-071`, `D15-EXPLORER-060`, `D17-BETA-063`, `D14-API-FREEZE-050` | course manifest、case/demo/launch manifests | 14 runnable steps、4 authentic cases incl failure、90s uncut demo、claim/version/bilingual checks;consent for external stories | +| `D8-RUNTIME-CHAOS-084` | Runtime retry/cancel/noncooperative chaos / PQG primary,native reviewers / high | `D7-CYCLE-CONFORMANCE-027`, `D7-PIPELINE-CONFORMANCE-013` | `tests/chaos/runtime` | bounded seeds、no deadlock/leak/unbounded retry、exact attempts | +| `D9-OPS-CONTROL-085` | Durable operational command surface / PQG / high | `D9-DURABLE-EXT-CONFORMANCE-034`, `D8-RUNTIME-CHAOS-084` | CLI/Python CLI/docs | status/watch/inspect/logs/pause/resume/cancel/retry JSON/exits/races | +| `CTRL-RELEASE-ROLLUP-086` | Final candidate-bound stable-vs-RC decision / INT primary,independent R3 review / critical | `D20-PROVENANCE-066`, `CTRL-ACCEPTANCE-070`, `CTRL-PATTERNS-071`, `CTRL-DOCS-073`, `CTRL-GROWTH-072`, `D17-USABILITY-076`, `D18-SUPPORT-READINESS-080`, `CTRL-EVIDENCE-BACKFILL-075`, `CTRL-RELEASE-MAP-074` | immutable roll-up、blocker manifest、signed decision | all blocking leaves Green or explicit full-RC/no-release; reopen invalidation; external reviewer + publishing authority only after decision | +| `D9-TS-REDACTION-087` | TS sink-before-write redaction / TS / critical | `D9-REDACTION-039` | `packages/persistence`, `packages/runtime` | flag truth、all TS sinks canary-free、identity preservation、legacy migration | +| `D9-PY-REDACTION-088` | Python sink-before-write redaction / PY / critical | `D9-REDACTION-039` | `python/src/graph_engineering` | same native acceptance and cleanup | +| `D9-REDACTION-CONFORMANCE-089` | Cross-language redaction/security join / INT primary,PQG canary + SRV review / critical | `D9-TS-REDACTION-087`, `D9-PY-REDACTION-088` | shared fixtures、canary report、signed disposition | identical flags/migration/identity、all sink-byte scans、independent security acceptance | + +### 9.2 精确修改现有任务 + +1. `D13-ADAPTERS-049`:owner 改为 `main + independent conformance reviewer`; + depends_on 改为 `D13-TS-ADAPTERS-081`, `D13-PY-ADAPTERS-082`;expected artifacts + 收窄到 shared fixtures/reports;不得再由两个 implementation lanes 自审。 +2. `D17-BETA-063`:收窄为 immutable beta artifact/API docs/bug burn-down;移除外部 + timing/report completion;外部证据移到 `D17-USABILITY-076`。D18 仍依赖 Beta + artifact,但不被 tester elapsed time 阻塞。 +3. `D20-PROVENANCE-066`:title/exit 收窄为 provenance assembly and rehearsal, + 不再自称 final go/no-go;final decision 由 `CTRL-RELEASE-ROLLUP-086` 生产。 +4. `D21-RELEASE-067`:depends_on 用 `CTRL-RELEASE-ROLLUP-086` 替代分散的控制项, + 并保留 D20;发布命令必须验证 roll-up candidate digest。 +5. `D19-RC-065`:新增 dependencies + `CTRL-PATTERNS-071`, `D18-EDUCATION-ASSETS-083`, + `D18-SUPPORT-READINESS-080`。后者按上表依赖 D16+D18,不依赖 D19,故没有环; + 若 post-RC rehearsal 必须消费 RC artifact,应另建后置 evidence step,不能让 + pre-publish runbook/roster 被它阻塞。 +6. `CTRL-DOCS-073`:增加 API freeze、Explorer/performance、security、patterns、 + education assets 前置,或拆成早期 planning docs 与 final candidate docs 两项; + 不能保持零依赖并作为 D19 gate。 +7. `CTRL-ACCEPTANCE-070`:明确只汇总 implementation/quality/usability leaves, + provenance leaves 由 D20 生产、最终由 086 汇总;不要声称自己在 D20 之前已经 + 验收全部 178。 +8. `D9-DURABLE-EXT-CONFORMANCE-034`:增加 router decision zero-rejudge replay、 + approval binding、redaction migration/canary joins。 +9. `D16-SECURITY-062`:保留对 `D9-REDACTION-039` 的新依赖,并增加 Explorer/ + observability 完整 surface;按下一条拆分后改依赖 + `D9-REDACTION-CONFORMANCE-089`;补 external reviewer availability 与 + legal/license owner。 +10. `D2-BUILDERS-YAML-020`:保留当前 `in_progress`;不得因 explicit-null 子缺口 + 已修复就跳过 builders/YAML/typed-port 等完整 expected scope 或提前 completed。 +11. `D9-REDACTION-039`:收窄为 INT-owned canonical redaction/migration contract; + 两种实现分别由 087/088 完成,089 做独立 join。`D9-DURABLE-EXT-SPEC-031` + 保留对 039 contract 的依赖;`D9-TS-DURABLE-EXT-032`/`D9-PY-DURABLE-EXT-033` + 分别增加 087/088;`D9-DURABLE-EXT-CONFORMANCE-034` 与 + `D16-SECURITY-062` 增加 089。 +12. Registry 根 `updated_at`:每次 task/dependency/status/evidence 原子更新时同步; + CI 拒绝小于任何 task timestamp 的值。 + +Pattern dependency 修改按 P1-03 表逐项加入,不用新增额外 pattern IDs。 + +## 10. 发布证据命令集 + +最终候选至少应从 clean checkout/packed artifacts 运行以下入口,并把原始结果与 +digests 写入 `codex_logs/release-evidence//`。当前不存在的入口由上述任务 +负责实现。 + +```bash +corepack pnpm install --frozen-lockfile +corepack pnpm validate:fixtures +corepack pnpm test:conformance +corepack pnpm build +corepack pnpm typecheck +corepack pnpm lint +corepack pnpm test +uv sync --project python --extra dev --locked +uv run --project python pytest +uv run --project python ruff check python/src python/tests +uv run --project python mypy --config-file python/pyproject.toml python/src +corepack pnpm check:packages +corepack pnpm check:packed-install +python3 scripts/check-python-artifacts.py +corepack pnpm check:docs +node scripts/check-doc-snippets.mjs +node scripts/check-task-registry.mjs --strict --candidate "$CANDIDATE_SHA" +node scripts/check-release-task-map.mjs --map codex_plans/delivery/release-task-map.json +node scripts/release-rollup.mjs --candidate "$CANDIDATE_SHA" --fail-on-open-blocking +``` + +CI 还必须为 Linux/macOS/Windows、Node 20/22、Python 3.11/3.12/3.13 生成不可变 +run URLs;coverage、100 randomized faults、1,000-node、provider opt-in、security、 +SBOM/checksum/attestation、external usability 与 support tabletop 均要有独立 artifact, +不能只把上面命令合并成一个“tests passed”摘要。 + +## 11. 退出条件 + +本审计可在以下条件同时满足后被标记 superseded,而不是被删除: + +1. P0-01/P0-02/P0-03 在同一 immutable candidate 上通过双语言与 adversarial + 验收,critical findings 有独立 reviewer disposition; +2. 178 release leaves 100% 映射,blocking 分类机器可读,最终 roll-up fail closed; +3. registry evidence/dependency/status/updated_at checks 全 Green,历史任务有候选 + revalidation overlay; +4. I01-I10、X01-X10、T01-T33、Q01-Q11 与 package/security/pattern/docs/support + rows 都有 candidate-bound evidence; +5. 外部权限与人类证据真实取得并脱敏,不能取得时选择明确的 full RC/no-release, + 不得降格 gate 或伪造 Green; +6. `corepack pnpm check:docs` 与更完整的 snippet/API/CLI/bilingual/claim checks 通过。 + +在此之前,正确的发布判定保持:**stable v1 no-go;完整 RC 仍 open;继续诚实维护 +source alpha 与明确 exclusions。** diff --git a/codex_plans/delivery/master-plan-coverage-matrix.md b/codex_plans/delivery/master-plan-coverage-matrix.md new file mode 100644 index 0000000..6c3fe0d --- /dev/null +++ b/codex_plans/delivery/master-plan-coverage-matrix.md @@ -0,0 +1,191 @@ +# Master-plan coverage matrix + +Updated: 2026-07-26 + +This is the canonical gap ledger for the +[21-day master plan](../Graph-Engineering-21-Day-Master-Plan.md). It answers a +stricter question than the progress scanner: whether every promised capability, +test, document, release control, and adoption prerequisite has objective +evidence. A task may be healthy while its deliverable is still open. `Green` +means the named scope is implemented and verified; `Partial` means useful code +exists but the master-plan promise is broader; `Open` means there is no accepted +implementation evidence yet; `External` requires people, credentials, elapsed +time, or hosted systems outside the repository. + +The matrix is append-only in meaning. A later correction must name the evidence +that supersedes an earlier assessment. Stars are an observed organic outcome, +not a shippable artifact: 6,000+ remains the breakout target, while stable-v1 +eligibility depends only on the release gates below. + +## Evidence rules + +A row becomes Green only when all applicable evidence exists: + +1. A normative contract or an explicit statement that no persistent contract is + required. +2. Native TypeScript and Python implementations when the public surface promises + both languages. +3. Shared, language-neutral conformance vectors for portable behavior. +4. Positive, negative, cancellation, cleanup, resource-bound, and restart tests + appropriate to the risk. +5. User documentation and an executable, provider-free example. +6. Clean repository gates, installable artifact checks, and a reviewed commit or + protected-branch pull request. +7. Release evidence for anything described as published, supported, secure, or + externally validated. + +Source files, enum members, planned CLI verbs, issue labels, and scanner +heartbeats are not implementation proof by themselves. + +## Day-by-day delivery coverage + +| Day | State | Evidence already present | Work still required for the plan | Registry control | +|---|---|---|---|---| +| 1 — contracts, ownership, governance | Partial | Public MIT repository, governance files, CI, protected `main`, canonical Graph IR/event schemas, logs, timer-backed scanner | Finish every listed planning/architecture/growth document; make completion evidence machine-verifiable; correct historical over-broad task titles | `CTRL-PLAN-COVERAGE-001`, `CTRL-EVIDENCE-002` | +| 2 — builders and canonical IR | Partial | TS/Python schema models, portable canonical JSON and shared hashes | General TS builder API, Python builder API, YAML loader, revision/content hashes for node/edge/schema, port/schema compatibility, concurrent-state/budget/capability validation | `D2-BUILDERS-YAML-020` | +| 3 — diagnostics and CLI | Partial | Shared invalid graph fixtures; TS `init`, `validate`, `compile`, `plan`, `doctor`; Quickstart | Python CLI and compatibility alias, exhaustive stable diagnostics, complete machine envelopes and exit-code reference | `D3-PY-CLI-021`, `D14-API-FREEZE-050` | +| 4 — scheduler and trace view | Partial | Native deterministic DAG scheduler; chain/diamond parity; bounded ready queue | Trace viewer, nested subgraphs/namespaces, explicit state reducers, executable stream/artifact edges | `D4-TRACE-SUBGRAPH-022`, `D15-EXPLORER-060` | +| 5 — pipeline, barrier, router | Partial; standalone pipeline delivered | Pure barrier/router evaluators; commit `3df201d` delivers bounded TS/Python pipelines, eight shared cases, hostile cleanup/configuration tests and full package gates | Scheduler-integrated deadline/quorum barriers, conditional edges, durable route replay/confidence escalation, stream-edge IR activation | `D7-PIPELINE-CONFORMANCE-013` Green; `D6-ROUTER-BARRIER-023` Open | +| 6 — terminal semantics and quorum | Partial | Structured scheduler and pipeline failures; retry/timeout/cancellation; attempt budgets; upstream isolation | Every planned node/edge terminal state, quorum/abstention at runtime, human/unknown escalation, failure-injection matrix | `D6-ROUTER-BARRIER-023`, `D11-VERIFY-SPEC-040` | +| 7 — bounded cycles and Alpha 1 | Partial | Source-only `v0.1.0-alpha.1` exists; static bounded-loop graph constructor exists | Executable `untilDry`, bounded `while`, evaluator-optimizer loops, global seen-set, semantic convergence, hard duration/cost/node limits and replayable exit reasons | `D7-CYCLE-SPEC-024`, `D7-TS-CYCLES-025`, `D7-PY-CYCLES-026`, `D7-CYCLE-CONFORMANCE-027` | +| 8 — retry/cancel operations | Partial | Native retry, timeout, cancellation and pipeline cleanup suites | Runtime chaos is isolated from durable operational commands; both must join without deadlock, leaks, unbounded retry or stale control | `D8-RUNTIME-CHAOS-084`, `D9-OPS-CONTROL-085`, `D8-CHAOS-OPS-030` | +| 9 — durable execution | Partial, strong local-DAG slice; critical redaction open | Event-sourced start/resume, CAS event stores, file checkpoints, stable activity keys, terminal idempotence, cross-language interrupted-history recovery | Freeze redaction/approval contracts, implement independent TS/Python sink-before-write lanes and canary join, then add Lease/LockManager, checkpoint acceleration, SQLite, ArtifactStore, replay/fork, non-idempotent confirmation and dual-resume races | `D9-REDACTION-039`, `D9-TS-REDACTION-087`, `D9-PY-REDACTION-088`, `D9-REDACTION-CONFORMANCE-089`, `D9-APPROVAL-077`, `D9-DURABLE-EXT-*` | +| 10 — budget/model/cost | Open | Graph-level concurrency/attempt limits provide a narrow bound | Token/money/time/node budgets, atomic reservations, model tier/router, usage and pricing snapshots, cost command/UI, hard stop before scheduling | `D10-BUDGET-SPEC-035`, `D10-TS-BUDGET-036`, `D10-PY-BUDGET-037`, `D10-BUDGET-CONFORMANCE-038` | +| 11 — verification | Open | Verified-fanout graph constructor is declarative only | Reflection, adversarial refutation, diverse lenses, citation verification, judge panels, versioned rubrics, votes, majority/quorum/abstention/unknown/human gating | `D11-VERIFY-SPEC-040`, `D11-TS-VERIFY-041`, `D11-PY-VERIFY-042`, `D11-VERIFY-CONFORMANCE-043` | +| 12 — isolation and policy | Open | Documentation accurately states current ambient-authority boundary | Capability manifests/enforcement, deny-by-default tool/filesystem/network/secrets, worktree leases/merge node, process/container isolation, escape tests and approvals | `D12-ISOLATION-SPEC-044`, `D12-TS-ISOLATION-045`, `D12-PY-ISOLATION-046`, `D12-ISOLATION-REDTEAM-047` | +| 13 — adapters and Alpha 2 | Open | Deterministic local mock execution | Independent TS/Python adapter lanes, shared streaming/tools/usage/retry/rate/cancel/fallback conformance, opt-in live provider evidence, doctor/score/badge | `D13-ADAPTER-SPEC-048`, `D13-TS-ADAPTERS-081`, `D13-PY-ADAPTERS-082`, `D13-ADAPTERS-049`, `D13-DX-051` | +| 14 — API freeze, plugins, patterns | Open | Read-only validation/planning MCP alpha and four TS graph constructors | Runtime MCP opt-in mutation policy, plugin SDK/discovery, public API review, all ten complete pattern skeletons and canonical unscoped npm distribution | `D14-API-FREEZE-050`, `D14-MCP-PLUGINS-052`, `D14-PATTERN-SKELETONS-053`, `D14-NPM-DIST-078` | +| 15 — production stores, workers, Explorer | Open | In-memory/JSONL events and local file checkpoints | SQLite/local artifacts default, PostgreSQL/S3 adapters, LockManager/distributed workers, React Explorer, JSONL/console/OTLP, critical-path/utilization views, first-run study | `D15-STORAGE-WORKERS-054`, `D15-EXPLORER-060`, `D15-PERFORMANCE-061` | +| 16 — security preflight | Partial | CodeQL, dependency review, Dependabot, private reporting, source package audits and an implementation-aligned security ledger | Close independent redaction conformance; complete privacy/retention, threat model, enforceable policy, secret scan, fuzz/property/chaos, license/SBOM, escape tests and zero unaccepted high/critical findings | `D9-REDACTION-CONFORMANCE-089`, `D16-PRIVACY-079`, `D16-SECURITY-062` | +| 17 — Beta | Open | Public alpha issues/discussions provide recruitment surface | First build immutable Beta/API artifacts; separately obtain consent-safe external usability evidence, at least five reports, 80% five-minute completion, retest and feedback disposition | `D17-BETA-063`, `D17-USABILITY-076` | +| 18 — compatibility and benchmark audit | Open | CI covers Node 20/22 and Python 3.11/3.12/3.13 on Linux | macOS/Windows, scale/resource tests, reproducible baselines, 100 randomized faults, education assets, support/incident readiness and no >10% unexplained regression | `D18-COMPAT-BENCH-064`, `D18-EDUCATION-ASSETS-083`, `D18-SUPPORT-READINESS-080`, `CTRL-ACCEPTANCE-070` | +| 19 — RC freeze | Open | npm tarball and Python wheel/sdist local rehearsals exist | Clean install and upgrade matrix, migration guide, full docs link/code checks, P0/P1 zero, signed `1.0.0-rc.1` candidate | `D19-RC-065` | +| 20 — provenance | Open/External | Source release and protected checks exist | Trusted npm/PyPI identity and rehearsal, checksums, SBOM, attestations and provenance verification; provenance does not self-authorize release | `D20-PROVENANCE-066` | +| 21 — release and support | Open/External | Repository, alpha release, issues and Discussions are public | Map all 178 leaves, revalidate historical evidence against one candidate, run a fail-closed stable-vs-RC roll-up, then publish only the authorized channel with support and transparent metrics | `CTRL-RELEASE-MAP-074`, `CTRL-EVIDENCE-BACKFILL-075`, `CTRL-RELEASE-ROLLUP-086`, `D21-RELEASE-067`, `CTRL-GROWTH-072` | + +## Product-capability coverage + +| Capability family | Current evidence | Missing acceptance evidence | State | +|---|---|---|---| +| Graph IR and compiler | Shared schema, canonical hashes, DAG compile parity | Full node kinds, typed ports, YAML/builders, nested graphs, state conflicts, policy/budget/capability diagnostics | Partial | +| DAG scheduling | Native ready-queue schedulers, deterministic diamond | 100-way and 1,000-node bounds, worker/distributed mode, full node-kind execution | Partial | +| Pipeline | Commit `3df201d`: standalone bounded native APIs, eight shared behavioral cases, docs and full local package gates | Durable per-item semantics deliberately excluded; stream IR remains declarative | Green for standalone scope; broader Graph IR scope Open | +| Barriers | Deterministic all/minimum/percentage settled evaluator | Durable wait, deadline, quorum, missing statistics in scheduler | Partial | +| Routers | Deterministic single/multicast evaluator | Runtime conditional edge selection, confidence escalation and durable replay | Partial | +| Cycles | Static finite graph constructor | Runtime bounded cycles, convergence/global seen set, budgets and exit reasons | Open | +| Dynamic GraphPatch | Schema vocabulary only | Append-only revision compiler, permissions/budget gates, dry run and malicious-patch tests | Open | +| Verification | Declarative pattern constructor | Runtime maker/verifier isolation, votes, citations, panels, reflection and unknown gates | Open | +| Durable state | Events, CAS, checkpoints, local start/resume | Leases, ArtifactStore/LockManager, SQLite/Postgres/S3, replay/fork and approvals | Partial | +| Cost/model routing | Narrow attempt/concurrency limits | Models, pricing, usage, reservations, cost views and hard budget scheduling | Open | +| Providers/tools | Deterministic local executors, read-only MCP | All official adapters and shared conformance, rate/circuit/fallback behavior | Open | +| Security/isolation | Honest boundary docs and supply-chain CI; the durable false-redaction signal is explicitly registered as a critical corrective task | Runtime enforcement, capabilities, approvals, truthful sink-before-write redaction, worktree/process/container providers | Open; `D9-REDACTION-039` blocks extension/release claims | +| Observability | Event history and Mermaid/DOT output | OTel, live status, trace/critical path metrics, web Explorer/time travel | Open | +| CLI/SDK DX | TS init/validate/compile/plan/doctor/visualize | Python CLI; remaining operational commands; scoring, badge, picker, artifacts, plugins | Partial | +| Education/patterns | Quickstarts and four TS constructors, two runnable examples | Ten complete cross-language pattern bundles and executable 14-step course | Partial | +| Release/community | Public alpha, governance, issue templates, Discussions | Trusted packages/provenance, external evidence, launch site/assets and sustained support | Partial/External | + +## Ten-pattern completeness ledger + +Every pattern must satisfy one bundle gate: YAML and JSON; native TS and Python; +fixtures and expected events; deterministic mock e2e; optional real-provider +setup; architecture diagram; declared token/money/time budgets; least-privilege +permissions; injected failure plus resume; tests; and Claude Code, Codex, MCP, +and shell launch guides. + +| Pattern | Current evidence | State | Control | +|---|---|---|---| +| Multi-source research diamond | TS constructor and provider-free showcase | Partial | `PATTERN-01-RESEARCH` | +| Cited deep research | No citation-verifier runtime bundle | Open | `PATTERN-02-CITED` | +| Route-auth security sweep | No complete runtime bundle | Open | `PATTERN-03-AUTH` | +| Diff-risk router and judge panel | Router constructor only | Partial | `PATTERN-04-DIFF` | +| Loop-until-dry discovery | Static bounded constructor only | Partial | `PATTERN-05-UNTIL-DRY` | +| File migration with worktrees | No isolation/merge implementation | Open | `PATTERN-06-MIGRATION` | +| CI failure sweeper | No complete bundle | Open | `PATTERN-07-CI` | +| Dependency update sweeper | No complete bundle | Open | `PATTERN-08-DEPS` | +| PR babysitter | No complete bundle | Open | `PATTERN-09-PR` | +| Scheduled ecosystem scan | No scheduling/provider bundle | Open | `PATTERN-10-ECOSYSTEM` | + +`D14-PATTERN-SKELETONS-053` creates the common cross-language structure; +`CTRL-PATTERNS-071` remains open until all ten rows satisfy the entire bundle +gate, not merely until ten directories exist. + +## Mandatory-test ledger + +| Test group | Included scenarios | Current status | Control | +|---|---|---|---| +| Compiler negatives | Missing/duplicate/unreachable nodes, ports/schemas, cycles, routers, loop bounds, unauthorized transforms | Core DAG subset Green; router/loop/policy cases open | `CTRL-ACCEPTANCE-070` | +| Parallel and streaming | 100-way concurrency, all failure policies, backpressure, barrier timeout, cancellation | Bounded pipeline and ordinary DAG subset Green; scale/deadline open | `D8-CHAOS-OPS-030`, `D18-COMPAT-BENCH-064` | +| Dynamic and verifier | Malicious patches, pass/reject/abstain, citation checks, quorum/unknown, seen-set convergence | Open | `D11-VERIFY-CONFORMANCE-043`, `D12-ISOLATION-REDTEAM-047` | +| Recovery | Every crash window, truthful redaction signal, dual resume, replay/fork, stale approvals, non-idempotent confirmation | Local DAG crash/resume subset Green; raw payloads currently contradict `redacted: true`; remaining cases open | `D9-REDACTION-039`, `D9-DURABLE-EXT-CONFORMANCE-034` | +| Isolation | Worktree conflicts, merge gate, allowed paths, port/temp/cache/database namespaces, prompt injection | Open | `D12-ISOLATION-REDTEAM-047` | +| Adapters/storage | Fallback, rate limit, circuit breaker, cancellation, secret redaction, shared storage/adapter suites | Open | `D13-ADAPTERS-049`, `D15-STORAGE-WORKERS-054` | +| Product e2e | Full CLI, all ten patterns, Explorer/replay, clean install/upgrade | Partial CLI; remainder open | `CTRL-PATTERNS-071`, `D19-RC-065` | +| Scale and chaos | 1,000 nodes, 100 randomized failures, kill/network/store/artifact faults | Open | `D18-COMPAT-BENCH-064` | + +## Quantitative release thresholds + +| Threshold | Current evidence | State | +|---|---|---| +| Compiler/scheduler/event store/policy coverage >=90% statements and >=85% branches | No consolidated threshold report | Open | +| >=250 unit/integration cases per language | Python exceeds the raw count; TS workspace exceeds it, but classification and coverage ownership need a release report | Partial | +| Shared adapter and storage conformance | Current compiler/runtime/persistence primitives are shared; provider and production storage matrices are absent | Partial | +| 100 randomized failure runs without deadlock/spawn/budget escape | No accepted report | Open | +| Linux/macOS/Windows; Node 20/22; Python 3.11/3.12/3.13 | Linux version matrix present; macOS and Windows absent | Partial | +| Mock normal CI; real-provider opt-in/nightly | Mock/local behavior exists; real-provider opt-in matrix absent | Partial | +| >10% performance regression blocks merge | No benchmark baseline/enforcement | Open | +| No unaccepted high/critical; secret/dependency/license/static scans pass | Several scans exist; complete secret/license/runtime-policy evidence absent | Partial | +| Trusted npm/PyPI, SBOM, checksums, attestations | Local artifacts only | Open/External | +| Quickstart <=3 commands; >=80% external testers finish <=5 minutes | Command count is within target; external study absent | Partial/External | +| Zero P0/P1 and >=5 external usability reports | No complete beta evidence | Open/External | + +No stable-v1 decision may treat aggregate test count as a substitute for +coverage, portability, chaos, external usability, or provenance evidence. + +## Required planning, architecture, delivery, and growth documents + +| Document | State | Control | +|---|---|---| +| `research/loop-engineering-benchmark.md` | Present; refresh before major release | `CTRL-DOCS-073` | +| `research/graph-engineering-source-review.md` | Present | `CTRL-DOCS-073` | +| `research/competitor-capability-matrix.md` | Present; domain review Open | `CTRL-DOCS-073` | +| `architecture/graph-ir-and-schema.md` | Present; implementation acceptance Open | `CTRL-DOCS-073` | +| `architecture/runtime-semantics.md` | Present; implementation acceptance Open | `CTRL-DOCS-073` | +| `architecture/persistence-and-recovery.md` | Present; implementation acceptance Open | `CTRL-DOCS-073` | +| `architecture/security-and-isolation.md` | Present; independent security review Open | `CTRL-DOCS-073` | +| `architecture/cross-language-conformance.md` | Present | `CTRL-DOCS-073` | +| `delivery/master-plan-coverage-matrix.md` | Present, maintained | `CTRL-PLAN-COVERAGE-001` | +| `delivery/task-dependency-graph.md` | Present, maintained | `CTRL-PLAN-COVERAGE-001` | +| `delivery/agent-ownership-map.md` | Present, maintained | `CTRL-PLAN-COVERAGE-001` | +| `delivery/release-checklist.md` | Present, maintained; mandatory evidence rows remain Open | `CTRL-PLAN-COVERAGE-001` | +| `delivery/full-plan-gap-audit.md` | Present; findings registered, remediation Open | `CTRL-PLAN-COVERAGE-001` | +| `delivery/d2-builder-yaml-implementation-brief.md` | Present; native implementation active | `D2-BUILDERS-YAML-020` | +| `delivery/d9-redaction-implementation-brief.md` | Present; critical implementation Open | `D9-REDACTION-039` | +| `growth/launch-plan.md` | Present as plan; launch execution Open | `CTRL-GROWTH-072` | +| `growth/content-calendar.md` | Present as plan; scheduled execution Open | `CTRL-GROWTH-072` | +| `growth/metrics-and-experiments.md` | Present as plan; observed outcomes Open | `CTRL-GROWTH-072` | + +## Post-audit registry closure + +The 2026-07-26 full-plan audit added 16 explicit controls instead of leaving +their work hidden inside broad aggregate tasks: + +| Closure family | New controls | Release effect | +|---|---|---| +| Machine evidence and decision | `CTRL-RELEASE-MAP-074`, `CTRL-EVIDENCE-BACKFILL-075`, `CTRL-RELEASE-ROLLUP-086` | Every `REL-*` leaf must map 178/178; historical status has zero candidate weight without revalidation; stable/RC decision fails closed. | +| Authority, privacy, usability, support | `D9-APPROVAL-077`, `D16-PRIVACY-079`, `D17-USABILITY-076`, `D18-SUPPORT-READINESS-080` | Human authority, consent, elapsed external evidence and support readiness can block release and cannot be fabricated. | +| Independent native lanes | `D13-TS-ADAPTERS-081`, `D13-PY-ADAPTERS-082`, `D9-TS-REDACTION-087`, `D9-PY-REDACTION-088`, `D9-REDACTION-CONFORMANCE-089` | Implementers no longer self-certify cross-language adapters or critical secret handling. | +| Runtime, operations, distribution, education | `D8-RUNTIME-CHAOS-084`, `D9-OPS-CONTROL-085`, `D14-NPM-DIST-078`, `D18-EDUCATION-ASSETS-083` | Chaos no longer blocks early durable specs; operations, canonical package and executable education each have independent gates. | + +Registry check at this checkpoint: 107 tasks, 107 unique IDs, zero dangling +dependencies, zero cycles, and `updated_at` not older than any task timestamp. +The scanner reports 6 of 77 evidence-required tasks satisfied; this is a scope +and evidence checkpoint, not a stable-release claim. + +## Exit rule + +The master plan is complete only when every capability and pattern row is Green, +every mandatory-test and quantitative threshold has a durable evidence link, all +required documents exist and pass checks, package provenance is verified, and +the release checklist records a go decision. External adoption and the 6,000+ +star target are reported honestly as outcomes; they cannot be fabricated or +declared complete by code changes. diff --git a/codex_plans/delivery/release-checklist.md b/codex_plans/delivery/release-checklist.md new file mode 100644 index 0000000..d76dde1 --- /dev/null +++ b/codex_plans/delivery/release-checklist.md @@ -0,0 +1,372 @@ +# Graph Engineering Stable-v1 Release Checklist + +- Authority: [Graph Engineering 21-Day Master Plan](../Graph-Engineering-21-Day-Master-Plan.md) +- Dependency source: [21-Day Task Dependency Graph](task-dependency-graph.md) +- Operational status source: [task registry](../../codex_logs/task-registry.json) +- Release rule: ship stable v1 only when every mandatory row is Green; otherwise + ship/support the accurately labeled complete RC and keep unmet work open + +## 1. Checklist protocol + +Every checklist row starts **Open**. This file intentionally records no current +Partial or Green claims. An alpha tag, a healthy progress scan, an existing +file, or an uncaptured local command is not stable-release evidence. + +Allowed status values: + +- **Open**: no qualifying evidence, or the check has not been executed against + the release candidate. +- **Partial**: exact evidence exists, but a required language, platform, + version, scenario, threshold, review, or artifact is still missing. +- **Green**: the entire row passed against the immutable candidate and the + Evidence slot contains the required references. +- **Blocked**: an explicit dependency, defect, external authority, or external + evidence gap prevents execution. Blocked is never equivalent to Green. + +Every Evidence slot must eventually contain the source revision, exact command +and result, immutable CI/report URL or committed evidence path, artifact digest +where relevant, date, and independent reviewer. A mutable dashboard URL or a +summary without raw evidence is insufficient. + +Owner codes are accountable lanes, not proof of assignment: + +- **Integration**: canonical spec, cross-language joins, release decision. +- **TS runtime**: TypeScript/Node implementation and npm artifacts. +- **Python runtime**: Python implementation and PyPI artifacts. +- **Platform/quality**: CLI, MCP, Explorer, docs, examples, matrices, security, + external testing, growth assets, and support operations. + +`REL-*` IDs are checklist coordination IDs. They must be mapped to the live task +registry before execution; their presence here does not assert that a registry +task exists or is complete. + +## 2. Candidate coordinates + +Fill these fields before changing any row from Open: + +```text +Candidate version: +Candidate source revision: +Canonical spec revision/hash: +TypeScript package digests: +Python wheel/sdist digests: +SBOM/checksum manifest: +CI matrix run: +Release manager: +Independent go/no-go reviewer: +Decision timestamp (UTC): +``` + +## 3. Stable-v1 decision joins + +| Gate | Mandatory decision condition | Owner | Task ID | Status | Evidence slot | +|---|---|---|---|---|---| +| `V1-01` | Durable recovery: successful internal nodes never rerun; crash windows, leases/CAS, replay/fork, stale approvals, and storage races pass | Integration | `REL-V1-01` | Open | Add candidate-bound `T19-T22`, `T30`, and storage evidence. | +| `V1-02` | Security: no unaccepted high/critical issue; deny-by-default capability/isolation, redaction, prompt-injection, dependency/license/static scans pass | Integration | `REL-V1-02` | Open | Add signed security review referencing `T09`, `T15`, `T23-T28`, `Q08`, and supply-chain rows. | +| `V1-03` | Cross-language conformance: every `X01-X10` row is Green with no TS/Python divergence | Integration | `REL-V1-03` | Open | Add shared conformance report, fixture revision, both runtime revisions, and reviewer. | +| `V1-04` | Package provenance: trusted npm/PyPI publishing, SBOMs, checksums, attestations, and source-to-package identity all pass | Integration | `REL-V1-04` | Open | Add `Q09` plus all mandatory `SC-*` artifact and identity references. | +| `V1-05` | External usability: Quickstart is at most three commands, at least 80% finish in five minutes, at least five external reports, and no P0/P1 defects | Platform/quality | `REL-V1-05` | Open | Add anonymized tester cohort/results, timing method, issue query, and reviewer. | +| `V1-06` | All `T01-T33`, `Q01-Q11`, required platform jobs, package checks, ten patterns, and mandatory assets are Green | Integration | `REL-V1-06` | Open | Add generated gate roll-up that links every leaf row without suppressing Open/Partial/Blocked entries. | +| `V1-07` | All planned Day-21 assets exist and at least a complete Beta/RC is supportable | Integration | `REL-V1-07` | Open | Add release asset manifest, package manifests, support roster, exclusions, and reviewer. | +| `V1-08` | Final label decision is stable v1 only if `V1-01` through `V1-07` are Green; otherwise full RC | Integration | `REL-V1-08` | Open | Add signed go/no-go record, chosen label/channel, candidate digests, and fallback decision. | + +## 4. Non-negotiable invariant checks + +| Gate | Mandatory condition | Owner | Task ID | Status | Evidence slot | +|---|---|---|---|---|---| +| `I01` | Canonical `spec/` governs both native runtimes and shared fixtures | Integration | `REL-I01` | Open | Add spec revision, fixture manifest, TS/Python conformance runs, and review. | +| `I02` | Failures remain structured values/events and are never silently replaced by null | Integration | `REL-I02` | Open | Add invalid-input/output and terminal-envelope test reports for both runtimes. | +| `I03` | Deterministic transforms perform plumbing; model nodes perform judgment | Integration | `REL-I03` | Open | Add compiler/policy fixtures and architecture review against the candidate. | +| `I04` | Implicit cycles, unbounded retries, and unbounded dynamic fan-out are rejected | Integration | `REL-I04` | Open | Add compiler/runtime limit tests, randomized runs, and stable error-code evidence. | +| `I05` | External effects are documented at-least-once and require idempotency or approval | Integration | `REL-I05` | Open | Add activity contract tests, idempotency/approval examples, and docs review. | +| `I06` | Telemetry and prompt/response capture are off by default and redacted when enabled | Platform/quality | `REL-I06` | Open | Add clean-install configuration test, trace/redaction fixtures, and security review. | +| `I07` | Dynamic patches pass compiler, policy, permission, and budget gates with hard depth/fan-out/node/attempt caps | Integration | `REL-I07` | Open | Add malicious-patch/dry-run fixtures and both-runtime results. | +| `I08` | Planners cannot expand authority; shell/write/network/secret and MCP mutations are deny-by-default | Platform/quality | `REL-I08` | Open | Add capability escalation, MCP approval, shell, and network denial evidence. | +| `I09` | Insufficient verifier quorum is unknown or human-gated, never implicit pass | Integration | `REL-I09` | Open | Add pass/reject/abstain/quorum fixtures, retained-vote evidence, and review. | +| `I10` | Release claims avoid unsupported “battle-tested,” “production proven,” or exactly-once language | Platform/quality | `REL-I10` | Open | Add reviewed package, site, docs, demo, and channel-copy claim audit. | + +## 5. Cross-language conformance checks + +| Gate | TS/Python equality requirement | Owner | Task ID | Status | Evidence slot | +|---|---|---|---|---|---| +| `X01` | Canonical bytes and stable graph/node/edge/schema hashes | Integration | `REL-X01` | Open | Add shared fixture manifest and byte/hash diff report. | +| `X02` | Compilation verdicts, diagnostics, and stable error codes | Integration | `REL-X02` | Open | Add positive/negative corpus output diff and both commands. | +| `X03` | Router single/multicast decisions and durable replay | Integration | `REL-X03` | Open | Add route-selection and replay fixture results. | +| `X04` | Barrier all/minimum/percentage/quorum/deadline settlement | Integration | `REL-X04` | Open | Add barrier/quorum corpus results including missing/failure statistics. | +| `X05` | Event-ordering constraints and terminal-history envelopes | Integration | `REL-X05` | Open | Add normalized event-log comparison and allowed-order proof. | +| `X06` | Terminal states and structured failure envelopes | Integration | `REL-X06` | Open | Add all terminal/failure-policy fixture results. | +| `X07` | Retry, timeout, cancellation, and exact attempt accounting | Integration | `REL-X07` | Open | Add coordinated failure/cancellation corpus and count comparison. | +| `X08` | Resume, replay, and fork results/lineage | Integration | `REL-X08` | Open | Add bidirectional persisted-history interop and lineage report. | +| `X09` | Stable JSON envelopes, CLI machine output, and exit codes | Integration | `REL-X09` | Open | Add TS/Python CLI golden outputs and exit-code matrix. | +| `X10` | Adapter and Event/Checkpoint/Artifact/Lock/storage conformance | Integration | `REL-X10` | Open | Add shared adapter/storage suite output for every official implementation. | + +## 6. Mandatory test scenarios + +| Gate | Mandatory candidate test | Owner | Task ID | Status | Evidence slot | +|---|---|---|---|---|---| +| `T01` | Missing node references are rejected | Integration | `REL-T01` | Open | Add shared fixture IDs and TS/Python compiler results. | +| `T02` | Duplicate node identities are rejected | Integration | `REL-T02` | Open | Add shared fixture IDs and TS/Python compiler results. | +| `T03` | Unreachable nodes are rejected | Integration | `REL-T03` | Open | Add shared fixture IDs and TS/Python compiler results. | +| `T04` | Invalid ports are rejected | Integration | `REL-T04` | Open | Add invalid endpoint/port corpus and stable diagnostics. | +| `T05` | Invalid graph, edge, node-input, and node-output schemas are rejected before unsafe persistence | Integration | `REL-T05` | Open | Add compiler/runtime schema corpus and structured error events. | +| `T06` | Implicit graph cycles are rejected | Integration | `REL-T06` | Open | Add cycle fixtures and stable error codes. | +| `T07` | Incomplete router without exhaustive cases/default is rejected | Integration | `REL-T07` | Open | Add router compiler/runtime fixtures. | +| `T08` | Unbounded loops are rejected | Integration | `REL-T08` | Open | Add missing/unsafe-bound fixtures and hard-stop results. | +| `T09` | Unauthorized transforms and capability expansion are rejected | Platform/quality | `REL-T09` | Open | Add policy fixtures, escalation attempts, and denial events. | +| `T10` | 100-way parallel concurrency stays within configured bounds | Integration | `REL-T10` | Open | Add deterministic load command, utilization trace, and resource report. | +| `T11` | Every failure policy passes, including retry, drop, stop, dead-letter, fail-fast, partial, and quorum | Integration | `REL-T11` | Open | Add policy-by-policy TS/Python results and terminal envelopes. | +| `T12` | Streaming uses bounded buffers, demand, and real downstream backpressure without a whole-stage barrier | Integration | `REL-T12` | Open | Add slow-consumer/pull-ahead traces and shared pipeline corpus. | +| `T13` | Barrier timeout/deadline reports complete success/failure/missing statistics | Integration | `REL-T13` | Open | Add clock-controlled barrier fixtures for both runtimes. | +| `T14` | Router replay reuses the durable decision without re-judging | Integration | `REL-T14` | Open | Add original/replay event comparison and zero-provider-call proof. | +| `T15` | Malicious dynamic patches and unsafe dry runs are rejected | Platform/quality | `REL-T15` | Open | Add depth/fan-out/node/attempt/capability/budget attack corpus. | +| `T16` | Verifier pass, reject, and abstain/unknown paths retain votes/evidence and gate correctly | Integration | `REL-T16` | Open | Add panel/quorum/rubric fixtures and persisted verdicts. | +| `T17` | Global seen-set convergence is deterministic | Integration | `REL-T17` | Open | Add duplicate/discovery-order corpus and exit-reason comparison. | +| `T18` | Hard iteration, duration, cost, node, fan-out, and attempt limits stop scheduling | Integration | `REL-T18` | Open | Add one boundary and one over-limit test per budget dimension. | +| `T19` | Crash recovery passes at every checkpoint and commit/release window | Integration | `REL-T19` | Open | Add crash-point matrix, resumed history, and no-rerun proof. | +| `T20` | Dual-resume lease/CAS races cannot advance one run twice | Integration | `REL-T20` | Open | Add synchronized race test and durable winner/loser events. | +| `T21` | Replay and fork preserve traceable lineage and expected results | Integration | `REL-T21` | Open | Add replay/fork graph/event hashes and lineage report. | +| `T22` | Stale approvals are rejected | Integration | `REL-T22` | Open | Add graph/run/revision approval mismatch fixtures. | +| `T23` | Worktree lease, path, test-gate, and merge conflicts are structured failures | Platform/quality | `REL-T23` | Open | Add conflict/cleanup test artifacts and preserved-worktree evidence. | +| `T24` | Process/container ports, temp files, caches, and database namespaces remain isolated | Platform/quality | `REL-T24` | Open | Add concurrent escape/isolation matrix and cleanup report. | +| `T25` | Provider fallback, rate limit, retry, cancellation, and circuit breaker behavior conforms | Integration | `REL-T25` | Open | Add shared mock adapter suite and opt-in provider evidence. | +| `T26` | Secrets are redacted from errors, events, traces, prompts, tools, and support bundles | Platform/quality | `REL-T26` | Open; corrective `D9-REDACTION-039` | First correct the current raw-payload/`redacted: true` contradiction, then add positive/negative seeded-secret scans across journal, checkpoint, artifact, stdout/stderr, log, trace, error and support-bundle bytes in both languages. | +| `T27` | Cancellation before start and while running propagates across runtime/provider/tool boundaries | Integration | `REL-T27` | Open | Add pre/running cancellation tests and abort/attempt traces. | +| `T28` | Prompt injection cannot expand capabilities or bypass approval/policy | Platform/quality | `REL-T28` | Open | Add adversarial prompts, denial events, and independent security review. | +| `T29` | Complete CLI init/add, validate/compile/plan/run, status/watch/inspect/logs, pause/resume/cancel/retry, replay/fork, cost/doctor/score/badge/visualize, worktree/artifact/plugin, and MCP flow passes | Platform/quality | `REL-T29` | Open | Add command/JSON/error/exit-code matrix on packed artifacts. | +| `T30` | Shared Event/Checkpoint/Artifact/Lock and SQLite/PostgreSQL/S3 storage conformance passes | Integration | `REL-T30` | Open | Add implementation-by-operation matrix, race results, and artifact digests. | +| `T31` | All ten patterns pass end-to-end in YAML/JSON, TypeScript, and Python | Platform/quality | `REL-T31` | Open | Add `REL-PAT01` through `REL-PAT10` roll-up and shared E2E run. | +| `T32` | A 1,000-node graph stays within documented compiler/scheduler/worker resource bounds | Platform/quality | `REL-T32` | Open | Add versioned benchmark input, limits, wall time, CPU, and memory report. | +| `T33` | Kill, network, store, and artifact chaos completes without deadlock, unbounded spawn, or budget escape | Platform/quality | `REL-T33` | Open | Add fault matrix, seeds, traces, terminal states, and resource report. | + +## 7. Quantitative quality thresholds + +| Gate | Mandatory threshold | Owner | Task ID | Status | Evidence slot | +|---|---|---|---|---|---| +| `Q01` | Compiler, scheduler, event store, and policy each reach at least 90% statement and 85% branch coverage | Integration | `REL-Q01` | Open | Add per-language, per-subsystem coverage artifacts tied to the candidate. | +| `Q02` | At least 250 unit/integration cases per language | Integration | `REL-Q02` | Open | Add collected-case inventory separating TS and Python and excluding skipped tests. | +| `Q03` | One shared adapter/storage conformance suite passes every implementation | Integration | `REL-Q03` | Open | Add suite revision, adapter/store matrix, commands, and reports. | +| `Q04` | 100 randomized failure runs finish without deadlock, unbounded spawn, or budget escape | Platform/quality | `REL-Q04` | Open | Add all 100 seeds/results, timeout policy, resource traces, and zero-failure summary. | +| `Q05` | Linux/macOS/Windows; Node 20/22; Python 3.11/3.12/3.13 support matrix passes | Platform/quality | `REL-Q05` | Open | Add Green roll-up for every mandatory `REL-MX-*` row. | +| `Q06` | Deterministic mock providers run in normal CI; real providers run only opt-in/nightly | Platform/quality | `REL-Q06` | Open | Add CI definitions, default no-credential run, and isolated nightly/opt-in evidence. | +| `Q07` | No performance regression above 10% without an approved baseline ADR | Integration | `REL-Q07` | Open | Add candidate/baseline benchmark diff; link approved ADR for any permitted regression. | +| `Q08` | No unaccepted high/critical vulnerability; secret, dependency, license, and static-analysis scans pass | Platform/quality | `REL-Q08` | Open; depends on `D9-REDACTION-039` | Add scanner versions/reports, corrective redaction evidence, triage disposition, and independent security sign-off. | +| `Q09` | Trusted npm/PyPI publishing, SBOMs, checksums, and attestations pass | Integration | `REL-Q09` | Open | Add Green roll-up for required `REL-SC-*` rows and artifact identities. | +| `Q10` | Quickstart uses at most three commands and at least 80% of external testers finish within five minutes | Platform/quality | `REL-Q10` | Open | Add exact Quickstart, cohort/timing data, completion calculation, and raw reports. | +| `Q11` | No P0/P1 defects and at least five external usability reports exist before stable v1 | Platform/quality | `REL-Q11` | Open | Add release-blocker query snapshot and at least five anonymized report references. | + +## 8. Cross-platform and runtime-version matrix + +Each Node cell must perform a clean packed install, build/typecheck, normal unit +and integration tests, deterministic mock smoke run, and CLI smoke test. Each +Python cell must install the wheel into a clean environment, run unit/integration +tests, Ruff/mypy checks, deterministic mock smoke run, and Python CLI smoke +test. No allowed-failure cell can turn `Q05` Green. + +| Matrix cell | Required environment | Owner | Task ID | Status | Evidence slot | +|---|---|---|---|---|---| +| `MX-N01` | Linux, Node 20 | TS runtime | `REL-MX-N01` | Open | Add OS image/version, Node/pnpm versions, packed-install command, CI run, and artifacts. | +| `MX-N02` | Linux, Node 22 | TS runtime | `REL-MX-N02` | Open | Add OS image/version, Node/pnpm versions, packed-install command, CI run, and artifacts. | +| `MX-N03` | macOS, Node 20 | TS runtime | `REL-MX-N03` | Open | Add macOS/Xcode image, Node/pnpm versions, packed-install command, CI run, and artifacts. | +| `MX-N04` | macOS, Node 22 | TS runtime | `REL-MX-N04` | Open | Add macOS/Xcode image, Node/pnpm versions, packed-install command, CI run, and artifacts. | +| `MX-N05` | Windows, Node 20 | TS runtime | `REL-MX-N05` | Open | Add Windows image/build, Node/pnpm versions, packed-install command, CI run, and artifacts. | +| `MX-N06` | Windows, Node 22 | TS runtime | `REL-MX-N06` | Open | Add Windows image/build, Node/pnpm versions, packed-install command, CI run, and artifacts. | +| `MX-P01` | Linux, Python 3.11 | Python runtime | `REL-MX-P01` | Open | Add OS image/version, Python/uv versions, wheel-install command, CI run, and artifacts. | +| `MX-P02` | Linux, Python 3.12 | Python runtime | `REL-MX-P02` | Open | Add OS image/version, Python/uv versions, wheel-install command, CI run, and artifacts. | +| `MX-P03` | Linux, Python 3.13 | Python runtime | `REL-MX-P03` | Open | Add OS image/version, Python/uv versions, wheel-install command, CI run, and artifacts. | +| `MX-P04` | macOS, Python 3.11 | Python runtime | `REL-MX-P04` | Open | Add macOS/Xcode image, Python/uv versions, wheel-install command, CI run, and artifacts. | +| `MX-P05` | macOS, Python 3.12 | Python runtime | `REL-MX-P05` | Open | Add macOS/Xcode image, Python/uv versions, wheel-install command, CI run, and artifacts. | +| `MX-P06` | macOS, Python 3.13 | Python runtime | `REL-MX-P06` | Open | Add macOS/Xcode image, Python/uv versions, wheel-install command, CI run, and artifacts. | +| `MX-P07` | Windows, Python 3.11 | Python runtime | `REL-MX-P07` | Open | Add Windows image/build, Python/uv versions, wheel-install command, CI run, and artifacts. | +| `MX-P08` | Windows, Python 3.12 | Python runtime | `REL-MX-P08` | Open | Add Windows image/build, Python/uv versions, wheel-install command, CI run, and artifacts. | +| `MX-P09` | Windows, Python 3.13 | Python runtime | `REL-MX-P09` | Open | Add Windows image/build, Python/uv versions, wheel-install command, CI run, and artifacts. | +| `MX-X01` | Combined TS/Python bidirectional conformance on Linux, macOS, and Windows using current supported versions | Integration | `REL-MX-X01` | Open | Add three-OS interop CI runs, fixture revision, normalized diffs, and artifacts. | +| `MX-X02` | Cross-platform path, newline, signal/cancellation, process, port, and filesystem behavior | Platform/quality | `REL-MX-X02` | Open | Add OS-specific regression suite and structured divergence report showing none. | + +## 9. Package install, upgrade, and distribution checks + +| Gate | Mandatory package check | Owner | Task ID | Status | Evidence slot | +|---|---|---|---|---|---| +| `PKG01` | Every publishable npm package packs only intended runtime files, types, README, license, and notices | TS runtime | `REL-PKG01` | Open | Add tarball manifests, sizes, digests, and package-content check output. | +| `PKG02` | The canonical npm `graph-engineering` distribution installs from its tarball outside the monorepo | TS runtime | `REL-PKG02` | Open | Add clean temporary-project install/build/run evidence on Node 20 and 22. | +| `PKG03` | Python wheel and sdist contain intended modules, `py.typed`, README, license, and notices | Python runtime | `REL-PKG03` | Open | Add wheel/sdist manifests, metadata validation, sizes, and digests. | +| `PKG04` | The canonical PyPI `graph-engineering` distribution installs outside the source tree | Python runtime | `REL-PKG04` | Open | Add clean-environment wheel and sdist install/run evidence on Python 3.11-3.13. | +| `PKG05` | Primary `graph` and documented `grapheng` compatibility executables resolve and report the candidate version | Platform/quality | `REL-PKG05` | Open | Add `which`/`where`, version, help, and smoke outputs for npm and Python installations. | +| `PKG06` | Supported npm prerelease-to-RC-to-candidate upgrade preserves documented config/state or performs explicit migration | TS runtime | `REL-PKG06` | Open | Add version path, before/after fixtures, commands, and rollback result. | +| `PKG07` | Supported PyPI prerelease-to-RC-to-candidate upgrade preserves documented config/state or performs explicit migration | Python runtime | `REL-PKG07` | Open | Add version path, before/after fixtures, commands, and rollback result. | +| `PKG08` | Clean install and upgrade pass on every supported OS/runtime matrix cell | Platform/quality | `REL-PKG08` | Open | Add roll-up linking `REL-MX-*` and package-specific install/upgrade jobs. | +| `PKG09` | Installed packages run the no-credential deterministic-mock Quickstart in no more than three commands | Platform/quality | `REL-PKG09` | Open | Add clean terminal transcripts, elapsed time, outputs, and produced trace/score. | +| `PKG10` | Full CLI machine-readable JSON, structured errors, and exit codes work from installed artifacts | Platform/quality | `REL-PKG10` | Open | Add packed/wheel CLI golden matrix and schema validation. | +| `PKG11` | Package versions, Graph IR/API version, release tag, docs, and generated assets agree | Integration | `REL-PKG11` | Open | Add automated version audit tied to candidate revision and artifact digests. | +| `PKG12` | Package imports/exports and public APIs match frozen Day-14 contracts; upgrade adds no undocumented break | Integration | `REL-PKG12` | Open | Add API diff, compatibility suite, and approved migration notes if needed. | +| `PKG13` | Registry namespace/alias audit is current; no empty squatting package is published | Integration | `REL-PKG13` | Open | Add registry lookup date/results and explicit ownership/alias decision. | + +## 10. Supply chain, security, and provenance checks + +| Gate | Mandatory supply-chain check | Owner | Task ID | Status | Evidence slot | +|---|---|---|---|---|---| +| `SC01` | npm trusted-publishing identity and least-privilege workflow rehearsal pass without long-lived release credentials | TS runtime | `REL-SC01` | Open | Add workflow revision, identity configuration, dry-run/rehearsal log, and reviewer. | +| `SC02` | PyPI trusted-publishing identity and least-privilege workflow rehearsal pass without long-lived release credentials | Python runtime | `REL-SC02` | Open | Add workflow revision, identity configuration, dry-run/rehearsal log, and reviewer. | +| `SC03` | SPDX or CycloneDX SBOM exists for source, npm artifacts, Python wheel/sdist, and deployable site/app artifacts | Platform/quality | `REL-SC03` | Open | Add SBOM filenames, formats, generation command, package mapping, and digests. | +| `SC04` | Published checksum manifest covers every release artifact | Platform/quality | `REL-SC04` | Open | Add checksum algorithm, signed manifest, artifact list, and verification run. | +| `SC05` | Build and publish attestations bind source revision, workflow identity, and each artifact digest | Integration | `REL-SC05` | Open | Add attestation URLs/files and independent verification output. | +| `SC06` | Source archive/tag, npm tarballs, Python artifacts, SBOM, checksums, and attestations all identify one candidate | Integration | `REL-SC06` | Open | Add source-to-artifact provenance map with no unexplained file/version drift. | +| `SC07` | Secret scan passes repository, history, packages, source maps, docs, traces, and support bundles | Platform/quality | `REL-SC07` | Open | Add scanner/version/config, report, seeded-secret control, and triage. | +| `SC08` | Production dependency scan passes or has no unaccepted high/critical finding | Platform/quality | `REL-SC08` | Open | Add npm/Python/system dependency reports and disposition references. | +| `SC09` | License scan passes and MIT license plus complete third-party notices ship where required | Platform/quality | `REL-SC09` | Open | Add license inventory, policy result, package manifests, and legal review owner. | +| `SC10` | Static analysis and CodeQL-equivalent checks pass the exact candidate | Platform/quality | `REL-SC10` | Open | Add workflow run, analyzer versions, findings, and dispositions. | +| `SC11` | Threat model covers providers, shell/MCP, dynamic patches, stores, worktrees, workers, Explorer, and publishing | Platform/quality | `REL-SC11` | Open | Add reviewed threat model revision and resolved/unaccepted-risk list showing none high/critical. | +| `SC12` | Fuzz and prompt-injection suites pass policy, parser, schema, redaction, and capability boundaries | Platform/quality | `REL-SC12` | Open | Add corpus/seeds, duration, crash list, minimized reproducers, and result. | +| `SC13` | Telemetry and prompt/response capture remain off by default in clean npm/PyPI installs | Platform/quality | `REL-SC13` | Open | Add network/trace/config observation from both clean package installations. | +| `SC14` | Release workflow produces reproducible or explained artifact manifests from a clean trusted runner | Integration | `REL-SC14` | Open | Add two clean build manifests/digests and reviewed explanation for permitted variance. | + +## 11. External testing and usability checks + +External evidence must come from people outside the maintainer group. Reports +must be consented, minimally retained, and redacted; raw prompts, credentials, +and user data are not release evidence. + +| Gate | Mandatory external check | Owner | Task ID | Status | Evidence slot | +|---|---|---|---|---|---| +| `UX01` | The canonical Quickstart contains no more than three user commands | Platform/quality | `REL-UX01` | Open | Add exact published Quickstart revision and command count review. | +| `UX02` | At least 80% of the recorded external cohort reaches a successful first run within five minutes | Platform/quality | `REL-UX02` | Open | Add cohort size, start/stop definition, anonymized timings, failures, and calculation. | +| `UX03` | At least five external usability reports cover install, first run, comprehension, and next action | Platform/quality | `REL-UX03` | Open | Add five or more consented report IDs, environment, outcome, and disposition. | +| `UX04` | Deterministic mock Quickstart needs no provider account, credential, or product telemetry opt-in | Platform/quality | `REL-UX04` | Open | Add clean-machine network/credential/config observation and transcript. | +| `UX05` | Tester environments include more than one OS and both TS and Python entry paths | Platform/quality | `REL-UX05` | Open | Add anonymized OS/runtime/language cohort matrix. | +| `UX06` | No unresolved P0/P1 defect exists in the candidate or documented first-run path | Integration | `REL-UX06` | Open | Add timestamped issue/incident query and release-manager sign-off. | +| `UX07` | Beta feedback is triaged; release-blocking fixes are retested externally | Platform/quality | `REL-UX07` | Open | Add feedback-to-issue mapping, fix revisions, and external retest results. | +| `UX08` | First-run docs and errors return actionable top remediation steps where promised | Platform/quality | `REL-UX08` | Open | Add doctor/score/error transcripts and tester comprehension notes. | +| `UX09` | External reports and galleries have consent, redaction, and no fabricated adopter claims | Platform/quality | `REL-UX09` | Open | Add consent/redaction audit and source reference for each public entry. | +| `UX10` | At least five external reports and the 80% timing calculation are independently reviewed | Integration | `REL-UX10` | Open | Add reviewer identity/date, sampling notes, exclusions, and signed conclusion. | + +## 12. Ten executable pattern checks + +Every pattern must satisfy the shared bundle `PB`: YAML/JSON, TypeScript, and +Python implementations; deterministic fixtures and expected events; mock +execution; opt-in real-provider setup; architecture diagram; budgets; +permissions; structured failure and durable-resume demos; tests; and Claude +Code, Codex, MCP, and shell guides. A skeleton is not a passing pattern. + +| Gate | Mandatory pattern check | Owner | Task ID | Status | Evidence slot | +|---|---|---|---|---|---| +| `PB` | Shared bundle schema/checker validates every required pattern artifact and rejects a missing component | Platform/quality | `REL-PAT00` | Open | Add bundle schema/check command, negative fixture, and ten-pattern manifest. | +| `P01` | Multi-source research diamond passes `PB` and cross-language E2E with settled source failures | Platform/quality | `REL-PAT01` | Open | Add artifact manifest, TS/Python/YAML runs, expected events, failure/resume trace, and guides. | +| `P02` | Cited deep research with citation verification passes `PB`; unsupported citations reject/unknown-gate | Platform/quality | `REL-PAT02` | Open | Add artifact manifest, citation fixtures/verdicts, cross-language E2E, resume trace, and guides. | +| `P03` | Route authentication security sweep passes `PB`; missing route/capability fails closed | Platform/quality | `REL-PAT03` | Open | Add artifact manifest, route/policy fixtures, cross-language E2E, denial trace, and guides. | +| `P04` | Diff risk router with diverse judge panel passes `PB`; votes/abstentions/evidence are retained | Platform/quality | `REL-PAT04` | Open | Add artifact manifest, panel/quorum fixtures, cross-language E2E, resume trace, and guides. | +| `P05` | Loop-until-dry bug discovery passes `PB`; hard-limit exhaustion is not called convergence | Platform/quality | `REL-PAT05` | Open | Add artifact manifest, seen-set/limit fixtures, cross-language E2E, exit reasons, and guides. | +| `P06` | File-by-file migration with worktrees and test gates passes `PB`; conflicts do not merge | Platform/quality | `REL-PAT06` | Open | Add artifact manifest, isolated migration E2E, conflict/test failure, resume trace, and guides. | +| `P07` | CI failure sweeper passes `PB`; external mutations are idempotent/approved and attempts are bounded | Platform/quality | `REL-PAT07` | Open | Add artifact manifest, fake CI fixtures, cross-language E2E, stop/resume trace, and guides. | +| `P08` | Dependency update sweeper passes `PB`; unsafe/conflicting updates remain isolated | Platform/quality | `REL-PAT08` | Open | Add artifact manifest, fake registry/update fixtures, security/test gates, resume trace, and guides. | +| `P09` | PR babysitter passes `PB`; stale approvals reject and writes are idempotent/approved | Platform/quality | `REL-PAT09` | Open | Add artifact manifest, fake PR fixtures, approval/replay E2E, resume trace, and guides. | +| `P10` | Scheduled ecosystem scan passes `PB`; schedule/fan-out/cost/duration are bounded | Platform/quality | `REL-PAT10` | Open | Add artifact manifest, fake ecosystem fixtures, chaos/resume E2E, resource report, and guides. | + +## 13. Documentation and launch-asset checks + +| Gate | Mandatory asset | Owner | Task ID | Status | Evidence slot | +|---|---|---|---|---|---| +| `DOC01` | Sixty-second, mock-first Quickstart aligned with `Q10` | Platform/quality | `REL-DOC01` | Open | Add published English/Chinese revisions, clean transcripts, video/time proof, and external result. | +| `DOC02` | Ninety-second uncut terminal demo with no hidden manual repair | Platform/quality | `REL-DOC02` | Open | Add source script, uncut media, candidate version, timestamp, and reproduction steps. | +| `DOC03` | Interactive linear-versus-graph visualization uses real topology/events and accurate limitations | Platform/quality | `REL-DOC03` | Open | Add deployed/source revision, fixture, smoke test, and screenshots/video. | +| `DOC04` | Fourteen-step executable roadmap covers all named topics and when not to use a graph | Platform/quality | `REL-DOC04` | Open | Add course manifest, fourteen runnable checks, docs-link report, and reviewer. | +| `DOC05` | Architecture essay accurately covers IR, execution, durability, at-least-once effects, safety, and limits | Integration | `REL-DOC05` | Open | Add reviewed essay revision and claim-to-spec cross-reference. | +| `DOC06` | Side-by-side TypeScript/Python examples remain behaviorally conformant | Platform/quality | `REL-DOC06` | Open | Add executable docs tests and `X01-X10` references. | +| `DOC07` | Performance and recovery benchmarks are reproducible with versioned inputs/environment/baseline | Platform/quality | `REL-DOC07` | Open | Add benchmark scripts, raw data, candidate/baseline revisions, reports, and ADR if needed. | +| `DOC08` | Four authentic case studies ship, including at least one failure story | Platform/quality | `REL-DOC08` | Open | Add four source/trace references, consent where needed, reproduction steps, and claim review. | +| `DOC09` | Graph Ready G0-G4 score and badge are deterministic and return top three remediation actions | Platform/quality | `REL-DOC09` | Open | Add scoring fixtures, CLI/site outputs, repeatability check, and badge asset. | +| `DOC10` | Pattern picker, anti-pattern, failure-mode, operations, and safety guides are complete | Platform/quality | `REL-DOC10` | Open | Add guide manifest, docs-link/tests report, pattern mapping, and security review. | +| `DOC11` | Interactive showcase and Explorer display topology, states, budget, critical path, utilization, waits, retries, verdicts, and replay/fork history | Platform/quality | `REL-DOC11` | Open | Add deployed/source revision, deterministic trace, UI smoke tests, and visual evidence. | +| `DOC12` | Adopter and trace galleries contain only consented, authentic entries | Platform/quality | `REL-DOC12` | Open | Add entry manifest, consent/redaction references, URLs, and reviewer. | +| `DOC13` | English canonical docs plus Chinese README/Quickstart/launch summary agree on version and claims | Platform/quality | `REL-DOC13` | Open | Add bilingual diff review, link check, version audit, and reviewer. | +| `DOC14` | Channel-specific GitHub, Hacker News, X, LinkedIn, Reddit, Dev.to, and Chinese-community material is ready | Platform/quality | `REL-DOC14` | Open | Add copy manifest, current links/version, claim audit, schedule, and owners. | +| `DOC15` | API, CLI/MCP machine-output, extension, provider, store, upgrade, security, and support references match the frozen candidate | Platform/quality | `REL-DOC15` | Open | Add documentation test/link reports, API diff, example runs, and reviewer. | +| `DOC16` | Site, release notes, changelog, package READMEs, and GitHub Release share one candidate identity and known-limit list | Platform/quality | `REL-DOC16` | Open | Add version/link/claim audit across every published surface. | + +## 14. Growth and community checks + +Stretch outcomes are tracked but do not waive or independently block technical +release gates. Organic conduct, truthful assets, and consent are mandatory. + +| Gate | Classification and requirement | Owner | Task ID | Status | Evidence slot | +|---|---|---|---|---|---| +| `GR01` | **Mandatory conduct:** no paid stars, bots, mutual-star schemes, fake adopters, or undisclosed promotion | Platform/quality | `REL-GR01` | Open | Add channel/partner disclosure audit and release-manager attestation. | +| `GR02` | **Mandatory asset:** release/content beats are prepared for Alpha 1, recovery, verifier, Alpha 2, Explorer, Beta, RC/security, and release | Platform/quality | `REL-GR02` | Open | Add beat manifest, truthful evidence source, asset URL/path, and publication state. | +| `GR03` | **Tracking, non-blocking:** Day 7/13/17/21 star outcomes 300/1,000/2,000/6,000+ are reported as stretch, not guarantee | Platform/quality | `REL-GR03` | Open | Add timestamped public metrics and copy audit separating outcome from gate. | +| `GR04` | **Tracking, non-blocking:** 2,000 CLI downloads, 500 runs, ten adopters, ten contributors, twenty-five external PRs, response p50 under twelve hours, ten invitations | Platform/quality | `REL-GR04` | Open | Add metric definitions, source snapshots, deduplication method, and current values. | +| `GR05` | **Tracking, non-blocking:** weekly successful runs, seven-day retention, time to first success, adopters, and non-maintainer merged PRs have dashboards | Platform/quality | `REL-GR05` | Open | Add privacy-preserving metric queries/snapshots and definitions. | +| `GR06` | **Mandatory decision policy:** visits/stars/installs/runs/retention signals trigger positioning, Quickstart, promotion pause, or reliability work as specified | Integration | `REL-GR06` | Open | Add current funnel diagnosis, selected response, owner, and review date. | +| `GR07` | **Mandatory community:** contributor pathway, governance, code/security reporting guidance, and authentic adopter submission process are published | Platform/quality | `REL-GR07` | Open | Add document URLs/revisions, workflow smoke tests, and reviewer. | + +## 15. Day-21 support readiness + +| Gate | Mandatory support check | Owner | Task ID | Status | Evidence slot | +|---|---|---|---|---|---| +| `SUP01` | Named release manager and TS, Python, platform, security, and community responders cover launch/support window | Integration | `REL-SUP01` | Open | Add roster, UTC coverage, escalation path, and acknowledgements. | +| `SUP02` | GitHub issue/discussion triage, incident intake, security disclosure, and status communication paths work | Platform/quality | `REL-SUP02` | Open | Add end-to-end dry-run tickets, response timestamps, and escalation evidence. | +| `SUP03` | npm and PyPI owners can deprecate/yank only according to policy and publish a corrected version without rewriting history | Integration | `REL-SUP03` | Open | Add owner/role audit and non-destructive rehearsal or documented provider procedure. | +| `SUP04` | Release notes document known limits, supported matrices, fallback modes, at-least-once effects, privacy defaults, and upgrade path | Platform/quality | `REL-SUP04` | Open | Add reviewed release-note revision and claims checklist. | +| `SUP05` | Incident runbook covers security escape, duplicate side effect, corrupt store/artifact, provider outage, package defect, and site outage | Platform/quality | `REL-SUP05` | Open | Add runbook revision, tabletop record, actions, and independent review. | +| `SUP06` | Support diagnostics redact secrets/prompts by default and collect raw content only with explicit consent | Platform/quality | `REL-SUP06` | Open | Add support-bundle tests, consent flow, seeded-secret scan, and docs. | +| `SUP07` | Response-time measurement exists for the controlled p50-under-twelve-hours goal | Platform/quality | `REL-SUP07` | Open | Add metric definition, queue query/snapshot, and on-call ownership. | +| `SUP08` | Release rollback/deprecation, forward-fix, and RC continuation decisions are prewritten and executable | Integration | `REL-SUP08` | Open | Add dry-run decision record, commands/procedures, approval boundaries, and reviewer. | + +## 16. RC fallback and no-go checklist + +| Gate | Mandatory fallback condition/action | Owner | Task ID | Status | Evidence slot | +|---|---|---|---|---|---| +| `RC01` | Automated roll-up treats every Open, Partial, or Blocked mandatory row as stable-v1 no-go | Integration | `REL-RC01` | Open | Add roll-up implementation/test with one failing fixture for each non-Green state. | +| `RC02` | If any stable gate is not Green, stable npm/PyPI tags and stable GitHub Release are not published | Integration | `REL-RC02` | Open | Add signed no-go decision and channel/tag protection evidence. | +| `RC03` | The fallback release is explicitly labeled full RC and never implies stable/production-proven status | Platform/quality | `REL-RC03` | Open | Add RC package tags, version strings, site/docs copy, and claim audit. | +| `RC04` | RC blocker manifest lists every Open/Partial/Blocked gate, impact, owner, task ID, workaround, and re-entry evidence | Integration | `REL-RC04` | Open | Add generated blocker manifest and reconciliation against this checklist. | +| `RC05` | Day 21 still produces all planned assets and at least a complete Beta/RC; a missing asset keeps the plan open | Integration | `REL-RC05` | Open | Add asset manifest and explicit missing-asset report showing none for a complete RC. | +| `RC06` | Last verified prerelease remains available; failed artifacts are invalidated and rebuilt rather than silently replaced | Integration | `REL-RC06` | Open | Add artifact/channel inventory, invalidation/deprecation action, and replacement digests. | +| `RC07` | Post-publish defects use deprecation/forward fix and a new version; package history and attestations are never rewritten | Integration | `REL-RC07` | Open | Add registry policy, rehearsal/tabletop, and reviewer. | +| `RC08` | Recovery fallback never claims irreversible external side effects were rolled back; it uses idempotency, approval, or compensation | Integration | `REL-RC08` | Open | Add incident/recovery scenarios, event traces, and docs review. | +| `RC09` | A reopened contract, durability, isolation, security, compatibility, or provenance gate reopens all dependent rows/artifacts | Integration | `REL-RC09` | Open | Add dependency-aware invalidation test and sample reopened-gate report. | +| `RC10` | RC receives the same support, disclosure, security response, and evidence collection until stable re-entry | Integration | `REL-RC10` | Open | Add continuing support roster, blocker review cadence, and next decision date. | + +## 17. Post-audit control-to-release overlay + +This overlay prevents the 16 newly explicit tasks from becoming orphaned work. +It is not the final 178-row machine map: `CTRL-RELEASE-MAP-074` must still emit +and validate `release-task-map.json` before any candidate roll-up. + +| Control | Release rows it must produce or review | Current state | +|---|---|---| +| `CTRL-RELEASE-MAP-074` | All 178 unique `REL-*` IDs, including explicit blocking/non-blocking classification | Open; no machine map yet. | +| `CTRL-EVIDENCE-BACKFILL-075` | Every blocking row whose producer was completed before evidence-policy cutoff | Open; historical status has zero candidate weight. | +| `D17-USABILITY-076` | `REL-UX01`-`REL-UX10`, `REL-Q10`, `REL-Q11`, adopter/case-study consent rows | Open/External; real reports required. | +| `D9-APPROVAL-077` | `REL-I05`, `REL-I08`, `REL-T22`, `REL-RC08`, stale approval and idempotency rows | Open. | +| `D14-NPM-DIST-078` | `REL-PKG02`, `REL-PKG05`, `REL-PKG13`, canonical install/bin rows | Open/External for namespace rehearsal. | +| `D16-PRIVACY-079` | `REL-I06`, `REL-UX09`, `REL-DOC12`, telemetry/capture/retention/withdrawal rows | Open/External for human data-owner approval. | +| `D18-SUPPORT-READINESS-080` | `REL-SUP01`-`REL-SUP08`, `REL-RC07`, rollback/yank/tabletop rows | Open/External for roster and registry authority. | +| `D13-TS-ADAPTERS-081`, `D13-PY-ADAPTERS-082`, join `D13-ADAPTERS-049` | `REL-X10`, `REL-T25`, `REL-T27`, `REL-Q03`, `REL-Q06`, adapter package/security rows | Open; mock evidence cannot impersonate live evidence. | +| `D18-EDUCATION-ASSETS-083` | Course, case-study, demo, bilingual and executable-doc rows including `REL-DOC01`-`REL-DOC16` where applicable | Open; existing plans are not executable assets. | +| `D8-RUNTIME-CHAOS-084` | `REL-T11`, `REL-T18`, `REL-T27`, `REL-Q04`, deadlock/leak/retry rows | Open. | +| `D9-OPS-CONTROL-085` | `REL-T29`, operational CLI JSON/exit/race rows and relevant support diagnostics | Open. | +| `CTRL-RELEASE-ROLLUP-086` | `REL-V1-01`-`REL-V1-08`, `REL-RC01`-`REL-RC10`, final candidate decision | Open; depends on every blocking evidence producer. | +| `D9-TS-REDACTION-087`, `D9-PY-REDACTION-088`, join `D9-REDACTION-CONFORMANCE-089` | `REL-V1-02`, `REL-I06`, `REL-T26`, `REL-Q08`, `REL-SC07`, `REL-SC12`, `REL-SUP06` | Open/Critical; raw payload plus `redacted: true` remains a release blocker. | + +`REL-T26` accepts evidence only when the wire flag is truthful, protection occurs +before every sink write, legacy misleading histories follow the frozen migration +rule, and negative canary fixtures prove that the scanner detects a seeded leak. +Registering these tasks or writing their design documents does not change any +row from Open. + +## 18. Final sign-off record + +This block stays empty until `REL-V1-08` is decided. A signature without Green +leaf evidence does not authorize stable release. + +```text +Decision: [ ] stable v1 [ ] full RC [ ] no release +Candidate version: +Candidate revision: +Decision rationale: +Open/Partial/Blocked mandatory gates: +Artifact manifest and digests: +Release manager: +Independent reviewer: +Security reviewer: +Decision timestamp (UTC): +Next review date if RC/no release: +``` diff --git a/codex_plans/delivery/task-dependency-graph.md b/codex_plans/delivery/task-dependency-graph.md new file mode 100644 index 0000000..66d90c7 --- /dev/null +++ b/codex_plans/delivery/task-dependency-graph.md @@ -0,0 +1,459 @@ +# Graph Engineering 21-Day Task Dependency Graph + +- Authority: [Graph Engineering 21-Day Master Plan](../Graph-Engineering-21-Day-Master-Plan.md) +- Evidence sources: [task registry](../../codex_logs/task-registry.json), + append-only run logs under `codex_logs/`, and CI/release artifacts +- Scope: Day 1 through Day 21, including product, quality, security, release, + and organic-launch work + +## 1. How to read this graph + +This document turns the calendar into dependency and gate relationships. It is +not a progress report. A node ID such as `D09` identifies planned scope; it does +not mean that the node is complete. Completion requires evidence in the task +registry, logs, tests, and release artifacts. No checkbox or position in this +document is completion evidence. + +The master plan's live checkpoint remains authoritative. In particular, the +checkpoint describes Days 1-4 as partial, Day 5 primitives as partial and the +pipeline slice as in progress, Day 6 as partial, and the Day 9 delivery as +limited to immutable local DAG recovery. The existing Alpha 1 tag does not +waive unfinished Day 7 scope or any later gate. + +Dependency terms: + +- **Hard dependency**: the predecessor's exit evidence is required before the + dependent node can pass its own exit gate. +- **Scaffold dependency**: fixtures, interfaces, documentation, or UI shells + may be prepared early, but cannot be represented as integrated or released. +- **Join gate**: every listed input must be green; one successful lane cannot + mask a divergent or missing lane. +- **Freeze**: changes after the freeze require an explicit reviewed exception, + migration/compatibility analysis, and rerunning all affected downstream + gates. +- **Fallback**: a fail-closed containment path, never an alternate way to call + the original gate successful. + +## 2. Global execution rules + +1. `spec/` is the cross-language protocol authority. Shared schemas and + conformance fixtures are integration-owned and must be reviewed before the + TypeScript and Python implementations merge. +2. TypeScript and Python work may run in parallel after their input contract is + frozen. They join at the same fixture and envelope gates; neither runtime is + a reference implementation that can silently override the other. +3. Platform work may scaffold ahead of a runtime gate, but public CLI, MCP, + Explorer, examples, and claims must use released behavior rather than mocks + unless they are explicitly labeled as mock-only. +4. There are at most four active lanes: integration, TypeScript, Python, and + platform/quality/growth. Each lane owns one active package, and shared-file + writes are serialized through integration. +5. Integrate twice daily. An implementer cannot be the sole reviewer of their + own work. Every merge needs focused tests and the relevant shared + conformance run. +6. A failed hard gate blocks dependent exits. Independent scaffolding may + continue, but it cannot be marked integrated, release-ready, or complete. +7. Failures remain structured values or events. Null substitution, unbounded + retry, implicit cycles, unbounded dynamic fan-out, and authority expansion + are never valid fallbacks. +8. External effects remain at-least-once. Recovery uses idempotency keys, + approval, or explicit compensation; it must not claim that an irreversible + external effect was rolled back. + +## 3. Dependency spine and parallel waves + +The calendar has a sequential release spine with join dependencies. The +current capability-critical segment called out by the master plan is pipeline +semantics, bounded convergence and hard budgets, model/cost routing, +verification, isolation, and provider adapters. Durable, cancellation, and +failure-envelope gates join that segment before API freeze. + +```mermaid +flowchart LR + D01[D01 contracts] --> D02[D02 canonical IR] + D02 --> D03[D03 compiler] + D03 --> D04[D04 scheduler] + D04 --> D05[D05 pipeline and barrier] + D05 --> D06[D06 router and failure states] + D06 --> D07[D07 bounded cycles and Alpha 1] + D07 --> D08[D08 retry cancellation chaos] + D08 --> D09[D09 durable recovery] + D07 --> D10[D10 budgets and model routing] + D09 --> D10 + D06 --> D11[D11 verifier semantics] + D09 --> D11 + D10 --> D11 + D08 --> D12[D12 isolation] + D11 --> D12 + D10 --> D13[D13 providers and Alpha 2] + D12 --> D13 + D09 --> D14[D14 public API freeze] + D11 --> D14 + D12 --> D14 + D13 --> D14 + D09 --> D15[D15 production stores and Explorer] + D13 --> D15 + D14 --> D15 + D12 --> D16[D16 security preflight] + D13 --> D16 + D15 --> D16 + D14 --> D17[D17 Beta] + D15 --> D17 + D16 --> D17 + D14 --> D18[D18 compatibility audit] + D17 --> D18 + D16 --> D19[D19 RC freeze] + D18 --> D19 + D16 --> D20[D20 provenance and go/no-go] + D17 --> D20 + D19 --> D20 + D20 --> D21[D21 stable release or full RC] +``` + +| Wave | Days | Parallel work | Required join before advancing | +|---|---:|---|---| +| W0: authority and ownership | 1 | Plans/contracts, both workspaces, CI/governance/registry audit | Contracts, owners, risks, and external-access blockers are explicit | +| W1: contract to compiler | 2-3 | TS builders/compiler, Python models/compiler, schema fixtures, CLI shell, Quickstart | Byte-equivalent IR and shared invalid-graph verdicts | +| W2: graph execution | 4-7 | Both schedulers/primitives, viewer/tests/demos, integration reviews | Deterministic diamond, real streaming, complete terminal states, bounded cycles, honest Alpha 1 | +| W3: control and judgment | 8-11 | Retry/cancel, persistence, budgets/providers, verifier UX and failure injection | Bounded attempts, durable recovery, budget stops, rejected/unknown gating, runtime parity | +| W4: containment and extensibility | 12-14 | Isolation, provider adapters, doctor/score, MCP/plugins, ten pattern skeletons | Escape tests, adapter contract, read-only MCP default, reviewed public API/IR freeze | +| W5: scale and hardening | 15-17 | Production stores/workers, Explorer/site, security/fuzz/SBOM, docs/testers | Performance/usability target, no unaccepted high/critical issue, tester-backed Beta | +| W6: audit and release | 18-21 | Compatibility fixes, package rehearsals, docs/assets/community support | Zero conformance drift, RC freeze, every mandatory gate and provenance artifact green | + +The longest release path is the merge spine from `D01` to `D21`. Work outside +that spine shortens elapsed time only when it does not consume a not-yet-frozen +contract. A late failure at `D09`, `D12`, `D16`, `D18`, or `D20` reopens every +downstream artifact that relied on the failed property. + +## 4. Day-by-day delivery nodes + +### W0-W1: authority, contracts, and compiler + +| Node | Hard entry gate | Required deliverables by lane | Exit gate | Failure containment and re-entry | +|---|---|---|---|---| +| `D01` | Repository identity and available owners; no prior day | **Integration:** materialize plans, restore remote, freeze initial IR/events/ADRs. **TS:** workspace/bootstrap. **Python:** workspace/bootstrap. **Platform:** CI, governance, registry-name audit. | Contracts, ownership, risks, and unavailable publishing authority are frozen and recorded. | Keep work local and unpublished; serialize shared-contract edits; record missing remote/registry authority as blocked. Re-enter only after ownership and risk decisions are reviewable. | +| `D02` | `D01`; draft protocol namespace and serialization rules available | **Integration:** review canonical serialization. **TS:** builders and schema types. **Python:** builders and Pydantic models. **Platform:** JSON Schema, negative fixtures, CLI contract. | Both languages produce byte-equivalent canonical IR and stable hashes for the shared corpus. | Freeze compiler integration; reduce to the smallest divergent fixture; change the canonical contract only through reviewed integration ownership; rerun both language suites. | +| `D03` | `D02` byte-equivalence and IR freeze | **Integration:** freeze diagnostics. **TS/Python:** compiler and DAG validation. **Platform:** `init`, `validate`, `plan`, and Quickstart v0. | Shared invalid-graph fixtures produce aligned verdicts, stable error codes, and JSON envelopes. | Do not start scheduler integration on ambiguous graphs; quarantine divergent diagnostics, add a shared negative fixture, and re-enter after both compilers reject/accept identically. | + +### W2: deterministic execution and Alpha 1 + +| Node | Hard entry gate | Required deliverables by lane | Exit gate | Failure containment and re-entry | +|---|---|---|---|---| +| `D04` | `D03` compiler/diagnostic gate | **Integration:** integrate chain and diamond. **TS/Python:** deterministic ready-queue scheduler and bounded fan-out/fan-in. **Platform:** trace viewer and concurrency tests. | Deterministic diamond parity: outputs, event constraints, hashes, and terminal envelopes agree. | Keep only deterministic mock execution enabled; do not publish concurrency/performance claims; fix ordering through shared fixtures before re-entry. | +| `D05` | `D04` scheduler parity and bounded concurrency | **Integration:** review scheduling semantics. **TS/Python:** pipeline, barrier, and backpressure. **Platform:** research demo and benchmarks. | Fast items flow without an accidental whole-stage barrier; buffers, demand, cancellation, retry/drop/stop/dead-letter, and barrier waits remain bounded and observable. | Mark the primitive experimental, stop on buffer/item/attempt limits, and withhold demo/benchmark claims. Re-enter with adversarial slow-consumer, cancellation, timeout, and parity evidence. | +| `D06` | `D05` scheduling contract; structured settled-result base | **Integration:** freeze state and failure envelopes. **TS/Python:** router, failures, quorum. **Platform:** diff-review flow and failure injection. | Every terminal state is defined; router/barrier/quorum semantics and failure envelopes match across languages. | Reject unknown envelope/state variants at the boundary, never coerce to null, and block cycles/verifiers that consume ambiguous outcomes. Add fixtures and refreeze deliberately. | +| `D07` | `D06`; pipeline and failure boundaries are bounded | **Integration:** Alpha 1 integration. **TS/Python:** bounded cycles with seen-set and hard iteration/duration/cost/node/attempt limits. **Platform:** discovery demo and build-in-public content. | Honest `0.1.0-alpha.1` artifact plus bounded-cycle exit reasons and release evidence; a tag alone does not satisfy missing scope. | Retain the last verified prerelease, label excluded capability explicitly, and do not broaden release claims. Re-enter after cycle-budget and package-install evidence passes. | + +### W3: bounded control, recovery, cost, and verification + +| Node | Hard entry gate | Required deliverables by lane | Exit gate | Failure containment and re-entry | +|---|---|---|---|---| +| `D08` | `D06` failure state machine and `D07` hard bounds | **Integration:** join independent runtime chaos (`084`) and durable operational controls (`085`). **TS/Python:** retry, timeout, cancellation, propagated abort and exact accounting. **Platform:** commands land only after extended durability. | No unbounded retry/deadlock/leak; cancellation terminates scheduling; operational commands reject stale/racing actions with stable envelopes. | Fail closed on exhausted/unsafe bounds, disable automatic retry for ambiguous effects, and keep operations diagnostic-only until D9 recovery exists. | +| `D09` | `D06` durable envelope; canonical `D9-REDACTION-039` and `D9-APPROVAL-077` contracts may advance without the later CLI/chaos join | **Integration:** redaction, approval, durable-extension contracts. **TS/Python:** independent redaction (`087/088`) then leases, stores, resume/replay/fork. **Platform/security:** conformance `089`, canary sinks and crash/race tests. | Persisted flags describe actual bytes; successful internal results never rerun; approvals are authority-bound; dual resume cannot advance twice; replay/fork lineage is traceable. | Disable unsupported recovery, never label raw bytes redacted, preserve history and stop before ambiguous non-idempotent replay. | +| `D10` | Budget contract needs `D06` accounting and `D07` cycle bounds; native crash-safe ledgers additionally need extended `D09` conformance | **Integration:** freeze units/reservations early. **TS/Python:** cost, budget and model router after durable accounting. **Platform:** cost UI and pricing snapshots. | Hard duration/cost/token/node/attempt budgets stop new scheduling and survive resume. | Use deterministic mock pricing/routing, reject unsafe unknown cost and withhold claims until snapshot/recovery tests pass. | +| `D11` | Verification contract needs `D06` quorum and approval `077`; native execution additionally consumes `D10` budget conformance | **Integration:** verifier-semantics review. **TS/Python:** adversarial refutation, diverse lenses, citations, judges, reflection and abstention/unknown. **Platform:** cited report and verifier tests. | Rejected and unknown results are gated; insufficient quorum never becomes implicit pass; votes, evidence, rubric and tie-break version are retained. | Return unknown or require a human gate, preserve evidence and never substitute maker context for isolated verification. | + +### W4: isolation, adapters, and public API freeze + +| Node | Hard entry gate | Required deliverables by lane | Exit gate | Failure containment and re-entry | +|---|---|---|---|---| +| `D12` | Threat contract may start after cycle, redaction and approval contracts; implementation/red-team later joins `D11` human-gate conformance | **Integration:** isolation/threat review. **TS/Python:** worktree/process/container isolation, leases, path policy, namespaces, cleanup and merge node. **Platform:** migration demo and escape tests. | Parallel writes, ports, temp files, caches and database namespaces stay isolated; conflicts are structured failures. | Deny shell/write/network/secret capabilities, serialize work, preserve conflicted worktrees and disable merge automation until escape/conflict tests pass. | +| `D13` | `D10` model contract and `D12` capability/isolation policy | **Integration:** Alpha 2 integration. **TS/Python:** deterministic mock, OpenAI, Anthropic, Gemini, OpenAI-compatible/local, HTTP, shell/subprocess, and MCP adapter contracts. **Platform:** doctor, Graph Ready score, badge, visualize. | Honest `0.2.0-alpha.2`; adapters share discovery, structured output, streaming, tools, usage, retry, rate-limit, and cancellation behavior. | Fall back to the deterministic mock and last verified alpha, disable a nonconforming adapter, and make doctor report the blocker. Real-provider flakiness cannot weaken normal CI. | +| `D14` | Full `D09`, `D11`, `D12`, and `D13` gates | **Integration:** public API freeze. **TS:** MCP/runtime extension interfaces. **Python:** plugin extension interfaces. **Platform:** read-only-default MCP and all ten pattern skeletons. | Reviewed alpha API and IR freeze, documented extension contracts, and skeleton coverage for every named pattern. | Delay the freeze, mark unstable surfaces experimental, keep MCP read-only, and reject mutations without policy/approval. Any post-freeze change requires compatibility review and downstream reruns. | + +### W5: production scale, security, and Beta + +| Node | Hard entry gate | Required deliverables by lane | Exit gate | Failure containment and re-entry | +|---|---|---|---|---| +| `D15` | `D09` storage semantics, `D13` adapters, and `D14` API freeze | **Integration:** performance review. **TS/Python:** PostgreSQL, S3-compatible artifacts, worker mode. **Platform:** Explorer, site, and video. | A clean first run is under five minutes, production stores pass conformance, and worker/resource bounds meet approved baselines. | Retain SQLite/local artifacts and single-worker mode, label production adapters unavailable, and block performance claims after an unapproved regression over 10%. | +| `D16` | `D12` isolation, `D13` providers, complete `D15` storage/Explorer surface and independent redaction join `089` | **Integration:** security preflight. **TS/Python:** independently reverify redaction/policy. **Platform:** privacy, canary, threat, fuzz, license and SBOM evidence. | No unaccepted high/critical blocker; secret, dependency, license and static scans pass with telemetry/capture off by default. | Disable affected surfaces, deny capability, rotate exposed credentials outside the repository and stay prerelease until full reviewed rescan. | +| `D17` | `D14` frozen API, `D15` performance and `D16` security | **Integration:** immutable Beta artifact and bug burn-down. **External/PQG:** separate `D17-USABILITY-076` consumes Beta plus privacy policy. | Beta artifact is immutable with zero repository-owned P0/P1; external usability evidence remains an independent roll-up dependency. | Keep the last alpha/beta and continue tester rounds; missing external evidence cannot be replaced with maintainer self-testing. | + +### W6: compatibility, RC, provenance, and release + +| Node | Hard entry gate | Required deliverables by lane | Exit gate | Failure containment and re-entry | +|---|---|---|---|---| +| `D18` | `D14` frozen contracts and `D17` complete Beta surface | **Integration:** compatibility audit. **TS/Python:** parity and compatibility fixes. **Platform:** reproducible benchmarks, cases, and launch copy. | No cross-language conformance divergence across canonical data, compilation, runtime, persistence, CLI envelopes, adapters, stores, and all patterns. | Block RC, freeze incompatible feature work, reduce every mismatch to a shared fixture, and rerun all affected matrices before re-entry. | +| `D19` | `D16` security gate and `D18` compatibility gate | **Integration:** release-candidate freeze. **TS/Python:** clean install and upgrade. **Platform:** release matrix and documentation tests. | `1.0.0-rc.1` is reproducibly installable/upgradable and complete feature freeze is enforced. | Retain Beta, allow only reviewed release-blocker fixes, invalidate affected RC artifacts, and restart compatibility/install/doc checks. | +| `D20` | `D16`, `D17`, and `D19`; all evidence collectors available | **Integration:** provenance and go/no-go. **TS:** npm rehearsal. **Python:** PyPI rehearsal. **Platform:** site, assets, and community readiness. | Every mandatory gate in Sections 7-9 is green; the Section 10 asset manifest is release-ready; trusted-publishing, SBOM, checksums, attestations, and install rehearsals are verified. | Do not publish stable packages. Rebuild from a clean trusted environment or ship a fully labeled RC with an explicit blocker list. Never infer external publishing authority. | +| `D21` | `D20` go decision | **Integration:** release/support. **TS:** npm release/support. **Python:** PyPI release/support. **Platform:** GitHub, site, content, and community launch/support. | All planned assets and at least a complete Beta/RC exist. Stable v1 ships only when recovery, security, conformance, provenance, and external-usability gates pass; otherwise the release remains a complete RC. | Publish/support only the label justified by evidence, deprecate a broken package version rather than rewriting history, disclose blockers, and continue RC support until re-entry criteria pass. | + +## 5. Cross-cutting product closure + +Calendar exits must also close the following surfaces. These rows prevent a +day-level demo from being mistaken for the complete product contract. + +| Closure node | Builds on | Required closure evidence | Release join | +|---|---|---|---| +| `S01` Graph IR and compiler | `D01-D03` | TS/Python/YAML/JSON compile to one versioned IR; stable hashes; identity, reachability, endpoint, port/schema, router, reducer, budget, capability, and loop validation | `D14`, `D18` | +| `S02` execution primitives | `D04-D08` | DAG, dry-run dynamic checked patches, bounded fan-out/fan-in, deterministic reduce plus semantic synthesis, pipeline, all/minimum/percentage/quorum/deadline barrier, single/multicast router, subgraph, human gate, bounded cycles, structured failures, cancellation | `D14`, `D18` | +| `S03` durable execution | `D06`, `D08-D09`, `D9-REDACTION-039` | Event/Checkpoint/Artifact/Lock interfaces; append-only truth, truthful redaction metadata, snapshot acceleration, lease/CAS, resume, replay, fork, approvals, idempotency guidance | `D15`, `D16`, `D18`, `D20` | +| `S04` providers and tools | `D10`, `D12-D13` | All official adapters meet the shared adapter contract; mock is normal-CI default; real-provider tests are opt-in/nightly | `D18`, `D20` | +| `S05` policy and isolation | `D06`, `D08`, `D9-REDACTION-039`, `D12`, `D16` | Deny-by-default tool/filesystem/network/secret policies, sink-before-write redaction, no authority expansion, worktree/process/container namespace isolation, safe merge | `D16`, `D20` | +| `S06` observability and Explorer | `D04`, `D08-D11`, `D9-REDACTION-039`, `D15` | JSONL and OpenTelemetry model, live topology/state/budget/critical path/retry/verdict views, replay/fork time travel, truthful redaction and opt-in capture | `D16-D20` | +| `S07` CLI and MCP | `D03`, `D08-D09`, `D13-D14` | `init`/`add`; `validate`/`compile`/`plan`/`run`; `status`/`watch`/`inspect`/`logs`; `pause`/`resume`/`cancel`/`retry`; `replay`/`fork`; `cost`; `doctor`/`score`/`badge`/`visualize`; worktree, artifact, plugin, and MCP commands; JSON output, error envelopes, exit codes; MCP read-only default | `D17-D20` | +| `S08` production storage/worker mode | `D09`, `D14-D15` | SQLite/local defaults plus PostgreSQL/S3 and workers under shared storage, race, resource, and chaos conformance | `D18-D20` | +| `S09` education and product parity | `D03-D15` | Fourteen-step executable course, pattern picker, anti-pattern/failure guides, operations/safety docs, authentic success/failure stories, showcase and galleries | `D17-D21` | +| `S10` governance and distribution | `D01`, `D16-D21` | MIT/third-party notices, contributor path, CI matrices, vulnerability response, trusted publishing, provenance, support plan, organic-only growth; legitimate `graph-engineering` npm/PyPI distributions and `graph`/`grapheng` executables without empty squatting packages | `D20-D21` | +| `S11` execution control | `D01-D21` | Planned architecture/delivery/growth documents, append-only decision/daily/incident evidence, 30-minute scanner with fixed-root locking and atomic snapshots, bounded evidence-based nudges, and no fabricated agent control | Every integration window and `D20` | + +The executable course must cover real data edges, fake-edge audits, contracts, +diamond topology, pipelines versus barriers, dynamic routing, adversarial +verification, isolation, convergence, model tiering, persistence, cost/latency +topology, safe self-routing, and when not to use a graph. + +`doctor` must check schemas, stores, providers, credentials, version drift, +orphan leases/worktrees, and security defaults, then return the top three +corrective actions. Graph Ready scoring is deterministic and follows the same +top-three-remediation rule. + +## 6. Ten executable pattern dependencies + +`PB` is a hard bundle gate applied independently to every pattern. It requires: + +- YAML/JSON, TypeScript, and Python implementations; +- deterministic fixtures, expected events, and mock execution; +- real-provider setup that is opt-in and never required by normal CI; +- an architecture diagram, explicit budgets, and explicit permissions; +- structured failure and durable resume demonstrations; +- unit/integration tests plus the shared pattern end-to-end suite; and +- Claude Code, Codex, MCP, and shell usage guides without unsupported private + API claims. + +Day 14 requires all ten skeletons. A skeleton is not `PB` completion. Pattern +end-to-end evidence joins at `D18`, release-asset closure at `D20`, and public +release at `D21`. + +| Pattern node | Earliest demo/skeleton | Hard capability dependencies | Pattern-specific exit and fallback | +|---|---:|---|---| +| `P01` Multi-source research diamond | Demo `D05`; skeleton `D14` | `D04` diamond/fan-in, `D05` streaming, `D09` resume, `D10` budget, `PB` | Deterministic reduction and bounded source fan-out; a failed source is a settled result, never null. | +| `P02` Cited deep research with citation verification | Cited demo `D11`; skeleton `D14` | `P01`, `D11` isolated verifier/evidence, `D13` adapters, `PB` | Unsupported/failed citations reject or become unknown/human-gated; never publish an implicit pass. | +| `P03` Route authentication security sweep | Skeleton `D14` | `D06` exhaustive router, `D08` cancellation, `D12` capability/isolation, `D16` security, `PB` | Missing route/default or denied capability fails closed; no unauthorized scan or write. | +| `P04` Diff risk router with diverse judge panel | Diff demo `D06`; skeleton `D14` | `D06` route/quorum, `D11` diverse panel/abstention, `D12` isolation, `PB` | All votes and evidence retained; insufficient quorum is unknown/human gate. | +| `P05` Loop-until-dry bug discovery | Discovery demo `D07`; skeleton `D14` | `D07` seen-set convergence and hard stops, `D10` cost budget, `D11` verifier, `PB` | Exit reason is explicit; limit exhaustion is not presented as convergence. | +| `P06` File-by-file migration with worktrees and test gates | Migration demo `D12`; skeleton `D14` | `D09` resume, `D12` leases/path policy/merge node, `D16` escape tests, `PB` | Conflict or test failure is structured; preserve isolated work and do not merge. | +| `P07` CI failure sweeper | Skeleton `D14` | `D08` retry/cancel, `D09` recovery, `D12` process isolation, `D13` shell/agent adapters, `PB` | CI mutations require idempotency/approval; exhausted attempts stop and retain diagnostics. | +| `P08` Dependency update sweeper | Skeleton `D14` | `D06` routing, `D09` recovery, `D12` isolation, `D13` HTTP/shell adapters, `D16` dependency/license scans, `PB` | Updates stay isolated until tests and policy pass; unsafe or conflicting updates remain unmerged. | +| `P09` PR babysitter | Skeleton `D14` | `D06` routing/quorum, `D09` durable waiting, `D11` gates, `D13-D14` adapters/MCP, `PB` | External writes are idempotent or explicitly approved; stale approvals are rejected. | +| `P10` Scheduled ecosystem scan | Skeleton `D14` | `D07-D10` fan-out/cycle/budget/recovery, `D13` providers, `D15` worker/storage, `PB` | Schedule, fan-out, cost, and duration are bounded; partial outages yield structured settled results and resume safely. | + +## 7. Mandatory semantic and conformance gates + +### 7.1 Non-negotiable invariants + +| Gate | Required condition | First enforcement | Final evidence | +|---|---|---:|---:| +| `I01` | Canonical `spec/` contract governs both runtimes and shared fixtures | `D01` | `D18-D20` | +| `I02` | Failures are structured values/events and are never silently replaced by null | `D03` | `D18-D20` | +| `I03` | Deterministic transforms do plumbing; model nodes perform judgment | `D02-D03` | `D11`, `D18` | +| `I04` | No implicit cycle, unbounded retry, or unbounded dynamic fan-out | `D03` | `D08`, `D18-D20` | +| `I05` | External effects are at-least-once and require idempotency or approval | `D06` | `D09`, `D16`, `D20` | +| `I06` | Telemetry and prompt/response capture are off by default and redacted when enabled | `D01` | `D16`, `D20` | +| `I07` | Dynamic patches pass the same compiler, policy, permission, and budget gates with capped depth/fan-out/nodes/attempts | `D07-D14` | `D18-D20` | +| `I08` | Planners cannot expand authority; shell, writes, network, secrets, and MCP mutation are deny-by-default | `D12-D14` | `D16`, `D20` | +| `I09` | Insufficient verification quorum is unknown or human-gated, never implicit pass | `D11` | `D18-D20` | +| `I10` | Stable v1 claims are limited to evidence; no unsupported “battle-tested” or “production proven” language | `D17` | `D20-D21` | + +### 7.2 Cross-language equality contract + +TypeScript and Python must agree on each row using shared fixtures and stable +machine-readable evidence. A mismatch in any row blocks `D18`. + +| Gate | Equality requirement | Primary join | +|---|---|---:| +| `X01` | Canonical bytes and hashes | `D02` | +| `X02` | Compilation verdicts and diagnostics/error codes | `D03` | +| `X03` | Route selection and replayed decisions | `D06`, `D09` | +| `X04` | Barrier and quorum settlement | `D05-D06` | +| `X05` | Event-ordering constraints | `D04`, `D09` | +| `X06` | Terminal states and structured failures | `D06`, `D08` | +| `X07` | Retry, timeout, cancellation, and attempt accounting | `D08` | +| `X08` | Resume, replay, and fork results | `D09` | +| `X09` | Stable JSON envelopes and CLI exit behavior | `D03`, `D14`, `D18` | +| `X10` | Adapter and storage conformance | `D13`, `D15`, `D18` | + +## 8. Mandatory test scenario ledger + +Every scenario below needs deterministic tests, structured expected results, +and the owning platform matrix where applicable. “Covered by nearby behavior” +is not sufficient evidence. + +| Gate | Mandatory scenario | Dependency / latest blocking join | +|---|---|---| +| `T01` | Missing node references | Compiler; `D03` | +| `T02` | Duplicate node identities | Compiler; `D03` | +| `T03` | Unreachable nodes | Compiler; `D03` | +| `T04` | Invalid ports | Compiler/schema; `D03` | +| `T05` | Invalid input/output/edge schemas | Compiler/runtime; `D03`, `D06` | +| `T06` | Implicit graph cycles | Compiler; `D03` | +| `T07` | Incomplete router without exhaustive cases/default | Router; `D06` | +| `T08` | Unbounded loops | Cycle compiler/runtime; `D07` | +| `T09` | Unauthorized transforms or capability expansion | Policy; `D12`, `D16` | +| `T10` | 100-way parallel concurrency under configured bounds | Scheduler; `D04`, `D15` | +| `T11` | Every failure policy, including retry/drop/stop/dead-letter/fail-fast/partial/quorum | Runtime; `D05-D08` | +| `T12` | Streaming, bounded buffering, demand, and downstream backpressure | Pipeline; `D05` | +| `T13` | Barrier timeout/deadline with success/failure/missing statistics | Barrier; `D05-D06` | +| `T14` | Router replay without re-judging | Router/persistence; `D06`, `D09` | +| `T15` | Malicious dynamic patches and dry-run rejection | Compiler/policy; `D07`, `D12`, `D16` | +| `T16` | Verifier pass, reject, and abstain/unknown | Verification; `D11` | +| `T17` | Global seen-set convergence | Cycles; `D07` | +| `T18` | Hard iteration, duration, cost, node, fan-out, and attempt stops | Cycles/budgets; `D07-D10` | +| `T19` | Crash recovery at every checkpoint/crash window | Persistence; `D09` | +| `T20` | Dual-resume lease/CAS races | Persistence; `D09`, `D15` | +| `T21` | Replay and fork lineage/results | Persistence; `D09` | +| `T22` | Stale approvals | Human gate/persistence; `D09`, `D11` | +| `T23` | Worktree lease and merge conflicts | Isolation; `D12` | +| `T24` | Process/container port, temp, cache, and database namespace isolation | Isolation; `D12`, `D16` | +| `T25` | Provider fallback and circuit breaking | Adapters; `D13` | +| `T26` | Secret redaction in errors, events, traces, prompts, and tools | Corrective `D9-REDACTION-039`; independent security revalidation in `D16` | +| `T27` | Cancellation before start and while running across runtime/provider/tool boundaries | Runtime/adapters; `D08`, `D13` | +| `T28` | Prompt-injection attempts cannot expand capability or bypass policy | Security; `D12`, `D16` | +| `T29` | Complete CLI init/add, validate/compile/plan/run, status/watch/inspect/logs, pause/resume/cancel/retry, replay/fork, cost/doctor/score/badge/visualize, worktree/artifact/plugin, and MCP flow | CLI; `D14`, `D17-D19` | +| `T30` | Shared Event/Checkpoint/Artifact/Lock and SQLite/PostgreSQL/S3 storage conformance | Persistence/storage; `D09`, `D15`, `D18` | +| `T31` | All ten pattern end-to-end suites in YAML/JSON, TS, and Python | Patterns; `D18-D20` | +| `T32` | 1,000-node graph resource bounds | Compiler/scheduler/worker; `D15`, `D18` | +| `T33` | Kill, network, store, and artifact chaos with no deadlock/budget escape | Runtime/persistence; `D09`, `D15-D18` | + +## 9. Quantitative release gates + +These are mandatory thresholds, not aspirational dashboards. A waiver is valid +only where the master plan explicitly allows one: a performance regression over +10% needs an approved baseline ADR. Other missed thresholds select RC rather +than stable v1. + +| Gate | Required threshold | Evidence due | +|---|---|---:| +| `Q01` | Compiler, scheduler, event store, and policy each have at least 90% statement and 85% branch coverage | `D18-D20` | +| `Q02` | At least 250 unit/integration cases per language | `D18-D20` | +| `Q03` | One shared adapter/storage conformance suite | `D15`, final `D18-D20` | +| `Q04` | 100 randomized failure runs with no deadlock, unbounded spawn, or budget escape | `D16-D20` | +| `Q05` | Linux/macOS/Windows; Node 20/22; Python 3.11/3.12/3.13 matrix | `D18-D20` | +| `Q06` | Deterministic mock providers in normal CI; real providers only opt-in/nightly | `D13-D20` | +| `Q07` | No performance regression above 10% without an approved baseline ADR | `D15-D20` | +| `Q08` | No unaccepted high/critical vulnerability; secret, dependency, license, and static-analysis scans pass | `D9-REDACTION-039`, `D16-D20` | +| `Q09` | Trusted npm/PyPI publishing, SBOM, checksums, and attestations | `D20` | +| `Q10` | Quickstart uses no more than three commands; at least 80% of external testers finish within five minutes | `D17-D20` | +| `Q11` | No P0/P1 defects and at least five external usability reports | `D17-D20` | + +Stable v1 is a conjunctive decision: + +```text +V1 = recovery + AND security + AND cross-language conformance + AND package provenance + AND external usability + AND complete planned-asset manifest + AND Q01..Q11 + AND T01..T33 +``` + +Day 21 still requires all planned assets and at least a complete Beta/RC. If the +stable expression is false, the required release outcome is an honestly labeled +RC, not a partially evidenced stable release; any missing asset keeps the plan +open rather than being silently dropped. + +## 10. Launch and education dependency ledger + +The following assets are planned deliverables. They depend on executable, +reproducible product evidence and cannot substitute for it. + +| Asset | Build window | Hard dependency / truth condition | +|---|---:|---| +| Sixty-second Quickstart | `D03-D20` | Mock-first clean install; at most three commands; external five-minute completion evidence | +| Ninety-second uncut terminal demo | `D15-D20` | Clean recorded run with no hidden manual repair | +| Interactive linear-versus-graph visualization | `D04-D15` | Real topology/events and accurate limitations | +| Fourteen-step executable roadmap | `D03-D18` | `S01-S09`, runnable examples, and “when not to use a graph” | +| Architecture essay | `D14-D20` | Frozen IR/API plus accurate durability, failure, and at-least-once claims | +| Side-by-side TS/Python examples | `D02-D18` | `X01-X10` parity evidence | +| Reproducible performance and recovery benchmarks | `D05`, `D09`, `D15-D18` | Versioned environment/data/baseline and no hidden provider dependency | +| Four case studies, including one authentic failure | `D11-D20` | Reproducible traces, explicit limits, no unsupported success claims | +| Graph Ready G0-G4 score and badge | `D13-D20` | Deterministic scoring plus top three remediation actions | +| Pattern picker and anti-pattern/failure-mode guides | `D14-D20` | All pattern skeletons and verified failure behavior | +| Operating and safety documentation | `D12-D20` | Threat, isolation, capability, recovery, and incident evidence | +| Interactive showcase | `D15-D21` | Explorer plus deterministic examples | +| Adopter gallery and trace gallery | `D17-D21` | Consent and real external/adopter evidence; never fabricated | +| English and Chinese launch summaries | `D18-D21` | Same verified claims and version across languages | +| GitHub, Hacker News, X, LinkedIn, Reddit, Dev.to, and Chinese-community material | `D18-D21` | Channel-specific copy, organic promotion only, current install links | + +Content beats depend on the matching evidence: build-in-public through Day 6, +Alpha 1 on Day 7, crash/resume on Day 9, verifier demo on Day 11, Alpha 2 on +Day 13, benchmarks/Explorer on Day 15, tester-backed Beta on Day 17, RC and +security story on Day 19, and coordinated release/support on Day 21. + +Star milestones (300/1,000/2,000/6,000+ on Days 7/13/17/21) are stretch +outcomes, not engineering gates. The controlled leading goals are 2,000 CLI +downloads, 500 successful or self-reported runs, ten public adopters, ten +outside contributors, twenty-five external PRs, response p50 below twelve +hours, and ten personalized trial invitations. They must remain organic. A miss +changes positioning, Quickstart, reliability, or retention work; it never +authorizes paid/fake stars or a false release claim. + +The north-star measures are weekly successful graph runs, seven-day retained +repositories, time to first success, external adopters, and non-maintainer +merged PRs. High visits with low stars triggers positioning work; stars without +installs triggers Quickstart work; installs without successful runs pauses +promotion; runs without retention triggers use-case and reliability work. + +## 11. Failure fallback and reopening rules + +| Failure class | Immediate containment | Re-entry evidence | Downstream impact | +|---|---|---|---| +| Contract or language divergence | Freeze merges; isolate a minimal shared fixture; preserve both outputs | Reviewed canonical decision plus TS/Python conformance | Reopen every consumer since the affected freeze | +| Deadlock, unbounded work, or budget escape | Cancel/halt scheduling; deny new dynamic work; retain trace | Deterministic reproducer plus randomized bounded run | Reopen scheduler, pipeline/cycle, persistence, performance, and security gates | +| Recovery ambiguity or duplicate advance | Stop resume/fork; preserve append-only history; require approval for non-idempotent work | Crash-window and dual-resume evidence | Reopen cost, verification, patterns, storage, Beta, and release | +| Provider or production-store failure | Disable adapter; use labeled mock or SQLite/local fallback | Shared adapter/storage suite and opt-in live evidence where applicable | Feature remains excluded from release claims until restored | +| Isolation or security escape | Deny capability, disable mutation/provider, preserve incident evidence | Threat review, exploit regression, fuzz/scans, independent review | Reopen all packages/assets that exposed the surface | +| Performance regression | Stop merge when over 10% unless a baseline ADR is approved | Reproducible before/after benchmark or approved ADR | Reopen Explorer/demo/case-study claims and release matrix | +| External usability miss | Continue RC/tester iteration; repair install/Quickstart/docs | At least five reports and 80% five-minute completion | Stable v1 remains blocked; technical work may continue | +| Provenance/publishing failure | Do not publish; rebuild in trusted clean environment | Verified rehearsal, identities, SBOM, checksums, attestations | Stable package release remains blocked | +| Post-publish package defect | Stop promotion; deprecate affected version; publish a new fixed version after gates | Repeated install/upgrade/security/conformance evidence | Never rewrite package history or silently replace artifacts | +| Growth milestone miss | Diagnose visits-to-stars, stars-to-installs, installs-to-runs, and runs-to-retention | Honest activation/retention experiment evidence | Does not waive or itself block technical release gates | + +## 12. Post-audit task-level closure graph + +This table supersedes any broader day-level edge when the two differ. It turns +the full-plan audit's 16 omitted lanes into executable, single-primary controls. + +| Control | Hard predecessors | Direct consumers / exit meaning | +|---|---|---| +| `CTRL-RELEASE-MAP-074` | `CTRL-PLAN-COVERAGE-001` | `CTRL-RELEASE-ROLLUP-086`; all 178 release leaves map uniquely with an explicit blocking bit. | +| `CTRL-EVIDENCE-BACKFILL-075` | `CTRL-EVIDENCE-002` | `CTRL-RELEASE-ROLLUP-086`; historical completed status has zero candidate weight without immutable revalidation. | +| `D17-USABILITY-076` | `D17-BETA-063`, `D16-PRIVACY-079` | Acceptance and roll-up; real consenting testers satisfy 5-report/80%-in-300s gates. | +| `D9-APPROVAL-077` | `D6-DURABLE-SPEC-010`, `D9-REDACTION-039` | D9 extended conformance, D11/D12 contracts and approval-dependent patterns. | +| `D14-NPM-DIST-078` | `D14-API-FREEZE-050`, `D3-CLI-002` | Package/release leaves; real unscoped package and both binaries pass clean installs. | +| `D16-PRIVACY-079` | redaction contract and D12 red-team | External usability; default-off collection, retention and withdrawal are accepted. | +| `D18-SUPPORT-READINESS-080` | D16 security and D18 compatibility | RC and final roll-up; support/incident/rollback/yank readiness is proved before publish. | +| `D13-TS-ADAPTERS-081` | `D13-ADAPTER-SPEC-048` | Adapter join `049`; TypeScript native suite passes independently. | +| `D13-PY-ADAPTERS-082` | `D13-ADAPTER-SPEC-048` | Adapter join `049`; Python native suite passes independently. | +| `D18-EDUCATION-ASSETS-083` | patterns, Explorer, Beta and API freeze | RC/docs; executable course, cases, demo and bilingual claim audit pass. | +| `D8-RUNTIME-CHAOS-084` | cycle and pipeline conformance | D8 join, durable operations and CI pattern; bounded seeded faults have no leak/deadlock. | +| `D9-OPS-CONTROL-085` | D9 extended conformance and runtime chaos | D8 join; status/watch/inspect/logs/pause/resume/cancel/retry reject races consistently. | +| `CTRL-RELEASE-ROLLUP-086` | provenance, acceptance, patterns, docs, growth, usability, support, mapping and backfill | `D21-RELEASE-067`; one candidate receives stable, complete-RC or no-release decision. | +| `D9-TS-REDACTION-087` | canonical redaction contract | TS durable extension and redaction join; every TS sink is canary-free. | +| `D9-PY-REDACTION-088` | canonical redaction contract | Python durable extension and redaction join; every Python sink is canary-free. | +| `D9-REDACTION-CONFORMANCE-089` | `087`, `088` | D9 extended conformance, Explorer and D16; wire/migration/identity parity plus independent security acceptance. | + +The live registry at this checkpoint contains 107 unique tasks, no dangling +dependency and no cycle. `D21-RELEASE-067` consumes the signed `086` decision; +neither provenance nor acceptance can authorize itself. Pattern tasks explicitly +depend on the runtime, durability, budget, verifier, isolation, adapter and +security producers their examples claim to demonstrate. + +## 13. Gate evidence protocol + +A day or gate can be recorded as complete only when its evidence includes: + +1. the immutable source revision and relevant graph/spec revision; +2. the exact test/validation command, environment matrix, result, and artifact; +3. shared-fixture IDs for every cross-language assertion; +4. coverage, performance, chaos, security, or usability reports where required; +5. an independent reviewer for public behavior; +6. structured blocker or waiver references, without deleting prior evidence; +7. package/image/SBOM/checksum/attestation identities for release artifacts; and +8. explicit exclusions and fallback mode when the full planned surface is not + present. + +The progress scanner is a liveness and artifact-presence signal. A healthy scan +does not satisfy any semantic, threshold, external-usability, provenance, or +release gate in this graph. diff --git a/codex_plans/growth/content-calendar.md b/codex_plans/growth/content-calendar.md new file mode 100644 index 0000000..8bd8502 --- /dev/null +++ b/codex_plans/growth/content-calendar.md @@ -0,0 +1,376 @@ +# Graph Engineering 21-day evidence-gated content calendar + +Status: **Calendar prepared; no publication is asserted by this document** +Campaign epoch: **Day 1 = 2026-07-26, America/Vancouver** +Campaign owner: `CTRL-GROWTH-072` (`planned`, blocked on `D13-DX-051` and `D15-EXPLORER-060`) +Document owner: `CTRL-DOCS-073` (`in_progress`) +Related plans: [organic launch plan](./launch-plan.md), [metrics and experiments](./metrics-and-experiments.md), [master plan Section 7](../Graph-Engineering-21-Day-Master-Plan.md#7-organic-launch-and-6000-star-target), and [release checklist](../delivery/release-checklist.md) + +## 1. Calendar contract + +This is a dependency-aware editorial queue, not an automatic scheduler and not +a record of already-published posts. A row becomes publishable only when its +evidence gate is accepted against the exact revision named in the asset. If the +gate is late, the content owner must choose one of three honest outcomes: + +1. **hold** the draft without public implication; +2. **narrow** it to a build note about behavior already verified; or +3. **replace** it with a limitations, test-method, or contributor-request post. + +The owner may never fill a calendar slot by upgrading a target to an +implemented claim. `6,000+` Day-21 stars and the intermediate 300/1,000/2,000 +star checkpoints are stretch outcomes only. Do not buy, automate, exchange, +give rewards for, or coordinate stars. No post may ask users to star as a +condition of access, support, recognition, or giveaway. Mapping: +`CTRL-GROWTH-072`; `REL-GR01`, `REL-GR03`, `REL-GR06`. + +## 2. Editorial states and owner codes + +### Publication states + +| State | Meaning | Permitted public wording | +| --- | --- | --- | +| `draft` | Copy or asset is being prepared; evidence may be absent | None; internal only | +| `evidence_wait` | Draft exists but a product, review, consent, authority, or candidate gate is Open | None, or a separately reviewed narrow build note | +| `ready` | Exact asset, evidence, claims, permissions, links, and support capacity are approved | Scheduled wording for the accepted candidate only | +| `published` | Authorized human published it and URL/time/copy digest were captured | Describe actual publication and current candidate | +| `held` | Owner intentionally did not publish due to a gate or capacity signal | “Not published”; no substitute success claim | +| `superseded` | A later correction or candidate invalidated the asset | Link correction; retain prior record rather than rewriting history | + +All calendar rows begin `draft` or `evidence_wait`. This document does not move +any row to `ready` or `published`. Mapping: `CTRL-DOCS-073` versus +`CTRL-GROWTH-072`; `REL-GR02`, `REL-DOC14-DOC16`. + +### Owner codes + +| Code | Responsibility | Cannot self-approve | +| --- | --- | --- | +| `RM` | Release label, candidate identity, coordinated go/no-go | Security/provenance or external evidence | +| `INT` | Canonical semantics, claim-to-spec review, integration | Its own R3 release decision | +| `TSR` / `PYR` | Native implementation proof and executable snippets | Cross-language parity alone | +| `PQG` | Docs, demos, site, Explorer, accessibility, asset manifest | Unsupported runtime/security claims | +| `COMM` | Channel adaptation, moderation, outreach, consent, support queue | Account authority, adopter consent, or technical truth | +| `SRV` | Security/privacy/redaction review and incident escalation | Release-manager decision alone | +| `SUP` | Triage rota, support capacity, incident/status operation | Product-gate waivers | +| `EXT` | Real tester, adopter, contributor, account/registry/hosting authority | Cannot be simulated, generated, or inferred | + +## 3. Content atom and manifest + +Every planned item receives a content ID such as `GE-D09-RECOVERY-EN-X`. Before +it can be `ready`, its manifest must record: + +- content ID, day/beat, canonical English source, translated/adapted variants; +- candidate version, source revision, fixture/trace/release evidence, and asset + digest; +- exact current-versus-target capability table and known-limit paragraph; +- source links and third-party attribution/license status; +- channel, account owner, rule-check date, disclosure, publication window, and + accessibility fields; +- UTM source/medium/campaign/content values with no personal identifier; +- technical, bilingual, security/privacy where applicable, and release-label + approvals; +- publication state, actual URL/time if published, correction link if + superseded, and reason if held; and +- support owner, stop condition, and review time. + +Manifest production maps to `CTRL-GROWTH-072` and `CTRL-DOCS-073`; +acceptance maps to `REL-GR01-GR02`, `REL-DOC13-DOC16`, and the source-specific +rows named in the daily calendar. + +## 4. Standard daily rhythm + +This cadence is a capacity ceiling, not a quota. One strong canonical item may +be adapted only where it is genuinely native to the channel. + +| Local time | Action | Owner | Required map | +| --- | --- | --- | --- | +| `08:30` | Inspect registry/dependency state, release checklist, overnight issues, security inbox owner signal, and previous funnel snapshot | `RM`, `COMM`, `SUP` | `CTRL-GROWTH-072`, `D21-RELEASE-067`; `REL-GR06`, `REL-SUP01-SUP02`, `REL-SUP05`, `REL-SUP07` | +| `09:00` | Decide `hold`/`narrow`/`ready`; bind content to a candidate and proof source | `RM`, technical owner | Source registry task; `REL-GR02`, `REL-DOC15-DOC16` | +| `10:00` | Technical and claim review; run snippets/links; compare English/Chinese limitations | `INT` or `TSR/PYR`, `PQG`, bilingual reviewer | `CTRL-DOCS-073`; `REL-DOC06`, `REL-DOC13-DOC16` | +| `11:00` | Channel-rule, disclosure, UTM, consent, accessibility, and support-capacity review | `COMM`, `SRV`, `SUP` | `CTRL-GROWTH-072`; `REL-GR01`, `REL-SUP01`, `REL-SUP06` | +| `12:00-15:00` | Authorized human publishes at most the approved channels, staggered; automation may prepare but not impersonate engagement | `COMM` + `EXT` authority | `D21-RELEASE-067`; `REL-GR01-GR02`, `REL-DOC14` | +| `+2h` | Capture aggregate snapshot; answer technical questions; stop if install/run/support/privacy thresholds fire | `COMM`, `SUP`, runtime owner | `CTRL-GROWTH-072`; `REL-GR04-GR06`, `REL-SUP02`, `REL-SUP06-SUP07` | +| `17:00` | Record decisions, corrections, missing data, and next experiment; no private message or raw user-run content in public evidence | `COMM`, `SRV` | `CTRL-GROWTH-072`; `REL-GR03-GR06`, `REL-SUP06` | + +During Day 21, add `+6h` and `+24h` reviews. If support p50 exceeds twelve +hours, the oldest ordinary item exceeds twenty-four hours, any P0/P1 defect is +open, or a security/privacy/package-identity incident is suspected, new +promotion stops. Mapping: `D21-RELEASE-067`; `REL-GR06`, `REL-SUP01-SUP08`, +`REL-RC06-RC10`. + +## 5. Twenty-one-day calendar + +Target dates below are derived from the plan epoch. They do not override product +dependencies. “Channels” describes prepared adaptations, not guaranteed access +or publication. All external posts require current account and community-rule +authority. + +### Days 1-6 — build in public without outrunning the alpha + +| Day / target date | Canonical content and honest headline frame | Evidence gate and fallback | Channel adaptations / CTA | Owner | Registry IDs | Release rows | Initial state | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| `1` / Jul 26 | **Manifesto:** “Prompts describe work. Graphs define what depends on what.” Include product/non-product boundary and current alpha scope | Use canonical IR, governance, and current README. If repository identity is not verified, keep internal | GitHub README/release note; X concept card; LinkedIn design premise; Chinese concept summary. CTA: inspect IR and correct ambiguities | `INT`, `PQG`, `COMM` | `D1-SPEC-001`, `D1-BRAND-001`, `D1-DOCS-001`, `CTRL-DOCS-073` | `REL-DOC05`, `REL-DOC13-DOC14`, `REL-GR01-GR02` | `evidence_wait` | +| `2` / Jul 27 | **Contract lesson:** “An edge exists only when data crosses it.” Show nodes, explicit entrypoints, typed payload, and one rejected graph | Current compiler/fixture only. State that general builders/YAML and component-level hashes are planned. Fallback: fixture walkthrough | Dev.to/Chinese tutorial draft; Reddit only in a relevant programming/agent community. CTA: run validator or submit a negative fixture | `INT`, `TSR/PYR`, `PQG` | `D1-SPEC-001`, `D2-BUILDERS-YAML-020` | `REL-DOC04-DOC06`, `REL-DOC10`, `REL-DOC14`, `REL-GR07` | `evidence_wait` | +| `3` / Jul 28 | **Quickstart:** “See a deterministic research diamond locally.” Publish a 60-second walkthrough draft and command transcript | Internal clean checkout must pass; public five-minute/three-command claim remains gated on external `REL-Q10`. Fallback: explicitly labeled alpha setup guide | GitHub canonical guide; short video draft; EN/ZH command cards. CTA: opt into timed first-run study | `PQG`, `TSR/PYR`, `COMM` | `D1-PLATFORM-001`, `D3-CLI-002`, `D3-PY-CLI-021`, `D17-BETA-063` | `REL-DOC01`, `REL-DOC13`, `REL-GR04`, `REL-Q10` | `evidence_wait` | +| `4` / Jul 29 | **Diamond demo:** “Cut fake edges; let independent nodes run.” Show chain versus diamond and structured settled results | Bind to deterministic example and actual static output. Fallback: diagram-only source note with no speedup claim | X animation/static card; LinkedIn architecture note; Dev.to/Chinese walkthrough. CTA: share a linear workflow that may contain fake edges | `TSR/PYR`, `PQG` | `D4-TS-PRIMITIVES-003`, `D4-PY-PRIMITIVES-004`, `D4-PATTERNS-004`, `D5-CLI-VISUALIZE-005` | `REL-DOC03`, `REL-DOC06`, `REL-GR02` | `evidence_wait` | +| `5` / Jul 30 | **Pipeline method:** “Parallel is a barrier; pipeline lets each item advance.” Publish test design and backpressure/failure questions | `D7-PIPELINE-CONFORMANCE-013` must close for product claim; performance result waits for D15. Fallback: semantics/design note labeled in progress | HN/Reddit only after conformance; shorter X diagram; Chinese technical note. CTA: inspect bounded cases, not star | `TSR/PYR`, `INT`, `PQG` | `D7-PIPELINE-SPEC-012`, `D7-PIPELINE-CONFORMANCE-013`, `D15-PERFORMANCE-061` | `REL-DOC05`, `REL-DOC07`, `REL-Q07`, `REL-GR02` | `evidence_wait` | +| `6` / Jul 31 | **Failure/router lesson:** “A failed node is a structured result, never a silent null.” Include deterministic routing and current integrated-boundary limits | Current primitive/failure evidence only. Do not claim durable quorum/deadline/conditional scheduling before D6 follow-up. Fallback: failure taxonomy tutorial | GitHub Discussion for feedback; Dev.to/Chinese tutorial; X failure card. CTA: contribute a failure fixture | `INT`, `PQG`, `COMM` | `D5-TS-ROUTER-005`, `D5-PY-ROUTER-005`, `D6-ROUTER-BARRIER-023` | `REL-DOC05`, `REL-DOC10`, `REL-DOC14`, `REL-GR07` | `evidence_wait` | + +### Days 7-13 — alpha, recovery, budgets, verification, and safety + +| Day / target date | Canonical content and honest headline frame | Evidence gate and fallback | Channel adaptations / CTA | Owner | Registry IDs | Release rows | Initial state | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| `7` / Aug 1 | **Alpha 1 / discovery beat:** “The first source alpha is public; bounded discovery remains on the roadmap.” Capture organic milestone snapshot without promising 300 | Existing source release may be described by actual date/revision. Discovery/cycle demo waits for D7 cycle conformance. Fallback: transparent alpha retrospective and open blockers | GitHub Release recap; HN only if current runnable value and clear limitations justify it; EN/ZH summaries. CTA: run current Quickstart/report friction | `RM`, `PQG`, `COMM` | `D5-SECURITY-RELEASE-008`, `D5-LAUNCH-READINESS-009`, `D7-CYCLE-CONFORMANCE-027` | `REL-GR02-GR04`, `REL-DOC13-DOC16`, `REL-RC03` | `evidence_wait` | +| `8` / Aug 2 | **Chaos lesson:** “Retries, timeouts, and cancellation need hard ceilings.” Show one injected failure and exact exit | Publish only after deterministic chaos evidence shows no unbounded retry and cleanup. Fallback: adversarial test plan | Dev.to/Reddit technical post; X bounded-work card; Chinese test note. CTA: propose/reproduce a bounded failure | `TSR/PYR`, `SRV`, `PQG` | `D8-CHAOS-OPS-030` | `REL-DOC08`, `REL-DOC10`, `REL-Q08`, `REL-GR06` | `evidence_wait` | +| `9` / Aug 3 | **Crash/resume beat:** “What was committed survives; what was not is retried honestly.” Show current immutable-local-DAG recovery and at-least-once boundary | Current D6 conformance supports narrow claim; leases/replay/fork/approval/artifacts wait for D9 extension. Any redaction/privacy blocker narrows payload display | 90-second uncut demo draft; architecture article; EN/ZH recovery diagram. CTA: reproduce crash window locally | `TSR/PYR`, `INT`, `SRV`, `PQG` | `D6-DURABLE-CONFORMANCE-011`, `D9-DURABLE-EXT-CONFORMANCE-034` | `REL-DOC02`, `REL-DOC05`, `REL-DOC08`, `REL-SUP04`, `REL-GR02` | `evidence_wait` | +| `10` / Aug 4 | **Cost/budget method:** “A graph must reserve before it spends.” Explain hard limits, model tiering, and why cheaper is not automatically better | Wait for portable ledger/routing conformance. No cost-saving percentage without reproducible data. Fallback: contract proposal/request for review | LinkedIn architecture note; Dev.to/Chinese cost-contract tutorial; X ledger diagram. CTA: review budget edge cases | `INT`, `TSR/PYR`, `PQG` | `D10-BUDGET-SPEC-035`, `D10-BUDGET-CONFORMANCE-038` | `REL-DOC05`, `REL-DOC07`, `REL-GR02` | `evidence_wait` | +| `11` / Aug 5 | **Verifier beat:** “A finding must survive evidence-aware skeptics; insufficient quorum is unknown.” Demonstrate vote retention/citations/reflection | D11 conformance plus exact pattern fixture must pass. Fallback: rubric and unknown-state design note | X panel diagram; HN/Reddit technical demo if runnable; Chinese verifier tutorial. CTA: try to refute the fixture | `TSR/PYR`, `PQG`, `SRV` | `D11-VERIFY-CONFORMANCE-043`, `PATTERN-02-CITED`, `PATTERN-04-DIFF` | `REL-PAT02`, `REL-PAT04`, `REL-DOC04`, `REL-DOC08`, `REL-GR02` | `evidence_wait` | +| `12` / Aug 6 | **Security architecture:** “Metadata is not a sandbox.” Explain ambient authority, capability targets, and current read-only MCP boundary | Architecture may publish with explicit implemented/target table; enforcement claims wait for D12 red-team. Vulnerabilities use private path | Architecture essay/Discussion; LinkedIn threat-boundary narrative; Chinese safety summary. CTA: threat-model review, not exploit disclosure | `INT`, `SRV`, `PQG` | `D12-ISOLATION-SPEC-044`, `D12-ISOLATION-REDTEAM-047`, `D2-MCP-001` | `REL-DOC05`, `REL-DOC10`, `REL-GR07`, `REL-SUP02`, `REL-SUP05-SUP06` | `evidence_wait` | +| `13` / Aug 7 | **Dual-language Alpha 2 + Graph Ready preview:** “One graph contract, two native runtimes.” Capture 1,000-star stretch snapshot only as observed outcome | Candidate/version, API review, parity, doctor/score fixtures required. If absent, publish a cross-language progress report without Alpha 2/badge claim | GitHub Release draft; Show HN candidate only if substantial; X/LinkedIn/Reddit/Dev.to and EN/ZH variants. CTA: compare fixture outputs | `RM`, `TSR/PYR`, `PQG`, `COMM` | `D13-DX-051`, `D14-API-FREEZE-050`, `D18-COMPAT-BENCH-064` | `REL-DOC06`, `REL-DOC09`, `REL-DOC13-DOC16`, `REL-GR02-GR04` | `evidence_wait` | + +### Days 14-18 — education, Explorer, Beta, adoption, and compatibility + +| Day / target date | Canonical content and honest headline frame | Evidence gate and fallback | Channel adaptations / CTA | Owner | Registry IDs | Release rows | Initial state | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| `14` / Aug 8 | **Roadmap/pattern map:** “Fourteen graph-engineering steps, each executable or explicitly pending.” Publish course tranche and complete-bundle standard | Never count skeleton folders as complete. Each lesson/pattern claim needs its runnable artifact, tests, budget, permissions, failure/resume, and guides | Dev.to series hub; GitHub docs; Chinese course index; relevant Reddit tutorial. CTA: adopt one scoped pattern issue | `PQG`, `TSR/PYR`, `COMM` | `D14-PATTERN-SKELETONS-053`, `CTRL-PATTERNS-071`, `PATTERN-01-RESEARCH` through `PATTERN-10-ECOSYSTEM` | `REL-PAT00-PAT10`, `REL-DOC04`, `REL-DOC10`, `REL-GR07` | `evidence_wait` | +| `15` / Aug 9 | **Benchmarks + Explorer:** “See where the graph waited, retried, and recovered.” Publish real topology and reproducible latency/recovery data | Explorer smoke/a11y and benchmark raw data/revision/baseline required; >10% unexplained regression blocks. Fallback: local static visualization and benchmark protocol | Interactive site/GitHub; uncut clip; HN/X/LinkedIn/Chinese demo tailored per channel. CTA: replay exact fixture | `PQG`, `TSR/PYR`, `INT` | `D15-EXPLORER-060`, `D15-PERFORMANCE-061` | `REL-DOC03`, `REL-DOC07`, `REL-DOC11`, `REL-Q07`, `REL-GR02` | `evidence_wait` | +| `16` / Aug 10 | **Safety validation:** “How we try to break the candidate before asking you to trust it.” Share methods, not a blanket secure claim; recruit final tester slots | D16 reports must bind exact candidate; any canary leak/high/critical finding stops promotion. Fallback: state audit is ongoing and list disabled surfaces | Security methods post; EN/ZH known-limits draft; direct manual invitations 7-10 only if capacity exists. CTA: consented test or private disclosure | `SRV`, `PQG`, `COMM`, `SUP` | `D16-SECURITY-062`, `D17-BETA-063`, `CTRL-GROWTH-072` | `REL-Q08`, `REL-GR01`, `REL-GR04`, `REL-SUP02`, `REL-SUP05-SUP06` | `evidence_wait` | +| `17` / Aug 11 | **Tester-backed Beta:** “Real users tried the clean path; here is where they succeeded and failed.” Capture 2,000-star stretch snapshot only if observed | At least five external usability reports, no P0/P1 defect, consent, and accepted sampling; do not fabricate Beta/adopter/gallery evidence | GitHub Beta release; case-study thread/article; adopter/trace gallery preview; EN/ZH summary. CTA: use or report one real workflow | `RM`, `PQG`, `COMM`, `EXT` | `D17-BETA-063`, `CTRL-GROWTH-072` | `REL-Q10-Q11`, `REL-DOC08`, `REL-DOC12-DOC14`, `REL-GR02-GR05` | `evidence_wait` | +| `18` / Aug 12 | **Compatibility and cases:** “Same graph, tested across supported runtimes and platforms.” Publish one success and the required authentic failure story | Full matrix, randomized failures, raw reports, candidate IDs, consent/attribution required. Fallback: partial matrix with missing cells labeled | Technical report on GitHub/Dev.to; LinkedIn lessons; Chinese compatibility summary; community-specific Q&A | `INT`, `TSR/PYR`, `PQG`, `EXT` | `D18-COMPAT-BENCH-064`, `CTRL-ACCEPTANCE-070` | `REL-Q05-Q07`, `REL-DOC06-DOC08`, `REL-DOC13-DOC14`, `REL-GR06` | `evidence_wait` | + +### Days 19-21 — RC, provenance, coordinated release, and support + +| Day / target date | Canonical content and honest headline frame | Evidence gate and fallback | Channel adaptations / CTA | Owner | Registry IDs | Release rows | Initial state | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| `19` / Aug 13 | **RC + security story:** “Candidate frozen; here is what passed, what remains, and how recovery/safety actually work.” | Exact RC, clean install/upgrade/migration, security report, known-limit/blocker manifest. Never say stable unless final gate later passes | GitHub RC release; architecture/security essay; EN/ZH summaries; support prebrief. CTA: verify candidate/report defect | `RM`, `SRV`, `PQG`, `SUP` | `D19-RC-065`, `D16-SECURITY-062`, `CTRL-DOCS-073` | `REL-DOC05`, `REL-DOC15-DOC16`, `REL-GR02`, `REL-SUP01-SUP08`, `REL-RC01-RC10` | `evidence_wait` | +| `20` / Aug 14 | **Provenance and launch rehearsal:** “Trace source to package before installing.” Finalize channel-specific drafts but do not preannounce unverified coordinates | Trusted publisher identity, SBOM/checksum/attestation rehearsal, link/claim/bilingual audit, support dry run. Fallback: source-only RC and explicit publication blocker | GitHub provenance guide; technical social preview only after links resolve; personalized reminder only to opted-in testers | `RM`, `SRV`, `PQG`, `COMM`, `SUP` | `D20-PROVENANCE-066`, `CTRL-ACCEPTANCE-070`, `CTRL-GROWTH-072`, `CTRL-DOCS-073` | `REL-Q08-Q09`, `REL-DOC13-DOC16`, `REL-GR01-GR07`, `REL-SUP01-SUP08` | `evidence_wait` | +| `21` / Aug 15 | **Coordinated release or transparent complete RC:** “Graph Engineering [exact version/status]: build, inspect, recover, and contribute.” Report Day-21 6,000+ only if timestamped public count actually shows it | Stable requires all conjunctive rows Green and authority; otherwise complete RC with blocker manifest. If assets missing, do not call the RC complete. Stop on incident/support thresholds | Canonical GitHub/site first, then one technical community, then X/LinkedIn/Reddit/Dev.to/Chinese variants where authorized; staggered support. CTA: run verified Quickstart and share evidence—not a required star | `RM`, all technical lanes, `PQG`, `COMM`, `SUP`, `EXT` | `D21-RELEASE-067`, `CTRL-ACCEPTANCE-070`, `CTRL-PATTERNS-071`, `CTRL-GROWTH-072`, `CTRL-DOCS-073` | `REL-V1-01-V1-08`, `REL-GR01-GR07`, `REL-DOC01-DOC16`, `REL-SUP01-SUP08`, `REL-RC01-RC10` | `evidence_wait` | + +## 6. Content-series production briefs + +### Series S1 — “The shape of work” (Days 1-6) + +Deliverables are a concept card, real/fake-edge worksheet, Quickstart clip, +diamond visualization, pipeline/barrier explanation, and failure/router +taxonomy. Each source example must run against the cited revision. No performance +claim may be inferred from a diagram. Owners: `INT`, `TSR/PYR`, `PQG`. +Mapping: `D1-SPEC-001`, `D4-PATTERNS-004`, +`D7-PIPELINE-CONFORMANCE-013`, `D6-ROUTER-BARRIER-023`; +`REL-DOC01`, `REL-DOC03-DOC07`, `REL-GR02`. + +### Series S2 — “Graphs that fail honestly” (Days 8-12) + +Deliverables are cancellation chaos, crash/resume trace, budget contract, +verifier/unknown demo, and ambient-authority threat boundary. Every post carries +one failure and one limit. Durable examples must say external effects are +at-least-once and must not expose raw secrets or prompts. Owners: `TSR/PYR`, +`INT`, `SRV`, `PQG`. Mapping: `D8-CHAOS-OPS-030`, +`D9-DURABLE-EXT-CONFORMANCE-034`, `D10-BUDGET-CONFORMANCE-038`, +`D11-VERIFY-CONFORMANCE-043`, `D12-ISOLATION-REDTEAM-047`; +`REL-DOC05`, `REL-DOC08`, `REL-DOC10`, `REL-SUP04-SUP06`. + +### Series S3 — “One protocol, two native runtimes” (Days 13-18) + +Deliverables are side-by-side code, Graph Ready output, course/pattern map, +Explorer walkthrough, external first-run report, and compatibility matrix. Any +missing platform/language/pattern cell remains visible. Owners: `TSR/PYR`, +`PQG`, `EXT`. Mapping: `D13-DX-051`, `D14-API-FREEZE-050`, +`CTRL-PATTERNS-071`, `D15-EXPLORER-060`, `D17-BETA-063`, +`D18-COMPAT-BENCH-064`; `REL-DOC04`, `REL-DOC06-DOC12`, +`REL-Q05-Q11`. + +### Series S4 — “Proof before promotion” (Days 19-21) + +Deliverables are RC/security story, source-to-package provenance, candidate +known-limit manifest, final launch summary, and a 24-hour evidence-based update. +The final update reports actual funnel data even if every stretch milestone is +missed. Owners: `RM`, `SRV`, `PQG`, `COMM`, `SUP`. Mapping: +`D19-RC-065`, `D20-PROVENANCE-066`, `D21-RELEASE-067`; +`REL-DOC15-DOC16`, `REL-GR01-GR06`, `REL-SUP01-SUP08`, +`REL-RC01-RC10`. + +## 7. Bilingual production workflow + +For each public launch asset: + +1. `PQG` freezes the English canonical copy against a candidate and claim map. + Mapping: `CTRL-DOCS-073`; `REL-DOC13`, `REL-DOC15-DOC16`. +2. A bilingual contributor translates meaning while retaining exact commands, + code, versions, support level, security/privacy defaults, at-least-once + semantics, and stable/RC label. Mapping: `CTRL-DOCS-073`; + `REL-DOC13`. +3. The technical owner diffs claims, numbers, links, and limitations, not merely + prose. Mapping: source registry task; `REL-DOC13-DOC16`. +4. A Chinese reviewer checks terminology and channel-native readability. Terms + that identify contracts—Graph IR, node, edge, barrier, pipeline, checkpoint, + replay, capability, idempotency—retain an English term at first use when a + translation could be ambiguous. Mapping: `CTRL-GROWTH-072`; + `REL-DOC13-DOC14`. +5. Both variants receive one shared content family ID and separate digests. A + correction in either language reopens the pair. Mapping: + `CTRL-DOCS-073`; `REL-DOC13`, `REL-RC09`. + +Minimum reusable bilingual copy blocks: + +| Block | English requirement | Chinese requirement | Mapping | +| --- | --- | --- | --- | +| Product identity | Vendor-neutral graph orchestration; not GraphRAG/GNN | 明确是多智能体工作流图编排,不是知识图谱、GraphRAG 或 GNN | `REL-DOC13`, `REL-DOC16` | +| Current scope | Exact alpha/RC/stable label and implemented subset | 使用同一版本和当前实现范围,不强化成熟度 | `REL-DOC13`, `REL-RC03` | +| Safety | Telemetry/prompt capture defaults and current authority limits | 保留默认隐私、ambient authority、外部副作用至少一次语义 | `REL-DOC13`, `REL-SUP04-SUP06` | +| Quickstart | Same verified commands and expected output | 命令、输出、包坐标与英文完全一致 | `REL-DOC01`, `REL-DOC13` | +| CTA | Run, inspect, report, contribute, or consent to test | 运行、检查、反馈、贡献或自愿参加测试;不以 star 为交换条件 | `REL-GR01`, `REL-GR07` | + +## 8. Channel-ready copy frames + +These are templates, not ready-to-publish claims. Bracketed fields must be +resolved from evidence; unresolved brackets force `evidence_wait`. + +### GitHub release + +```text +Graph Engineering [version] — [stable/full RC/source alpha] + +What is verified in this candidate: +- [three evidence-linked capabilities] + +What is not included or not yet proven: +- [known limits and blocker-manifest link] + +Try it: [verified Quickstart] +Verify it: [tests/provenance/benchmark] +Get help or contribute: [support/contribution links] +``` + +Mapping: `D19-RC-065`, `D21-RELEASE-067`; `REL-DOC15-DOC16`, +`REL-SUP04`, `REL-RC03-RC05`. + +### Hacker News / technical community + +```text +Show HN: Graph Engineering — [specific verified differentiator] + +We built [current scope] because [concrete problem]. The smallest reproducible +example is [link/commands]. The trade-off or missing piece is [limit]. We would +especially value feedback on [bounded technical question]. +``` + +Mapping: `CTRL-GROWTH-072`; `REL-DOC14`, `REL-GR01-GR02`, +`REL-GR06-GR07`. + +### X thread + +```text +1/ The work is a graph when outputs—not prose order—create dependencies. +2/ [real topology visual with alt text] +3/ [uncut verified result and version] +4/ [failure/safety/current-scope limit] +5/ Reproduce it: [source link] +``` + +Mapping: source product task plus `CTRL-GROWTH-072`; `REL-DOC02-DOC03`, +`REL-DOC14`, `REL-GR01-GR02`. + +### Chinese launch summary + +```text +Graph Engineering [版本]:[准确的 alpha/RC/stable 标签] + +已经验证:[三项带证据的当前能力] +尚未实现或尚未完成验证:[限制和 blocker] +五分钟复现:[经过验证的中文 Quickstart] +反馈与贡献:[支持、Issue、Discussion、安全报告路径] +``` + +Mapping: `CTRL-DOCS-073`, `D21-RELEASE-067`; `REL-DOC01`, +`REL-DOC13-DOC16`, `REL-GR07`. + +## 9. Engagement and moderation playbook + +### Response categories + +| Incoming item | Response action | Owner | Mapping | +| --- | --- | --- | --- | +| Install or run failure | Ask for version, OS/runtime, exact sanitized command/error; reproduce; link one canonical issue; never request secrets/raw prompts by default | `SUP`, runtime owner | `D17-BETA-063`, `D21-RELEASE-067`; `REL-Q10-Q11`, `REL-SUP02`, `REL-SUP06-SUP07` | +| Technical criticism | Confirm the claim/evidence, correct publicly when wrong, or explain trade-off with source; do not argue from stars | `INT`, `COMM` | `CTRL-GROWTH-072`; `REL-GR03`, `REL-GR06` | +| Feature request | Clarify use case and constraints; map to issue/pattern; do not promise roadmap date | `COMM`, `INT` | `REL-GR07`, relevant registry task | +| Contribution | Acknowledge, reproduce/check scope, assign correct reviewer, preserve contributor credit without coercing promotion | `COMM`, lane owner | `CTRL-PATTERNS-071` or source task; `REL-GR04`, `REL-GR07`, `REL-SUP07` | +| Adoption/case-study offer | Request scoped consent, evidence, redaction, and withdrawal preference; publish only after review | `COMM`, `SRV`, `EXT` | `D17-BETA-063`; `REL-DOC08`, `REL-DOC12`, `REL-SUP06` | +| Security report | Move to private documented channel; acknowledge under security policy; never paste exploit/secret publicly | `SRV` | `D16-SECURITY-062`; `REL-SUP02`, `REL-SUP05-SUP06` | +| Abuse/harassment/spam | Apply Code of Conduct and moderation policy; retain minimal evidence; do not turn conflict into engagement content | `COMM`, `RM` | `D1-DOCS-001`; `REL-GR07`, `REL-SUP02` | + +No automated system sends replies, likes, follows, direct messages, or community +submissions. Draft assistance is permitted only with human verification and +authorized publication. Mapping: `CTRL-GROWTH-072`; `REL-GR01`. + +## 10. Stop, correction, and resumption rules + +| Trigger | Calendar action | Resumption evidence | Mapping | +| --- | --- | --- | --- | +| Candidate/release gate reopens | Hold all candidate-specific content; mark previously published copy for correction/supersession | New candidate identity, rerun evidence, claim/link audit | `D19-RC-065`, `D21-RELEASE-067`; `REL-RC06`, `REL-RC09` | +| Security, privacy, provenance, package-integrity, or secret-canary failure | Stop promotion immediately; invoke incident/private disclosure path | Independent review, fixed candidate, exploit/canary regression, new provenance | `D16-SECURITY-062`, `D20-PROVENANCE-066`; `REL-Q08-Q09`, `REL-SUP05-SUP06` | +| P0/P1 defect, data loss, corrupt recovery, duplicate external effect | Stop affected CTA/release promotion; publish impact/workaround if authorized | Reproducer, fix, recovery/side-effect tests, forward version | `D17-BETA-063`, `D21-RELEASE-067`; `REL-Q11`, `REL-SUP04-SUP05`, `REL-RC07-RC08` | +| External Quickstart success below 80% after at least five accepted reports | Pause broad promotion; prioritize install/docs/reliability | Repeated cohort meets `REL-Q10` with sampling record | `D17-BETA-063`; `REL-Q10`, `REL-GR06` | +| Stars without installs or installs without successful runs | Shift CTA to Quickstart or pause promotion; do not intensify vanity campaign | Funnel diagnosis and improved activation evidence | `CTRL-GROWTH-072`; `REL-GR03-GR06` | +| Support p50 above 12h or oldest ordinary launch item above 24h | Hold next beats and move content capacity to triage | Queue snapshot shows restored coverage/capacity | `D21-RELEASE-067`; `REL-SUP01-SUP02`, `REL-SUP07`, `REL-GR06` | +| Community removes/flags post or rules were misunderstood | Stop that channel; contact moderators only if appropriate; no repost evasion | Rule review and explicit permission or permanent withdrawal | `CTRL-GROWTH-072`; `REL-GR01`, `REL-DOC14` | +| Adopter withdraws consent | Unpublish gallery/case/quote promptly and retain only minimal withdrawal record | New scoped consent required for any reuse | `D17-BETA-063`; `REL-DOC08`, `REL-DOC12`, `REL-SUP06` | +| Translation changes claim strength/version/limit | Hold both language variants and all derived channel assets | Bilingual technical diff and new digests | `CTRL-DOCS-073`; `REL-DOC13-DOC16`, `REL-RC09` | + +## 11. Weekly editorial reviews + +### End of Day 7 + +- Audit Days 1-7 publication states and prove any actual URLs; no blank is + silently called published. Mapping: `CTRL-GROWTH-072`; `REL-GR02`. +- Capture Day-7 stars only as timestamped observed data; 300 remains stretch. + Mapping: `REL-GR03`. +- Diagnose concept -> Quickstart interest using privacy-safe aggregates and + qualitative issues. Mapping: `REL-GR05-GR06`. +- Decide whether Day 8-13 should emphasize positioning, onboarding, reliability, + or use-case proof. Mapping: `REL-GR06`. + +### End of Day 13 + +- Verify Alpha 2/Graph Ready wording against actual candidate; otherwise correct + or withdraw it. Mapping: `D13-DX-051`, `D14-API-FREEZE-050`; + `REL-DOC09`, `REL-DOC16`. +- Capture 1,000-star checkpoint only as an observed stretch outcome and report + activation beside it. Mapping: `REL-GR03-GR05`. +- Rebalance channels by qualified Quickstart/start-report evidence, not raw + impressions. Mapping: `REL-GR06`. + +### End of Day 17 + +- Require real external Beta/usability evidence before “tester-backed,” adopter, + or gallery language. Mapping: `D17-BETA-063`; `REL-Q10-Q11`, + `REL-DOC08`, `REL-DOC12`. +- Capture 2,000-star checkpoint as non-blocking outcome. Mapping: `REL-GR03`. +- Stop promotion if successful-run or retention evidence is weak; allocate Days + 18-20 to reliability/Quickstart instead. Mapping: `REL-GR05-GR06`. + +### End of Day 21 + +- Publish stable only if final conjunctive decision is Green; otherwise publish + complete RC or no release according to evidence. Mapping: + `D21-RELEASE-067`; `REL-RC01-RC10`, `REL-V1-01-V1-08`. +- Report actual 6,000+ result only if a timestamped public source shows it; a + miss is diagnostic and never concealed. Mapping: `REL-GR03`. +- Publish a 24-hour update containing activation, retention availability, + defects, corrections, support load, and next review—not only stars. Mapping: + `CTRL-GROWTH-072`; `REL-GR04-GR06`, `REL-SUP07`. + +## 12. Calendar completion gate + +This calendar is operationally complete only after every row has: + +- an honest terminal state (`published`, `held`, or `superseded`), with no + implied publication from a draft; +- candidate/evidence/claim/translation/channel review; +- authorized URL and timestamp when published; +- disclosure, consent, UTM/privacy record, and accessibility fields; +- support/stop-condition review; and +- an aggregate metric snapshot or explicit `not_available` reason. + +That evidence is expected under the future release-evidence structure and must +be reconciled with `REL-GR01-GR07`, `REL-DOC01-DOC16`, and +`REL-SUP01-SUP08`. The existence of this calendar closes none of those rows. diff --git a/codex_plans/growth/launch-plan.md b/codex_plans/growth/launch-plan.md new file mode 100644 index 0000000..f73f921 --- /dev/null +++ b/codex_plans/growth/launch-plan.md @@ -0,0 +1,412 @@ +# Graph Engineering 21-day organic launch plan + +Status: **Approved planning artifact; launch execution is not complete** +Plan epoch: **Day 1 = 2026-07-26, America/Vancouver** +Primary registry owner: `CTRL-GROWTH-072` (`planned`, dependency-blocked) +Document-production owner: `CTRL-DOCS-073` (`in_progress`) +Canonical inputs: [master plan Section 7](../Graph-Engineering-21-Day-Master-Plan.md#7-organic-launch-and-6000-star-target), [release checklist](../delivery/release-checklist.md), [dependency ledger](../delivery/task-dependency-graph.md), and [current source review](../research/graph-engineering-source-review.md) + +## 1. Outcome, boundaries, and claim discipline + +The launch objective is to make a technically credible Graph Engineering +release easy to understand, try, verify, reuse, and contribute to. The desired +loop is exactly: + +```text +clear concept + -> sixty-second demo + -> five-minute successful run + -> shareable trace or Graph Ready score + -> user pattern or adapter + -> authentic case study + -> new user +``` + +`6,000+` GitHub stars by Day 21 is a **stretch awareness outcome**. It is not a +promise, release gate, forecast, or result the project can manufacture. The +dated popularity-parity reference is 9,416 stars for Loop Engineering on +2026-07-26; it is a moving benchmark and must not be presented as current +without a fresh, timestamped source. Stable-v1 eligibility is controlled by +recovery, security, conformance, provenance, external usability, and every +mandatory release row—not stars. + +The following are prohibited without exception: + +- purchasing stars, followers, traffic, reviews, downloads, or testimonials; +- bots, scripted account actions, click farms, giveaway-for-star campaigns, + reciprocal or coordinated star rings, and fake adopters; +- bulk unsolicited direct messages, scraped contact lists, repeated + cross-posting against a community's rules, or undisclosed sponsorship; +- implying an endorsement by Anthropic, Andrew Ng, OpenAI, a tester, adopter, + or community that has not explicitly granted it; +- calling an unexecuted demo, benchmark, case study, security property, + production adapter, or release gate complete; and +- treating high reach or star counts as proof of successful runs, retention, + safety, or production readiness. + +These conduct rules are mandatory under `REL-GR01`; the channel disclosure and +release-manager attestation remain **Open**. Every action in this document is +organic and must be cancelled if it cannot meet that row. + +## 2. Status truth table + +This table prevents a prepared plan from being confused with executed launch +work. “Verified current” refers only to repository-local or already-recorded +evidence; it does not turn an open release-checklist row Green. + +| Surface | Current state on 2026-07-26 | Evidence and limitation | Registry / release rows | +| --- | --- | --- | --- | +| Public source repository and source release | **Verified current** | The public repository and `v0.1.0-alpha.1` source release are recorded in the [master plan](../Graph-Engineering-21-Day-Master-Plan.md); package-registry publication is not inferred | `D1-BRAND-001`, `D5-SECURITY-RELEASE-008`; `REL-SC01-SC14` remain governed separately | +| Current concept and five-minute guide | **Implemented in alpha, gate not accepted** | [README](../../README.md), [Quickstart](../../docs/QUICKSTART.md), deterministic graph fixture, and native example scripts exist; no accepted external 80% five-minute cohort yet | `D1-DOCS-001`, `D1-PLATFORM-001`, `D5-DX-RELEASE-AUDIT-007`; `REL-DOC01`, `REL-Q10` Open | +| Native TypeScript/Python alpha and local durable start/resume | **Verified current, bounded scope** | Current docs and registry show both runtimes and immutable local-DAG durable recovery; later leases/replay/fork/adapters are not implemented | `D1-TS-001`, `D1-PY-001`, `D6-DURABLE-CONFORMANCE-011`; `REL-DOC05-DOC06` Open | +| Alpha 1 release beat | **Artifact exists; campaign publication unverified** | A release artifact is not evidence that channel-specific Alpha 1 launch posts or metrics snapshots ran | `D5-LAUNCH-READINESS-009`, `CTRL-GROWTH-072`; `REL-GR02-GR05` Open | +| Pipeline milestone | **In progress** | Native/conformance completion and release gates are not yet closed | `D7-PY-PIPELINE-012`, `D7-PIPELINE-CONFORMANCE-013`; relevant claims must wait | +| Graph Ready, adapters, Explorer, benchmarks, Beta, RC, stable release | **Planned / not implemented or not accepted** | These are downstream dependencies, not content-ready facts | `D13-DX-051`, `D13-ADAPTERS-049`, `D15-EXPLORER-060`, `D15-PERFORMANCE-061`, `D17-BETA-063`, `D19-RC-065`, `D21-RELEASE-067`; `REL-DOC02-DOC16`, `REL-GR02` Open | +| Organic metrics and experiments | **Planned / not collected by this plan** | No dashboard, timestamped funnel snapshot, experiment result, or 6,000-star result is asserted here | `CTRL-GROWTH-072`; `REL-GR03-GR06` Open | +| Community and support operation | **Foundation exists; launch operation Open** | Governance, contribution, support, and security files exist, but launch roster, dry runs, response evidence, and adopter workflow are not accepted | `D1-DOCS-001`, `D21-RELEASE-067`; `REL-GR07`, `REL-SUP01-SUP08` Open | + +`CTRL-GROWTH-072` depends on `D13-DX-051` and `D15-EXPLORER-060` and is +currently `planned`. Writing these three growth plans is allowed under the +unblocked `CTRL-DOCS-073`; it does not satisfy the growth task's implementation, +site, asset, metric, external-evidence, or support requirements. + +## 3. Success hierarchy + +When objectives conflict, use this order: + +1. **Safety and truth:** no unsafe release, secret disclosure, false claim, + fabricated proof, prohibited promotion, or missing attribution. +2. **First success:** a qualified visitor can select the correct language, + install from a verified source, run the deterministic mock, and understand + the output in five minutes. +3. **Repeated value:** the repository runs a useful graph again at least seven + days later, with failure/recovery behavior understood. +4. **Authentic adoption:** users publish consented usage, patterns, traces, case + studies, issues, or integrations. +5. **Healthy contribution:** outside contributors can find, validate, submit, + and receive a timely response to meaningful work. +6. **Awareness:** visits, shares, and organic stars grow because the earlier + layers are useful. + +The controlled leading goals from the master plan are 2,000 CLI downloads, 500 +successful or explicitly self-reported runs, ten public adopters, ten outside +contributors, twenty-five external PRs, response-time p50 below twelve hours, +and ten personalized trial invitations. These are targets, not facts or +guarantees. The measurement contract is in +[metrics-and-experiments.md](./metrics-and-experiments.md). + +## 4. Audience and evidence-backed positioning + +### A1 — Agent and framework builders + +- Problem: multi-step agents become implicit linear chains, lose failure + structure, and mix orchestration with model judgment. +- Present-alpha proof: typed graph IR, explicit entrypoints, native schedulers, + structured failures, deterministic parallel graph example, and local durable + start/resume. +- Future proof required before claim: dynamic bounded cycles, verifier panels, + official provider adapters, budgets, and isolation. +- CTA: run the deterministic Quickstart, inspect the graph, then file a concrete + missing-contract issue. +- Mapping: `D1-SPEC-001`, `D4-PATTERNS-004`, `D6-DURABLE-CONFORMANCE-011`, + later `D7-CYCLE-CONFORMANCE-027`/`D11-VERIFY-CONFORMANCE-043`; + `REL-DOC01`, `REL-DOC04-DOC06`, `REL-GR06-GR07`. + +### A2 — TypeScript and Python platform teams + +- Problem: orchestration semantics drift across SDKs and failures become + language-specific surprises. +- Positioning: one portable Graph IR and shared fixtures, with native runtimes + rather than a thin client in one language. +- Proof required: candidate-bound `X01-X10` conformance, executable side-by-side + examples, and platform matrices; current alpha is narrower. +- CTA: reproduce the same fixture in both languages and report any divergence. +- Mapping: `D1-TS-001`, `D1-PY-001`, `D7-PIPELINE-CONFORMANCE-013`, later + `D18-COMPAT-BENCH-064`; `REL-DOC06`, `REL-X01-X10`, `REL-Q05`. + +### A3 — Reliability, security, and developer-tool maintainers + +- Problem: retries, resume, shell/network access, and parallel writers make + agent demos unsafe when failure boundaries are vague. +- Present-alpha proof: structured failures, bounded scheduler attempts, local + persistence integrity, read-only MCP, and explicit current limitations. +- Claims withheld: worktree/process/container isolation, distributed leases, + secret-safe durable payloads, mutating MCP, and production security are + targets until their gates pass. +- CTA: review threat/failure contracts, reproduce a fixture, or contribute a + negative test—never test a vulnerability in public issues. +- Mapping: `D2-MCP-001`, `D6-DURABLE-CONFORMANCE-011`, later + `D12-ISOLATION-REDTEAM-047`/`D16-SECURITY-062`; `REL-DOC05`, `REL-DOC10`, + `REL-SUP02`, `REL-SUP05-SUP06`. + +### A4 — Educators, researchers, and technical creators + +- Problem: graph architecture is compelling conceptually but hard to teach as + reproducible, bounded software. +- Positioning: a fourteen-step executable path from fake-edge audit to bounded + self-routing, with “when not to use a graph.” +- Proof required: runnable checks and attribution review; inspiration is not an + endorsement or private API compatibility claim. +- CTA: try one lesson or pattern and publish an independent result with its + version and limitations. +- Mapping: `D14-PATTERN-SKELETONS-053`, `CTRL-PATTERNS-071`, + `CTRL-DOCS-073`; `REL-DOC04`, `REL-PAT00-PAT10`, `REL-GR07`. + +### A5 — Open-source contributors and early adopters + +- Problem: promising projects often lack scoped issues, response expectations, + reproducible failures, and recognition grounded in real work. +- Positioning: explicit governance, security path, pattern requests, review + gates, and honest RC fallback. +- CTA: choose a labeled issue, submit a reproducible pattern/trace, or join a + consented usability session. +- Mapping: `D1-DOCS-001`, `D17-BETA-063`, `D21-RELEASE-067`; + `REL-GR07`, `REL-SUP01-SUP08`, `REL-Q10-Q11`. + +## 5. Message architecture and bilingual contract + +### One-sentence concept + +English: + +> Prompts describe work; loops repeat work; Graph Engineering makes branching, +> verification, durable state, and convergence explicit and portable. + +Chinese: + +> Prompt 描述工作,loop 重复工作;Graph Engineering 把分支、验证、持久状态和收敛变成显式且可移植的图协议。 + +### Proof stack + +Every public asset uses the same sequence: + +1. state the user problem without attacking a competitor; +2. show one real graph and the exact candidate/version; +3. show the command and unedited result or reproducible fixture; +4. name the failure/recovery/safety boundary; +5. distinguish current behavior from target-v1 behavior; +6. offer one useful CTA: run, inspect, report, contribute, or opt into a study; +7. link to source, not a cropped result alone. + +English is canonical for technical contracts. Chinese launch copy must preserve +version, commands, support level, known limits, security language, at-least-once +effect semantics, telemetry default, and RC/stable label. Translation may adapt +examples and idiom but cannot strengthen claims. One English reviewer, one +Chinese reviewer, and the relevant technical owner approve the pair. Any source +change invalidates both variants. Mapping: `CTRL-DOCS-073`, +`CTRL-GROWTH-072`; `REL-DOC13-DOC16`, `REL-GR02`. + +## 6. Asset dependency and acceptance matrix + +No asset publishes merely because a calendar slot arrives. “Ready” means the +listed product evidence exists, the claim review passes, and the exact asset is +recorded in the launch manifest. + +| Asset | Earliest evidence dependency | Required acceptance evidence | Owner | Registry / release rows | Current status | +| --- | --- | --- | --- | --- | --- | +| 60-second Quickstart | Current mock-first CLI/fixture plus external study | At most three user commands; clean install transcript; English/Chinese parity; at least 80% of accepted tester cohort succeeds within five minutes | `PQG` + `TSR/PYR` + `EXT` | `D1-PLATFORM-001`, `D17-BETA-063`; `REL-DOC01`, `REL-Q10` | Source guide exists; asset/gate **Open** | +| 90-second uncut terminal demo | Frozen candidate and clean packaged install | Uncut recording, source script, timestamp, candidate digest, no hidden repair | `PQG` + `INT` | `D19-RC-065`; `REL-DOC02`, `REL-DOC16` | **Open** | +| Linear-versus-graph visualization | Real topology/event fixture; Explorer only when implemented | Source/deploy revision, deterministic fixture, smoke/accessibility evidence, limitations | `PQG` | `D5-CLI-VISUALIZE-005`, later `D15-EXPLORER-060`; `REL-DOC03`, `REL-DOC11` | Static CLI visualization exists; interactive asset **Open** | +| Fourteen-step executable roadmap | Runtime prerequisites and all lesson checks | Course manifest, fourteen runnable checks, failure paths, “when not to use,” attribution audit | `PQG` + domain owners | `D14-PATTERN-SKELETONS-053`, `CTRL-PATTERNS-071`; `REL-DOC04`, `REL-PAT00-PAT10` | **Open** | +| Architecture essay | API/IR freeze and security/recovery truth | Claim-to-spec cross-reference, independent review, explicit target/current labels | `INT` + `SRV` | `D14-API-FREEZE-050`, `D16-SECURITY-062`; `REL-DOC05`, `REL-SUP04` | **Open** | +| Side-by-side TS/Python examples | Shared fixture and candidate parity | Executable docs test and `X01-X10` evidence | `TSR/PYR` + `PQG` | `D18-COMPAT-BENCH-064`; `REL-DOC06`, `REL-X01-X10` | Narrow examples exist; gate **Open** | +| Performance/recovery benchmark | Storage/Explorer candidate and stable baseline | Script, raw data, environment, revision, baseline, variance, failure cases; >10% regression handled | `PQG` + runtime owners | `D15-PERFORMANCE-061`, `D18-COMPAT-BENCH-064`; `REL-DOC07`, `REL-Q07` | **Open** | +| Four case studies, one failure | Reproducible trace and consent when external | Source/trace, reproduction, limits, consent, claim review; failure not sanitized away | `PQG` + `EXT` | `D11-VERIFY-CONFORMANCE-043`, `D17-BETA-063`; `REL-DOC08`, `REL-DOC12` | **Open/External** | +| Graph Ready G0-G4 badge | Deterministic score implementation | Repeatability fixtures, top three remediation actions, badge output | `PQG` | `D13-DX-051`; `REL-DOC09` | **Open** | +| Pattern picker and guides | Ten complete bundles | Bundle manifest, verified failure/resume, budgets/permissions, reviewer | `PQG` + native lanes | `D14-PATTERN-SKELETONS-053`, `CTRL-PATTERNS-071`; `REL-DOC10`, `REL-PAT00-PAT10` | **Open** | +| Interactive showcase | Explorer and deterministic examples | Source/deploy revision, smoke/a11y tests, accurate state/budget/replay views | `PQG` | `D15-EXPLORER-060`; `REL-DOC11` | **Open** | +| Adopter and trace galleries | Real opt-in submissions | Consent scope/date, redacted entry, current URL, withdrawal path, reviewer | `COMM` + `EXT` | `D17-BETA-063`, `CTRL-GROWTH-072`; `REL-DOC12`, `REL-GR07`, `REL-SUP06` | **Open/External** | +| EN/ZH launch summaries | Frozen candidate/known-limit list | Bilingual diff, version/link audit, technical owner sign-off | `PQG` + bilingual reviewer | `D19-RC-065`, `CTRL-DOCS-073`; `REL-DOC13`, `REL-DOC16` | **Open** | +| Channel-specific launch set | Every referenced asset is accepted | Copy manifest, channel-rule check, UTM audit, disclosure, schedule, owner | `COMM` + `RM` | `CTRL-GROWTH-072`, `D21-RELEASE-067`; `REL-DOC14`, `REL-GR01-GR02` | **Open** | + +## 7. Channel adaptation and organic distribution + +One canonical evidence packet may be adapted; identical spam blasts are not an +acceptable distribution plan. Before posting, the owner must verify current +community rules and account permissions. “External” means the repository cannot +prove or perform the action alone. + +| Channel | Native format and audience fit | First CTA | Adaptation rule | Organic/permission guardrail | UTM source | Mapping | +| --- | --- | --- | --- | --- | --- | --- | +| GitHub repository/release | Durable source of truth: README hero, release notes, demo, discussions, issues | Run deterministic Quickstart | Put commands, version, limitations, and contribution route before marketing copy | Requires repository/release authority; never rewrite release history or hide RC label | `github` | `D19-RC-065`, `D21-RELEASE-067`; `REL-DOC01`, `REL-DOC16`, `REL-GR07`, `REL-SUP02` | +| Hacker News | Concise “Show HN” with problem, implementation, trade-offs, technical evidence | Inspect/run source | Lead with what works today and invite technical criticism; no engagement pod or vote solicitation | Follow current Show HN rules; disclose affiliation; one canonical submission, no brigading | `hackernews` | `CTRL-GROWTH-072`; `REL-DOC14`, `REL-GR01-GR02`, `REL-GR06` | +| X | Short visual thread: concept, topology, uncut clip, limit, source | Watch then run | Use one idea per post and alt text; replies answer evidence questions, not repeat CTA | Requires authorized account; no automated replies/likes/follows/DMs, paid amplification, or copied third-party media | `x` | `CTRL-GROWTH-072`; `REL-DOC02-DOC03`, `REL-DOC14`, `REL-GR01-GR02` | +| LinkedIn | Engineering narrative and architectural decision with measured result | Read architecture/case | Explain team/reliability implications; avoid inflated “revolutionary” claims | Disclose maintainer relationship and any employer/community connection; no automated outreach | `linkedin` | `D14-API-FREEZE-050`; `REL-DOC05`, `REL-DOC08`, `REL-DOC14`, `REL-GR01` | +| Reddit | Community-specific technical write-up and reproducible example | Discuss trade-offs / run fixture | Select only genuinely relevant communities; rewrite for their rules and answer in-thread | Check self-promotion ratio and moderator rules; no mass cross-post or vote request | `reddit` | `CTRL-GROWTH-072`; `REL-DOC06`, `REL-DOC10`, `REL-DOC14`, `REL-GR01` | +| Dev.to | Searchable tutorial with complete code and failure path | Complete one pattern | Teach first; canonical link and version at top; update stale commands | Respect canonical/AI-content/disclosure rules; do not duplicate copyrighted source text | `devto` | `CTRL-DOCS-073`, `CTRL-PATTERNS-071`; `REL-DOC04`, `REL-DOC10`, `REL-DOC14` | +| Chinese developer communities | Chinese-native explainer/tutorial for communities such as Juejin, V2EX, SegmentFault, Zhihu, or WeChat where permitted | 中文 Quickstart / 复现实验 | Translate meaning, not only words; retain exact CLI/version/security limits and link canonical English contract | Verify each community's rules and account authority; do not scrape/contact users or imply endorsement | `cn_` | `CTRL-DOCS-073`, `CTRL-GROWTH-072`; `REL-DOC13-DOC14`, `REL-GR01-GR02` | +| Direct maintainer/creator invitations | One-to-one, relevant, personalized request to test a specific workflow | Opt into a trial; no star ask | State why the person's public work makes the trial relevant and offer an easy decline | Maximum ten in plan; manual only; no scraped private data, repeat follow-up, or expectation of coverage/star | `invite` | `D17-BETA-063`, `CTRL-GROWTH-072`; `REL-GR01`, `REL-GR04`, `REL-Q10-Q11` | + +## 8. Twenty-one-day execution schedule + +The day number is a dependency-aware campaign slot, not permission to publish an +unverified claim. If a product gate slips, replace the intended release claim +with an honest build note or leave the slot empty. Historical milestones may be +documented retrospectively only with their actual date and evidence. + +| Day | Organic action and deliverable | Publish condition / fallback | Owner | Registry IDs | Release rows | +| ---: | --- | --- | --- | --- | --- | +| `1` | Freeze concept sentence, audiences, conduct policy, source links, channel accounts/permissions, and bilingual glossary | Publish only a repository-foundation note backed by current files; otherwise prepare internally | `INT`, `PQG`, `COMM` | `D1-SPEC-001`, `D1-BRAND-001`, `D1-DOCS-001`, `CTRL-DOCS-073` | `REL-GR01`, `REL-GR07`, `REL-DOC05`, `REL-DOC13` | +| `2` | Show nodes, typed data edges, explicit entrypoints, and cross-language contract; solicit one contract review | Use current canonical fixture; do not claim general builders/YAML or component hashes until `D2-BUILDERS-YAML-020` | `INT`, `TSR/PYR`, `PQG` | `D1-SPEC-001`, `D2-BUILDERS-YAML-020` | `REL-DOC05-DOC06`, `REL-GR02` | +| `3` | Run the current Quickstart internally from a clean checkout; draft EN/ZH 60-second script and tester form | Public “under five minutes” claim waits for `REL-Q10`; failures become onboarding issues | `PQG`, `TSR/PYR` | `D1-PLATFORM-001`, `D3-CLI-002`, `D3-PY-CLI-021` | `REL-DOC01`, `REL-Q10`, `REL-GR06` | +| `4` | Demonstrate chain versus diamond and explain real versus fake edges; capture deterministic topology | Use actual fixture/events; label static visualization versus future Explorer | `PQG`, `TSR/PYR` | `D4-TS-PRIMITIVES-003`, `D4-PY-PRIMITIVES-004`, `D5-CLI-VISUALIZE-005` | `REL-DOC03`, `REL-DOC06`, `REL-GR02` | +| `5` | Teach pipeline versus barrier and publish benchmark method before results | Release result only after pipeline conformance and reproducibility review; otherwise explain test design | `TSR/PYR`, `PQG` | `D7-PIPELINE-CONFORMANCE-013`, `D15-PERFORMANCE-061` | `REL-DOC07`, `REL-Q07`, `REL-GR02` | +| `6` | Explain structured failures, deterministic routing, and why null is not a failure value; open scoped feedback thread | Claim only implemented primitive/runtime behavior; integrated durable quorum waits remain withheld | `INT`, `PQG` | `D5-TS-ROUTER-005`, `D5-PY-ROUTER-005`, `D6-ROUTER-BARRIER-023` | `REL-DOC05`, `REL-DOC10`, `REL-GR07` | +| `7` | Alpha 1 + discovery/loop concept beat; record star/install/run snapshots as outcomes | Source alpha exists, but bounded cycles/discovery demo publish only after its exact fixture passes; otherwise retrospective alpha note | `RM`, `PQG`, `COMM` | `D5-LAUNCH-READINESS-009`, `D7-CYCLE-CONFORMANCE-027` | `REL-GR02-GR04`, `REL-DOC14`, `REL-RC03` | +| `8` | Publish bounded retry/cancellation failure injection and invite adversarial reproductions | Wait for chaos evidence; any unbounded path pauses promotion and opens P0/P1 incident | `TSR/PYR`, `SRV`, `PQG` | `D8-CHAOS-OPS-030` | `REL-DOC08`, `REL-DOC10`, `REL-Q08`, `REL-GR06` | +| `9` | Crash/resume beat with an unedited failure-and-recovery trace and explicit local-DAG scope | Current narrow recovery may be shown if revision-bound; lease/replay/fork claims wait for D9 extension | `TSR/PYR`, `PQG` | `D6-DURABLE-CONFORMANCE-011`, `D9-DURABLE-EXT-CONFORMANCE-034` | `REL-DOC02`, `REL-DOC05`, `REL-DOC08`, `REL-GR02` | +| `10` | Explain budget/model-routing contract and publish cost methodology, not speculative savings | No savings/performance claim until shared budget fixtures and provider accounting pass | `INT`, `TSR/PYR`, `PQG` | `D10-BUDGET-CONFORMANCE-038` | `REL-DOC05`, `REL-DOC07`, `REL-GR02` | +| `11` | Verifier/reflection/citation demo; ask users to try to refute one finding | Publish only candidate-bound votes/evidence including unknown/abstention; otherwise design note | `TSR/PYR`, `PQG`, `SRV` | `D11-VERIFY-CONFORMANCE-043`, `PATTERN-02-CITED`, `PATTERN-04-DIFF` | `REL-PAT02`, `REL-PAT04`, `REL-DOC08`, `REL-GR02` | +| `12` | Security/isolation architecture note and threat-model review invitation | Never present target capability/worktree/process/container controls as implemented; private disclosure path only for vulnerabilities | `INT`, `SRV`, `PQG` | `D12-ISOLATION-SPEC-044`, `D12-ISOLATION-REDTEAM-047` | `REL-DOC05`, `REL-DOC10`, `REL-GR07`, `REL-SUP02` | +| `13` | Dual-language Alpha 2 beat, side-by-side TS/Python run, Graph Ready preview, outcome snapshot | Alpha 2 label and Graph Ready badge require exact candidate and passing gates; otherwise parity progress note | `RM`, `TSR/PYR`, `PQG` | `D13-DX-051`, `D14-API-FREEZE-050` | `REL-DOC06`, `REL-DOC09`, `REL-GR02-GR04` | +| `14` | Release executable roadmap tranche and ten-pattern map; invite scoped pattern contributions | Label skeleton versus complete bundle; no directory-count success claim | `PQG`, `TSR/PYR`, `COMM` | `D14-PATTERN-SKELETONS-053`, `CTRL-PATTERNS-071`, `PATTERN-01-RESEARCH` through `PATTERN-10-ECOSYSTEM` | `REL-PAT00-PAT10`, `REL-DOC04`, `REL-DOC10`, `REL-GR07` | +| `15` | Benchmarks + Explorer beat with interactive linear/graph and recovery views | Publish only reproducible candidate-bound data and tested deployment; >10% unexplained regression blocks | `PQG`, `TSR/PYR` | `D15-EXPLORER-060`, `D15-PERFORMANCE-061` | `REL-DOC03`, `REL-DOC07`, `REL-DOC11`, `REL-Q07`, `REL-GR02` | +| `16` | Share security-testing method and known-limit draft; recruit last external usability sessions | No “secure” blanket claim; seeded-secret or high/critical failure stops campaign and invokes incident path | `SRV`, `PQG`, `COMM` | `D16-SECURITY-062`, `D17-BETA-063` | `REL-Q08`, `REL-SUP05-SUP06`, `REL-GR06` | +| `17` | Tester-backed Beta beat, first-success cohort results, authentic adopter/trace entries, outcome snapshot | Requires real consented testers and no P0/P1 defect; otherwise continue private/RC testing without “tester-backed” claim | `RM`, `PQG`, `EXT` | `D17-BETA-063`, `CTRL-GROWTH-072` | `REL-Q10-Q11`, `REL-DOC08`, `REL-DOC12`, `REL-GR02-GR05` | +| `18` | Publish compatibility/scale report and one success plus one failure case; finish channel drafts | Candidate matrix, raw benchmark data, consent, and claim audit required | `INT`, `TSR/PYR`, `PQG` | `D18-COMPAT-BENCH-064`, `CTRL-DOCS-073` | `REL-Q05-Q07`, `REL-DOC07-DOC08`, `REL-DOC13-DOC14` | +| `19` | RC + security story, known-limit manifest, clean install/upgrade evidence; prebrief support | Stable language forbidden; use RC unless every stable gate is Green | `RM`, `SRV`, `PQG`, `SUP` | `D19-RC-065`, `D16-SECURITY-062` | `REL-DOC15-DOC16`, `REL-GR02`, `REL-SUP01-SUP08`, `REL-RC01-RC10` | +| `20` | Provenance explainer, source-to-package verification, final bilingual/channel audit, launch rehearsal | No package/install CTA to unverified coordinates; external registry/site authority must be demonstrated | `RM`, `SRV`, `PQG` | `D20-PROVENANCE-066`, `CTRL-ACCEPTANCE-070`, `CTRL-DOCS-073` | `REL-Q08-Q09`, `REL-DOC13-DOC16`, `REL-GR01-GR07`, `REL-SUP01-SUP08` | +| `21` | Coordinated GitHub/site/content/community release or transparent complete-RC launch; staff support; capture outcome snapshot | Publish stable only on conjunctive Green decision; otherwise say complete RC and list blockers. Report 6,000+ only if observed | `RM`, `COMM`, `SUP`, all lanes | `D21-RELEASE-067`, `CTRL-GROWTH-072`, `CTRL-ACCEPTANCE-070`, `CTRL-PATTERNS-071` | `REL-GR01-GR07`, `REL-SUP01-SUP08`, `REL-RC01-RC10`, `REL-V1-01-V1-08` | + +## 9. Launch-day operating sequence + +Each step below is an action, not a current completion claim. + +### T minus 24 hours + +1. `RM` freezes candidate revision, version, artifact manifest, known-limit list, + and stable/RC decision input. Mapping: `D19-RC-065`, + `D20-PROVENANCE-066`; `REL-DOC16`, `REL-RC01-RC09`. +2. `SRV` verifies security/provenance evidence and confirms there is no open + accepted-high/critical exception hidden from copy. Mapping: + `D16-SECURITY-062`, `D20-PROVENANCE-066`; `REL-Q08-Q09`, `REL-SUP05-SUP06`. +3. `PQG` reruns every published command/link and bilingual claim against the + frozen identity. Mapping: `CTRL-DOCS-073`, `CTRL-ACCEPTANCE-070`; + `REL-DOC01-DOC16`. +4. `COMM` validates channel rules, owners, disclosure, accessibility, UTM + values, and scheduled-copy identity. Mapping: `CTRL-GROWTH-072`; + `REL-GR01-GR02`, `REL-DOC14`. +5. `SUP` acknowledges coverage and performs issue/discussion/incident/security + route dry runs. Mapping: `D21-RELEASE-067`; `REL-SUP01-SUP08`. + +### Launch window + +1. Publish GitHub Release and canonical site/README first, using stable or RC + exactly as decided. Mapping: `D21-RELEASE-067`; `REL-DOC16`, + `REL-RC02-RC06`. +2. Verify every install link and checksum from an unauthenticated clean + environment before external amplification. Mapping: `D20-PROVENANCE-066`; + `REL-Q09`, `REL-PKG01-PKG11`. +3. Release channel posts in a staggered order so support can observe failures: + owned GitHub, one primary technical community, then social/tutorial channels. + Mapping: `CTRL-GROWTH-072`; `REL-GR01-GR02`, `REL-GR06`. +4. Pin one limitations/support message and answer technical questions with links + to exact evidence. Mapping: `D21-RELEASE-067`; `REL-SUP02`, `REL-SUP04`. +5. Record aggregate, timestamped snapshots; do not store raw prompt/run content + or private messages. Mapping: `CTRL-GROWTH-072`; `REL-GR03-GR05`, + `REL-SUP06`. + +### T plus 2, 6, and 24 hours + +At each checkpoint `RM`, `COMM`, `SUP`, and one runtime owner review install +failures, successful runs, open P0/P1 defects, support age, security reports, +channel moderation, and privacy complaints. They choose exactly one state: + +- **continue:** candidate and funnel health meet thresholds; +- **narrow:** pause weak channels and focus on supported use cases; +- **pause promotion:** installs do not become successful runs, support is + overloaded, or evidence is ambiguous; +- **incident:** safety, security, data loss, corrupt artifact, package defect, + or false candidate identity is suspected; or +- **retract/supersede copy:** a claim or link is false even when code is sound. + +Mapping: `D21-RELEASE-067`, `CTRL-GROWTH-072`; `REL-GR06`, +`REL-SUP01-SUP08`, `REL-RC06-RC10`. + +## 10. External authority and consent gates + +Planning does not confer account access or consent. + +| External action | Minimum authority/evidence before action | If absent | Mapping | +| --- | --- | --- | --- | +| Push/tag/GitHub Release/change repository settings | Authenticated repository role, protected-branch/release policy, exact candidate decision | Prepare draft only; do not imply publication | `D19-RC-065`, `D21-RELEASE-067`; `REL-SC01`, `REL-RC02` | +| Publish npm/PyPI | Verified package ownership/trusted publisher and provenance rehearsal | Link source install only if truthful; do not publish/squat | `D20-PROVENANCE-066`; `REL-Q09`, `REL-SUP03` | +| Deploy site/showcase | Hosting authority, candidate-bound deployment, smoke/accessibility/privacy check | Keep source-local preview; no live URL claim | `D15-EXPLORER-060`, `D21-RELEASE-067`; `REL-DOC03`, `REL-DOC11`, `REL-DOC16` | +| Post to social/community accounts | Authorized human owner, current rules reviewed, disclosure accepted | Save draft; do not automate or impersonate | `CTRL-GROWTH-072`; `REL-GR01-GR02`, `REL-DOC14` | +| Publish adopter logo/name/quote/trace | Explicit scoped consent, redaction review, source/URL, withdrawal contact | Omit entry; never create a placeholder that looks real | `D17-BETA-063`; `REL-DOC08`, `REL-DOC12`, `REL-GR07`, `REL-SUP06` | +| Send a personalized invitation | Publicly relevant contact route or prior consent; manual individualized message | Do not scrape, buy, infer private address, or repeatedly follow up | `CTRL-GROWTH-072`; `REL-GR01`, `REL-GR04` | +| Publish external tester result | Consent for aggregate/public use, sampling notes, anonymization, exact candidate | Report “evidence not available”; stable remains blocked where required | `D17-BETA-063`; `REL-Q10-Q11`, `REL-GR04-GR05` | +| Publish security finding/resolution | Security owner approval and disclosure timeline; no live exploit/secret | Use private security path and generic status message | `D16-SECURITY-062`; `REL-SUP02`, `REL-SUP05-SUP06` | + +## 11. Support, crisis, and reputation response + +### Severity and first response + +| Severity | Trigger | Immediate action | Public posture | Registry / rows | +| --- | --- | --- | --- | --- | +| `SEV0` | Credential/secret exposure, isolation escape, malicious package, active supply-chain compromise | Stop all promotion and affected distribution; preserve redacted evidence; rotate/revoke through authorized humans; security incident lead takes control | Short verified status only; no exploit details before coordinated disclosure | `D16-SECURITY-062`, `D20-PROVENANCE-066`; `REL-Q08-Q09`, `REL-SUP02`, `REL-SUP05-SUP06`, `REL-RC06-RC09` | +| `SEV1` | Data loss/corruption, duplicate non-idempotent effect, unusable package, widespread install/run failure, false stable identity | Pause campaign; deprecate/disable safely; publish workaround/known limit; open incident | Acknowledge impact, scope, workaround, next update; no blame or false ETA | `D21-RELEASE-067`; `REL-SUP03-SUP05`, `REL-SUP08`, `REL-RC06-RC10` | +| `SEV2` | Reproducible functional defect or major docs mismatch with bounded workaround | Stop the affected CTA/channel; triage owner and fix/forward version | Correct the claim/link and connect reports to one canonical issue | `D18-COMPAT-BENCH-064`, `D21-RELEASE-067`; `REL-GR06`, `REL-SUP02`, `REL-SUP04` | +| `SEV3` | Question, feature request, isolated confusion, civil criticism | Triage, reproduce, answer with evidence, label/route | Thank reporter, avoid defensiveness, state current boundary | `CTRL-GROWTH-072`; `REL-GR07`, `REL-SUP07` | + +Security reports never move to public issues merely to improve response metrics. +Harassment, doxxing, or code-of-conduct violations are moderated under the +published governance path and excluded from growth experiments. Response p50 +below twelve hours is a controlled support goal; it does not authorize rushed, +unsafe fixes. If p50 exceeds twelve hours or the oldest ordinary launch item +exceeds twenty-four hours, stop new promotional beats until the queue returns +to capacity. Mapping: `D21-RELEASE-067`; `REL-SUP01-SUP02`, `REL-SUP05`, +`REL-SUP07`, `REL-GR06-GR07`. + +## 12. Launch decision and evidence packet + +The launch owner creates one immutable/superseding evidence packet per beat. It +must contain: + +- beat ID, candidate version/revision, asset digest, and actual publication + timestamp or explicit `not_published` state; +- proof source for every feature, benchmark, adoption, or security claim; +- English/Chinese/channel copy revisions and claim-review identities; +- channel URL, disclosure, rules-check date, authorized owner, and UTM values; +- aggregate metric snapshot, data source, query/collection method, dedup rule, + privacy/retention notes, and missing-data label; +- support/incident state and whether any pause/stop condition fired; +- external consent/authority references without embedding private messages, + secrets, raw prompts, or user data; and +- reviewer, decision, exceptions, next review, and superseded packet link. + +Expected evidence belongs under the future +`codex_logs/release-evidence/growth/` structure defined by the delivery +controls; this plan does not create or fabricate those records. Mapping: +`CTRL-GROWTH-072`, `CTRL-ACCEPTANCE-070`, `D21-RELEASE-067`; +`REL-GR01-GR07`, `REL-DOC12-DOC16`, `REL-SUP01-SUP08`. + +## 13. Exit conditions + +### Growth-plan execution is complete only when + +- every required asset has an accepted evidence packet or is explicitly listed + as missing; `REL-GR02` and `REL-DOC01-DOC16` cannot become Green from a draft; +- `REL-GR01` has an organic-conduct/disclosure attestation; +- `REL-GR03-GR05` have timestamped, privacy-safe snapshots with stable metric + definitions and honest missing values; +- `REL-GR06` has at least one recorded funnel diagnosis and owner decision; +- `REL-GR07` and `REL-SUP01-SUP08` have workflow dry-run/operation evidence; +- external adopters, galleries, usability reports, and permissions are real and + consented; and +- the stable/RC label matches the final conjunctive release decision. + +### It is not complete merely because + +- these documents exist; +- a release tag or social draft exists; +- the repository receives any particular number of stars; +- a maintainer successfully runs the project; +- a skeleton, mock, screenshot, or generated testimonial resembles adoption; or +- an external account/registry could theoretically be accessed. + +If Day 21 arrives with a sound candidate but incomplete stable evidence, the +correct launch is an accurately labeled complete RC with a blocker manifest and +continuing support. If required launch assets themselves are incomplete, even +the complete-RC asset gate stays Open. Mapping: `D21-RELEASE-067`; +`REL-RC01-RC10`, `REL-V1-01-V1-08`. diff --git a/codex_plans/growth/metrics-and-experiments.md b/codex_plans/growth/metrics-and-experiments.md new file mode 100644 index 0000000..2b7b559 --- /dev/null +++ b/codex_plans/growth/metrics-and-experiments.md @@ -0,0 +1,530 @@ +# Graph Engineering organic metrics and experiment protocol + +Status: **Measurement design prepared; dashboard, observations, and experiment results are not yet implemented or collected** +Plan epoch: **2026-07-26, America/Vancouver** +Primary registry owner: `CTRL-GROWTH-072` (`planned`, dependency-blocked) +Document owner: `CTRL-DOCS-073` (`in_progress`) +Inputs: [master plan Section 7](../Graph-Engineering-21-Day-Master-Plan.md#7-organic-launch-and-6000-star-target), [release checklist](../delivery/release-checklist.md), [launch plan](./launch-plan.md), and [content calendar](./content-calendar.md) + +## 1. Measurement objective and non-negotiable rules + +Measurement exists to improve first success, repeated value, authentic adoption, +contribution, and support. It is not a justification for surveillance or +manufactured popularity. + +1. Product telemetry and prompt/response capture remain **off by default**. + This plan does not authorize enabling them. Mapping: `D16-SECURITY-062`, + `CTRL-GROWTH-072`; `REL-GR05`, `REL-SUP06`. +2. Raw prompts, model responses, graph inputs/outputs, credentials, + authorization headers, secrets, private support messages, and user data are + not growth metrics. Mapping: `D16-SECURITY-062`; `REL-Q08`, `REL-SUP06`. +3. `6,000+` stars on Day 21 is an observed stretch outcome only. The Day + 7/13/17 checkpoints of 300/1,000/2,000 are also non-blocking. No stars are + purchased, automated, reciprocated, rewarded, or fabricated. Mapping: + `CTRL-GROWTH-072`; `REL-GR01`, `REL-GR03`. +4. Stable-v1 does not depend on stars. Security, recovery, cross-language + conformance, provenance, external usability, and every mandatory checklist + row remain conjunctive. Mapping: `D21-RELEASE-067`; + `REL-RC01-RC10`, `REL-V1-01-V1-08`. +5. Every number has a definition, time window, source, query/manual method, + deduplication rule, privacy class, owner, and timestamp. Unknown is reported + as `not_available`, never zero and never estimated without a labeled model. + Mapping: `CTRL-GROWTH-072`; `REL-GR03-GR05`. +6. External adoption, consent, account analytics, registry downloads, hosting + logs, and elapsed seven-day retention cannot be generated from repository + work. They remain `External` until real evidence exists. Mapping: + `D17-BETA-063`, `D20-PROVENANCE-066`; `REL-Q09-Q11`, `REL-GR04-GR05`. + +## 2. Status and result vocabulary + +| Label | Meaning | +| --- | --- | +| `defined` | This document specifies the metric or experiment; no collection/result implied | +| `instrumented` | Collection/query is implemented, reviewed, and has positive/negative tests | +| `observed` | A timestamped value from an identified source exists | +| `validated` | Definition, source, deduplication, privacy, and candidate binding were independently reviewed | +| `not_available` | Source, permission, elapsed time, sample, or instrumentation does not exist | +| `invalid` | Data violated definition/privacy/integrity rules and is excluded with a reason | + +All metrics and experiments in this file are currently `defined` only unless a +separate release-evidence record proves otherwise. The current public alpha and +existing Quickstart do not imply any install, run, retention, adopter, or +contributor result. Mapping: `CTRL-DOCS-073` versus `CTRL-GROWTH-072`; +`REL-GR03-GR05` remain Open. + +## 3. Measurement hierarchy + +### Tier 0 — mandatory guardrails + +- zero manufactured/paid/reciprocal growth; +- zero known secret/prompt/user-data capture by growth collection; +- zero unsupported product/release/adoption claims; +- no unresolved P0/P1 defect during broad promotion; +- no unaccepted high/critical security finding; and +- support capacity within its stop thresholds. + +Any Tier-0 breach stops the affected experiment or campaign regardless of +conversion. Mapping: `D16-SECURITY-062`, `D21-RELEASE-067`; +`REL-GR01`, `REL-GR06`, `REL-Q08`, `REL-Q11`, `REL-SUP01-SUP08`. + +### Tier 1 — product north stars + +1. weekly successful external graph runs; +2. seven-day retained repositories/cohort members; +3. time to first successful run; +4. authentic external adopters; and +5. non-maintainer merged PRs. + +Mapping: `CTRL-GROWTH-072`; `REL-GR05`. + +### Tier 2 — activation and community outcomes + +- CLI/package downloads, successful or self-reported runs, external usability + reports, outside contributors, external PRs, personalized trial invitations, + and support-response p50. +- Master-plan targets: 2,000 downloads, 500 successful/self-reported runs, ten + public adopters, ten outside contributors, twenty-five external PRs, ten + manual personalized invitations, and response p50 below twelve hours. + +These are influenceable goals, not guaranteed results. Mapping: +`CTRL-GROWTH-072`, `D17-BETA-063`; `REL-GR04`, `REL-Q10-Q11`, +`REL-SUP07`. + +### Tier 3 — awareness diagnostics + +Qualified repository/site visits, Quickstart link clicks, content referral +sessions, organic stars, forks, and discussion engagement help diagnose the +funnel. They do not prove installation, success, or retention. Mapping: +`CTRL-GROWTH-072`; `REL-GR03`, `REL-GR06`. + +## 4. Metric dictionary + +### 4.1 Guardrail and release metrics + +| Metric ID | Definition / formula | Source and cadence | Privacy / integrity rule | Owner | Registry / rows | +| --- | --- | --- | --- | --- | --- | +| `M-GUARD-01 organic_conduct_breaches` | Count of confirmed paid, bot, reciprocal, reward-for-star, fake-adopter, spam, or undisclosed-promotion events; target `0` | Disclosure audit per beat and final attestation | Retain minimal incident facts; never normalize a breach as “campaign traffic” | `COMM`, `RM` | `CTRL-GROWTH-072`; `REL-GR01` | +| `M-GUARD-02 unsupported_claims` | Count of published statements that lack matching candidate evidence or strengthen target to implemented; target `0` | Content-manifest diff before publish and correction review daily | Store copy digest/URL and correction, not private drafting conversation | `INT`, `PQG` | `CTRL-DOCS-073`; `REL-DOC13-DOC16`, `REL-GR02` | +| `M-GUARD-03 open_p0_p1` | Open accepted P0/P1 product defects on candidate; broad-promotion threshold `0` | Candidate defect query at every launch checkpoint | Security issues reported only as redacted aggregate/status | `RM`, `SUP`, `SRV` | `D17-BETA-063`, `D21-RELEASE-067`; `REL-Q11`, `REL-SUP05` | +| `M-GUARD-04 high_critical_security` | Unaccepted high/critical findings; release/promotion threshold `0` | Candidate security report before D16/D20/D21 beats | Restricted raw report; public aggregate cannot expose exploit or secret | `SRV` | `D16-SECURITY-062`; `REL-Q08`, `REL-SUP05-SUP06` | +| `M-GUARD-05 support_capacity` | Issue/discussion response p50 and oldest ordinary unacknowledged launch item | Daily; `created_at` to first substantive maintainer response | Exclude bots; report private security queue separately without content | `SUP`, `COMM` | `D21-RELEASE-067`; `REL-GR04`, `REL-SUP01-SUP02`, `REL-SUP07` | + +### 4.2 Awareness metrics + +| Metric ID | Definition / formula | Source and deduplication | Privacy / caveat | Owner | Registry / rows | +| --- | --- | --- | --- | --- | --- | +| `M-AWR-01 qualified_visits` | Aggregate GitHub repository visitors plus site sessions that reach a technical/Quickstart surface; report sources separately, never sum incompatible uniques | Authorized GitHub Traffic snapshot and privacy-reviewed site aggregate, daily | GitHub window/permissions may limit history; no fingerprint or cross-site identity | `COMM` | `CTRL-GROWTH-072`; `REL-GR05-GR06` | +| `M-AWR-02 quickstart_clicks` | Aggregate navigation to canonical Quickstart by UTM content family | First-party aggregate event or hosting log after policy review; one event per session where available | No user ID, full IP, raw query, referrer path with sensitive data, or product telemetry | `COMM`, `SRV` | `CTRL-GROWTH-072`; `REL-GR05`, `REL-SUP06` | +| `M-AWR-03 organic_stars` | Public GitHub stargazer count at timestamp; checkpoints 300/1,000/2,000/6,000+ are stretch | Public count snapshot at Day 7/13/17/21; value is stock, change is difference between comparable snapshots | Awareness only; do not infer unique active users or causality | `COMM` | `CTRL-GROWTH-072`; `REL-GR03` | +| `M-AWR-04 forks` | Public fork count at timestamp and change over window | Public GitHub count snapshot | Fork is intent/experimentation, not success or retention | `COMM` | `CTRL-GROWTH-072`; `REL-GR05-GR06` | +| `M-AWR-05 channel_referrals` | Aggregate sessions/clicks grouped by allowed `utm_source`, `utm_medium`, campaign, and content ID | First-party aggregate only; unknown/direct retained separately | Never identify individual visitor or combine into behavioral profile | `COMM`, `SRV` | `CTRL-GROWTH-072`; `REL-GR05-GR06`, `REL-SUP06` | + +### 4.3 Activation and usability metrics + +| Metric ID | Definition / formula | Source and deduplication | Privacy / caveat | Owner | Registry / rows | +| --- | --- | --- | --- | --- | --- | +| `M-ACT-01 cli_downloads` | npm package download events plus PyPI file-download events during window, shown both separately and as a labeled arithmetic total; target total `2,000` | Official registry aggregate API/UI after legitimate publication; exclude maintainer/test downloads only when source supports it | Downloads are requests, not people, installs, or success; mirrors/caches/bots may inflate | `COMM`, package owners | `D20-PROVENANCE-066`; `REL-GR04`, `REL-Q09` | +| `M-ACT-02 accepted_first_run_attempts` | External participants/repositories that begin the frozen clean Quickstart under study protocol | Consented session form; one accepted attempt per participant/repository/candidate | Minimal cohort key held privately; public report aggregate/anonymized | `PQG`, `EXT` | `D17-BETA-063`; `REL-Q10-Q11`, `REL-GR04` | +| `M-ACT-03 first_run_success_rate` | `accepted attempts reaching documented success / accepted attempts`; stable gate >=80%, minimum five reports | Timed external study transcript/form; failures remain denominator | Exclude maintainer/agent/CI runs; publish sampling and confidence limitation | `PQG`, `EXT` | `D17-BETA-063`, `CTRL-ACCEPTANCE-070`; `REL-Q10-Q11` | +| `M-ACT-04 time_to_first_success` | Elapsed minutes from first documented command to first expected successful result; report median, p80, range, N | External stopwatch/session form, candidate-bound | Pause time only under predeclared study rule; never infer from web tracking | `PQG`, `EXT` | `D17-BETA-063`; `REL-GR05`, `REL-Q10` | +| `M-ACT-05 successful_or_self_reported_runs` | Disjoint sum of verified external study successes, consented public trace submissions, and explicit self-reports; target `500` | Source precedence prevents double count: study > trace > self-report. If cross-source identity cannot be resolved privately, report lower/upper bound instead of a false exact count | CI, maintainer, demo, generated, and duplicate reports excluded; no default runtime telemetry | `PQG`, `COMM`, `EXT` | `CTRL-GROWTH-072`, `D17-BETA-063`; `REL-GR04-GR05`, `REL-SUP06` | +| `M-ACT-06 activation_issue_rate` | Accepted first-run attempts with install/config/runtime failure divided by accepted attempts, categorized by stage | External cohort plus canonical launch-labeled issues | Error excerpts must be sanitized; raw prompts/data not requested | `SUP`, runtime owner | `D17-BETA-063`; `REL-Q10-Q11`, `REL-GR06`, `REL-SUP06` | + +### 4.4 Retention, adoption, and contribution metrics + +| Metric ID | Definition / formula | Source and deduplication | Privacy / caveat | Owner | Registry / rows | +| --- | --- | --- | --- | --- | --- | +| `M-RET-01 seven_day_eligible` | External repositories/participants with a verified first success at least seven full days before observation | Consented tester/adopter cohort or public repository evidence | Elapsed time cannot be accelerated; cohort inclusion frozen at Day 0 | `PQG`, `EXT` | `D17-BETA-063`, `CTRL-GROWTH-072`; `REL-GR05` | +| `M-RET-02 seven_day_retained` | Eligible cohort with a second useful successful run or maintained integration between Day 7 and Day 13 after first success | Consented follow-up or public commit/run evidence; one per repo/participant | A star, page visit, or unchanged dependency does not count as retained use | `PQG`, `EXT` | `CTRL-GROWTH-072`; `REL-GR05` | +| `M-RET-03 seven_day_retention_rate` | `seven_day_retained / seven_day_eligible`; report N and unavailable until cohort matures | Same frozen cohort; no survival-model estimate presented as observed | Do not chase non-consenting users or infer private repository activity | `PQG`, `EXT` | `CTRL-GROWTH-072`; `REL-GR05` | +| `M-ADOPT-01 public_adopters` | Distinct external people/organizations with public or explicitly consented evidence of a useful Graph Engineering integration/run; target `10` | Canonical public URL or scoped consent record; dedup by adopter, not posts | No fake placeholder, testimonial, logo, or inferred use from a star/fork/download | `COMM`, `EXT` | `D17-BETA-063`; `REL-DOC08`, `REL-DOC12`, `REL-GR04`, `REL-GR07` | +| `M-CONTRIB-01 outside_contributors` | Distinct non-maintainer, non-bot humans with an accepted issue reproduction, docs/code/pattern contribution, or merged PR; target `10` | GitHub actor and governance-defined contribution classes; one human once | Public account only; do not deanonymize or merge identities across accounts | `COMM` | `D21-RELEASE-067`; `REL-GR04`, `REL-GR07` | +| `M-CONTRIB-02 external_prs_opened` | PRs opened by non-maintainer/non-bot contributors during epoch; target `25`; report opened/closed/merged separately | GitHub PR metadata; dedup by PR number | Volume is not quality; automated dependency PRs excluded | `COMM` | `D21-RELEASE-067`; `REL-GR04-GR05`, `REL-GR07` | +| `M-CONTRIB-03 nonmaintainer_prs_merged` | External PRs merged after normal review, reported as count and rate over eligible external PRs | GitHub PR metadata and CODEOWNERS review evidence | Never lower review/safety bar to improve metric | `COMM`, lane owners | `CTRL-GROWTH-072`; `REL-GR05`, `REL-GR07` | +| `M-COMM-01 trial_invitations` | Manual, relevant, personalized opt-in invitations sent; target maximum and goal `10` | Consent-respecting outreach log with recipient category/reason/status, not message body | No scraping, purchased lists, bulk DM, repeat pressure, star ask, or assumed endorsement | `COMM` | `CTRL-GROWTH-072`; `REL-GR01`, `REL-GR04` | + +## 5. UTM and referral contract + +UTM exists only to compare aggregate channel/content families. It must never +carry a username, email, repository name, issue number tied to a private person, +message ID, prompt, experiment subject ID, secret, or free-form personal data. + +### Allowed fields + +| Field | Allowed values / pattern | Example | Rule | +| --- | --- | --- | --- | +| `utm_source` | `github`, `hackernews`, `x`, `linkedin`, `reddit`, `devto`, `cn_`, `invite`, `direct` | `hackernews` | Lowercase controlled vocabulary; a community slug describes channel, not person | +| `utm_medium` | `owned`, `organic_social`, `community`, `tutorial`, `referral`, `manual_invite` | `community` | No `paid`, because paid promotion is outside this plan and undisclosed promotion is forbidden | +| `utm_campaign` | `ge_21d_2026_07` or versioned successor | `ge_21d_2026_07` | Shared campaign identifier, no user cohort identity | +| `utm_content` | content manifest ID matching `^[a-z0-9_]{1,64}$` | `d09_recovery_en_hn` | Identifies asset/language/channel variant only | +| `utm_term` | Omitted | — | No keywords, names, or audience microtargeting in this launch | + +Mapping: `CTRL-GROWTH-072`; `REL-GR01`, `REL-GR05-GR06`, +`REL-SUP06`. + +### Collection boundary + +1. Prefer aggregate platform/source analytics and coarse daily counts. +2. If the project site is deployed, a privacy/security review must approve the + hosting-log and aggregate-event path before collection. No cookies, + fingerprinting, cross-site profile, or product-run telemetry is introduced + by default. +3. Normalize and count allowed UTM values, then discard or redact raw query + strings according to the approved retention policy. Unknown values become + `other_invalid`; they are not stored verbatim. +4. Do not join UTM events to GitHub identities, package downloads, support + messages, adopter records, or runtime data to reconstruct an individual + journey. +5. Report attribution as directional. Last-click or source counts do not prove + causality, and incomparable platform “impressions” remain separate. + +Implementation is not current. It maps to `CTRL-GROWTH-072` after +`D15-EXPLORER-060`; privacy validation maps to `D16-SECURITY-062`; +`REL-GR05-GR06`, `REL-SUP06` remain Open. + +## 6. Privacy, consent, retention, and access + +| Data class | Examples | Collection rule | Retention / publication | Access | Mapping | +| --- | --- | --- | --- | --- | --- | +| `Public aggregate` | timestamped stars, forks, registry download totals, public PR counts | May collect from identified public source | Keep dated snapshots and source/method; publish aggregates | `COMM`, reviewers | `REL-GR03-GR05` | +| `Authorized aggregate` | GitHub Traffic, hosting aggregate, platform post analytics | Requires account authority and rule/privacy review | Retain minimal exported aggregate for campaign comparison; no raw visitor log in repo | Authorized `COMM`, `SRV` reviewer | `REL-GR05-GR06`, `REL-SUP06` | +| `Consented research` | timed Quickstart, success/failure, seven-day follow-up | Explicit purpose, scope, optional public use, withdrawal route | Restricted local evidence for approved period; publish anonymized aggregate; delete/withdraw as policy requires | Named study owner/reviewer | `D17-BETA-063`; `REL-Q10-Q11`, `REL-GR05` | +| `Consented public adoption` | adopter name/logo/quote/public trace | Separate scoped publication consent and redaction review | Publish only approved fields; record consent version and withdrawal action | `COMM`, `SRV`, adopter | `REL-DOC08`, `REL-DOC12`, `REL-GR07` | +| `Restricted support/security` | private issue details, vulnerability report, sanitized diagnostics | Collect only what is necessary under support/security policy | Never copy into growth dashboard; publish status/aggregate only | `SUP`/`SRV` need-to-know | `REL-SUP02`, `REL-SUP05-SUP06` | +| `Forbidden growth data` | raw prompts/outputs, credentials, auth headers, full IP/referrer/query logs, scraped/private contacts | Do not collect for growth | If accidentally captured, stop, isolate, follow incident/deletion policy | `SRV` | `D16-SECURITY-062`; `REL-Q08`, `REL-SUP06` | + +Consent must be affirmative, purpose-specific, revocable, and separate from a +request to star, promote, or contribute. Declining cannot reduce support. An +adopter can approve aggregate study use without approving name/logo/quote. A +security reporter is never converted into a marketing lead. Mapping: +`CTRL-GROWTH-072`, `D17-BETA-063`; `REL-GR01`, `REL-GR07`, +`REL-SUP02`, `REL-SUP06`. + +## 7. Evidence record and dashboard design + +### Snapshot record + +Every metric snapshot must provide these fields: + +```text +snapshot_id +observed_at_utc +campaign_day +candidate_version +candidate_revision +metric_id +window_start_utc +window_end_utc +value | numerator+denominator | lower+upper_bound | not_available_reason +unit +source_name +source_url_or_restricted_reference +collection_method_revision +deduplication_revision +privacy_class +consent_basis_if_applicable +known_biases +collector +reviewer +supersedes +``` + +No raw personal/run/prompt content belongs in this record. The future dashboard +must expose definition and `N`, distinguish stock from flow, show missing data, +and retain prior snapshots instead of silently overwriting them. Mapping: +`CTRL-GROWTH-072`; `REL-GR03-GR05`, `REL-SUP06`. + +### Dashboard panels + +| Panel | Required display | Decision use | Mapping | +| --- | --- | --- | --- | +| Guardrails | conduct incidents, unsupported claims, P0/P1, security status, support age | Immediate stop/go | `REL-GR01`, `REL-GR06`, `REL-Q08`, `REL-Q11`, `REL-SUP07` | +| Awareness | qualified visits by source, Quickstart clicks, stars/forks as timestamped stocks and deltas | Positioning/channel diagnosis only | `REL-GR03`, `REL-GR05-GR06` | +| Activation | downloads by registry, accepted attempts, success rate, time-to-success, run evidence bounds, failure stage | Quickstart/package/reliability decisions | `REL-GR04-GR06`, `REL-Q10-Q11` | +| Retention | eligible cohort, retained count/rate, maturity date | Use-case/product-value decisions | `REL-GR05` | +| Adoption/contribution | adopters, contributors, PR opened/merged/closed, consent state | Community/product loop | `REL-GR04-GR05`, `REL-GR07` | +| Content/experiment | asset state, channel referrals, hypothesis, exposure, decision, stop reason | Stop weak/spammy work; prioritize evidence-backed content | `REL-GR01-GR02`, `REL-GR06` | +| Support | new/acknowledged/resolved, p50, oldest, severity, correction count | Capacity and crisis control | `REL-SUP01-SUP08` | + +The dashboard is not implemented by this document. It remains under +`CTRL-GROWTH-072`; `REL-GR05` stays Open. + +## 8. Checkpoint targets and controlled inputs + +The team can control preparation and response inputs; it can only influence +independent user outcomes. Each checkpoint reports both. + +| Checkpoint | Controlled inputs due | Influenceable outcomes to observe, never guarantee | Decision | Registry / rows | +| --- | --- | --- | --- | --- | +| Day 3 | Canonical Quickstart candidate, EN/ZH draft, clean internal transcript, consent form/study protocol | First external attempts if authority/cohort exists; no invented N | Fix command count/install friction before broader CTA | `D1-PLATFORM-001`, `D17-BETA-063`; `REL-DOC01`, `REL-Q10`, `REL-GR06` | +| Day 7 | Days 1-7 asset manifest, organic-conduct audit, support path, at least three carefully selected invitation candidates (send only if capacity/permission exists) | 300-star stretch snapshot, visits, downloads, reports | Reposition if visits do not reach Quickstart; repair activation before promotion | `D5-LAUNCH-READINESS-009`, `CTRL-GROWTH-072`; `REL-GR01-GR06` | +| Day 13 | Recovery/verifier/Alpha 2 evidence or honest holds, six cumulative invitation candidates, source-specific snapshots | 1,000-star stretch, downloads, successful reports, contributions | Shift to Quickstart/reliability/use-case based on funnel | `D13-DX-051`, `D14-API-FREEZE-050`; `REL-GR02-GR06` | +| Day 17 | Five or more accepted usability reports, exact sampling, ten total relevant invitation candidates, adopter consent workflow, support coverage | 2,000-star stretch, >=80% five-minute success if sample permits, adopters/retention as actually available | P0/P1 or <80% first success pauses broad promotion | `D17-BETA-063`; `REL-Q10-Q11`, `REL-GR01-GR07` | +| Day 21 | Every planned asset terminally accounted for, final claim/disclosure audit, support rota, dashboard snapshots, stable/RC decision | 6,000+ stretch, 2,000 downloads, 500 runs, 10 adopters/contributors/invitations, 25 PRs, p50 <12h—all reported honestly | Continue/narrow/pause regardless of star result; stable label follows gates only | `D21-RELEASE-067`, `CTRL-GROWTH-072`; `REL-GR01-GR07`, `REL-SUP01-SUP08`, `REL-RC01-RC10` | + +## 9. Funnel diagnosis policy + +The daily owner evaluates stages in order. A downstream metric cannot compensate +for an upstream safety or activation failure. + +| Observed pattern | Interpretation to test | Required response | Promotion state | Registry / rows | +| --- | --- | --- | --- | --- | +| Low qualified visits across prepared channels | Concept/distribution mismatch or content gate holds | Interview consenting target users; tighten problem/use-case; test one relevant channel, not more spam | `narrow` | `CTRL-GROWTH-072`; `REL-GR01`, `REL-GR06` | +| High visits, low stars and low Quickstart clicks | Positioning/proof may not be compelling; stars alone are not the desired fix | Clarify concrete problem, current proof, audience, and CTA; run E01 | `continue` only within support capacity | `REL-GR03`, `REL-GR06` | +| Stars, low downloads | Awareness is not converting to trial; package/Quickstart coordinates may be unclear | Audit install links, candidate labels, package availability, command count; run E02 | `narrow` to onboarding | `D20-PROVENANCE-066`; `REL-GR03-GR06`, `REL-Q09-Q10` | +| Downloads, low accepted successful runs | Reliability/config/docs failure or downloads are noisy | **Pause broad promotion**, reproduce failure stages, repair package/Quickstart/runtime, repeat external cohort | `pause` | `D17-BETA-063`; `REL-GR04-GR06`, `REL-Q10-Q11` | +| Successful runs, low seven-day retention | Initial demo lacks recurring use-case/value or reliability | Interview consented successes, prioritize one repeated workflow/pattern; run E04 | `narrow` to use-case evidence | `CTRL-GROWTH-072`, `CTRL-PATTERNS-071`; `REL-GR05-GR06` | +| Retention, low public adoption | Users may need privacy-safe proof path or integration guidance | Offer opt-in anonymized/public case paths; never pressure logo/quote | `continue` | `D17-BETA-063`; `REL-DOC08`, `REL-DOC12`, `REL-GR07` | +| Contributions opened, few merged | Scope/docs/review latency or quality mismatch | Improve issue acceptance criteria, reviewer assignment, and feedback; do not merge for metric | `continue` or `narrow` | `D21-RELEASE-067`; `REL-GR04-GR07`, `REL-SUP07` | +| Support age/p50 above threshold | Campaign exceeds maintainer capacity | Stop next promotional beat; reassign content capacity to triage | `pause` | `REL-GR06`, `REL-SUP01-SUP02`, `REL-SUP07` | +| Any conduct/security/privacy/P0/P1 breach | Guardrail failure | Stop affected collection/promotion/release; incident and correction path | `stop` | `D16-SECURITY-062`, `D21-RELEASE-067`; `REL-GR01`, `REL-Q08`, `REL-Q11`, `REL-SUP05-SUP06` | + +## 10. Experiment protocol + +Every experiment is pre-registered before exposure with hypothesis, eligible +audience, exact variants, primary metric, guardrails, minimum observation, +maximum duration, stopping conditions, analysis method, owner, and candidate. +Only one material variable changes per comparison. Results are directional when +sample size is small; no statistical-significance theater or post-hoc metric +shopping is allowed. + +Global experiment rules: + +- no experiment varies safety language, known limits, release label, consent, + price/access, or support quality; +- no individual tracking, dark patterns, deceptive countdowns, fake social + proof, hidden paid placement, or required star; +- exposure follows community rules and account authority; +- stop immediately on Tier-0 guardrail breach; +- stop broad promotion on installs-without-success, P0/P1, support-capacity, + security/privacy, or package/provenance thresholds; and +- publish a result only with exposure counts, source, window, candidate, + limitations, and stopped/held variants. + +Mapping for the protocol: `CTRL-GROWTH-072`, `D16-SECURITY-062`; +`REL-GR01`, `REL-GR05-GR06`, `REL-SUP06`. + +### E01 — concept/positioning frame + +| Field | Pre-registration | +| --- | --- | +| Hypothesis | A concrete “data dependencies form the graph” frame yields more qualified Quickstart visits than a broad “multi-agent framework” frame | +| Variants | A: problem + real/fake edge; B: native dual-language runtime + durable evidence. Both show identical limitations and CTA | +| Eligible exposure | Two comparable owned/content placements, not simultaneous duplicate posts in the same community | +| Primary metric | Quickstart clicks per qualified content session, by coarse aggregate source | +| Guardrails | Support load, correction rate, no stronger claims, no paid/automated exposure | +| Minimum / maximum | At least 100 aggregate sessions per variant where naturally available; maximum 72 hours. If unavailable, record inconclusive rather than extend spam | +| Stop | Any claim error/community complaint; >2x support capacity; candidate invalidation | +| Decision | Adopt a frame only if direction is consistent and activation quality does not fall; otherwise retain audience-specific frames | +| Map | `CTRL-GROWTH-072`; `REL-DOC14`, `REL-GR01`, `REL-GR05-GR06` | + +### E02 — Quickstart CTA and language path + +| Field | Pre-registration | +| --- | --- | +| Hypothesis | A direct language choice followed by one mock-first path reduces first-success time compared with a feature-heavy landing path | +| Variants | A: choose TypeScript/Python then exact Quickstart; B: concise overview then same choice. Commands and package identity remain identical | +| Eligible exposure | Consented external usability sessions; optional aggregate landing allocation only after privacy review | +| Primary metric | Five-minute success rate and time-to-first-success; clicks are secondary | +| Guardrails | Same known limits; no credential required; telemetry off; failures retained | +| Minimum / maximum | At least five total external reports for gate evidence, strive for ten across language paths; maximum through Day 17 | +| Stop | Any secret request, wrong package coordinate, P0/P1, or aggregate success below 80% once N>=5; pause promotion and fix | +| Decision | Select simpler path only with equal/greater success and no language exclusion; otherwise repair both | +| Map | `D17-BETA-063`, `D20-PROVENANCE-066`; `REL-DOC01`, `REL-DOC13`, `REL-Q09-Q11`, `REL-GR06` | + +### E03 — recovery proof versus happy-path proof + +| Field | Pre-registration | +| --- | --- | +| Hypothesis | An uncut crash/resume proof produces more qualified runs and fewer durability misconceptions than a happy-path-only demo | +| Variants | A: 90-second failure/recovery; B: same graph happy path. Both state current local-DAG scope and at-least-once effects | +| Eligible exposure | Technical tutorial/social audiences after exact recovery evidence passes | +| Primary metric | Accepted run reports per qualified referral; misconception/correction count as guardrail | +| Guardrails | No raw secrets/prompts in trace; no replay/fork/lease/exactly-once overclaim | +| Minimum / maximum | 72 hours or 100 qualified aggregate sessions per variant, whichever occurs first; inconclusive allowed | +| Stop | Redaction/privacy defect, corrupt/duplicate recovery, unsupported claim, or support overload | +| Decision | Use recovery proof only if it improves qualified activation without raising misconception/correction rate | +| Map | `D6-DURABLE-CONFORMANCE-011`, `D9-DURABLE-EXT-CONFORMANCE-034`; `REL-DOC02`, `REL-DOC05`, `REL-DOC08`, `REL-SUP04-SUP06` | + +### E04 — recurring use-case path + +| Field | Pre-registration | +| --- | --- | +| Hypothesis | A task-specific complete pattern produces better seven-day retained use than a generic graph demo | +| Variants | A: one complete pattern matching participant need; B: general research diamond. No incomplete skeleton enters A | +| Eligible exposure | Consented participants with first success and at least seven days elapsed | +| Primary metric | Seven-day retained count/rate; qualitative reason for repeat/non-repeat | +| Guardrails | Same support; no pressure to publish; no private-repo inference; hard graph budgets | +| Minimum / maximum | Report cohort N; maximum 14 elapsed days after inclusion. Small N remains directional | +| Stop | Pattern gate failure, unsafe external effect, consent withdrawal, or P0/P1 defect | +| Decision | Prioritize only a use case with observed recurring value and reliable execution; otherwise improve core onboarding/reliability | +| Map | `CTRL-PATTERNS-071`, `D17-BETA-063`; `REL-PAT00-PAT10`, `REL-GR05-GR07`, `REL-Q11` | + +### E05 — bilingual activation parity + +| Field | Pre-registration | +| --- | --- | +| Hypothesis | Native Chinese explanation with identical commands/limits improves Chinese-community first success without introducing claim drift | +| Variants | English canonical versus reviewed Chinese-native adaptation for consenting bilingual or respective-language cohorts; not random forced language | +| Eligible exposure | Authorized channels and external participants selecting language | +| Primary metric | First-success rate/time by language path; claim-diff defects are guardrail | +| Guardrails | Same version, commands, release label, privacy, security, at-least-once semantics; no stronger translation | +| Minimum / maximum | At least five accepted reports per reported language before comparison; otherwise descriptive only; through Day 21 | +| Stop | Any command/version/claim divergence or community rule issue | +| Decision | Keep language-native assets only when parity review stays Green; missing sample is not evidence of inferiority | +| Map | `CTRL-DOCS-073`, `D17-BETA-063`; `REL-DOC01`, `REL-DOC13-DOC14`, `REL-Q10` | + +### E06 — interactive Explorer versus static trace + +| Field | Pre-registration | +| --- | --- | +| Hypothesis | A real interactive topology/critical-path view increases successful inspection/replay tasks compared with a static diagram | +| Variants | A: tested Explorer; B: deterministic Mermaid/DOT plus textual trace; same graph/candidate | +| Eligible exposure | External usability sessions after D15 Explorer passes smoke/accessibility | +| Primary metric | Completion of a predefined “find failed/waiting/critical node” task and time | +| Guardrails | No fabricated/live data, no prompt capture, accessible fallback, same limitations | +| Minimum / maximum | Five accepted sessions per available variant; stop at Day 18 for launch decision | +| Stop | Data exposure, inaccessible core task, wrong event state, deployment failure, or candidate mismatch | +| Decision | Keep static fallback regardless; promote Explorer only if correct and usable | +| Map | `D5-CLI-VISUALIZE-005`, `D15-EXPLORER-060`; `REL-DOC03`, `REL-DOC11`, `REL-Q10`, `REL-SUP06` | + +### E07 — contribution entry point + +| Field | Pre-registration | +| --- | --- | +| Hypothesis | A scoped fixture/pattern issue with runnable acceptance evidence yields more reviewable outside contributions than a generic “contributions welcome” CTA | +| Variants | A: one scoped issue linked to fixture/test/owner; B: contribution guide landing. Same review bar | +| Eligible exposure | Organic repository visitors and manual relevant invitations, no bulk outreach | +| Primary metric | Eligible external PRs and accepted reproductions per CTA; merged count secondary | +| Guardrails | Review latency, contributor experience, no merge for metric, no star requirement | +| Minimum / maximum | Through Day 21; report counts/status, no significance claim required | +| Stop | Support p50 >12h, oldest item >24h, abusive/spam traffic, or maintainer capacity unavailable | +| Decision | Expand only scoped paths that receive timely, quality review; otherwise reduce intake and improve docs | +| Map | `CTRL-PATTERNS-071`, `D21-RELEASE-067`; `REL-GR04-GR07`, `REL-SUP01-SUP02`, `REL-SUP07` | + +## 11. Experiment allocation and concurrency + +No more than two public experiments run concurrently, and only one may affect +the Quickstart path. `RM`/`COMM` maintain an exposure ledger so a major release +beat, incident, or external news spike is not falsely attributed to a copy +variant. Experiments do not run during a SEV0/SEV1 incident, candidate +invalidation, security embargo, package rollback, or support-capacity pause. + +Recommended dependency order: + +1. E01 may run with truthful current-alpha content after channel/privacy review. +2. E02 requires real external study and package/link truth. +3. E03 requires accepted recovery/redaction boundaries. +4. E05 requires bilingual assets and real cohorts. +5. E06 waits for the Explorer. +6. E04 waits for complete patterns and seven elapsed days. +7. E07 runs only when reviewers have capacity. + +Mapping: `CTRL-GROWTH-072`, source dependencies above; +`REL-GR01`, `REL-GR06`, `REL-SUP01-SUP08`. + +## 12. Campaign-wide stopping and resumption conditions + +| Condition | Required action | Resumption condition | Registry / rows | +| --- | --- | --- | --- | +| Any paid/bot/reciprocal/fake/undisclosed growth | Stop and quarantine campaign data/content; investigate and disclose as appropriate | Release-manager and independent conduct review; invalid traffic excluded; controls corrected | `CTRL-GROWTH-072`; `REL-GR01` | +| Secret/prompt/user-data appears in metric/log/support bytes | Stop collection and promotion, restrict evidence, invoke incident response | Canary regression proves absence across sinks; privacy/security review | `D16-SECURITY-062`; `REL-Q08`, `REL-SUP05-SUP06` | +| Wrong version/package/checksum/provenance link | Stop install CTA and affected content; do not silently edit historical package | Trusted rebuild/rehearsal, new digest, explicit correction | `D20-PROVENANCE-066`; `REL-Q09`, `REL-RC06-RC09` | +| Open P0/P1, unaccepted high/critical, data loss/corruption, duplicate unsafe effect | Stop broad promotion and release path; preserve redacted evidence | Fix plus candidate-bound regression/independent review; forward version if published | `D16-SECURITY-062`, `D21-RELEASE-067`; `REL-Q08`, `REL-Q11`, `REL-SUP05`, `REL-RC07-RC09` | +| First-success rate <80% at N>=5 | Pause broad campaign and run failure-stage diagnosis | New external cohort on corrected candidate meets gate | `D17-BETA-063`; `REL-Q10-Q11`, `REL-GR06` | +| Downloads rise but no credible successful-run evidence | Treat downloads as noisy; pause promotion rather than optimize downloads | Verified/self-reported run evidence and failure diagnosis | `CTRL-GROWTH-072`; `REL-GR04-GR06` | +| Support p50 >12h or oldest ordinary item >24h | Stop new beats; shift owners to support | Queue and roster review show restored capacity | `D21-RELEASE-067`; `REL-SUP01-SUP02`, `REL-SUP07`, `REL-GR06` | +| Consent withdrawn or collection purpose changes | Stop use/publication; remove affected public asset/data under policy | Fresh scoped consent or permanent exclusion | `D17-BETA-063`; `REL-DOC08`, `REL-DOC12`, `REL-SUP06` | +| Experiment sample/traffic too small by maximum duration | Stop as inconclusive; do not extend via spam/paid activity or claim a winner | New pre-registration and naturally available cohort, if still useful | `CTRL-GROWTH-072`; `REL-GR01`, `REL-GR06` | +| Star checkpoint missed | Record actual value and funnel diagnosis | No “recovery” requirement; continue only where activation/support gates justify | `CTRL-GROWTH-072`; `REL-GR03-GR06` | + +## 13. External adoption evidence ladder + +| Level | Evidence | May support | Cannot support | +| --- | --- | --- | --- | +| `E0 awareness` | Public star/fork/view/download aggregate | Awareness statement with timestamp/method | User, successful run, adopter, retention, production-readiness claim | +| `E1 interest` | Relevant issue/discussion, Quickstart click, opt-in invitation acceptance | Qualitative demand/problem evidence | Successful use or adoption | +| `E2 first success` | Consented timed session, reproducible public trace/report on exact candidate | External usability/run count, time-to-success | Seven-day retention or production use | +| `E3 repeated use` | Same consented/public repository performs useful second run after seven days | Retained cohort | Broader market adoption beyond cohort | +| `E4 adoption` | Public/consented integration with reproducible use and scope | Authentic adopter/case study/gallery | Endorsement beyond consent; production safety unless proven | +| `E5 contribution` | Non-maintainer accepted reproduction, contribution, or merged PR | Contributor/PR metrics | Adoption or retention by itself | + +Stable external usability requires the checklist's real reports and success +thresholds, not E0/E1 proxies. Adopter/gallery claims require E4 consent. +Mapping: `D17-BETA-063`, `CTRL-ACCEPTANCE-070`; +`REL-Q10-Q11`, `REL-DOC08`, `REL-DOC12`, `REL-GR04-GR05`, +`REL-GR07`. + +## 14. Anti-gaming and data-quality audit + +At Days 7, 13, 17, and 21, an owner and independent reviewer check: + +1. star/download/traffic spikes against public events and report anomalies + without accusing users absent evidence; +2. maintainer, CI, bot, mirrored-cache, duplicate submission, and test traffic + exclusions where the source permits them; +3. no contributor/adopter identity is counted twice across renamed accounts or + multiple artifacts when a reviewed public/consented match exists; +4. bounded run counts are not presented as exact when cross-source duplication + is unresolved; +5. deleted/withdrawn consented evidence is removed from future public totals + according to policy, with aggregate corrections recorded; +6. missing source permissions/windows are `not_available`, not reconstructed; +7. no copied screenshot or social claim substitutes for source URL/query; and +8. campaign conduct remains organic and disclosed. + +Mapping: `CTRL-GROWTH-072`; `REL-GR01`, `REL-GR03-GR05`, +`REL-SUP06`. + +## 15. Review cadence and decision owners + +| Cadence | Review | Required participants | Output | Mapping | +| --- | --- | --- | --- | --- | +| Per asset | Claim, version, link, UTM, disclosure, consent, accessibility | Technical owner, `PQG`, `COMM`; `SRV` when sensitive | `ready`/`hold` plus manifest digest | `CTRL-DOCS-073`, `CTRL-GROWTH-072`; `REL-DOC13-DOC16`, `REL-GR01-GR02` | +| Daily | Guardrails, funnel stage, support capacity, experiment exposure | `COMM`, `SUP`, one technical owner | Continue/narrow/pause/incident and next owner | `CTRL-GROWTH-072`; `REL-GR05-GR06`, `REL-SUP07` | +| Day 7/13/17/21 | Full metric snapshot, anti-gaming audit, star stretch observation, content state | `RM`, `COMM`, independent reviewer | Signed/superseding checkpoint | `REL-GR01-GR06` | +| Pre-Beta | Usability sample, consent, defects, time-to-success | `PQG`, `EXT`, runtime owners, `SRV` | Beta/no-go evidence | `D17-BETA-063`; `REL-Q10-Q11`, `REL-SUP06` | +| Pre-release | Candidate, security/provenance, support, complete asset/metric state | `RM`, `SRV`, independent go/no-go, all lanes | Stable/full-RC/no-release decision | `D20-PROVENANCE-066`, `D21-RELEASE-067`; `REL-RC01-RC10`, `REL-V1-01-V1-08` | +| T+24h / T+7d | Incidents, activation, support, corrections, matured retention cohort | `RM`, `COMM`, `SUP`, `EXT` as consented | Public aggregate update and next experiment | `D21-RELEASE-067`, `CTRL-GROWTH-072`; `REL-GR04-GR06`, `REL-SUP01-SUP08` | + +## 16. Release-checklist reconciliation + +| Checklist row | What this document supplies | Evidence still required before Green | +| --- | --- | --- | +| `REL-GR01` | Prohibited-conduct rules, disclosure and anti-gaming audit | Actual channel/partner audit and release-manager attestation | +| `REL-GR02` | Beat schedule, asset states, evidence-gated publication contract | Real asset manifest, paths/URLs, proof sources, publication states | +| `REL-GR03` | Stretch-only definition and snapshot method | Timestamped Day 7/13/17/21 public values and copy audit | +| `REL-GR04` | Stable definitions for downloads, runs, adopters, contributors, PRs, response, invitations | Source snapshots, dedup reports, current values | +| `REL-GR05` | North-star dictionary, dashboard design, privacy rules | Implemented queries/dashboard and real snapshots/cohorts | +| `REL-GR06` | Funnel decision and stop/resume policy | At least one current diagnosis, selected response, owner, review date | +| `REL-GR07` | Adoption/contribution/consent evidence rules | Published and smoke-tested contributor/community/adopter workflows | +| `REL-SUP06` | Data classification, forbidden fields, consent and sink rules | Support-bundle/collection implementation, seeded-secret negative scan, review | +| `REL-SUP07` | Response p50 definition and capacity stop line | Actual queue query/snapshot, coverage owner, current value | + +No row is made Green by this plan. `CTRL-GROWTH-072` remains planned until its +dependencies and expected assets/tests are genuinely complete. + +## 17. Completion conditions + +The metric and experiment program is complete only when: + +- every headline metric has an implemented, reviewed source/method or an honest + `not_available` record; +- dashboards show definitions, windows, N, bounds, biases, candidate identity, + and timestamped values without personal/raw runtime content; +- Day 7/13/17/21 stretch snapshots report actual stars without promise or + manufacturing; +- external Quickstart/adoption/retention evidence is real, consented, and has + elapsed for the claimed window; +- every experiment has a pre-registration, exposure record, guardrail review, + decision, and inconclusive state where appropriate; +- at least one funnel diagnosis invokes the required positioning, + Quickstart, pause, reliability, or retention response; +- support and crisis thresholds are operational; and +- an independent reviewer reconciles the evidence to `REL-GR01-GR07` and + relevant documentation/support/release rows. + +This document itself is only the definition layer. It contains no claim that +6,000 stars, 2,000 downloads, 500 runs, ten adopters, ten contributors, +twenty-five PRs, ten invitations, seven-day retention, or sub-twelve-hour +response p50 has been achieved. diff --git a/codex_plans/research/competitor-capability-matrix.md b/codex_plans/research/competitor-capability-matrix.md new file mode 100644 index 0000000..2d7e20f --- /dev/null +++ b/codex_plans/research/competitor-capability-matrix.md @@ -0,0 +1,266 @@ +# Graph Engineering competitor capability matrix + +Snapshot date: **2026-07-26** + +This matrix compares Graph Engineering with Loop Engineering and two +representative runtime references already named by the local research corpus: +LangGraph as an agent-graph/persistence comparator, and Temporal as a durable +execution comparator. Temporal is not presented as an agent-graph product, and +Loop Engineering is primarily a methodology/toolchain benchmark rather than a +like-for-like runtime. + +No internet refresh was performed for this document. Unknown is an intentional +result, not an invitation to infer absence. Before publishing a competitive +claim, re-audit the named version and official primary source and record the +date. + +## 1. Sources and evidence policy + +Local authorities: + +- [21-day master plan](../Graph-Engineering-21-Day-Master-Plan.md) +- [Loop Engineering benchmark snapshot](loop-engineering-benchmark.md) +- [Graph Engineering source review](graph-engineering-source-review.md) +- [current implementation coverage](../delivery/master-plan-coverage-matrix.md) +- [stable-v1 release checklist](../delivery/release-checklist.md) + +Selected current-repository evidence: + +- [Graph IR schema](../../spec/graph.schema.json) and + [runtime semantics](../../spec/runtime-semantics.md) +- [pipeline contract](../../spec/pipeline-semantics.md), + [TypeScript pipeline](../../packages/runtime/src/pipeline.ts), and + [Python pipeline](../../python/src/graph_engineering/pipeline.py) +- [durable recovery contract](../../spec/durable-recovery-semantics.md), + [TypeScript durable runtime](../../packages/runtime/src/durable.ts), and + [Python durable runtime](../../python/src/graph_engineering/durable.py) +- [TypeScript CLI](../../packages/cli/src/cli.ts), + [read-only MCP server](../../packages/mcp-server/src/server.ts), and + [pattern constructors](../../packages/patterns/src/patterns.ts) +- [Quickstart](../../docs/QUICKSTART.md), + [failure modes](../../docs/FAILURE_MODES.md), and + [security boundary](../../docs/SECURITY.md) + +### Evidence labels + +| Label | Meaning | Permitted wording | +|---|---|---| +| `V` | Verified by the local repository or dated local benchmark | “Present in the cited snapshot/revision.” This is not automatically stable-release evidence. | +| `S` | A useful verified slice exists, but the promised surface is broader | “Partial” or “available for the named scope,” with the missing scope stated. | +| `P` | Planned in the master plan but not evidenced as implemented | “Planned,” never “ships,” “supports,” or “complete.” | +| `R` | An official external reference is listed locally, but its contents/version were not captured for this audit | “Reference identified; capability not audited here.” | +| `U` | No adequate local evidence | “Unknown.” Absence of evidence is not evidence that the competitor lacks it. | +| `N/A` | The row is not a sensible like-for-like claim for that comparator | Explain the category difference instead of assigning a winner. | + +Graph Engineering `V` and `S` labels describe the current repository snapshot, +not a published stable package. The release checklist remains Open until its +candidate-bound evidence slots are filled. + +## 2. Comparator scope + +| Comparator | Locally supported product interpretation | Evidence boundary in this document | +|---|---|---| +| **Graph Engineering** | Vendor-neutral, dual-language graph orchestration runtime plus CLI/MCP/patterns/docs; full 21-day platform is the target | Repository files and coverage matrix support current-state claims; all future scope remains `P` or `U` | +| **Loop Engineering** | Methodology and toolchain with a strong concept, CLI/DX, content, safety guidance and community flywheel; it points users to companion projects for a general runtime | Dated local benchmark of its public repository; no fresh inspection and no inference about undocumented companion behavior | +| **LangGraph** | Representative agent-graph persistence comparator because its official persistence documentation is linked in the source review | `R` only for the existence of the persistence reference; detailed current capabilities, editions, languages, limits, DX and popularity are `U` here | +| **Temporal** | Representative durable-execution comparator because its official durable execution documentation is linked in the source review | `R` only for the durable-execution reference; agent-graph-specific and current product/community details are `U` or `N/A` here | + +## 3. Runtime and orchestration capability matrix + +| Capability | Graph Engineering | Loop Engineering | LangGraph | Temporal | +|---|---|---|---|---| +| Versioned language-neutral graph contract | `V/S` JSON Schema Graph IR and canonical protocol exist; full node kinds, typed ports, nested graphs and policy validation are Partial | `U` Not established by the local benchmark; the benchmark characterizes a methodology/toolchain | `U` No local audited contract snapshot | `N/A/U` Durable workflow comparator; no local agent-graph contract audit | +| Native TypeScript runtime | `V/S` Deterministic DAG, pipeline and local durable slices exist | `U` General runtime behavior is not evidenced locally; benchmark says companion projects are used | `U` | `U` | +| Native Python runtime | `V/S` Native compiler/scheduler/pipeline/local durable slices exist; Python is not a TS client | `U` | `R/U` Persistence reference is specifically a Python documentation URL, but no capability audit was captured | `U` | +| Canonical cross-language bytes/hashes | `V/S` Shared compiler/runtime fixtures and canonical hashes exist for the current slice | `U` | `U` | `U` | +| Deterministic DAG/diamond scheduling | `V/S` Native ready-queue schedulers and diamond parity exist; scale and all node kinds remain open | `U` | `U` | `U` | +| Typed data edges and port/schema checks | `S/P` Endpoint/schema foundations exist; complete port compatibility, mapping, reducers and stream/artifact lowering are planned | `U` The local benchmark does not establish a runtime edge contract | `U` | `N/A/U` Not audited as an agent graph | +| Bounded fan-out/fan-in | `S` Ready-queue bounds and pattern constructors exist; complete dynamic fan-out policy is planned | `U` | `U` | `U` | +| Per-item streaming pipeline/backpressure | `V/S` Standalone bounded TS/Python APIs and shared cases exist; Graph IR `stream` edges and durable item queues are explicitly not activated | `U` | `U` | `U` | +| Barrier and quorum semantics | `S` Pure all/minimum/percentage evaluators exist; scheduler deadline/quorum/missing-state behavior is planned | `U` | `U` | `U` | +| Conditional routing and replay | `S/P` Pure single/multicast selection exists; scheduler conditional edges, confidence escalation and durable decision replay are planned | `U` | `U` | `U` | +| Bounded convergent cycles | `P` Static constructor exists, but executable `untilDry`, global seen set, semantic convergence and all hard exits are not complete | `U` | `U` | `U` | +| Dynamic checked graph revisions | `P` GraphPatch revision, permission, budget and malicious-patch gates are planned | `U` | `U` | `U` | +| Verifier/judge/reflection runtime | `P` Declarative verified-fanout constructor exists; votes, citations, rubrics, abstention/unknown and human gates remain open | `U` | `U` | `N/A/U` Not audited as an agent-verification system | +| Structured terminal failures | `V/S` Scheduler and pipeline failures, retries, timeouts, cancellation and upstream isolation exist for current slices | `V` Failure/safety guidance is a benchmarked product strength; runtime enforcement is `U` | `U` | `U` | +| Durable node results and resume | `V/S` Event-sourced immutable local-DAG start/resume and terminal idempotence exist | `U` Benchmark says a general runtime is delegated to companion projects | `R/U` Official persistence reference identified; exact semantics not audited | `R/U` Official durable execution reference identified; exact semantics not audited | +| Leases, replay, fork and approvals | `P` Full LockManager, dual-resume, replay/fork, stale approval and non-idempotent confirmation remain planned | `U` | `U` | `R/U` Durable execution is the comparator category, but these exact features were not locally audited | +| SQLite/PostgreSQL/S3 and worker mode | `P` Local event/checkpoint implementations exist; planned default/production stores and workers are not complete | `U` | `U` | `U` | +| External effects contract | `V/P` Repository invariant says at-least-once with idempotency/approval; complete runtime policy/approval enforcement remains planned | `U` Guidance may exist, but exact semantics are not established by the benchmark | `U` | `U` | +| Hard token/money/time/node budgets | `P/S` Narrow concurrency/attempt limits exist; atomic reservations, model usage/pricing and full hard budgets are planned | `V/U` Budget guidance is benchmarked; enforceable runtime behavior is unknown | `U` | `U` | +| Provider/model routing | `P` Deterministic local mock exists; official provider adapters/model tiers are planned | `U` Tool-aware starters are verified, but provider runtime conformance is unknown | `U` | `N/A/U` Not audited as a model-routing product | + +## 4. Product and developer-experience matrix + +| Product/DX surface | Graph Engineering | Loop Engineering | LangGraph | Temporal | +|---|---|---|---|---| +| Memorable, repeatable concept | `S` “Prompts describe work; loops repeat; graphs branch, verify, remember and converge” is defined; market validation is unknown | `V` A memorable concept and short explanation are benchmarked strengths | `U` | `U` | +| One CLI front door | `S` TS `init/validate/compile/plan/doctor/visualize` subset exists; Python CLI and full operational surface are open | `V` Unified CLI is benchmarked | `U` | `U` | +| Quickstart in at most three commands | `V/S` Local Quickstart meets command-count intent; external five-minute completion evidence is absent | `V` Five-minute onboarding is a benchmarked product strength | `U` | `U` | +| Doctor, readiness score and badge | `S/P` Doctor exists; complete G0–G4 score, top-three remediation and badge are planned | `V` Doctor, readiness score and badge are benchmarked | `U` | `U` | +| MCP and agent-harness entry points | `S/P` Read-only validation/planning MCP exists; runtime mutation policy and documented Claude Code/Codex/generic integrations are planned | `V` Tool-aware starters for multiple agent harnesses are benchmarked | `U` | `N/A/U` No local agent-harness audit | +| Executable patterns/starters/examples | `S` Four TS constructors and provider-free examples exist; ten complete YAML/JSON/TS/Python bundles are open | `V` Extensive patterns, starters and examples are benchmarked | `U` | `U` | +| Fourteen-step executable course | `P` Course topics and acceptance are planned; complete runnable course is absent | `V/S` Large educational/content surface is verified, not necessarily this exact course | `U` | `U` | +| Failure, safety and operations guidance | `V/S` Concepts, failure and security-boundary docs exist; complete operations/threat evidence is open | `V` Safety, failure, budget, state, worktree and operating guidance are benchmarked | `U` | `U` | +| Runtime visualization | `S/P` Deterministic Mermaid/DOT exists; live Explorer, critical path, utilization and replay/fork time travel are planned | `V` Interactive showcase is benchmarked; exact runtime trace depth is not locally audited | `U` | `U` | +| Stable machine-readable envelopes | `S` TS CLI and runtime envelopes exist for the current slice; full commands and dual-language CLI reference are open | `U` | `U` | `U` | +| Mock-first, credential-free normal CI | `V/S` Current examples/runtime use deterministic local execution; complete provider CI policy is planned | `U` | `U` | `U` | +| Package distribution | `S/P` npm workspace and Python build artifacts exist locally; trusted npm/PyPI stable publication is not evidenced | `V/S` Thirteen package manifests are recorded; package quality/publication details were not re-audited here | `U` | `U` | +| Interactive site, trace/adopter galleries | `P` Planned and evidence/consent-gated | `V` Interactive showcase and contributor recognition are benchmarked; exact gallery scope is not re-audited | `U` | `U` | +| English and Chinese launch material | `P` English canonical plus Chinese launch/Quickstart parity is planned | `U` | `U` | `U` | + +## 5. Safety, security and operational-control matrix + +| Control | Graph Engineering | Loop Engineering | LangGraph | Temporal | +|---|---|---|---|---| +| No implicit cycles or unbounded retries/fan-out | `S/P` Compiler and runtime bounds cover current slices; dynamic cycles/fan-out and randomized proof remain open | `V/U` Safety/budget guidance is verified; enforcement is unknown | `U` | `U` | +| Failures never silently become null | `V/S` Repository invariant and current native structured results support this for implemented slices | `U` | `U` | `U` | +| Cancellation and cleanup | `V/S` Native scheduler/pipeline adversarial suites exist; future providers/tools/stores still need conformance | `U` | `U` | `U` | +| Deny-by-default tool/fs/network/secret capabilities | `P` Required by plan; current docs state ambient-authority limitations and enforcement is open | `V/U` Safety/worktree guidance is verified; runtime authority enforcement is unknown | `U` | `U` | +| Worktree/process/container isolation | `P` Planned leases, path policy, namespaces and tested merge node are absent | `V/U` Worktree guidance is benchmarked; enforced isolation is unknown | `U` | `U` | +| Planner cannot expand authority | `P` Normative invariant; enforcement/adversarial proof remain open | `U` | `U` | `N/A/U` Not locally audited in agent-planner terms | +| Prompt-injection resistance | `P` Capability-denial and adversarial suite planned | `U` | `U` | `N/A/U` | +| Secret redaction | `P/S` Security docs exist; end-to-end redaction evidence across errors/events/traces/prompts/tools is open | `U` | `U` | `U` | +| Telemetry and prompt capture off by default | `V/P` Fixed product invariant; clean packed-install/network evidence and OTel implementation remain open | `U` | `U` | `U` | +| Crash/dual-resume/chaos evidence | `S/P` Local crash-window slice exists; leases, store/artifact/network chaos and 100 randomized runs are open | `U` | `R/U` Persistence reference only | `R/U` Durable execution reference only | +| Supply-chain CI | `V/S` CodeQL, dependency review and Dependabot exist; full secret/license/SBOM/attestation evidence is open | `V/S` Nineteen workflows are recorded; their exact security coverage was not locally classified | `U` | `U` | +| Trusted publishing and provenance | `P/External` Local package rehearsals exist; registry identities, checksums, SBOM and attestations are open | `U` | `U` | `U` | +| Human approval for risky external effects | `P` Required by contract/release plan; runtime approval and stale-approval behavior remain open | `U` | `U` | `U` | + +## 6. Community and market-surface matrix + +| Community/market signal | Graph Engineering | Loop Engineering | LangGraph | Temporal | +|---|---|---|---|---| +| Public repository | `V` Public `reacher-z/GraphEngineering` repository and source alpha are recorded | `V` Public benchmark repository | `R/U` Official documentation link exists; repository metrics not captured | `R/U` Official documentation link exists; repository metrics not captured | +| Dated star/fork evidence | `U` No current Graph Engineering star/fork snapshot is stored in the required local sources | `V` 9,416 stars, 1,290 forks and 60 watchers at the 2026-07-26 benchmark snapshot | `U` Do not guess | `U` Do not guess | +| Repository/content scale | `S` Multi-package dual-language alpha with docs/examples/governance; full planned surface is incomplete | `V` Roughly 599 files, about 40k core text/code lines, thirteen package manifests and nineteen workflows in the snapshot | `U` | `U` | +| Governance/contribution entry points | `V/S` MIT, issue/PR templates, Discussions/security pathways exist; external contribution outcomes are unknown | `V` Good-first-issue inventory, contribution automation and recognition are benchmarked | `U` | `U` | +| Authentic adopters | `U/External` No accepted ten-adopter evidence in the local plan corpus | `U` Not quantified by the local benchmark | `U` | `U` | +| Outside contributors/PRs | `U/External` Controlled goals exist; current accepted counts are not stored here | `U` Not quantified in the local benchmark | `U` | `U` | +| Retained usage/successful runs | `U/External` Measurement is planned; accepted baseline is absent | `U` Star awareness is explicitly not treated as retention evidence | `U` | `U` | +| Launch/content flywheel | `P/S` Public alpha and source materials exist; complete course/site/case/channel calendar remains open | `V` Effective community/content flywheel is a benchmarked strength | `U` | `U` | +| Organic-growth guardrails | `V` Plan prohibits paid/fake/bot/mutual-star schemes and separates stars from release quality | `U` No claim made by this local audit | `U` | `U` | + +## 7. What Graph Engineering may claim today + +Evidence-supported positioning: + +- Graph Engineering is building a native TypeScript and Python graph runtime + around a language-neutral schema and shared conformance fixtures. +- The current repository contains deterministic DAG execution, standalone + bounded pipelines, pure route/barrier evaluators, structured failures and a + meaningful local event-sourced recovery slice. +- It also contains an early TS CLI, a read-only MCP server, deterministic + visualization, provider-free examples and honest security/failure-boundary + documentation. +- Loop Engineering is the product-surface benchmark: Graph Engineering still + needs to match its onboarding, tools, content, safety guidance and community + flywheel while completing the runtime advantages promised by the plan. + +Claims that remain prohibited until their gates are Green: + +- “Complete graph platform,” “production-ready,” “battle-tested,” or + “production proven.” +- Full replay/fork, distributed leases/workers, exactly-once external effects, + enforced isolation, official provider parity, complete verifier panels or + durable per-item stream recovery. +- Technical superiority over LangGraph or Temporal; the local evidence only + records links to their persistence/durability documentation. +- Popularity parity, 6,000 stars, retained adoption or community leadership + without a timestamped public measurement. + +## 8. Differentiation thesis and proof obligations + +| Intended differentiation | Proof required before public “better” wording | Current state | +|---|---|---| +| Real runtime rather than methodology alone | Packed dual-language runtime, complete public API, deterministic examples and independent user success | Partial | +| Typed, versioned data edges | Full port/schema/mapping/reducer validation and canonical migration/version evidence | Partial/Open | +| Native TS/Python parity | `X01-X10` candidate-bound conformance with no divergence | Partial | +| Wider execution without false barriers | Pipeline/backpressure, 100-way concurrency, 1,000-node bounds and reproducible latency topology benchmarks | Partial/Open | +| Durable recovery and memory | Complete leases/checkpoints/artifacts/resume/replay/fork/crash-race evidence | Partial/Open | +| Confidence through verification | Pass/reject/abstain, citation, diverse panel, retained votes, unknown/human gate and isolated evidence | Open | +| Safe self-routing and dynamic revisions | Malicious-patch, authority, budget, fan-out/depth/node/attempt and dry-run tests | Open | +| Isolation for parallel writers | Worktree/process/container escape, conflict, cleanup and merge-gate tests | Open | +| Cost-aware topology/model tiering | Versioned pricing/usage, atomic reservations and hard-stop-before-schedule tests | Open | +| Runtime Explorer/time travel | Real-event topology, critical path/utilization/waits/retries/verdicts and replay/fork UI tests | Open | +| Better open-source activation | External five-minute success, retained-run and contribution evidence; no manufactured growth | Open/External | + +The defensible strategy is therefore “match the product surface, prove the +runtime differences,” not “declare every unknown competitor cell absent.” + +## 9. Controllable leading indicators + +The plan contains both inputs the team controls and outcomes it can only +influence. They must not be mixed when explaining progress. + +### Directly controllable inputs and quality gates + +| Indicator | Target/control | Owner | Evidence | Correct response when missed | +|---|---|---|---|---| +| Quickstart command count | No more than three user commands | `PQG` | Published revision plus independent command-count review | Remove steps or automate setup before promotion | +| Deterministic first run | Mock-first, no provider credential or product telemetry opt-in | Native lanes + `PQG` | Clean-machine network/credential/config transcript | Fix packaging/defaults; do not blame provider setup | +| First-success usability method | Recruit and time a real cohort with reproducible instructions | `PQG` + `EXT` | Sampling notes, anonymized results and issue links | Iterate onboarding and repeat the cohort | +| Cross-language parity | Every applicable `X01-X10` row Green | `INT`, `TSR`, `PYR` | Shared fixture revision and both reports | Freeze the divergent feature and reduce to a shared case | +| Safety/recovery/provenance | All mandatory release rows Green | `INT`/`SRV` | Candidate-bound checklist leaf evidence | Stay RC; never relax the label | +| Ten complete pattern bundles | All PB elements, not just directories | `PQG` plus native lanes | Per-pattern artifact/test/guide manifest | Keep incomplete pattern labeled skeleton/experimental | +| Fourteen-step executable course | Fourteen runnable checks plus “when not to use a graph” | `PQG` | Course manifest, docs and execution report | Close missing runtime/example dependency first | +| Content/release assets | Required evidence-backed beats and channel variants prepared | `PQG` | Asset manifest, current links/version and claim audit | Delay the unsupported beat or narrow its claim | +| Maintainer response time | Controlled p50 goal below twelve hours | `PQG`/support rota | Timestamped queue snapshot and metric definition | Reallocate triage/support capacity | +| Personalized trial invitations | Ten relevant, individualized invitations | `PQG` | Consent-respecting outreach log without private message content | Improve targeting/message; never mass-spam | + +### Influenceable adoption outcomes + +| Outcome | Plan goal | Guarantee status | Interpretation | +|---|---:|---|---| +| CLI downloads | 2,000 | Not guaranteed | Useful awareness/activation signal; validate successful use rather than counting installs alone | +| Successful or self-reported graph runs | 500 | Not guaranteed | More meaningful than stars, but instrumentation/definition must be privacy-safe and stable | +| Seven-day retained repositories | Track as north-star | Not guaranteed | Indicates recurring value; define cohort and avoid telemetry by default | +| Public adopters | 10 | Not guaranteed | Count only authentic public/consented evidence | +| Outside contributors | 10 | Not guaranteed | Separate maintainers, automation and outside people | +| External PRs | 25 | Not guaranteed | Report opened, merged and rejected states honestly | +| Organic stars | Day 7: 300; Day 13: 1,000; Day 17: 2,000; Day 21: 6,000+ stretch | Explicitly not guaranteed | Awareness outcome only; never a stable-release gate or substitute for activation/retention | + +## 10. The 6,000-star and popularity-parity boundary + +1. **6,000+ is a breakout OKR, not an engineering acceptance condition.** Code, + plans, content and outreach can improve the probability; no agent can promise + that independent GitHub users will star the repository. +2. **The dated Loop Engineering benchmark is 9,416 stars and 1,290 forks on + 2026-07-26.** It is a moving long-term popularity benchmark, not a number to + silently reuse as current. Refresh only from a timestamped source when + browsing is explicitly in scope. +3. **Stable v1 does not depend on stars.** It depends on recovery, security, + cross-language conformance, provenance, usability and all mandatory release + gates. Missing stars never permits a false release claim; excess stars never + waive a failed gate. +4. **Growth must remain organic.** Paid/fake stars, bots, mutual-star schemes, + fake adopters, fake testimonials and undisclosed promotion are forbidden. +5. **Report the funnel, not only the vanity number.** Visits, stars, installs, + successful runs, retention, adopters, contributors and response time need + stable definitions and timestamped evidence. +6. **Use misses diagnostically:** high visits with low stars triggers + positioning work; stars without installs triggers Quickstart/package work; + installs without successful runs pauses promotion for reliability; runs + without retention triggers use-case and product-value work. + +## 11. Required follow-up competitor audit + +This document deliberately leaves most LangGraph and Temporal cells Unknown. +Before an external comparison, create a versioned, primary-source-only audit +that records: + +- product/version/date and open-source versus hosted boundary; +- supported languages and install/package coordinates; +- graph/workflow contract, streaming, routing, cycles and dynamic-revision + semantics; +- persistence, checkpoint, replay/fork, lease and external-effect semantics; +- provider/tool interfaces, budgets, cancellation and failure envelopes; +- isolation, capabilities, telemetry/redaction and supply-chain posture; +- CLI, local-first Quickstart, visualization, docs and pattern surface; +- license, governance, contributor/adoption evidence and dated public metrics; +- reproducible examples or tests for every comparative technical claim; and +- explicit Unknown cells where official evidence is unavailable. + +Until that audit exists, LangGraph and Temporal are architectural references, +not defeated competitors, and Graph Engineering’s differentiation remains a +set of proof obligations. diff --git a/docs/CONCEPTS.md b/docs/CONCEPTS.md index f6776c2..902e2ef 100644 --- a/docs/CONCEPTS.md +++ b/docs/CONCEPTS.md @@ -14,8 +14,8 @@ behavior that exists today and the intended v1 architecture. | Area | Current alpha | Target v1 | | --- | --- | --- | | Portable format | Versioned JSON Graph IR, canonical hash, compiler diagnostics, conformance fixtures | Stable compatibility and migration policy | -| Native runtimes | TypeScript and Python IR, compilers, ready-queue schedulers, and event-sourced start/resume pass shared conformance cases; APIs remain unstable | Distributed execution and a broader cross-language conformance corpus | -| Topologies | DAG fan-out/fan-in; deterministic settled all/minimum/percentage evaluation; TypeScript constructors for diamonds, verifier fan-out, declarative routing, and finite loop expansion | Streaming pipelines, scheduler-applied routing, verifier policies, quorum/deadline barriers, subgraphs, and dynamic bounded loops | +| Native runtimes | TypeScript and Python IR, compilers, ready-queue schedulers, standalone bounded pipelines, and event-sourced start/resume pass shared conformance cases; APIs remain unstable | Distributed execution and a broader cross-language conformance corpus | +| Topologies | DAG fan-out/fan-in; standalone per-item pipelines with bounded buffers/backpressure; deterministic settled all/minimum/percentage evaluation; TypeScript constructors for diamonds, verifier fan-out, declarative routing, and finite loop expansion | Graph-integrated/durable item streaming, scheduler-applied routing, verifier policies, quorum/deadline barriers, subgraphs, and dynamic bounded loops | | Persistence | Native local event stores drive scheduler start/resume from authoritative history; atomic checkpoint stores exist separately but do not accelerate the scheduler | Checkpoint acceleration, replay, fork, leases, artifact stores, and production databases | | Security | Graph bounds and a trusted `sideEffects` declaration gate ambiguous durable retries; executors still have ambient process authority | Enforced capabilities, worktree/process/container isolation, redaction, approval gates, and policy-audited dynamic graphs | @@ -149,10 +149,38 @@ global collection barrier. Item A can be in stage three while item B remains in stage one. Pipelines reduce tail latency when downstream processing does not need the complete cross-item set. -Graph IR reserves `edge.mode: "stream"`, but the current runtime does **not** -implement streaming, bounded buffers, or backpressure. Those are target-v1 -features. Until then, describing an edge as `stream` does not make execution a -pipeline. +The current TypeScript and Python runtimes implement this as a **standalone** +bounded-pipeline API, separate from graph execution. A run has a synchronous or +asynchronous source, an immutable ordered stage list, bounded queues between +stages, and a global in-flight credit window. Credit is acquired before a source +pull and held until the consumer receives that item's terminal result. A slow +consumer therefore propagates pressure through the bounded result/reorder +buffer, stage queues, and source rather than accumulating an unbounded result +array. + +Stages remain ordered for one item, but a successful item enters the next stage +as soon as queue capacity and a stage slot are available; it does not wait for +the other items in its current stage. Per-stage `concurrency` bounds active +handler attempts. Terminal `ordering: "input"` may hold a later completed result +behind an earlier one, while `ordering: "completion"` exposes committed terminal +order. Neither option serializes internal stage execution, downstream work, or +side effects. + +Every accepted item has a structured `succeeded`, `failed`, `dropped`, or +`cancelled` result. `drop` is explicit, `dead-letter` is an in-memory failed +terminal record rather than a durable external queue, and `stop` ends deliberate +source intake while already accepted items drain. JSON `null` remains data, not +a failure sentinel. Item count, in-flight work, queue depth, concurrency, retry +attempts, timeout, and computed total attempts all have hard bounds. + +This does not change Graph IR execution. Graph IR stream mode remains declarative: +`edge.mode: "stream"` does not make `runGraph` lower stream edges into queues; +it still waits for one terminal result per upstream node. There are no durable item identities, +offsets, acknowledgements, queue snapshots, stream joins/windows, replay, or +fork. If a graph node calls the standalone API, the complete pipeline belongs to +that single node attempt and may rerun in full after a crash. See the +[runtime pipeline API](../packages/runtime/README.md#standalone-bounded-pipeline) +and [bounded pipeline semantics](../spec/pipeline-semantics.md). ### Barrier @@ -243,16 +271,21 @@ Graph shape controls latency and cost: - wider fan-out reduces wall time but increases concurrent resource pressure; - barriers make the critical path wait for the slowest required input; -- pipelines improve flow but require bounded buffers and backpressure; +- standalone pipelines improve flow through bounded buffers and end-to-end + backpressure; - retries multiply attempts; - verification multiplies calls in exchange for confidence; - cycles and dynamic expansion can grow without a static node count. Current compilation can enforce graph `maxFanOut` and `maxDepth`; the native runtimes enforce effective concurrency, node retry counts, timeouts, and -`maxTotalAttempts`, and both expose run cancellation. Cost budgets, dynamic node -budgets, provider rate limits, and -critical-path cost estimation remain target-v1 capabilities. +`maxTotalAttempts`, and both expose run cancellation. Standalone pipelines also +enforce `maxItems`, `maxInFlight`, a lowerable `maxStages` budget capped at +2048, per-boundary buffer capacity, per-stage concurrency/retry limits, and a +safe derived maximum-attempt bound. Their inner +attempts are not charged to an enclosing graph's `maxTotalAttempts`. Cost +budgets, dynamic graph-node budgets, provider rate limits, and critical-path +cost estimation remain target-v1 capabilities. ## Durable execution: the semantic boundary diff --git a/docs/FAILURE_MODES.md b/docs/FAILURE_MODES.md index 18428b2..6fb48dc 100644 --- a/docs/FAILURE_MODES.md +++ b/docs/FAILURE_MODES.md @@ -7,24 +7,24 @@ must handle. ## Status and guarantees -The TypeScript and Python runtimes provide both an ordinary in-memory DAG -scheduler and separate event-sourced start/resume operations. Both native -implementations pass shared ready-queue and durable-recovery conformance cases, -bound concurrency/attempts, retry and time out node attempts, preserve structured -failures, skip affected descendants, and let independent branches continue. Both -expose cooperative run cancellation and reject graph inputs or node results that -are not detached portable finite JSON. +The TypeScript and Python runtimes provide an ordinary in-memory DAG scheduler, +separate event-sourced start/resume operations, and a standalone bounded-pipeline +API. Both native implementations pass shared ready-queue, durable-recovery, and +pipeline conformance cases, bound concurrency/attempts, preserve structured +failures, and expose cooperative cancellation. Graph inputs, node results, and +pipeline items must be detached portable finite JSON. Durable runs write authoritative scheduler events and reconstruct continuation from the complete event history. They bind graph, original input, and caller-supplied implementation identity, reuse committed successes, preserve consumed attempt budgets, and make terminal resume side-effect free. Standalone checkpoint stores exist, but scheduler checkpoint acceleration does not. Replay, -fork, dynamic graph patches, distributed leases, streaming pipelines, -conditional routing, verifier panels, explicit loop primitives, provider rate -limiting, worktree isolation, and capability enforcement also remain future -work. Sections marked **target v1** are operational requirements, not current -claims. +fork, dynamic graph patches, distributed leases, Graph IR-integrated or durable +item streaming, conditional routing, verifier panels, explicit loop primitives, +provider rate limiting, worktree isolation, and capability enforcement also +remain future work. The current pipeline is a lazy single-consumer in-memory API, +not durable graph streaming. Sections marked **target v1** are operational +requirements, not current claims. ## Failure is data @@ -252,6 +252,205 @@ Use provider SDK rate limits and randomized delay in executors where necessary. limits, jitter, retry-after handling, and circuit breaking. A breaker-open result is structured and should route to fallback or pause, not trigger more fan-out. +## Standalone bounded-pipeline failures + +The current `runPipeline`/`run_pipeline` APIs are standalone, bounded, and +single-pass. They do not use graph node statuses or graph failure codes. Every +accepted item has a terminal result, while failures that occur before a source +value is accepted belong to the run summary. + +| Pipeline code | Scope | Meaning | +|---|---|---| +| `INVALID_INPUT` | Item | The accepted source value could not be snapshotted as portable JSON | +| `STAGE_EXECUTION_FAILED` | Item/stage | A handler threw, rejected, or cancelled itself without pipeline cancellation | +| `STAGE_TIMEOUT` | Item/stage | The configured attempt timer won | +| `INVALID_OUTPUT` | Item/stage | A handler returned a non-portable JSON value | +| `ITEM_CANCELLED` | Item/stage | Caller cancellation or consumer close prevented completion | +| `SOURCE_FAILED` | Run | Iterator creation or iteration failed; no item is fabricated for the failed pull | +| `ITEM_LIMIT_REACHED` | Run | Exactly `maxItems` values were accepted, so intake stopped without another pull | + +A terminal item failure has `retryable: false`: if another attempt had been +allowed, it would already have happened before the terminal result committed. + +### Source failure is confused with item failure + +**Symptom:** A source yields a valid prefix and then throws, but an operator +looks for a failed item at the next index. + +**Behavior:** The prefix is already accepted and drains to item results. The +summary records `runFailure.code: "SOURCE_FAILED"`; the failed source pull has no +`itemIndex` because it did not produce an accepted item. A source-iterator +factory failure is the same run-level class and starts no handler. + +**Mitigation:** Inspect both item results and `PipelineSummary.runFailure`. Keep +source acquisition idempotent where possible. A failure raised only by the +source's `return()` cleanup hook is observed for diagnostics but does not +replace the first run failure or explicit consumer close. + +### An invalid item is silently treated as null + +**Symptom:** A sparse array, cycle, bigint, non-finite number, unsafe integer, +class instance, or other non-portable value enters the source. + +**Behavior:** Admission has already assigned an index and consumed the item +budget. Snapshot failure emits a `failed` item with `INVALID_INPUT`, +`inputBound: false`, zero stages, and zero attempts; intake continues. Valid JSON +`null` has `inputBound: true` and remains ordinary data. Source values and stage +outputs are detached before a later pull or downstream release, so caller-owned +mutation cannot rewrite accepted work. + +**Mitigation:** Validate/serialize at the source boundary, but keep the +structured item result as the audit record. Never collapse `inputBound: false` +and a bound `null` input into one representation. + +### The item budget unexpectedly fails a finite run + +**Symptom:** A source believed to contain exactly `maxItems` values produces all +those results and still ends with `ITEM_LIMIT_REACHED`. + +**Cause:** The producer checks the hard budget before another source pull. It +does not peek past the limit to distinguish an exhausted source from an infinite +one. + +**Mitigation:** For a known finite source, configure `maxItems` strictly above +the expected count. Treat the limit as a run failure, retain the drained item +results, and do not add a diagnostic “one extra pull” that could trigger more +unbounded or side-effecting source work. + +### An unbounded stage generator blocks configuration + +**Symptom:** Constructing a pipeline never returns because its stage iterable +does not terminate. + +**Behavior:** `maxStages`/`max_stages` defaults to the protocol hard maximum of +2048 and can be lowered. Construction inspects at most that many accepted stage +declarations plus one overflow value, then rejects synchronously before reading +the overflow stage's properties, constructing the item-source iterator, or +calling a handler. + +**Mitigation:** Use a finite stage collection and set the stage budget near the +expected topology size. This bound limits the number of iterator pulls; it +cannot preempt one hostile synchronous `next()` call or property getter that +itself never returns. Isolate untrusted configuration producers in a process. + +### Stop, drop, and dead-letter are collapsed together + +The failure policy applies after a stage failure can no longer retry: + +- `dead-letter` emits `failed`, prevents that item from entering downstream + stages, and lets source intake and other items continue. It does **not** write + a durable or external dead-letter queue; the returned terminal result is the + record. +- `drop` emits `dropped` with its failure and prevents downstream work. Drop is + explicit, never silent disappearance. +- `stop` emits `failed`, requests source intake to stop, and lets every item + already accepted at the concurrent boundary drain. Up to `maxInFlight` items + may already belong to that accepted set; they must not be discarded merely + because a sibling stopped intake. + +Any failed or dropped item makes a normally drained summary `failed`. Caller +cancellation/consumer close has higher summary-status precedence. Stage policy +does not apply to `SOURCE_FAILED`, which is a run-level condition. + +### A retry repeats an external effect + +Only `STAGE_EXECUTION_FAILED` and `STAGE_TIMEOUT` can retry, and only while the +bounded `maxAttempts` budget remains. `maxAttempts` includes the first attempt. +Invalid input, invalid output, and pipeline cancellation never retry. Queue wait +does not consume the stage timeout; the timer starts immediately before handler +invocation. Retry delay is cancellable and does not occupy a stage concurrency +slot. + +Pipeline retries are in-memory at-least-once attempts. The API has no durable +retry claim and no exactly-once effect boundary. A mutating handler should derive +an application idempotency key from stable run/item/stage identity and reuse it +across attempts, reconcile an ambiguous timeout before retrying, and ignore the +attempt number when identifying the logical effect. A graph-node wrapper does +not change this rule. + +### Input ordering is mistaken for a stage barrier + +**Symptom:** A fast later item completes internally but is not returned while a +slow earlier item is still running. + +**Cause:** The default `ordering: "input"` holds terminal delivery in index order. +It does not prevent the later item from entering downstream stages. Switching to +`"completion"` changes terminal delivery only; it still does not serialize or +reorder internal side effects. + +**Mitigation:** Choose output ordering for the consumer contract, not as a +concurrency control. Use gates/probes rather than sleep timing to test that a +fast item entered the next stage before the slow sibling finished. + +### Completion waits while the consumer is idle + +**Symptom:** Handlers appear finished, but `run.completion` remains pending. + +**Cause:** Global in-flight credit is held until a terminal result is returned to +the consumer. An open consumer that stops reading can fill the bounded +result/reorder buffer; automatically collecting an unbounded output array just +to resolve completion would violate end-to-end backpressure. + +**Mitigation:** Naturally drain the iterator before awaiting completion, or call +`close()`/`aclose()` when stopping early. Input-order head-of-line blocking is +bounded by `maxInFlight`; stage queues are bounded by `bufferCapacity`. + +### Cancellation is mistaken for preemption or rollback + +Caller cancellation or consumer close stops intentional source intake, wakes +runtime queue/retry waiters, signals active handlers, and settles accepted +non-terminal items as `cancelled`. No new handler attempt starts after +cancellation is observed. Cancellation before the first read creates no source +iterator and performs no pull. + +Cancellation remains cooperative. It cannot undo completed effects, and it +cannot forcibly stop arbitrary JavaScript/Python code, a request, or a process +that ignores the supplied signal. Reconcile or compensate external state +explicitly; do not report cancellation as rollback. + +### Early exit abandons cleanup + +In TypeScript, breaking a `for await` loop calls iterator `return()`, which +delegates to idempotent `close()`. Code that calls `next()` manually must call +`close()` in `finally`. In Python, use the pipeline async context manager or call +`aclose()` in `finally`; breaking an arbitrary custom async iterator does not +portably invoke it. + +Explicit close accounts all accepted work in the summary even when the consumer +did not receive every terminal record, so `emitted` may be smaller than +`accepted`. It also wakes a pending consumer read and must settle completion +without more reads. Merely dropping a run object or consumer task is not a +portable cleanup guarantee. + +### Non-cooperative source or handler outlives the pipeline + +A synchronous source or handler can block its event-loop thread; the runtime +cannot observe cancellation until control returns. An asynchronous handler that +ignores its attempt signal may also complete an external effect after timeout or +cancellation. The pipeline detaches and observes a late outcome to avoid an +unhandled rejection, but observing it cannot retract the effect. A source that +ignores its close/return hook may likewise finish its pending operation later. + +Use cooperative asynchronous APIs, pass the signal through every provider/tool +call, release application resources in `finally`, and isolate blocking or +untrusted work in a killable process/container when the application provides +one. The pipeline cleans up runtime-owned producers, workers, waiters, timers, +and listeners; it cannot clean up arbitrary tasks spawned and abandoned by user +code. Process/container providers are not supplied by this current alpha. + +### A standalone pipeline is mistaken for durable stream execution + +`edge.mode: "stream"` remains declarative and `runGraph` still consumes one +terminal value per upstream node. The standalone pipeline persists no item ID, +queue content, offset, acknowledgement, retry claim, stream join/window, replay, +or fork state. Calling it inside a graph node makes the complete pipeline part of +one node attempt: a crash can replay the whole pipeline, and its inner attempts +do not consume the graph's `maxTotalAttempts`. + +Materialize only a bounded portable-JSON node output, include inner retry cost in +the application budget, and never describe this boundary as durable item +streaming or production-ready exactly-once processing. + ## Failure containment mistakes ### One failed branch aborts unrelated work @@ -443,3 +642,4 @@ within a bounded policy, and repeating the activity is safe. - [Security architecture](./SECURITY.md) - [Current runtime boundary](../packages/runtime/README.md) - [Portable runtime semantics](../spec/runtime-semantics.md) +- [Standalone bounded-pipeline semantics](../spec/pipeline-semantics.md) diff --git a/packages/core/test/compiler.test.ts b/packages/core/test/compiler.test.ts index 0abc0b8..835f447 100644 --- a/packages/core/test/compiler.test.ts +++ b/packages/core/test/compiler.test.ts @@ -75,6 +75,21 @@ describe("graph compiler conformance", () => { ]); }); + it.each([ + "invalid-null-metadata-description.graph.json", + "invalid-null-state-schema.graph.json", + "invalid-null-output-port.graph.json", + "invalid-null-node-retry.graph.json", + ] as const)("rejects schema-optional fields when explicitly null in %s", (name) => { + const result = compileGraph(fixture(name)); + expect(result).toMatchObject({ + valid: false, + graphHash: null, + canonicalGraph: null, + diagnostics: [expect.objectContaining({ code: "GE1007_INVALID_GRAPH" })], + }); + }); + it("validates bounded retry and policy fields at the IR boundary", () => { const invalid = graph({ nodes: [node("a", { retry: { maxAttempts: 0 } })], diff --git a/packages/runtime/README.md b/packages/runtime/README.md index 03ea002..51155f2 100644 --- a/packages/runtime/README.md +++ b/packages/runtime/README.md @@ -1,7 +1,8 @@ # `@graph-engineering/runtime` A small, deterministic TypeScript scheduler for the Graph Engineering v1alpha1 -IR, with both in-memory execution and event-sourced durable continuation. +IR, with in-memory execution, event-sourced durable continuation, and a separate +standalone bounded-pipeline API. ## In-memory execution @@ -20,6 +21,181 @@ const result = await runGraph(graph, { query: "graph engineering" }, { `runGraph` does not persist progress. Use the separate durable operations when a run must continue from committed scheduler history after process loss. +## Standalone bounded pipeline + +`runPipeline` moves independent portable-JSON items through the same ordered +stages. Different items can occupy different stages at once; bounded queues and +a global in-flight window carry consumer backpressure all the way to source +pulls. + +```ts +import { + runPipeline, + type PipelineStage, +} from "@graph-engineering/runtime"; + +const stages: PipelineStage[] = [ + { + id: "double", + concurrency: 2, + handler: ({ input }) => { + if (typeof input !== "number") throw new TypeError("expected a number"); + return input * 2; + }, + }, + { + id: "label", + handler: ({ input, itemIndex }) => ({ itemIndex, value: input }), + }, +]; + +const run = runPipeline([1, 2, 3], stages, { + bufferCapacity: 2, + maxInFlight: 2, + // Set this above a known finite source length. Reaching the exact limit is a + // bounded run failure because the pipeline deliberately does not peek again. + maxItems: 4, + ordering: "input", +}); + +for await (const item of run) { + console.log(item.itemIndex, item.status, item.output); +} + +const summary = await run.completion; +console.log(summary.status, summary.accepted, summary.emitted); +``` + +The factory is synchronous and lazy: it validates and copies the stage/options +configuration immediately, but does not create or advance the source iterator +until the first `next()` from a consumer. One run is a single-pass, +single-consumer `AsyncIterableIterator`; `[Symbol.asyncIterator]()` returns the +same object, and overlapping `next()` calls reject instead of racing delivery. +The source may be synchronous or asynchronous; the stage iterable must be +finite. An empty stage list is a valid identity pipeline, and an empty source +completes without item results. + +The public surface is: + +```ts +runPipeline( + source: Iterable | AsyncIterable, + stages: Iterable, + options?: PipelineOptions, +): PipelineRun; + +interface PipelineRun extends AsyncIterableIterator { + readonly completion: Promise; + close(reason?: unknown): Promise; +} +``` + +Breaking a `for await` loop invokes the iterator's `return()`, which delegates +to idempotent `close()`. If you drive `next()` manually, call `close()` in a +`finally` block when stopping early. `close()` and `completion` resolve to the +same terminal summary. On natural exhaustion, consume the iterator before +awaiting `completion`: results remain bounded by retaining in-flight credit +until the consumer receives them, so completion may legitimately wait for an +open consumer to drain or close. + +### Stages and options + +Each `PipelineStage` has a non-empty unique `id`, a synchronous or asynchronous +`handler`, and these optional controls: + +| Field | Default | Contract | +|---|---:|---| +| `concurrency` | `1` | Safe positive integer; counts active handler attempts | +| `timeoutMs` | none | Integer from `0` through `2^31 - 1`; starts immediately before the handler call | +| `retry.maxAttempts` | `1` | Safe positive integer including the first attempt | +| `retry.initialDelayMs` | `0` | Finite non-negative retry delay, at most `2^31 - 1` | +| `retry.backoffMultiplier` | `1` | Finite number at least `1` | +| `retry.maxDelayMs` | `initialDelayMs` | Finite non-negative computed-delay cap, at most `2^31 - 1` | +| `onFailure` | `"dead-letter"` | `"stop"`, `"drop"`, or `"dead-letter"` | + +After failed attempt `k`, the delay before attempt `k + 1` is +`min(maxDelayMs, initialDelayMs * backoffMultiplier ** (k - 1))`. + +The handler receives a frozen context containing a detached `input`, zero-based +`itemIndex`, `stageId`, zero-based `stageIndex`, one-based `attempt`, and a +cooperative `AbortSignal`. A validated handler output is detached before it can +enter the next stage. The pipeline does not infer map, filter, flatten, or merge +operations; express each one as an explicit stage. + +`PipelineOptions` defaults are: + +| Field | Default | Contract | +|---|---:|---| +| `bufferCapacity` | `16` | Safe positive integer per source/stage boundary | +| `maxInFlight` | `16` | Safe positive integer global admission window | +| `maxItems` | `1000` | Safe positive integer hard admission budget | +| `maxStages` | `2048` | Lowerable stage-copy budget; hard maximum `2048` | +| `ordering` | `"input"` | Terminal delivery is `"input"` or `"completion"` order | +| `cancellationSignal` | none | Caller-owned `AbortSignal` | + +The producer acquires in-flight credit before it pulls the source. Credit is +released only when the terminal item result is delivered to the consumer, so +`accepted - emitted <= maxInFlight` even with a slow consumer. Each boundary +queue stays at or below `bufferCapacity`. Input ordering can delay a fast later +result behind a slow earlier result, but the reorder buffer remains bounded; +ordering never serializes internal stages or their side effects. + +`maxItems` includes invalid inputs. At the exact limit the source is not probed +again, accepted work drains, and the summary contains +`runFailure.code: "ITEM_LIMIT_REACHED"`. The constructor also rejects an unsafe +derived bound for `maxItems * sum(stage maxAttempts)` before source iteration. +It also rejects a stage iterable with more than `maxStages` entries before the +item source is constructed, so an accidental infinite stage generator cannot +consume an unbounded number of configuration pulls. + +### Results and failure policies + +Every accepted item is accounted as `succeeded`, `failed`, `dropped`, or +`cancelled`. Valid JSON `null` remains a real input/output; `inputBound` +distinguishes it from an invalid source value that could not be snapshotted. +Portable JSON is detached, acyclic, finite, and uses interoperable safe +integer bounds; finite non-integer numbers remain valid. Invalid input yields an +`INVALID_INPUT` item with zero attempts and does not stop intake. Invalid handler +output yields `INVALID_OUTPUT`. + +Only `STAGE_EXECUTION_FAILED` and `STAGE_TIMEOUT` can retry. Retry delay is +cancellable and does not hold a stage concurrency slot. When attempts are +exhausted, `onFailure` applies: + +- `dead-letter` emits a `failed` result and continues other work. It is a + structured terminal record, not a durable external dead-letter queue. +- `drop` emits an explicit `dropped` result with its failure; nothing disappears + silently. +- `stop` emits `failed`, stops deliberate future source intake, and lets items + already accepted at the concurrent boundary drain to terminal results. + +A source iterator exception is a run-level `SOURCE_FAILED`, never a fabricated +item. Already accepted items drain unless caller cancellation or consumer close +wins. Summary status is `cancelled` after caller cancellation/close, otherwise +`failed` for a run failure or any failed/dropped item, and otherwise +`succeeded`. + +### Cancellation and scope boundary + +Cancellation stops intentional intake, wakes runtime queue/retry waiters, +signals active handlers, and accounts accepted unfinished items as cancelled. +It is cooperative: JavaScript cannot preempt a synchronous source/handler that +blocks the event loop, and a handler that ignores its signal may finish an +external effect after the pipeline has detached and observed its late outcome. +Make mutating handlers idempotent using stable item/stage identity, reconcile +ambiguous effects, and isolate blocking or untrusted work in an application- +managed process/container. + +This API is standalone and in-memory. It does not activate Graph IR +`edge.mode: "stream"`, alter `runGraph`'s one-result-per-node model, or persist +item identities, queues, offsets, acknowledgements, retry claims, stream joins, +windows, replay, or fork. If a graph node calls `runPipeline`, the entire +pipeline is part of that one node attempt; a crash can replay it in full, and +inner attempts do not consume the graph's `maxTotalAttempts`. Materialize only +a bounded portable-JSON result, and do not claim durable item streaming or +exactly-once effects. See the +[bounded pipeline semantics](../../spec/pipeline-semantics.md). + ## Durable start and resume ```ts @@ -95,7 +271,7 @@ stopped before resume. See the configured bounded retry policy. Finite non-integer doubles remain valid; - transform and barrier nodes default to deterministic identity executors. -Edge `condition`/`map`, JSON Schema I/O validation, streaming edges, scheduler -checkpoint acceleration, distributed workers, and distributed leases are -intentionally scheduled for later alphas. They are not silently emulated in this -package. +Edge `condition`/`map`, JSON Schema I/O validation, Graph IR streaming edges, +scheduler checkpoint acceleration, distributed workers, and distributed leases +are intentionally scheduled for later alphas. The standalone bounded-pipeline +API above does not silently implement those graph or durable-stream surfaces. diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index 865b683..45ed1ad 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -1,4 +1,5 @@ export { runGraph } from "./scheduler.js"; +export { runPipeline } from "./pipeline.js"; export { resumeDurableGraphRun, resumeGraphRun, @@ -35,6 +36,22 @@ export type { NodeRunResult, NodeRunStatus, OutputRunFailure, + PipelineFailureCode, + PipelineFailurePolicy, + PipelineHandler, + PipelineHandlerContext, + PipelineItemFailure, + PipelineItemResult, + PipelineItemStatus, + PipelineOptions, + PipelineOrdering, + PipelineRetryOptions, + PipelineRun, + PipelineRunFailure, + PipelineRunStatus, + PipelineSource, + PipelineStage, + PipelineSummary, RuntimeFailureCode, SchedulerOptions, } from "./types.js"; diff --git a/packages/runtime/src/pipeline.ts b/packages/runtime/src/pipeline.ts new file mode 100644 index 0000000..16d8284 --- /dev/null +++ b/packages/runtime/src/pipeline.ts @@ -0,0 +1,1321 @@ +import { snapshotJson } from "./json.js"; +import type { + JsonValue, + PipelineFailureCode, + PipelineFailurePolicy, + PipelineHandler, + PipelineHandlerContext, + PipelineItemFailure, + PipelineItemResult, + PipelineOptions, + PipelineOrdering, + PipelineRetryOptions, + PipelineRun, + PipelineRunFailure, + PipelineRunStatus, + PipelineSource, + PipelineStage, + PipelineSummary, +} from "./types.js"; + +const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER; +const MAX_TIMER_DELAY_MS = 2_147_483_647; +const DEFAULT_BUFFER_CAPACITY = 16; +const DEFAULT_MAX_IN_FLIGHT = 16; +const DEFAULT_MAX_ITEMS = 1_000; +const MAX_PIPELINE_STAGES = 2_048; +const DEFAULT_MAX_STAGES = MAX_PIPELINE_STAGES; + +interface NormalizedRetry { + readonly maxAttempts: number; + readonly initialDelayMs: number; + readonly backoffMultiplier: number; + readonly maxDelayMs: number; +} + +interface NormalizedStage { + readonly id: string; + readonly handler: PipelineHandler; + readonly concurrency: number; + readonly timeoutMs?: number; + readonly retry: NormalizedRetry; + readonly onFailure: PipelineFailurePolicy; +} + +interface NormalizedOptions { + readonly bufferCapacity: number; + readonly maxInFlight: number; + readonly maxItems: number; + readonly maxStages: number; + readonly ordering: PipelineOrdering; + readonly cancellationSignal?: AbortSignal; +} + +interface ItemState { + readonly itemIndex: number; + readonly input: JsonValue; + value: JsonValue; + completedStages: number; + totalAttempts: number; +} + +type AttemptOutcome = + | { readonly succeeded: true; readonly output: JsonValue } + | { + readonly succeeded: false; + readonly code: PipelineFailureCode; + readonly message: string; + readonly causeName?: string; + }; + +class PipelineAbortError extends Error { + constructor() { + super("pipeline operation was cancelled"); + this.name = "PipelineAbortError"; + } +} + +interface SemaphoreWaiter { + readonly resolve: () => void; + readonly reject: (error: unknown) => void; + readonly signal?: AbortSignal; + readonly onAbort?: () => void; +} + +class AsyncSemaphore { + readonly #limit: number; + readonly #waiters: SemaphoreWaiter[] = []; + #available: number; + + constructor(limit: number) { + this.#limit = limit; + this.#available = limit; + } + + get used(): number { + return this.#limit - this.#available; + } + + acquire(signal?: AbortSignal): Promise { + if (signal?.aborted) return Promise.reject(new PipelineAbortError()); + if (this.#available > 0) { + this.#available -= 1; + return Promise.resolve(); + } + return new Promise((resolve, reject) => { + const waiter: SemaphoreWaiter = { + resolve, + reject, + ...(signal === undefined ? {} : { signal }), + }; + if (signal !== undefined) { + const onAbort = () => { + const index = this.#waiters.indexOf(waiter); + if (index >= 0) this.#waiters.splice(index, 1); + reject(new PipelineAbortError()); + }; + Object.defineProperty(waiter, "onAbort", { value: onAbort, enumerable: true }); + signal.addEventListener("abort", onAbort, { once: true }); + } + this.#waiters.push(waiter); + }); + } + + release(): void { + while (this.#waiters.length > 0) { + const waiter = this.#waiters.shift() as SemaphoreWaiter; + waiter.signal?.removeEventListener("abort", waiter.onAbort as () => void); + if (waiter.signal?.aborted) continue; + waiter.resolve(); + return; + } + if (this.#available >= this.#limit) { + throw new Error("pipeline semaphore released without a matching acquire"); + } + this.#available += 1; + } +} + +function positiveInteger(value: unknown, name: string): number { + if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1) { + throw new TypeError(`${name} must be an integer from 1 to ${MAX_SAFE_INTEGER}`); + } + return value; +} + +function timerInteger(value: unknown, name: string): number { + if ( + typeof value !== "number" || + !Number.isInteger(value) || + value < 0 || + value > MAX_TIMER_DELAY_MS + ) { + throw new TypeError(`${name} must be an integer from 0 to ${MAX_TIMER_DELAY_MS}`); + } + return value; +} + +function timerNumber(value: unknown, name: string): number { + if ( + typeof value !== "number" || + !Number.isFinite(value) || + value < 0 || + value > MAX_TIMER_DELAY_MS + ) { + throw new TypeError(`${name} must be a finite number from 0 to ${MAX_TIMER_DELAY_MS}`); + } + return value; +} + +function normalizeRetry(retry: PipelineRetryOptions | undefined, index: number): NormalizedRetry { + if (retry !== undefined && (typeof retry !== "object" || retry === null)) { + throw new TypeError(`stages[${index}].retry must be an object`); + } + const maxAttempts = positiveInteger( + retry?.maxAttempts === undefined ? 1 : retry.maxAttempts, + `stages[${index}].retry.maxAttempts`, + ); + const initialDelayMs = timerNumber( + retry?.initialDelayMs === undefined ? 0 : retry.initialDelayMs, + `stages[${index}].retry.initialDelayMs`, + ); + const backoffMultiplier = + retry?.backoffMultiplier === undefined ? 1 : retry.backoffMultiplier; + if ( + typeof backoffMultiplier !== "number" || + !Number.isFinite(backoffMultiplier) || + backoffMultiplier < 1 + ) { + throw new TypeError(`stages[${index}].retry.backoffMultiplier must be finite and at least 1`); + } + const maxDelayMs = timerNumber( + retry?.maxDelayMs === undefined ? initialDelayMs : retry.maxDelayMs, + `stages[${index}].retry.maxDelayMs`, + ); + return Object.freeze({ maxAttempts, initialDelayMs, backoffMultiplier, maxDelayMs }); +} + +function captureStageProperty( + stage: PipelineStage, + index: number, + key: Key, +): PipelineStage[Key] { + try { + return Reflect.get(stage, key) as PipelineStage[Key]; + } catch (error) { + throw new TypeError( + `stages[${index}].${String(key)} getter failed: ${errorMessage(error)}`, + ); + } +} + +function normalizeStages( + stages: Iterable, + maxItems: number, + maxStages: number, +): readonly NormalizedStage[] { + if (stages === null || stages === undefined || typeof stages[Symbol.iterator] !== "function") { + throw new TypeError("stages must be a finite iterable of PipelineStage values"); + } + const result: NormalizedStage[] = []; + const seen = new Set(); + let attemptsPerItem = 0; + let index = 0; + for (const stage of stages) { + if (index >= maxStages) { + throw new TypeError(`pipeline stage count exceeds maxStages limit of ${maxStages}`); + } + if (typeof stage !== "object" || stage === null) { + throw new TypeError(`stages[${index}] must be a PipelineStage`); + } + // Structural stage objects can expose accessors or Proxy traps. Capture + // each public field exactly once before validation and retain only these + // values so later reads cannot observe a different configuration. + const id = captureStageProperty(stage, index, "id"); + const handler = captureStageProperty(stage, index, "handler"); + const configuredConcurrency = captureStageProperty(stage, index, "concurrency"); + const configuredTimeoutMs = captureStageProperty(stage, index, "timeoutMs"); + const retryOptions = captureStageProperty(stage, index, "retry"); + const configuredOnFailure = captureStageProperty(stage, index, "onFailure"); + + if (typeof id !== "string" || id.length === 0) { + throw new TypeError(`stages[${index}].id must be a non-empty string`); + } + if (seen.has(id)) throw new TypeError(`duplicate pipeline stage id '${id}'`); + seen.add(id); + if (typeof handler !== "function") { + throw new TypeError(`stages[${index}].handler must be a function`); + } + const concurrency = positiveInteger( + configuredConcurrency === undefined ? 1 : configuredConcurrency, + `stages[${index}].concurrency`, + ); + const timeoutMs = + configuredTimeoutMs === undefined + ? undefined + : timerInteger(configuredTimeoutMs, `stages[${index}].timeoutMs`); + const retry = normalizeRetry(retryOptions, index); + const onFailure = configuredOnFailure === undefined ? "dead-letter" : configuredOnFailure; + if (!(["stop", "drop", "dead-letter"] as const).includes(onFailure)) { + throw new TypeError(`stages[${index}].onFailure is invalid`); + } + attemptsPerItem += retry.maxAttempts; + if (!Number.isSafeInteger(attemptsPerItem)) { + throw new TypeError("pipeline attempt bound exceeds the portable safe range"); + } + result.push(Object.freeze({ + id, + handler, + concurrency, + ...(timeoutMs === undefined ? {} : { timeoutMs }), + retry, + onFailure, + })); + index += 1; + } + if (attemptsPerItem > 0 && maxItems > Math.floor(MAX_SAFE_INTEGER / attemptsPerItem)) { + throw new TypeError("pipeline maximum attempt count exceeds the portable safe range"); + } + return Object.freeze(result); +} + +function normalizeOptions(options: PipelineOptions): NormalizedOptions { + const bufferCapacity = positiveInteger( + options.bufferCapacity === undefined ? DEFAULT_BUFFER_CAPACITY : options.bufferCapacity, + "bufferCapacity", + ); + const maxInFlight = positiveInteger( + options.maxInFlight === undefined ? DEFAULT_MAX_IN_FLIGHT : options.maxInFlight, + "maxInFlight", + ); + const maxItems = positiveInteger( + options.maxItems === undefined ? DEFAULT_MAX_ITEMS : options.maxItems, + "maxItems", + ); + const maxStages = positiveInteger( + options.maxStages === undefined ? DEFAULT_MAX_STAGES : options.maxStages, + "maxStages", + ); + if (maxStages > MAX_PIPELINE_STAGES) { + throw new TypeError(`maxStages must be an integer from 1 to ${MAX_PIPELINE_STAGES}`); + } + const ordering = options.ordering === undefined ? "input" : options.ordering; + if (ordering !== "input" && ordering !== "completion") { + throw new TypeError("ordering must be 'input' or 'completion'"); + } + const cancellationSignal = options.cancellationSignal; + if (cancellationSignal !== undefined && !(cancellationSignal instanceof AbortSignal)) { + throw new TypeError("cancellationSignal must be an AbortSignal"); + } + return Object.freeze({ + bufferCapacity, + maxInFlight, + maxItems, + maxStages, + ordering, + ...(cancellationSignal === undefined ? {} : { cancellationSignal }), + }); +} + +function validateSource(source: PipelineSource): void { + if (source === null || source === undefined) throw new TypeError("source must be iterable"); + const candidate = source as Partial & AsyncIterable>; + if ( + typeof candidate[Symbol.iterator] !== "function" && + typeof candidate[Symbol.asyncIterator] !== "function" + ) { + throw new TypeError("source must be iterable or async iterable"); + } +} + +function errorName(error: unknown): string { + const kind = error === null ? "object" : typeof error; + if ((kind === "object" && error !== null) || kind === "function") { + try { + const name = Reflect.get(error as object, "name"); + if (typeof name === "string" && name.length > 0) return name; + } catch { + // Hostile/revoked proxies and diagnostic getters must not escape the + // structured pipeline failure path. + } + } + return kind; +} + +function errorMessage(error: unknown): string { + if (typeof error === "string") return error; + const kind = error === null ? "object" : typeof error; + if ((kind === "object" && error !== null) || kind === "function") { + try { + const message = Reflect.get(error as object, "message"); + if (typeof message === "string" && message.length > 0) return message; + } catch { + // Fall through to a separately guarded name lookup. + } + try { + const name = Reflect.get(error as object, "name"); + if (typeof name === "string" && name.length > 0) return name; + } catch { + // Diagnostic access is best-effort and must be total. + } + } + return "non-Error value"; +} + +function itemFailure( + code: PipelineFailureCode, + message: string, + itemIndex: number, + attempt: number, + fields: { stageId?: string; stageIndex?: number; causeName?: string } = {}, +): PipelineItemFailure { + return Object.freeze({ code, message, itemIndex, attempt, retryable: false, ...fields }); +} + +function snapshotRecord(record: Record): Readonly> { + const result = Object.create(null) as Record; + for (const [key, value] of Object.entries(record)) { + Object.defineProperty(result, key, { value, enumerable: true }); + } + return Object.freeze(result); +} + +function raceWithAbort(promise: Promise, signal: AbortSignal): Promise { + promise.catch(() => undefined); + if (signal.aborted) return Promise.reject(new PipelineAbortError()); + return new Promise((resolve, reject) => { + const onAbort = () => reject(new PipelineAbortError()); + signal.addEventListener("abort", onAbort, { once: true }); + promise.then( + (value) => { + signal.removeEventListener("abort", onAbort); + resolve(value); + }, + (error: unknown) => { + signal.removeEventListener("abort", onAbort); + reject(error); + }, + ); + }); +} + +function abortableDelay(milliseconds: number, signal: AbortSignal): Promise { + if (milliseconds <= 0) { + return signal.aborted ? Promise.reject(new PipelineAbortError()) : Promise.resolve(); + } + return new Promise((resolve, reject) => { + const onAbort = () => { + clearTimeout(timer); + reject(new PipelineAbortError()); + }; + const timer = setTimeout(() => { + signal.removeEventListener("abort", onAbort); + resolve(); + }, milliseconds); + signal.addEventListener("abort", onAbort, { once: true }); + if (signal.aborted) onAbort(); + }); +} + +async function runHandlerAttempt( + stage: NormalizedStage, + stageIndex: number, + itemIndex: number, + input: JsonValue, + attempt: number, + runSignal: AbortSignal, +): Promise { + if (runSignal.aborted) { + return { succeeded: false, code: "ITEM_CANCELLED", message: "pipeline was cancelled" }; + } + const attemptController = new AbortController(); + const relayAbort = () => attemptController.abort(runSignal.reason); + runSignal.addEventListener("abort", relayAbort, { once: true }); + const context: PipelineHandlerContext = Object.freeze({ + input: snapshotJson(input), + itemIndex, + stageId: stage.id, + stageIndex, + attempt, + signal: attemptController.signal, + }); + + let timer: ReturnType | undefined; + let timedOut = false; + const resolved = (value: unknown): AttemptOutcome => { + if (runSignal.aborted && !timedOut) { + return { succeeded: false, code: "ITEM_CANCELLED", message: "pipeline was cancelled" }; + } + try { + return { succeeded: true, output: snapshotJson(value) }; + } catch (error) { + return { + succeeded: false, + code: "INVALID_OUTPUT", + message: errorMessage(error), + causeName: errorName(error), + }; + } + }; + const rejected = (error: unknown): AttemptOutcome => ({ + succeeded: false, + code: runSignal.aborted && !timedOut ? "ITEM_CANCELLED" : "STAGE_EXECUTION_FAILED", + message: + runSignal.aborted && !timedOut + ? "pipeline was cancelled" + : `stage '${stage.id}' failed: ${errorMessage(error)}`, + causeName: errorName(error), + }); + let execution: Promise; + try { + const returned = stage.handler(context); + const isThenable = + (typeof returned === "object" && returned !== null) || typeof returned === "function" + ? typeof (returned as { then?: unknown }).then === "function" + : false; + if (isThenable) { + execution = Promise.resolve(returned).then(resolved, rejected); + } else { + // A synchronous handler can schedule a microtask that mutates its owned + // return object. Capture it in the same call stack, before that task runs. + execution = Promise.resolve(resolved(returned)); + } + } catch (error) { + execution = Promise.resolve(rejected(error)); + } + execution.catch(() => undefined); + + let cancellationListener: (() => void) | undefined; + const cancellation = new Promise((resolve) => { + const onAbort = () => { + attemptController.abort(runSignal.reason); + resolve({ succeeded: false, code: "ITEM_CANCELLED", message: "pipeline was cancelled" }); + }; + cancellationListener = onAbort; + if (runSignal.aborted) onAbort(); + else runSignal.addEventListener("abort", onAbort, { once: true }); + }); + + const racers: Promise[] = [execution, cancellation]; + if (stage.timeoutMs !== undefined) { + racers.push(new Promise((resolve) => { + timer = setTimeout(() => { + timedOut = true; + attemptController.abort(new DOMException("Stage timed out", "TimeoutError")); + resolve({ + succeeded: false, + code: "STAGE_TIMEOUT", + message: `stage '${stage.id}' timed out after ${stage.timeoutMs} ms`, + causeName: "TimeoutError", + }); + }, stage.timeoutMs); + })); + } + try { + return await Promise.race(racers); + } finally { + if (timer !== undefined) clearTimeout(timer); + runSignal.removeEventListener("abort", relayAbort); + if (cancellationListener !== undefined) { + runSignal.removeEventListener("abort", cancellationListener); + } + } +} + +function retryDelay(retry: NormalizedRetry, failedAttempt: number): number { + const scaled = retry.initialDelayMs * retry.backoffMultiplier ** Math.max(0, failedAttempt - 1); + return Math.min(retry.maxDelayMs, Number.isFinite(scaled) ? scaled : retry.maxDelayMs); +} + +type PipelineIterator = Iterator | AsyncIterator; + +class SourceAdapter { + readonly #source: PipelineSource; + #iterator: PipelineIterator | undefined; + #closed = false; + + constructor(source: PipelineSource) { + this.#source = source; + } + + start(): void { + const asyncFactory = (this.#source as AsyncIterable)[Symbol.asyncIterator]; + this.#iterator = typeof asyncFactory === "function" + ? asyncFactory.call(this.#source) + : (this.#source as Iterable)[Symbol.iterator](); + // External cancellation is observed while the iterator factory runs. If + // that synchronous application code cancelled before returning, honour + // the already-requested close now that its iterator is available. + if (this.#closed) this.#closeIterator(); + } + + async next(signal: AbortSignal): Promise> { + if (this.#iterator === undefined) throw new Error("pipeline source was not started"); + let pending: Promise>; + try { + pending = Promise.resolve(this.#iterator.next()); + } catch (error) { + return await Promise.reject(error); + } + return await raceWithAbort(pending, signal); + } + + close(): void { + if (this.#closed) return; + this.#closed = true; + this.#closeIterator(); + } + + #closeIterator(): void { + try { + // `return` itself may be an accessor supplied by application code, so + // getter lookup belongs inside the diagnostic-only cleanup boundary. + const iterator = this.#iterator; + const close = iterator?.return; + if (typeof close !== "function") return; + Promise.resolve(Reflect.apply(close, iterator, []) as IteratorResult | Promise>) + .catch(() => undefined); + } catch { + // Source cleanup errors are diagnostic-only and never replace the first + // run failure or explicit consumer cancellation. + } + } +} + +const PIPELINE_END = Symbol("pipeline-end"); + +class DeliveryQueue { + readonly #values: Array = []; + readonly #waiters: Array<(value: PipelineItemResult | typeof PIPELINE_END) => void> = []; + + put(value: PipelineItemResult | typeof PIPELINE_END): void { + const waiter = this.#waiters.shift(); + if (waiter === undefined) this.#values.push(value); + else waiter(value); + } + + take(): Promise { + const value = this.#values.shift(); + if (value !== undefined) return Promise.resolve(value); + return new Promise((resolve) => this.#waiters.push(resolve)); + } +} + +interface StageExecution { + readonly output: JsonValue; + readonly attempts: number; + readonly failure?: PipelineItemFailure; + /** Success keeps the worker permit until its output enters the next buffer. */ + readonly releaseSuccessPermit?: () => void; +} + +class PipelineRunImpl implements PipelineRun { + readonly completion: Promise; + + readonly #source: SourceAdapter; + readonly #stages: readonly NormalizedStage[]; + readonly #options: NormalizedOptions; + readonly #completionResolve: (summary: PipelineSummary) => void; + readonly #runController = new AbortController(); + readonly #intakeController = new AbortController(); + readonly #delivery = new DeliveryQueue(); + readonly #credit: AsyncSemaphore; + readonly #stageSlots: readonly AsyncSemaphore[]; + readonly #stageQueues: readonly AsyncSemaphore[]; + readonly #queueDepths: number[]; + readonly #queueMaxima: number[]; + readonly #activeAttempts: number[]; + readonly #activeMaxima: number[]; + readonly #statusCounts: Record = { + succeeded: 0, + failed: 0, + dropped: 0, + cancelled: 0, + }; + readonly #itemTasks = new Set>(); + readonly #terminalIndices = new Set(); + readonly #reorder = new Map(); + + #started = false; + #consumerClosed = false; + #advancing = false; + #sourceFinished = false; + #processingFinished = false; + #endSignalled = false; + #summary: PipelineSummary | undefined; + #producer: Promise | undefined; + #externalAbortListener: (() => void) | undefined; + #accepted = 0; + #emitted = 0; + #nextInputResult = 0; + #maxObservedInFlight = 0; + #runFailure: PipelineRunFailure | undefined; + + constructor(source: PipelineSource, stages: readonly NormalizedStage[], options: NormalizedOptions) { + this.#source = new SourceAdapter(source); + this.#stages = stages; + this.#options = options; + this.#credit = new AsyncSemaphore(options.maxInFlight); + this.#stageSlots = stages.map((stage) => new AsyncSemaphore(stage.concurrency)); + this.#stageQueues = stages.map(() => new AsyncSemaphore(options.bufferCapacity)); + this.#queueDepths = stages.map(() => 0); + this.#queueMaxima = stages.map(() => 0); + this.#activeAttempts = stages.map(() => 0); + this.#activeMaxima = stages.map(() => 0); + let resolveCompletion!: (summary: PipelineSummary) => void; + this.completion = new Promise((resolve) => { + resolveCompletion = resolve; + }); + this.#completionResolve = resolveCompletion; + } + + [Symbol.asyncIterator](): PipelineRun { + return this; + } + + async next(): Promise> { + if (this.#advancing) throw new TypeError("concurrent pipeline iteration is not allowed"); + if (this.#consumerClosed) return { done: true, value: undefined }; + this.#advancing = true; + try { + this.#start(); + const delivered = await this.#delivery.take(); + if (delivered === PIPELINE_END) { + this.#consumerClosed = true; + this.#finishSummary(this.#runController.signal.aborted ? "cancelled" : undefined); + return { done: true, value: undefined }; + } + this.#emitted += 1; + this.#credit.release(); + this.#maybeFinishSummary(); + return { done: false, value: delivered }; + } finally { + this.#advancing = false; + } + } + + async return(): Promise> { + await this.close(new DOMException("Pipeline consumer closed", "AbortError")); + return { done: true, value: undefined }; + } + + async close(reason: unknown = new DOMException("Pipeline consumer closed", "AbortError")): Promise { + if (this.#summary !== undefined) return this.#summary; + this.#consumerClosed = true; + if (!this.#started) { + this.#runController.abort(reason); + this.#intakeController.abort(reason); + this.#sourceFinished = true; + this.#processingFinished = true; + return this.#finishSummary("cancelled"); + } + this.#cancel(reason); + await (this.#producer ?? Promise.resolve()); + await Promise.allSettled([...this.#itemTasks]); + this.#processingFinished = true; + this.#signalEnd(); + return this.#finishSummary("cancelled"); + } + + #start(): void { + if (this.#started) return; + this.#started = true; + const externalSignal = this.#options.cancellationSignal; + if (externalSignal?.aborted) { + this.#cancel(externalSignal.reason); + this.#sourceFinished = true; + this.#processingFinished = true; + this.#signalEnd(); + return; + } + if (externalSignal !== undefined) { + this.#externalAbortListener = () => this.#cancel(externalSignal.reason); + externalSignal.addEventListener("abort", this.#externalAbortListener, { once: true }); + // Preserve the pre-start guarantee if cancellation occurs between the + // initial check and listener registration. + if (externalSignal.aborted) { + this.#cancel(externalSignal.reason); + this.#sourceFinished = true; + this.#processingFinished = true; + this.#signalEnd(); + return; + } + } + try { + this.#source.start(); + } catch (error) { + this.#runFailure = Object.freeze({ + code: "SOURCE_FAILED", + message: `pipeline source failed to create its iterator: ${errorMessage(error)}`, + causeName: errorName(error), + }); + // Iterator construction is application code. The external listener is + // already active, so synchronous and queued cancellation both retain + // summary priority while preserving this source failure diagnostic. + this.#sourceFinished = true; + this.#processingFinished = true; + this.#signalEnd(); + return; + } + if (externalSignal?.aborted) { + this.#cancel(externalSignal.reason); + this.#sourceFinished = true; + this.#processingFinished = true; + this.#signalEnd(); + return; + } + this.#producer = this.#produce().catch((error: unknown) => { + if (!this.#runController.signal.aborted && this.#runFailure === undefined) { + this.#runFailure = Object.freeze({ + code: "SOURCE_FAILED", + message: `pipeline source failed: ${errorMessage(error)}`, + causeName: errorName(error), + }); + } + }).finally(() => { + this.#sourceFinished = true; + this.#source.close(); + this.#maybeFinishProcessing(); + }); + } + + #cancel(reason: unknown): void { + if (!this.#runController.signal.aborted) this.#runController.abort(reason); + if (!this.#intakeController.signal.aborted) this.#intakeController.abort(reason); + this.#source.close(); + } + + #stopIntake(reason: unknown): void { + if (!this.#intakeController.signal.aborted) this.#intakeController.abort(reason); + this.#source.close(); + } + + async #produce(): Promise { + while (!this.#intakeController.signal.aborted) { + if (this.#accepted >= this.#options.maxItems) { + this.#runFailure ??= Object.freeze({ + code: "ITEM_LIMIT_REACHED", + message: `pipeline accepted its maxItems limit of ${this.#options.maxItems}`, + }); + this.#stopIntake(new Error("pipeline item limit reached")); + break; + } + + try { + await this.#credit.acquire(this.#intakeController.signal); + } catch (error) { + if (error instanceof PipelineAbortError) break; + throw error; + } + let queuedStage: number | undefined; + try { + if (this.#stages.length > 0) { + queuedStage = 0; + await this.#enterStageQueue(0, this.#intakeController.signal); + } + if (this.#intakeController.signal.aborted) throw new PipelineAbortError(); + const step = await this.#source.next(this.#intakeController.signal); + // The source promise can settle immediately before caller cancellation + // wins the next microtask. Do not admit that value after intake closed. + if (this.#intakeController.signal.aborted) throw new PipelineAbortError(); + // IteratorResult fields are application-owned and may be accessors. + // Capture `done` exactly once, then close the cancellation window its + // getter can open before accepting or binding an item. + const done = step.done; + if (this.#intakeController.signal.aborted) throw new PipelineAbortError(); + if (done) { + if (queuedStage !== undefined) this.#leaveStageQueue(queuedStage); + this.#credit.release(); + break; + } + const itemIndex = this.#accepted; + this.#accepted += 1; + this.#maxObservedInFlight = Math.max( + this.#maxObservedInFlight, + this.#accepted - this.#emitted, + ); + + let input: JsonValue; + try { + // Snapshot before any later source pull. Sources may reuse and mutate + // the same object between calls to next(). + input = snapshotJson(step.value); + } catch (error) { + if (queuedStage !== undefined) this.#leaveStageQueue(queuedStage); + // Reading IteratorResult.value and portable-JSON snapshot getters are + // application code. They may synchronously cancel the caller before + // throwing, in which case cancellation owns the accepted item. + let invalidMessage = "non-Error value"; + let invalidCauseName = "unknown"; + if (!this.#runController.signal.aborted) { + invalidMessage = errorMessage(error); + invalidCauseName = errorName(error); + } + const cancelled = this.#runController.signal.aborted; + this.#commitResult(Object.freeze({ + itemIndex, + status: cancelled ? "cancelled" : "failed", + inputBound: false, + completedStages: 0, + totalAttempts: 0, + failure: cancelled + ? itemFailure( + "ITEM_CANCELLED", + "pipeline cancellation prevented input binding", + itemIndex, + 0, + ) + : itemFailure("INVALID_INPUT", invalidMessage, itemIndex, 0, { + causeName: invalidCauseName, + }), + })); + continue; + } + + const task = this.#processItem({ + itemIndex, + input, + value: input, + completedStages: 0, + totalAttempts: 0, + }, queuedStage !== undefined); + this.#itemTasks.add(task); + task.catch(() => undefined).finally(() => { + this.#itemTasks.delete(task); + this.#maybeFinishProcessing(); + }); + } catch (error) { + if (queuedStage !== undefined) this.#leaveStageQueue(queuedStage); + this.#credit.release(); + if (error instanceof PipelineAbortError) break; + this.#runFailure ??= Object.freeze({ + code: "SOURCE_FAILED", + message: `pipeline source failed: ${errorMessage(error)}`, + causeName: errorName(error), + }); + this.#stopIntake(error); + break; + } + } + } + + async #processItem(item: ItemState, queued: boolean): Promise { + try { + if (this.#runController.signal.aborted) { + if (queued && this.#stages.length > 0) this.#leaveStageQueue(0); + this.#commitResult(this.#cancelledResult(item)); + return; + } + + for (const [stageIndex, stage] of this.#stages.entries()) { + if (!queued) { + try { + await this.#enterStageQueue(stageIndex, this.#runController.signal); + } catch (error) { + if (error instanceof PipelineAbortError) { + this.#commitResult(this.#cancelledResult(item, stage, stageIndex)); + return; + } + throw error; + } + } + queued = false; + + const executed = await this.#executeStage(stage, stageIndex, item); + item.totalAttempts += executed.attempts; + const failure = + executed.failure !== undefined && + this.#runController.signal.aborted && + executed.failure.code !== "ITEM_CANCELLED" + ? itemFailure( + "ITEM_CANCELLED", + "pipeline cancellation won before the stage outcome committed", + item.itemIndex, + Math.max(1, executed.attempts), + { stageId: stage.id, stageIndex }, + ) + : executed.failure; + if (failure !== undefined) { + const status = + failure.code === "ITEM_CANCELLED" + ? "cancelled" + : stage.onFailure === "drop" + ? "dropped" + : "failed"; + if (stage.onFailure === "stop" && failure.code !== "ITEM_CANCELLED") { + this.#stopIntake(new Error(`pipeline stopped by item ${item.itemIndex} at '${stage.id}'`)); + } + this.#commitResult(Object.freeze({ + itemIndex: item.itemIndex, + status, + inputBound: true, + input: item.input, + completedStages: item.completedStages, + totalAttempts: item.totalAttempts, + failure, + })); + return; + } + + item.value = executed.output; + item.completedStages += 1; + if (this.#runController.signal.aborted) { + executed.releaseSuccessPermit?.(); + const nextStage = this.#stages[stageIndex + 1]; + this.#commitResult( + nextStage === undefined + ? this.#cancelledResult(item) + : this.#cancelledResult(item, nextStage, stageIndex + 1), + ); + return; + } + + if (stageIndex + 1 < this.#stages.length) { + try { + // Keep the upstream worker permit until this validated output is + // admitted to the bounded downstream queue. This is the actual + // stage-to-stage backpressure boundary, not only a depth metric. + await this.#enterStageQueue(stageIndex + 1, this.#runController.signal); + queued = true; + } catch (error) { + executed.releaseSuccessPermit?.(); + if (error instanceof PipelineAbortError) { + this.#commitResult( + this.#cancelledResult( + item, + this.#stages[stageIndex + 1] as NormalizedStage, + stageIndex + 1, + ), + ); + return; + } + throw error; + } + executed.releaseSuccessPermit?.(); + } else { + const result = this.#runController.signal.aborted + ? this.#cancelledResult(item) + : Object.freeze({ + itemIndex: item.itemIndex, + status: "succeeded" as const, + inputBound: true, + input: item.input, + output: item.value, + completedStages: item.completedStages, + totalAttempts: item.totalAttempts, + }); + this.#commitResult(result); + executed.releaseSuccessPermit?.(); + return; + } + } + + // An empty stage list is an identity pipeline. + this.#commitResult(Object.freeze({ + itemIndex: item.itemIndex, + status: "succeeded", + inputBound: true, + input: item.input, + output: item.input, + completedStages: 0, + totalAttempts: 0, + })); + } catch (error) { + if (queued && this.#stages.length > 0) { + // The only queued state not consumed by #executeStage is the next stage. + const index = Math.min(item.completedStages, this.#stages.length - 1); + this.#leaveStageQueue(index); + } + if (!this.#terminalIndices.has(item.itemIndex)) { + this.#commitResult( + this.#runController.signal.aborted + ? this.#cancelledResult(item) + : Object.freeze({ + itemIndex: item.itemIndex, + status: "failed", + inputBound: true, + input: item.input, + completedStages: item.completedStages, + totalAttempts: item.totalAttempts, + failure: itemFailure( + "STAGE_EXECUTION_FAILED", + `pipeline coordinator failed: ${errorMessage(error)}`, + item.itemIndex, + item.totalAttempts, + { causeName: errorName(error) }, + ), + }), + ); + } + } + } + + async #executeStage( + stage: NormalizedStage, + stageIndex: number, + item: ItemState, + ): Promise { + const slot = this.#stageSlots[stageIndex] as AsyncSemaphore; + for (let attempt = 1; attempt <= stage.retry.maxAttempts; attempt += 1) { + try { + await slot.acquire(this.#runController.signal); + } catch (error) { + if (attempt === 1) this.#leaveStageQueue(stageIndex); + if (error instanceof PipelineAbortError) { + return { + output: item.value, + attempts: attempt - 1, + failure: itemFailure( + "ITEM_CANCELLED", + "pipeline was cancelled before the stage attempt", + item.itemIndex, + attempt - 1, + { stageId: stage.id, stageIndex }, + ), + }; + } + throw error; + } + if (attempt === 1) this.#leaveStageQueue(stageIndex); + // An immediately available semaphore still resumes through a promise + // job. Cancellation may win that job boundary after the slot is granted + // but before an attempt starts, so it must not consume attempt or + // concurrency accounting and must not invoke the handler. + if (this.#runController.signal.aborted) { + slot.release(); + return { + output: item.value, + attempts: attempt - 1, + failure: itemFailure( + "ITEM_CANCELLED", + "pipeline was cancelled before the stage attempt", + item.itemIndex, + attempt - 1, + { stageId: stage.id, stageIndex }, + ), + }; + } + + this.#activeAttempts[stageIndex] = (this.#activeAttempts[stageIndex] ?? 0) + 1; + this.#activeMaxima[stageIndex] = Math.max( + this.#activeMaxima[stageIndex] ?? 0, + this.#activeAttempts[stageIndex] ?? 0, + ); + let outcome: AttemptOutcome; + let holdPermitForSuccess = false; + try { + outcome = await runHandlerAttempt( + stage, + stageIndex, + item.itemIndex, + item.value, + attempt, + this.#runController.signal, + ); + holdPermitForSuccess = outcome.succeeded; + } catch (error) { + // Keep the coordinator total even if application-owned diagnostic + // access or another unexpected attempt boundary throws. + const message = errorMessage(error); + const causeName = errorName(error); + const cancelled = this.#runController.signal.aborted; + outcome = cancelled + ? { succeeded: false, code: "ITEM_CANCELLED", message: "pipeline was cancelled" } + : { + succeeded: false, + code: "STAGE_EXECUTION_FAILED", + message: `stage '${stage.id}' failed: ${message}`, + causeName, + }; + } finally { + this.#activeAttempts[stageIndex] = (this.#activeAttempts[stageIndex] ?? 1) - 1; + // A successful attempt intentionally retains its permit until the + // validated output reaches the bounded downstream queue. Every other + // path, including an unexpected throw, releases it here. + if (!holdPermitForSuccess) slot.release(); + } + + if (outcome.succeeded) { + let released = false; + return { + output: outcome.output, + attempts: attempt, + releaseSuccessPermit: () => { + if (released) return; + released = true; + slot.release(); + }, + }; + } + + const canRetry = + (outcome.code === "STAGE_EXECUTION_FAILED" || outcome.code === "STAGE_TIMEOUT") && + attempt < stage.retry.maxAttempts && + !this.#runController.signal.aborted; + if (!canRetry) { + return { + output: item.value, + attempts: attempt, + failure: itemFailure(outcome.code, outcome.message, item.itemIndex, attempt, { + stageId: stage.id, + stageIndex, + ...(outcome.causeName === undefined ? {} : { causeName: outcome.causeName }), + }), + }; + } + try { + await abortableDelay(retryDelay(stage.retry, attempt), this.#runController.signal); + } catch (error) { + if (!(error instanceof PipelineAbortError)) throw error; + return { + output: item.value, + attempts: attempt, + failure: itemFailure( + "ITEM_CANCELLED", + "pipeline cancellation interrupted retry delay", + item.itemIndex, + attempt, + { stageId: stage.id, stageIndex }, + ), + }; + } + } + throw new Error("bounded pipeline retry loop exited without an outcome"); + } + + async #enterStageQueue(stageIndex: number, signal: AbortSignal): Promise { + const queue = this.#stageQueues[stageIndex] as AsyncSemaphore; + await queue.acquire(signal); + this.#queueDepths[stageIndex] = (this.#queueDepths[stageIndex] ?? 0) + 1; + this.#queueMaxima[stageIndex] = Math.max( + this.#queueMaxima[stageIndex] ?? 0, + this.#queueDepths[stageIndex] ?? 0, + ); + } + + #leaveStageQueue(stageIndex: number): void { + if ((this.#queueDepths[stageIndex] ?? 0) <= 0) return; + this.#queueDepths[stageIndex] = (this.#queueDepths[stageIndex] ?? 1) - 1; + (this.#stageQueues[stageIndex] as AsyncSemaphore).release(); + } + + #cancelledResult( + item: ItemState, + stage?: NormalizedStage, + stageIndex?: number, + ): PipelineItemResult { + return Object.freeze({ + itemIndex: item.itemIndex, + status: "cancelled", + inputBound: true, + input: item.input, + completedStages: item.completedStages, + totalAttempts: item.totalAttempts, + failure: itemFailure( + "ITEM_CANCELLED", + "pipeline cancellation prevented the item from completing", + item.itemIndex, + 0, + { + ...(stage === undefined ? {} : { stageId: stage.id }), + ...(stageIndex === undefined ? {} : { stageIndex }), + }, + ), + }); + } + + #commitResult(result: PipelineItemResult): void { + if (this.#terminalIndices.has(result.itemIndex)) return; + this.#terminalIndices.add(result.itemIndex); + this.#statusCounts[result.status] += 1; + if (this.#consumerClosed) { + this.#credit.release(); + return; + } + if (this.#options.ordering === "completion") { + this.#delivery.put(result); + return; + } + this.#reorder.set(result.itemIndex, result); + while (this.#reorder.has(this.#nextInputResult)) { + this.#delivery.put(this.#reorder.get(this.#nextInputResult) as PipelineItemResult); + this.#reorder.delete(this.#nextInputResult); + this.#nextInputResult += 1; + } + } + + #maybeFinishProcessing(): void { + if (!this.#sourceFinished || this.#itemTasks.size > 0 || this.#processingFinished) return; + this.#processingFinished = true; + this.#signalEnd(); + this.#maybeFinishSummary(); + } + + #signalEnd(): void { + if (this.#endSignalled) return; + this.#endSignalled = true; + this.#delivery.put(PIPELINE_END); + } + + #maybeFinishSummary(): void { + if (!this.#processingFinished || this.#summary !== undefined) return; + if (!this.#consumerClosed && this.#emitted < this.#accepted) return; + this.#finishSummary(this.#runController.signal.aborted ? "cancelled" : undefined); + } + + #finishSummary(forcedStatus?: PipelineRunStatus): PipelineSummary { + if (this.#summary !== undefined) return this.#summary; + const status = forcedStatus ?? ( + this.#runFailure !== undefined || this.#statusCounts.failed > 0 || this.#statusCounts.dropped > 0 + ? "failed" + : "succeeded" + ); + const concurrency: Record = Object.create(null) as Record; + const queueDepth: Record = Object.create(null) as Record; + for (const [index, stage] of this.#stages.entries()) { + Object.defineProperty(concurrency, stage.id, { + value: this.#activeMaxima[index] ?? 0, + enumerable: true, + }); + Object.defineProperty(queueDepth, stage.id, { + value: this.#queueMaxima[index] ?? 0, + enumerable: true, + }); + } + const summary: PipelineSummary = Object.freeze({ + status, + accepted: this.#accepted, + emitted: this.#emitted, + succeeded: this.#statusCounts.succeeded, + failed: this.#statusCounts.failed, + dropped: this.#statusCounts.dropped, + cancelled: this.#statusCounts.cancelled, + maxObservedInFlight: this.#maxObservedInFlight, + stageMaxObservedConcurrency: snapshotRecord(concurrency), + stageMaxObservedQueueDepth: snapshotRecord(queueDepth), + ...(this.#runFailure === undefined ? {} : { runFailure: this.#runFailure }), + }); + this.#summary = summary; + if (this.#externalAbortListener !== undefined) { + this.#options.cancellationSignal?.removeEventListener("abort", this.#externalAbortListener); + } + this.#completionResolve(summary); + return summary; + } +} + +/** Construct a lazy, bounded, single-consumer pipeline over portable JSON items. */ +export function runPipeline( + source: PipelineSource, + stages: Iterable, + options: PipelineOptions = {}, +): PipelineRun { + validateSource(source); + const normalizedOptions = normalizeOptions(options); + const normalizedStages = normalizeStages( + stages, + normalizedOptions.maxItems, + normalizedOptions.maxStages, + ); + return new PipelineRunImpl(source, normalizedStages, normalizedOptions); +} diff --git a/packages/runtime/src/types.ts b/packages/runtime/src/types.ts index 8e2cfa3..75ec8d5 100644 --- a/packages/runtime/src/types.ts +++ b/packages/runtime/src/types.ts @@ -106,3 +106,102 @@ export interface SchedulerOptions { concurrency?: number; signal?: AbortSignal; } + +export type PipelineFailureCode = + | "INVALID_INPUT" + | "STAGE_EXECUTION_FAILED" + | "STAGE_TIMEOUT" + | "INVALID_OUTPUT" + | "ITEM_CANCELLED"; + +export type PipelineItemStatus = "succeeded" | "failed" | "dropped" | "cancelled"; +export type PipelineRunStatus = "succeeded" | "failed" | "cancelled"; +export type PipelineOrdering = "input" | "completion"; +export type PipelineFailurePolicy = "stop" | "drop" | "dead-letter"; + +export interface PipelineItemFailure { + code: PipelineFailureCode; + message: string; + itemIndex: number; + stageId?: string; + stageIndex?: number; + attempt: number; + retryable: false; + causeName?: string; +} + +export interface PipelineItemResult { + itemIndex: number; + status: PipelineItemStatus; + inputBound: boolean; + input?: JsonValue; + output?: JsonValue; + completedStages: number; + totalAttempts: number; + failure?: PipelineItemFailure; +} + +export interface PipelineRunFailure { + code: "SOURCE_FAILED" | "ITEM_LIMIT_REACHED"; + message: string; + causeName?: string; +} + +export interface PipelineSummary { + status: PipelineRunStatus; + accepted: number; + emitted: number; + succeeded: number; + failed: number; + dropped: number; + cancelled: number; + maxObservedInFlight: number; + stageMaxObservedConcurrency: Readonly>; + stageMaxObservedQueueDepth: Readonly>; + runFailure?: PipelineRunFailure; +} + +export interface PipelineHandlerContext { + readonly input: JsonValue; + readonly itemIndex: number; + readonly stageId: string; + readonly stageIndex: number; + readonly attempt: number; + readonly signal: AbortSignal; +} + +export type PipelineHandler = ( + context: PipelineHandlerContext, +) => unknown | Promise; + +export interface PipelineRetryOptions { + maxAttempts?: number; + initialDelayMs?: number; + backoffMultiplier?: number; + maxDelayMs?: number; +} + +export interface PipelineStage { + id: string; + handler: PipelineHandler; + concurrency?: number; + timeoutMs?: number; + retry?: PipelineRetryOptions; + onFailure?: PipelineFailurePolicy; +} + +export interface PipelineOptions { + bufferCapacity?: number; + maxInFlight?: number; + maxItems?: number; + maxStages?: number; + ordering?: PipelineOrdering; + cancellationSignal?: AbortSignal; +} + +export type PipelineSource = Iterable | AsyncIterable; + +export interface PipelineRun extends AsyncIterableIterator { + readonly completion: Promise; + close(reason?: unknown): Promise; +} diff --git a/packages/runtime/test/pipeline.test.ts b/packages/runtime/test/pipeline.test.ts new file mode 100644 index 0000000..e5ea8c4 --- /dev/null +++ b/packages/runtime/test/pipeline.test.ts @@ -0,0 +1,1206 @@ +import { describe, expect, it, vi } from "vitest"; +import { + runPipeline, + type PipelineHandlerContext, + type PipelineItemResult, + type PipelineStage, +} from "../src/index.js"; + +function gate(): { readonly promise: Promise; readonly open: () => void } { + let open!: () => void; + const promise = new Promise((resolve) => { + open = resolve; + }); + return { promise, open }; +} + +async function collect(run: AsyncIterable): Promise { + const results: PipelineItemResult[] = []; + for await (const result of run) results.push(result); + return results; +} + +async function flushMicrotasks(turns = 20): Promise { + for (let turn = 0; turn < turns; turn += 1) await Promise.resolve(); +} + +describe("runPipeline", () => { + it("is lazy, is its own iterator, and preserves JSON null presence", async () => { + let iterated = 0; + let pulls = 0; + const source: Iterable = { + [Symbol.iterator]() { + iterated += 1; + return { + next(): IteratorResult { + pulls += 1; + return pulls === 1 ? { done: false, value: null } : { done: true, value: undefined }; + }, + }; + }, + }; + + const run = runPipeline(source, [], { maxItems: 2, maxInFlight: 1 }); + expect(run[Symbol.asyncIterator]()).toBe(run); + expect(iterated).toBe(0); + expect(pulls).toBe(0); + + let completionSettled = false; + void run.completion.then(() => { completionSettled = true; }); + await flushMicrotasks(2); + expect(completionSettled).toBe(false); + expect(iterated).toBe(0); + + const first = await run.next(); + expect(first.done).toBe(false); + expect(first.value).toMatchObject({ + itemIndex: 0, + status: "succeeded", + inputBound: true, + input: null, + output: null, + completedStages: 0, + totalAttempts: 0, + }); + expect(Object.hasOwn(first.value as object, "failure")).toBe(false); + expect((await run.next()).done).toBe(true); + expect(await run.completion).toMatchObject({ + status: "succeeded", + accepted: 1, + emitted: 1, + succeeded: 1, + }); + }); + + it("bounds retries and dead-letters invalid output without stopping siblings", async () => { + const attempts = new Map(); + const run = runPipeline( + [1, 2, 3], + [{ + id: "prepare", + concurrency: 2, + retry: { maxAttempts: 2 }, + handler: async (context) => { + attempts.set(context.itemIndex, (attempts.get(context.itemIndex) ?? 0) + 1); + if (context.itemIndex === 0 && context.attempt === 1) throw new Error("transient"); + if (context.itemIndex === 2) return Number.POSITIVE_INFINITY; + return { value: context.input }; + }, + }], + { bufferCapacity: 1, maxInFlight: 3, maxItems: 4 }, + ); + + const results = await collect(run); + expect(results.map(({ itemIndex }) => itemIndex)).toEqual([0, 1, 2]); + expect(results.map(({ status }) => status)).toEqual(["succeeded", "succeeded", "failed"]); + expect(results[0]?.totalAttempts).toBe(2); + expect(results[2]?.totalAttempts).toBe(1); + expect(results[2]?.failure).toMatchObject({ + code: "INVALID_OUTPUT", + stageId: "prepare", + stageIndex: 0, + attempt: 1, + retryable: false, + }); + expect(attempts.get(2)).toBe(1); + const summary = await run.completion; + expect(summary).toMatchObject({ status: "failed", succeeded: 2, failed: 1 }); + expect(summary.maxObservedInFlight).toBeLessThanOrEqual(3); + expect(summary.stageMaxObservedConcurrency.prepare).toBeLessThanOrEqual(2); + expect(summary.stageMaxObservedQueueDepth.prepare).toBeLessThanOrEqual(1); + }); + + it("contains hostile error getters and releases the stage slot for the next item", async () => { + let messageReads = 0; + let nameReads = 0; + const hostile = new Proxy(new Error("hidden"), { + get(target, property, receiver) { + if (property === "message") { + messageReads += 1; + throw new Error("message getter failed"); + } + if (property === "name") { + nameReads += 1; + throw new Error("name getter failed"); + } + return Reflect.get(target, property, receiver); + }, + }); + const calls: number[] = []; + const safetyController = new AbortController(); + const run = runPipeline( + [0, 1], + [{ + id: "serial", + concurrency: 1, + handler: ({ input }) => { + if (typeof input !== "number") throw new TypeError("expected number"); + calls.push(input); + if (input === 0) throw hostile; + return input; + }, + }], + { + bufferCapacity: 1, + maxInFlight: 1, + maxItems: 3, + cancellationSignal: safetyController.signal, + }, + ); + const safetyTimer = setTimeout(() => { + safetyController.abort(new Error("stage-slot regression timed out")); + }, 1_000); + + try { + const results = await collect(run); + expect(calls).toEqual([0, 1]); + expect(results[0]).toMatchObject({ + itemIndex: 0, + status: "failed", + totalAttempts: 1, + failure: { + code: "STAGE_EXECUTION_FAILED", + stageId: "serial", + attempt: 1, + causeName: "object", + }, + }); + expect(results[0]?.failure?.message).toContain("stage 'serial' failed"); + expect(results[1]).toMatchObject({ itemIndex: 1, status: "succeeded", output: 1 }); + expect(messageReads).toBeGreaterThan(0); + expect(nameReads).toBeGreaterThan(0); + expect(await run.completion).toMatchObject({ + status: "failed", + succeeded: 1, + failed: 1, + stageMaxObservedConcurrency: { serial: 1 }, + }); + } finally { + clearTimeout(safetyTimer); + await run.close(); + } + }); + + it("emits drop as an explicit structured terminal record", async () => { + const stages: PipelineStage[] = [{ + id: "validate", + onFailure: "drop", + handler: ({ itemIndex }) => { + if (itemIndex === 0) throw Object.assign(new Error("bad item"), { name: "Rejected" }); + return "accepted"; + }, + }]; + const run = runPipeline(["bad", "good"], stages, { maxItems: 3, maxInFlight: 2 }); + stages.length = 0; + + const results = await collect(run); + expect(results[0]).toMatchObject({ + itemIndex: 0, + status: "dropped", + input: "bad", + completedStages: 0, + totalAttempts: 1, + failure: { code: "STAGE_EXECUTION_FAILED", causeName: "Rejected" }, + }); + expect(Object.hasOwn(results[0] as object, "output")).toBe(false); + expect(results[1]).toMatchObject({ status: "succeeded", output: "accepted" }); + expect((await run.completion).dropped).toBe(1); + }); + + it("stop closes its source without one extra pull", async () => { + let pulls = 0; + let closes = 0; + const source: Iterable = { + [Symbol.iterator]() { + return { + next(): IteratorResult { + pulls += 1; + return { done: false, value: pulls === 1 ? "stop" : "must-not-pull" }; + }, + return(): IteratorResult { + closes += 1; + return { done: true, value: undefined }; + }, + }; + }, + }; + const run = runPipeline( + source, + [{ id: "gate", onFailure: "stop", handler: () => { throw new Error("stop"); } }], + { bufferCapacity: 1, maxInFlight: 1, maxItems: 3 }, + ); + + const results = await collect(run); + expect(results).toHaveLength(1); + expect(results[0]?.status).toBe("failed"); + expect(pulls).toBe(1); + expect(closes).toBe(1); + }); + + it("enforces maxItems without probing the source again", async () => { + let pulls = 0; + const source: Iterable = { + [Symbol.iterator]() { + return { + next(): IteratorResult { + pulls += 1; + return { done: false, value: pulls }; + }, + }; + }, + }; + const run = runPipeline(source, [], { maxItems: 2, maxInFlight: 2 }); + expect(await collect(run)).toHaveLength(2); + expect(pulls).toBe(2); + expect(await run.completion).toMatchObject({ + status: "failed", + accepted: 2, + emitted: 2, + runFailure: { code: "ITEM_LIMIT_REACHED" }, + }); + }); + + it("drains an accepted prefix after an asynchronous source error", async () => { + async function* source(): AsyncGenerator { + yield 1; + yield 2; + throw Object.assign(new Error("offline"), { name: "SourceOffline" }); + } + const run = runPipeline(source(), [], { maxItems: 4, maxInFlight: 3 }); + expect((await collect(run)).map(({ output }) => output)).toEqual([1, 2]); + expect(await run.completion).toMatchObject({ + status: "failed", + accepted: 2, + emitted: 2, + runFailure: { code: "SOURCE_FAILED", causeName: "SourceOffline" }, + }); + }); + + it("lets a fast item enter a later stage before a slow earlier item finishes", async () => { + const releaseSlow = gate(); + let fastReachedSecond = false; + const run = runPipeline( + ["slow", "fast"], + [ + { + id: "first", + concurrency: 2, + handler: async (context) => { + if (context.itemIndex === 0) { + await releaseSlow.promise; + return "slow-first"; + } + return "fast-first"; + }, + }, + { + id: "second", + handler: (context) => { + if (context.itemIndex === 1) { + fastReachedSecond = true; + releaseSlow.open(); + return "fast-done"; + } + return "slow-done"; + }, + }, + ], + { bufferCapacity: 1, maxInFlight: 2, maxItems: 3, ordering: "input" }, + ); + + const results = await collect(run); + expect(fastReachedSecond).toBe(true); + expect(results.map(({ itemIndex }) => itemIndex)).toEqual([0, 1]); + expect(results.map(({ output }) => output)).toEqual(["slow-done", "fast-done"]); + }); + + it("delivers terminal records in completion order when requested", async () => { + const releaseSlow = gate(); + const run = runPipeline( + [0, 1], + [{ + id: "work", + concurrency: 2, + handler: async (context) => { + if (context.itemIndex === 0) await releaseSlow.promise; + return context.itemIndex === 0 ? "slow" : "fast"; + }, + }], + { maxItems: 3, maxInFlight: 2, ordering: "completion" }, + ); + const first = await run.next(); + expect(first.value?.itemIndex).toBe(1); + releaseSlow.open(); + expect((await run.next()).value?.itemIndex).toBe(0); + expect((await run.next()).done).toBe(true); + }); + + it("holds global credit until delivery and bounds slow-consumer pull-ahead", async () => { + let pulls = 0; + const source: Iterable = { + [Symbol.iterator]() { + return { + next(): IteratorResult { + if (pulls >= 5) return { done: true, value: undefined }; + const value = pulls; + pulls += 1; + return { done: false, value }; + }, + }; + }, + }; + const run = runPipeline(source, [], { + bufferCapacity: 1, + maxInFlight: 2, + maxItems: 6, + ordering: "completion", + }); + const first = await run.next(); + await flushMicrotasks(); + const pullsWhilePaused = pulls; + const rest = await collect(run); + + expect(pullsWhilePaused).toBeLessThanOrEqual(3); + expect(new Set([first.value, ...rest].map((item) => item?.itemIndex))).toEqual( + new Set([0, 1, 2, 3, 4]), + ); + expect((await run.completion).maxObservedInFlight).toBeLessThanOrEqual(2); + }); + + it("applies real downstream-buffer backpressure to additional upstream attempts", async () => { + const downstreamStarted = gate(); + const releaseDownstream = gate(); + let upstreamCalls = 0; + const run = runPipeline( + [0, 1, 2, 3, 4], + [ + { + id: "upstream", + concurrency: 1, + handler: (context) => { + upstreamCalls += 1; + return context.input; + }, + }, + { + id: "downstream", + concurrency: 1, + handler: async (context) => { + if (context.itemIndex === 0) { + downstreamStarted.open(); + await releaseDownstream.promise; + } + return context.input; + }, + }, + ], + { bufferCapacity: 1, maxInFlight: 5, maxItems: 6 }, + ); + + const firstRead = run.next(); + await downstreamStarted.promise; + await flushMicrotasks(); + const callsWhileBlocked = upstreamCalls; + releaseDownstream.open(); + const first = await firstRead; + const rest = await collect(run); + + expect(callsWhileBlocked).toBeLessThanOrEqual(3); + expect([first.value, ...rest]).toHaveLength(5); + expect((await run.completion).stageMaxObservedQueueDepth.downstream).toBeLessThanOrEqual(1); + }); + + it("does not construct or pull a pre-cancelled source", async () => { + let iterated = 0; + let pulls = 0; + const source: Iterable = { + [Symbol.iterator]() { + iterated += 1; + return { + next(): IteratorResult { + pulls += 1; + return { done: false, value: 1 }; + }, + }; + }, + }; + const controller = new AbortController(); + controller.abort(new Error("pre-cancelled")); + const run = runPipeline(source, [], { cancellationSignal: controller.signal }); + + expect(await collect(run)).toEqual([]); + expect(iterated).toBe(0); + expect(pulls).toBe(0); + expect((await run.completion).status).toBe("cancelled"); + }); + + it("gives cancellation priority when iterator construction aborts and throws", async () => { + const controller = new AbortController(); + const source: Iterable = { + [Symbol.iterator](): Iterator { + controller.abort(new Error("cancel from factory")); + throw new Error("factory failed after cancellation"); + }, + }; + const run = runPipeline(source, [], { cancellationSignal: controller.signal }); + + expect(await collect(run)).toEqual([]); + expect(await run.completion).toMatchObject({ + status: "cancelled", + accepted: 0, + runFailure: { code: "SOURCE_FAILED" }, + }); + }); + + it("gives queued cancellation priority when iterator construction throws", async () => { + const controller = new AbortController(); + const removeEventListener = vi.spyOn(controller.signal, "removeEventListener"); + const source: Iterable = { + [Symbol.iterator](): Iterator { + queueMicrotask(() => controller.abort(new Error("queued cancel from factory"))); + throw new Error("factory failed before queued cancellation"); + }, + }; + const run = runPipeline(source, [], { cancellationSignal: controller.signal }); + + expect(await collect(run)).toEqual([]); + expect(await run.completion).toMatchObject({ + status: "cancelled", + accepted: 0, + runFailure: { code: "SOURCE_FAILED" }, + }); + expect(removeEventListener).toHaveBeenCalledWith("abort", expect.any(Function)); + }); + + it("ignores a throwing source return getter without hanging completion", async () => { + const source: Iterable = { + [Symbol.iterator]() { + return { + next(): IteratorResult { + return { done: true, value: undefined }; + }, + get return(): never { + throw new Error("cleanup getter failed"); + }, + }; + }, + }; + const run = runPipeline(source, [], { maxItems: 2, maxInFlight: 1 }); + + expect(await run.next()).toEqual({ done: true, value: undefined }); + expect(await run.completion).toMatchObject({ status: "succeeded", accepted: 0, emitted: 0 }); + }); + + it("does not admit a source value when cancellation wins after pull settlement", async () => { + const controller = new AbortController(); + let pulls = 0; + const source: AsyncIterable = { + [Symbol.asyncIterator]() { + return { + next(): Promise> { + pulls += 1; + const settled = Promise.resolve({ done: false as const, value: 42 }); + void settled.then(() => queueMicrotask(() => controller.abort(new Error("cancel")))); + return settled; + }, + }; + }, + }; + const run = runPipeline(source, [], { + maxItems: 2, + maxInFlight: 1, + cancellationSignal: controller.signal, + }); + + expect(await collect(run)).toEqual([]); + expect(pulls).toBe(1); + expect(await run.completion).toMatchObject({ + status: "cancelled", + accepted: 0, + emitted: 0, + }); + }); + + it("does not admit a source value when its done getter cancels and returns false", async () => { + const controller = new AbortController(); + let doneReads = 0; + let valueReads = 0; + const source: Iterable = { + [Symbol.iterator]() { + return { + next(): IteratorResult { + return { + get done(): false { + doneReads += 1; + controller.abort(new Error("cancel from done getter")); + return false; + }, + get value(): number { + valueReads += 1; + return 42; + }, + }; + }, + }; + }, + }; + const run = runPipeline(source, [], { + maxItems: 2, + maxInFlight: 1, + cancellationSignal: controller.signal, + }); + + expect(await collect(run)).toEqual([]); + expect(doneReads).toBe(1); + expect(valueReads).toBe(0); + expect(await run.completion).toMatchObject({ + status: "cancelled", + accepted: 0, + emitted: 0, + }); + }); + + it("retains a source failure when its done getter cancels and throws", async () => { + const controller = new AbortController(); + const source: Iterable = { + [Symbol.iterator]() { + return { + next(): IteratorResult { + return { + get done(): never { + controller.abort(new Error("cancel from throwing done getter")); + throw Object.assign(new Error("done getter failed"), { name: "DoneGetterError" }); + }, + value: 42, + }; + }, + }; + }, + }; + const run = runPipeline(source, [], { + maxItems: 2, + maxInFlight: 1, + cancellationSignal: controller.signal, + }); + + expect(await collect(run)).toEqual([]); + expect(await run.completion).toMatchObject({ + status: "cancelled", + accepted: 0, + emitted: 0, + runFailure: { code: "SOURCE_FAILED", causeName: "DoneGetterError" }, + }); + }); + + it("gives cancellation priority when a value getter aborts and throws during snapshot", async () => { + const controller = new AbortController(); + let pulls = 0; + const source: Iterable = { + [Symbol.iterator]() { + return { + next(): IteratorResult { + pulls += 1; + return { + done: false, + get value(): number { + controller.abort(new Error("cancel from value getter")); + throw new Error("value getter failed after cancellation"); + }, + }; + }, + }; + }, + }; + const run = runPipeline(source, [], { + maxItems: 2, + maxInFlight: 1, + cancellationSignal: controller.signal, + }); + + const results = await collect(run); + expect(pulls).toBe(1); + expect(results).toHaveLength(1); + expect(results[0]).toMatchObject({ + itemIndex: 0, + status: "cancelled", + inputBound: false, + completedStages: 0, + totalAttempts: 0, + failure: { code: "ITEM_CANCELLED", attempt: 0, retryable: false }, + }); + expect(Object.hasOwn(results[0] as object, "input")).toBe(false); + expect(Object.hasOwn(results[0] as object, "output")).toBe(false); + expect(await run.completion).toMatchObject({ + status: "cancelled", + accepted: 1, + emitted: 1, + failed: 0, + cancelled: 1, + }); + }); + + it("does not start or count a stage attempt when cancellation wins after slot acquisition", async () => { + const controller = new AbortController(); + let pulled = false; + let handlerCalls = 0; + const source: Iterable = { + [Symbol.iterator]() { + return { + next(): IteratorResult { + if (pulled) return { done: true, value: undefined }; + pulled = true; + return { + done: false, + get value(): number { + queueMicrotask(() => controller.abort(new Error("cancel after slot acquisition"))); + return 42; + }, + }; + }, + }; + }, + }; + const run = runPipeline( + source, + [{ + id: "work", + handler: ({ input }) => { + handlerCalls += 1; + return input; + }, + }], + { maxItems: 2, maxInFlight: 1, cancellationSignal: controller.signal }, + ); + + const result = await run.next(); + expect(handlerCalls).toBe(0); + expect(result.value).toMatchObject({ + itemIndex: 0, + status: "cancelled", + inputBound: true, + input: 42, + completedStages: 0, + totalAttempts: 0, + failure: { + code: "ITEM_CANCELLED", + stageId: "work", + stageIndex: 0, + attempt: 0, + }, + }); + expect((await run.next()).done).toBe(true); + expect(await run.completion).toMatchObject({ + status: "cancelled", + accepted: 1, + emitted: 1, + stageMaxObservedConcurrency: { work: 0 }, + }); + }); + + it("settles a running item as cancelled and signals its handler", async () => { + const started = gate(); + let handlerObservedAbort = false; + const controller = new AbortController(); + const run = runPipeline( + [1], + [{ + id: "work", + handler: async ({ signal }) => { + started.open(); + await new Promise((resolve) => { + const onAbort = () => { + handlerObservedAbort = true; + resolve(); + }; + if (signal.aborted) onAbort(); + else signal.addEventListener("abort", onAbort, { once: true }); + }); + return "late-success"; + }, + }], + { maxItems: 2, maxInFlight: 1, cancellationSignal: controller.signal }, + ); + const pending = run.next(); + await started.promise; + controller.abort(new Error("cancel")); + const result = await pending; + + expect(result.value).toMatchObject({ + status: "cancelled", + totalAttempts: 1, + failure: { code: "ITEM_CANCELLED" }, + }); + expect(handlerObservedAbort).toBe(true); + expect((await run.next()).done).toBe(true); + expect((await run.completion).status).toBe("cancelled"); + }); + + it("gives cancellation priority before a rejected stage outcome commits", async () => { + const controller = new AbortController(); + const run = runPipeline( + [1], + [{ + id: "work", + onFailure: "stop", + handler: () => { + const rejected = Promise.reject(new Error("stage failed")); + void rejected.catch(() => queueMicrotask(() => controller.abort(new Error("cancel")))); + return rejected; + }, + }], + { maxItems: 2, maxInFlight: 1, cancellationSignal: controller.signal }, + ); + + const result = await run.next(); + expect(result.value).toMatchObject({ + status: "cancelled", + totalAttempts: 1, + failure: { code: "ITEM_CANCELLED", stageId: "work", stageIndex: 0 }, + }); + expect((await run.next()).done).toBe(true); + expect(await run.completion).toMatchObject({ status: "cancelled", failed: 0, cancelled: 1 }); + }); + + it("identifies the next stage when cancellation interrupts downstream admission", async () => { + const controller = new AbortController(); + const downstreamStarted = gate(); + const allUpstreamReturned = gate(); + let upstreamReturns = 0; + const run = runPipeline( + [0, 1, 2], + [ + { + id: "upstream", + concurrency: 3, + handler: ({ input }) => { + upstreamReturns += 1; + if (upstreamReturns === 3) allUpstreamReturned.open(); + return input; + }, + }, + { + id: "downstream", + concurrency: 1, + handler: async () => { + downstreamStarted.open(); + return await new Promise(() => undefined); + }, + }, + ], + { + bufferCapacity: 1, + maxInFlight: 3, + maxItems: 4, + cancellationSignal: controller.signal, + }, + ); + const firstRead = run.next(); + await downstreamStarted.promise; + await allUpstreamReturned.promise; + await flushMicrotasks(); + controller.abort(new Error("cancel blocked admission")); + const first = await firstRead; + const results = [first.value, ...await collect(run)]; + const blocked = results.find((item) => item?.itemIndex === 2); + + expect(blocked).toMatchObject({ + status: "cancelled", + completedStages: 1, + totalAttempts: 1, + failure: { + code: "ITEM_CANCELLED", + stageId: "downstream", + stageIndex: 1, + attempt: 0, + }, + }); + }); + + it("times out and retries to the exact finite attempt bound", async () => { + let calls = 0; + const run = runPipeline( + [1], + [{ + id: "work", + timeoutMs: 2, + retry: { maxAttempts: 2 }, + handler: () => { + calls += 1; + return new Promise(() => undefined); + }, + }], + { maxItems: 2, maxInFlight: 1 }, + ); + const result = await run.next(); + + expect(result.value).toMatchObject({ + status: "failed", + totalAttempts: 2, + failure: { code: "STAGE_TIMEOUT", attempt: 2 }, + }); + expect(calls).toBe(2); + expect((await run.next()).done).toBe(true); + }); + + it("for-await break delegates to idempotent close and accounts accepted work", async () => { + const run = runPipeline([0, 1, 2, 3], [], { maxItems: 5, maxInFlight: 2 }); + for await (const item of run) { + expect(item.itemIndex).toBe(0); + break; + } + const firstSummary = await run.completion; + const secondSummary = await run.close(); + + expect(firstSummary.status).toBe("cancelled"); + expect(firstSummary.accepted).toBe( + firstSummary.succeeded + firstSummary.failed + firstSummary.dropped + firstSummary.cancelled, + ); + expect(secondSummary).toBe(firstSummary); + }); + + it("close wakes a pending read and completes without further demand", async () => { + const started = gate(); + const run = runPipeline( + [1], + [{ + id: "work", + handler: async ({ signal }) => { + started.open(); + await new Promise((resolve) => { + if (signal.aborted) resolve(); + else signal.addEventListener("abort", () => resolve(), { once: true }); + }); + return "late"; + }, + }], + { maxItems: 2, maxInFlight: 1 }, + ); + const pendingRead = run.next(); + await started.promise; + const summary = await run.close(); + const delivered = await pendingRead; + + expect(delivered.done).toBe(true); + expect(summary).toMatchObject({ status: "cancelled", accepted: 1, emitted: 0, cancelled: 1 }); + expect(await run.close()).toBe(summary); + }); + + it("rejects concurrent next without stealing the pending result", async () => { + const started = gate(); + const release = gate(); + const run = runPipeline( + [1], + [{ + id: "work", + handler: async (context) => { + started.open(); + await release.promise; + return context.input; + }, + }], + { maxItems: 2 }, + ); + const firstRead = run.next(); + await started.promise; + await expect(run.next()).rejects.toThrow(/concurrent/i); + release.open(); + expect((await firstRead).value?.output).toBe(1); + expect((await run.next()).done).toBe(true); + }); + + it("snapshots a reused source value before the next pull mutates it", async () => { + const shared = { values: [1] }; + let pull = 0; + const source: Iterable = { + [Symbol.iterator]() { + return { + next(): IteratorResult { + pull += 1; + if (pull === 1) return { done: false, value: shared }; + if (pull === 2) { + shared.values.push(2); + return { done: false, value: "second" }; + } + return { done: true, value: undefined }; + }, + }; + }, + }; + const results = await collect(runPipeline(source, [], { maxItems: 3, maxInFlight: 2 })); + expect(results[0]?.input).toEqual({ values: [1] }); + expect(results[0]?.output).toEqual({ values: [1] }); + }); + + it("snapshots handler output before a queued post-return mutation", async () => { + const owned = { values: [1] }; + const run = runPipeline( + [0], + [{ + id: "work", + handler: () => { + queueMicrotask(() => owned.values.push(2)); + return owned; + }, + }], + { maxItems: 2 }, + ); + const result = await run.next(); + + expect(result.value?.output).toEqual({ values: [1] }); + await flushMicrotasks(); + expect(owned).toEqual({ values: [1, 2] }); + expect((await run.next()).done).toBe(true); + }); + + it("distinguishes invalid input absence from valid null", async () => { + const run = runPipeline([undefined, null], [], { maxItems: 3, maxInFlight: 2 }); + const results = await collect(run); + expect(results[0]).toMatchObject({ + status: "failed", + inputBound: false, + completedStages: 0, + totalAttempts: 0, + failure: { code: "INVALID_INPUT", attempt: 0 }, + }); + expect(Object.hasOwn(results[0] as object, "input")).toBe(false); + expect(Object.hasOwn(results[0] as object, "output")).toBe(false); + expect(results[1]).toMatchObject({ status: "succeeded", inputBound: true, input: null, output: null }); + }); + + it("fails synchronously when stage id or handler getters throw without constructing source", () => { + let iterated = 0; + const source: Iterable = { + [Symbol.iterator]() { + iterated += 1; + return [1][Symbol.iterator](); + }, + }; + const identity: PipelineStage["handler"] = ({ input }) => input; + + let idReads = 0; + const badId: PipelineStage = { + get id(): string { + idReads += 1; + throw new Error("id exploded"); + }, + handler: identity, + }; + let idError: unknown; + try { + runPipeline(source, [badId]); + } catch (error) { + idError = error; + } + expect(idError).toBeInstanceOf(TypeError); + expect((idError as Error).message).toMatch(/stages\[0\]\.id getter failed.*id exploded/); + expect(idReads).toBe(1); + + let handlerReads = 0; + const badHandler: PipelineStage = { + id: "bad-handler", + get handler(): PipelineStage["handler"] { + handlerReads += 1; + throw new Error("handler exploded"); + }, + }; + let handlerError: unknown; + try { + runPipeline(source, [badHandler]); + } catch (error) { + handlerError = error; + } + expect(handlerError).toBeInstanceOf(TypeError); + expect((handlerError as Error).message).toMatch( + /stages\[0\]\.handler getter failed.*handler exploded/, + ); + expect(handlerReads).toBe(1); + expect(iterated).toBe(0); + }); + + it("captures every structural stage property once and retains the id and handler snapshots", async () => { + let iterated = 0; + const source: Iterable = { + [Symbol.iterator]() { + iterated += 1; + return [7][Symbol.iterator](); + }, + }; + const reads = { + id: 0, + handler: 0, + concurrency: 0, + timeoutMs: 0, + retry: 0, + onFailure: 0, + }; + let capturedHandlerCalls = 0; + let replacementHandlerCalls = 0; + const capturedHandler: PipelineStage["handler"] = ({ input }) => { + capturedHandlerCalls += 1; + return { handler: "captured", input }; + }; + const replacementHandler: PipelineStage["handler"] = () => { + replacementHandlerCalls += 1; + return "replacement"; + }; + const stage: PipelineStage = { + get id(): string { + reads.id += 1; + return reads.id === 1 ? "captured-id" : "mutated-id"; + }, + get handler(): PipelineStage["handler"] { + reads.handler += 1; + return reads.handler === 1 ? capturedHandler : replacementHandler; + }, + get concurrency(): number { + reads.concurrency += 1; + return reads.concurrency === 1 ? 1 : 2; + }, + get timeoutMs(): undefined { + reads.timeoutMs += 1; + return undefined; + }, + get retry(): PipelineStage["retry"] { + reads.retry += 1; + return { maxAttempts: reads.retry === 1 ? 1 : 2 }; + }, + get onFailure(): PipelineStage["onFailure"] { + reads.onFailure += 1; + return reads.onFailure === 1 ? "dead-letter" : "stop"; + }, + }; + + const run = runPipeline(source, [stage], { maxItems: 2, maxInFlight: 1 }); + expect(iterated).toBe(0); + expect(reads).toEqual({ + id: 1, + handler: 1, + concurrency: 1, + timeoutMs: 1, + retry: 1, + onFailure: 1, + }); + + const results = await collect(run); + expect(results[0]).toMatchObject({ + status: "succeeded", + output: { handler: "captured", input: 7 }, + totalAttempts: 1, + }); + expect(capturedHandlerCalls).toBe(1); + expect(replacementHandlerCalls).toBe(0); + expect(reads.id).toBe(1); + expect(reads.handler).toBe(1); + expect(await run.completion).toMatchObject({ + stageMaxObservedConcurrency: { "captured-id": 1 }, + stageMaxObservedQueueDepth: { "captured-id": 1 }, + }); + }); + + it("validates numeric bounds, derived attempts, and duplicate IDs synchronously", () => { + let iterated = false; + const source: Iterable = { + [Symbol.iterator]() { + iterated = true; + return [][Symbol.iterator](); + }, + }; + const identity = ({ input }: PipelineHandlerContext) => input; + + expect(() => runPipeline(source, [], { maxInFlight: 0 })).toThrow(TypeError); + expect(() => runPipeline(source, [], { maxItems: 1.5 })).toThrow(TypeError); + expect(() => runPipeline(source, [], { maxStages: 0 })).toThrow(TypeError); + expect(() => runPipeline(source, [], { maxStages: 2_049 })).toThrow(TypeError); + expect(() => runPipeline(source, [], { maxStages: null as never })).toThrow(TypeError); + expect(() => runPipeline(source, [], { bufferCapacity: null as never })).toThrow(TypeError); + expect(() => runPipeline(source, [], { maxInFlight: null as never })).toThrow(TypeError); + expect(() => runPipeline(source, [], { maxItems: null as never })).toThrow(TypeError); + expect(() => runPipeline(source, [], { ordering: null as never })).toThrow(TypeError); + expect(() => runPipeline(source, [], { bufferCapacity: Number.POSITIVE_INFINITY })).toThrow(TypeError); + expect(() => runPipeline(source, [{ id: "bad", handler: identity, concurrency: 0 }])).toThrow(TypeError); + expect(() => runPipeline(source, [{ id: "bad", handler: identity, concurrency: null as never }])).toThrow(TypeError); + expect(() => runPipeline(source, [{ id: "bad", handler: identity, timeoutMs: 0.5 }])).toThrow(TypeError); + expect(() => runPipeline(source, [{ id: "bad", handler: identity, onFailure: null as never }])).toThrow(TypeError); + expect(() => runPipeline(source, [{ id: "bad", handler: identity, retry: null as never }])).toThrow(TypeError); + expect(() => runPipeline(source, [{ + id: "bad", + handler: identity, + retry: { maxAttempts: 2, initialDelayMs: Number.NaN }, + }])).toThrow(TypeError); + expect(() => runPipeline(source, [{ + id: "bad", + handler: identity, + retry: { maxAttempts: null as never }, + }])).toThrow(TypeError); + expect(() => runPipeline(source, [{ + id: "bad", + handler: identity, + retry: { initialDelayMs: null as never }, + }])).toThrow(TypeError); + expect(() => runPipeline(source, [{ + id: "bad", + handler: identity, + retry: { backoffMultiplier: null as never }, + }])).toThrow(TypeError); + expect(() => runPipeline(source, [{ + id: "bad", + handler: identity, + retry: { maxDelayMs: null as never }, + }])).toThrow(TypeError); + expect(() => runPipeline(source, [ + { id: "same", handler: identity }, + { id: "same", handler: identity }, + ])).toThrow(/duplicate/i); + expect(() => runPipeline( + source, + [{ id: "unsafe", handler: identity, retry: { maxAttempts: 2 } }], + { maxItems: Number.MAX_SAFE_INTEGER }, + )).toThrow(/attempt/i); + expect(iterated).toBe(false); + }); + + it("bounds an infinite stage iterable before constructing the item source", () => { + let sourceIterated = false; + let yieldedStages = 0; + let stageIteratorClosed = false; + let overflowStageReads = 0; + const source: Iterable = { + [Symbol.iterator]() { + sourceIterated = true; + return [1][Symbol.iterator](); + }, + }; + function* infiniteStages(): Generator { + try { + while (true) { + const id = `stage-${yieldedStages}`; + yieldedStages += 1; + if (yieldedStages === 4) { + yield { + get id() { + overflowStageReads += 1; + return id; + }, + get handler() { + overflowStageReads += 1; + return ({ input }: PipelineHandlerContext) => input; + }, + }; + } else { + yield { id, handler: ({ input }) => input }; + } + } + } finally { + stageIteratorClosed = true; + } + } + + expect(() => runPipeline(source, infiniteStages(), { maxStages: 3 })).toThrow( + /pipeline stage count exceeds maxStages limit of 3/, + ); + expect(yieldedStages).toBe(4); + expect(overflowStageReads).toBe(0); + expect(stageIteratorClosed).toBe(true); + expect(sourceIterated).toBe(false); + }); +}); diff --git a/python/README.md b/python/README.md index 53c3f0e..beb9b05 100644 --- a/python/README.md +++ b/python/README.md @@ -45,6 +45,57 @@ read-only snapshot of completed values. A failure is returned as a typed `NodeFailure`; independent siblings finish, while descendants are marked `UPSTREAM_FAILED`. +## Bounded standalone pipelines + +`run_pipeline` moves each accepted item through the same ordered stages without +waiting for every item to finish one stage before the next stage starts. Source +intake, stage queues, stage concurrency, per-item attempts, and the total item +count are all bounded. + +```python +from graph_engineering import PipelineStage, run_pipeline + +source_items = ["a", "b", "c"] + + +async def enrich(context): + return {"value": context.input, "enriched": True} + + +async def run_items(): + async with run_pipeline( + source_items, + [PipelineStage("enrich", enrich, concurrency=4)], + buffer_capacity=8, + max_in_flight=16, + max_items=1_000, + ) as run: + results = [item async for item in run] + summary = await run.completion() + return results, summary +``` + +The source is not advanced until the first read or async-context entry. The +runtime acquires an in-flight credit before every source pull and releases it +only when the consumer receives that item's terminal record, so a slow consumer +eventually stops source intake. Results are structured as `succeeded`, `failed`, +`dropped`, or `cancelled`; JSON `None` remains distinct from an absent input or +output in `to_dict()` projections. Stage policies are `dead-letter`, `drop`, and +`stop`, and retries and timeouts are bounded and cooperatively cancellable. + +Stage configuration is also synchronously bounded. `max_stages` defaults to +`2048`, which is also the hard protocol maximum. The factory accepts a finite +sequence with exactly that budget, but inspects at most `max_stages + 1` entries +and raises `ValueError` if another stage is present. Overflow is rejected before +the extra stage's properties or the item source are accessed; callers may set a +smaller positive `max_stages` budget for untrusted configuration. + +This primitive is in-memory and standalone. It does not activate Graph IR +`edge.mode: "stream"`, persist item queues, or provide item-level crash recovery. +When called inside a durable graph node, the complete pipeline is part of that +single node attempt and external effects remain at-least-once. See the canonical +contract in [`spec/pipeline-semantics.md`](../spec/pipeline-semantics.md). + ## Settled barrier primitive `evaluate_settled_barrier` is a deterministic, model-free decision primitive diff --git a/python/src/graph_engineering/__init__.py b/python/src/graph_engineering/__init__.py index b0a078e..e8965f1 100644 --- a/python/src/graph_engineering/__init__.py +++ b/python/src/graph_engineering/__init__.py @@ -32,6 +32,25 @@ NodeSpec, RetryPolicy, ) +from .pipeline import ( + PipelineFailureCode, + PipelineFailurePolicy, + PipelineHandler, + PipelineHandlerContext, + PipelineItemFailure, + PipelineItemResult, + PipelineItemStatus, + PipelineOrdering, + PipelineRetryOptions, + PipelineRun, + PipelineRunFailure, + PipelineRunFailureCode, + PipelineRunStatus, + PipelineSource, + PipelineStage, + PipelineSummary, + run_pipeline, +) from .primitives import ( AllBarrierPolicy, MinimumBarrierPolicy, @@ -99,6 +118,22 @@ "NodeSpec", "NodeStatus", "PercentageBarrierPolicy", + "PipelineFailureCode", + "PipelineFailurePolicy", + "PipelineHandler", + "PipelineHandlerContext", + "PipelineItemFailure", + "PipelineItemResult", + "PipelineItemStatus", + "PipelineOrdering", + "PipelineRetryOptions", + "PipelineRun", + "PipelineRunFailure", + "PipelineRunFailureCode", + "PipelineRunStatus", + "PipelineSource", + "PipelineStage", + "PipelineSummary", "PrimitiveErrorCode", "PrimitiveValidationError", "PrimitiveValidationIssue", @@ -130,6 +165,7 @@ "evaluate_settled_barrier", "resume_graph_run", "run_graph", + "run_pipeline", "start_graph_run", "try_compile_graph", ] diff --git a/python/src/graph_engineering/models.py b/python/src/graph_engineering/models.py index 1c6084e..ef2b522 100644 --- a/python/src/graph_engineering/models.py +++ b/python/src/graph_engineering/models.py @@ -3,9 +3,10 @@ from __future__ import annotations import math -from typing import Annotated, Literal, TypeAlias +from collections.abc import Mapping +from typing import Annotated, Literal, TypeAlias, cast -from pydantic import BaseModel, ConfigDict, Field, field_validator +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from pydantic import JsonValue as PydanticJsonValue JsonPrimitive: TypeAlias = str | int | float | bool | None @@ -45,6 +46,22 @@ class StrictModel(BaseModel): strict=True, ) + @model_validator(mode="before") + @classmethod + def present_optional_fields_are_not_null(cls, value: object) -> object: + """Keep schema-optional fields absent instead of accepting explicit null.""" + + if not isinstance(value, Mapping): + return value + for field_name, field in cls.model_fields.items(): + if field.is_required(): + continue + alias = field.alias or field_name + for input_name in {field_name, alias}: + if input_name in value and value[input_name] is None: + raise ValueError(f"{alias} cannot be null when present") + return value + class Metadata(StrictModel): name: Annotated[str, Field(pattern=r"^[a-z][a-z0-9-]{0,62}$")] @@ -105,15 +122,8 @@ class EdgeSpec(StrictModel): schema_: JsonObject | None = Field(default=None, alias="schema") -class GraphPolicies(BaseModel): - """Known policies plus forward-compatible extension keys.""" - - model_config = ConfigDict( - extra="allow", - frozen=True, - populate_by_name=True, - strict=True, - ) +class _KnownGraphPolicies(StrictModel): + """Validation-only view of the policy keys defined by Graph IR v1alpha1.""" max_concurrency: SafePositiveInteger | None = Field(default=None, alias="maxConcurrency") max_dynamic_nodes: SafeNonNegativeInteger | None = Field(default=None, alias="maxDynamicNodes") @@ -133,6 +143,72 @@ def max_cost_usd_is_portable(cls, value: float | None) -> float | None: return value +class GraphPolicies(StrictModel): + """Lossless policy map with typed accessors for the v1alpha1 keys. + + Policy objects deliberately keep every JSON key in ``model_extra``. A + Python field name such as ``max_concurrency`` is a valid future extension + key and must not be mistaken for the canonical ``maxConcurrency`` key. + """ + + model_config = ConfigDict( + extra="allow", + frozen=True, + populate_by_name=False, + strict=True, + ) + __pydantic_extra__: dict[str, JsonValue] = Field(init=False) + + @classmethod + def known_keys(cls) -> frozenset[str]: + """Return the canonical policy keys defined by this IR version.""" + + return frozenset( + field.alias or name for name, field in _KnownGraphPolicies.model_fields.items() + ) + + @model_validator(mode="before") + @classmethod + def validate_known_policy_keys(cls, value: object) -> object: + if not isinstance(value, Mapping): + return value + _KnownGraphPolicies.model_validate( + {key: item for key, item in value.items() if key in cls.known_keys()} + ) + return value + + def _known_value(self, alias: str) -> JsonValue | None: + return self.__pydantic_extra__.get(alias) + + @property + def max_concurrency(self) -> int | None: + return cast(int | None, self._known_value("maxConcurrency")) + + @property + def max_dynamic_nodes(self) -> int | None: + return cast(int | None, self._known_value("maxDynamicNodes")) + + @property + def max_depth(self) -> int | None: + return cast(int | None, self._known_value("maxDepth")) + + @property + def max_fan_out(self) -> int | None: + return cast(int | None, self._known_value("maxFanOut")) + + @property + def max_total_attempts(self) -> int | None: + return cast(int | None, self._known_value("maxTotalAttempts")) + + @property + def max_duration_ms(self) -> int | None: + return cast(int | None, self._known_value("maxDurationMs")) + + @property + def max_cost_usd(self) -> float | None: + return cast(float | None, self._known_value("maxCostUsd")) + + class GraphSpec(StrictModel): api_version: Literal["graphengineering.reacher-z.github.io/v1alpha1"] = Field( alias="apiVersion" diff --git a/python/src/graph_engineering/pipeline.py b/python/src/graph_engineering/pipeline.py new file mode 100644 index 0000000..4cc5586 --- /dev/null +++ b/python/src/graph_engineering/pipeline.py @@ -0,0 +1,1377 @@ +"""Bounded, standalone per-item pipeline runtime.""" + +from __future__ import annotations + +import asyncio +import inspect +import math +from collections.abc import ( + AsyncIterable, + AsyncIterator, + Awaitable, + Callable, + Iterable, + Iterator, + Mapping, + Sequence, +) +from dataclasses import dataclass +from enum import StrEnum +from types import MappingProxyType +from typing import TypeAlias, TypeVar, cast + +from .models import MAX_SAFE_INTEGER, JsonObject, JsonValue +from .portable_json import PortableJsonError, portable_json_snapshot +from .scheduler import CancellationSignal + +MAX_TIMER_MILLISECONDS = 2**31 - 1 +_DEFAULT_BUFFER_CAPACITY = 16 +_DEFAULT_MAX_IN_FLIGHT = 16 +_DEFAULT_MAX_ITEMS = 1000 +_MAX_PIPELINE_STAGES = 2048 +_DEFAULT_MAX_STAGES = _MAX_PIPELINE_STAGES + + +class PipelineItemStatus(StrEnum): + SUCCEEDED = "succeeded" + FAILED = "failed" + DROPPED = "dropped" + CANCELLED = "cancelled" + + +class PipelineRunStatus(StrEnum): + SUCCEEDED = "succeeded" + FAILED = "failed" + CANCELLED = "cancelled" + + +class PipelineFailureCode(StrEnum): + INVALID_INPUT = "INVALID_INPUT" + STAGE_EXECUTION_FAILED = "STAGE_EXECUTION_FAILED" + STAGE_TIMEOUT = "STAGE_TIMEOUT" + INVALID_OUTPUT = "INVALID_OUTPUT" + ITEM_CANCELLED = "ITEM_CANCELLED" + + +class PipelineRunFailureCode(StrEnum): + SOURCE_FAILED = "SOURCE_FAILED" + ITEM_LIMIT_REACHED = "ITEM_LIMIT_REACHED" + + +class PipelineOrdering(StrEnum): + INPUT = "input" + COMPLETION = "completion" + + +class PipelineFailurePolicy(StrEnum): + STOP = "stop" + DROP = "drop" + DEAD_LETTER = "dead-letter" + + +@dataclass(frozen=True, slots=True) +class PipelineRetryOptions: + max_attempts: int | float = 1 + initial_delay_ms: int | float = 0 + backoff_multiplier: int | float = 1 + max_delay_ms: int | float | None = None + + +@dataclass(frozen=True, slots=True) +class PipelineHandlerContext: + input: JsonValue + item_index: int + stage_id: str + stage_index: int + attempt: int + cancel_signal: CancellationSignal + + +PipelineHandler: TypeAlias = Callable[[PipelineHandlerContext], object | Awaitable[object]] + + +@dataclass(frozen=True, slots=True) +class PipelineStage: + id: str + handler: PipelineHandler + concurrency: int | float = 1 + timeout_ms: int | float | None = None + retry: PipelineRetryOptions | None = None + on_failure: PipelineFailurePolicy | str = PipelineFailurePolicy.DEAD_LETTER + + +@dataclass(frozen=True, slots=True) +class PipelineItemFailure: + code: PipelineFailureCode + message: str + item_index: int + attempt: int + retryable: bool = False + stage_id: str | None = None + stage_index: int | None = None + cause_name: str | None = None + + def to_dict(self) -> JsonObject: + result: JsonObject = { + "code": self.code, + "message": self.message, + "itemIndex": self.item_index, + } + if self.stage_id is not None: + result["stageId"] = self.stage_id + if self.stage_index is not None: + result["stageIndex"] = self.stage_index + result["attempt"] = self.attempt + result["retryable"] = False + if self.cause_name is not None: + result["causeName"] = self.cause_name + return result + + +@dataclass(frozen=True, slots=True) +class PipelineItemResult: + item_index: int + status: PipelineItemStatus + input_bound: bool + completed_stages: int + total_attempts: int + input: JsonValue = None + output: JsonValue = None + failure: PipelineItemFailure | None = None + + def to_dict(self) -> JsonObject: + result: JsonObject = { + "itemIndex": self.item_index, + "status": self.status, + "inputBound": self.input_bound, + } + if self.input_bound: + result["input"] = portable_json_snapshot(self.input) + if self.status is PipelineItemStatus.SUCCEEDED: + result["output"] = portable_json_snapshot(self.output) + result["completedStages"] = self.completed_stages + result["totalAttempts"] = self.total_attempts + if self.failure is not None: + result["failure"] = self.failure.to_dict() + return result + + +@dataclass(frozen=True, slots=True) +class PipelineRunFailure: + code: PipelineRunFailureCode + message: str + cause_name: str | None = None + + def to_dict(self) -> JsonObject: + result: JsonObject = {"code": self.code, "message": self.message} + if self.cause_name is not None: + result["causeName"] = self.cause_name + return result + + +@dataclass(frozen=True, slots=True) +class PipelineSummary: + status: PipelineRunStatus + accepted: int + emitted: int + succeeded: int + failed: int + dropped: int + cancelled: int + max_observed_in_flight: int + stage_max_observed_concurrency: Mapping[str, int] + stage_max_observed_queue_depth: Mapping[str, int] + run_failure: PipelineRunFailure | None = None + + def to_dict(self) -> JsonObject: + result: JsonObject = { + "status": self.status, + "accepted": self.accepted, + "emitted": self.emitted, + "succeeded": self.succeeded, + "failed": self.failed, + "dropped": self.dropped, + "cancelled": self.cancelled, + "maxObservedInFlight": self.max_observed_in_flight, + "stageMaxObservedConcurrency": dict(self.stage_max_observed_concurrency), + "stageMaxObservedQueueDepth": dict(self.stage_max_observed_queue_depth), + } + if self.run_failure is not None: + result["runFailure"] = self.run_failure.to_dict() + return result + + +PipelineSource: TypeAlias = Iterable[object] | AsyncIterable[object] + + +@dataclass(frozen=True, slots=True) +class _Options: + buffer_capacity: int + max_in_flight: int + max_items: int + ordering: PipelineOrdering + cancellation_signal: asyncio.Event | None + + +def _positive_integer(value: object, name: str) -> int: + if type(value) is int: + integer = value + elif type(value) is float and math.isfinite(value): + floating = value + if not floating.is_integer(): + raise TypeError(f"{name} must be an integer from 1 to {MAX_SAFE_INTEGER}") + integer = int(floating) + else: + raise TypeError(f"{name} must be an integer from 1 to {MAX_SAFE_INTEGER}") + if integer < 1 or integer > MAX_SAFE_INTEGER: + raise TypeError(f"{name} must be an integer from 1 to {MAX_SAFE_INTEGER}") + return integer + + +def _max_stages(value: object) -> int: + if type(value) is int: + integer = value + elif type(value) is float and math.isfinite(value): + floating = value + if not floating.is_integer(): + raise TypeError(f"max_stages must be an integer from 1 to {_MAX_PIPELINE_STAGES}") + integer = int(floating) + else: + raise TypeError(f"max_stages must be an integer from 1 to {_MAX_PIPELINE_STAGES}") + if integer < 1 or integer > _MAX_PIPELINE_STAGES: + raise TypeError(f"max_stages must be an integer from 1 to {_MAX_PIPELINE_STAGES}") + return integer + + +def _timer(value: object, name: str) -> int | float: + if type(value) not in {int, float}: + raise TypeError(f"{name} must be a finite number from 0 to {MAX_TIMER_MILLISECONDS}") + numeric = cast(int | float, value) + if not math.isfinite(numeric) or numeric < 0 or numeric > MAX_TIMER_MILLISECONDS: + raise TypeError(f"{name} must be a finite number from 0 to {MAX_TIMER_MILLISECONDS}") + return numeric + + +def _timeout_milliseconds(value: object, name: str) -> int: + if type(value) is int: + milliseconds = value + elif type(value) is float and math.isfinite(value): + floating = value + if not floating.is_integer(): + raise TypeError(f"{name} must be an integer from 0 to {MAX_TIMER_MILLISECONDS}") + milliseconds = int(floating) + else: + raise TypeError(f"{name} must be an integer from 0 to {MAX_TIMER_MILLISECONDS}") + if milliseconds < 0 or milliseconds > MAX_TIMER_MILLISECONDS: + raise TypeError(f"{name} must be an integer from 0 to {MAX_TIMER_MILLISECONDS}") + return milliseconds + + +def _snapshot_stages( + stages: object, + max_items: int, + max_stages: int, +) -> tuple[PipelineStage, ...]: + if not isinstance(stages, Sequence) or isinstance(stages, (str, bytes, bytearray)): + raise TypeError("stages must be a finite sequence of PipelineStage values") + copied: list[PipelineStage] = [] + seen: set[str] = set() + maximum_attempts_per_item = 0 + iterator = iter(stages) + exhausted = False + try: + for index in range(max_stages + 1): + try: + stage = next(iterator) + except StopIteration: + exhausted = True + break + if index == max_stages: + raise ValueError(f"pipeline stage count exceeds max_stages limit of {max_stages}") + if type(stage) is not PipelineStage: + raise TypeError(f"stages[{index}] must be a PipelineStage") + if type(stage.id) is not str or not stage.id: + raise TypeError(f"stages[{index}].id must be a non-empty string") + if stage.id in seen: + raise ValueError(f"duplicate pipeline stage id {stage.id!r}") + seen.add(stage.id) + if not callable(stage.handler): + raise TypeError(f"stages[{index}].handler must be callable") + concurrency = _positive_integer(stage.concurrency, f"stages[{index}].concurrency") + timeout_ms = ( + None + if stage.timeout_ms is None + else _timeout_milliseconds(stage.timeout_ms, f"stages[{index}].timeout_ms") + ) + retry = stage.retry or PipelineRetryOptions() + if type(retry) is not PipelineRetryOptions: + raise TypeError(f"stages[{index}].retry must be PipelineRetryOptions") + max_attempts = _positive_integer( + retry.max_attempts, f"stages[{index}].retry.max_attempts" + ) + initial_delay_ms = _timer( + retry.initial_delay_ms, f"stages[{index}].retry.initial_delay_ms" + ) + if ( + type(retry.backoff_multiplier) not in {int, float} + or not math.isfinite(retry.backoff_multiplier) + or retry.backoff_multiplier < 1 + ): + raise TypeError( + f"stages[{index}].retry.backoff_multiplier must be finite and at least 1" + ) + max_delay_ms = ( + initial_delay_ms + if retry.max_delay_ms is None + else _timer(retry.max_delay_ms, f"stages[{index}].retry.max_delay_ms") + ) + try: + policy = PipelineFailurePolicy(stage.on_failure) + except ValueError: + raise ValueError(f"stages[{index}].on_failure is invalid") from None + maximum_attempts_per_item += max_attempts + if maximum_attempts_per_item > MAX_SAFE_INTEGER: + raise ValueError("pipeline attempt bound exceeds the portable safe range") + copied.append( + PipelineStage( + id=stage.id, + handler=stage.handler, + concurrency=concurrency, + timeout_ms=timeout_ms, + retry=PipelineRetryOptions( + max_attempts=max_attempts, + initial_delay_ms=initial_delay_ms, + backoff_multiplier=retry.backoff_multiplier, + max_delay_ms=max_delay_ms, + ), + on_failure=policy, + ) + ) + except BaseException: + # Match JavaScript IteratorClose on abrupt configuration failure. A + # hostile close hook must not replace the deterministic validation + # error that caused the unwind. + if not exhausted: + try: + close = getattr(iterator, "close", None) + if callable(close): + close() + except BaseException: + pass + raise + if maximum_attempts_per_item and max_items > MAX_SAFE_INTEGER // maximum_attempts_per_item: + raise ValueError("pipeline maximum attempt count exceeds the portable safe range") + return tuple(copied) + + +async def _invoke(handler: PipelineHandler, context: PipelineHandlerContext) -> object: + value = handler(context) + return await value if inspect.isawaitable(value) else value + + +async def _invoke_and_snapshot( + handler: PipelineHandler, + context: PipelineHandlerContext, +) -> JsonValue: + output = await _invoke(handler, context) + try: + return portable_json_snapshot(output) + except PortableJsonError: + raise + except (Exception, asyncio.CancelledError) as exc: + # Snapshotting is a distinct protocol boundary from handler execution. + # Even if portable-JSON diagnostics encounter hostile application + # metadata, a returned unsupported value remains INVALID_OUTPUT. + raise PortableJsonError(_exception_message(exc)) from exc + + +_END = object() +_T = TypeVar("_T") + + +def _consume_future_outcome(future: asyncio.Future[_T]) -> None: + try: + future.result() + except BaseException: + return + + +def _exception_name(error: BaseException) -> str: + try: + name = type(error).__name__ + except BaseException: + return "Exception" + return name if type(name) is str and name else "Exception" + + +def _exception_message(error: BaseException) -> str: + name = _exception_name(error) + try: + message = str(error) + except BaseException: + return name + return message or name + + +class _SourceAdapter: + def __init__(self, source: PipelineSource) -> None: + self._source = source + self._iterator: AsyncIterator[object] | None = None + self._sync_iterator: Iterator[object] | None = None + self._closed = False + + def start(self) -> None: + if isinstance(self._source, AsyncIterable): + self._iterator = self._source.__aiter__() + else: + self._sync_iterator = iter(self._source) + + async def next(self) -> tuple[bool, object | None]: + if self._iterator is not None: + try: + return True, await anext(self._iterator) + except StopAsyncIteration: + return False, None + if self._sync_iterator is None: + raise AssertionError("pipeline source was not started") + try: + return True, next(self._sync_iterator) + except StopIteration: + return False, None + + async def close(self) -> None: + if self._closed: + return + self._closed = True + target = self._iterator if self._iterator is not None else self._sync_iterator + if target is None: + return + closer = getattr(target, "aclose", None) + if callable(closer): + outcome = closer() + if inspect.isawaitable(outcome): + close_future = asyncio.ensure_future(outcome) + await asyncio.sleep(0) + if close_future.done(): + _consume_future_outcome(close_future) + else: + close_future.add_done_callback(_consume_future_outcome) + return + closer = getattr(target, "close", None) + if callable(closer): + outcome = closer() + if inspect.isawaitable(outcome): + close_future = asyncio.ensure_future(outcome) + await asyncio.sleep(0) + if close_future.done(): + _consume_future_outcome(close_future) + else: + close_future.add_done_callback(_consume_future_outcome) + + +class PipelineRun(AsyncIterator[PipelineItemResult]): + """Single-pass asynchronous view over one bounded pipeline execution.""" + + def __init__( + self, + source: PipelineSource, + stages: tuple[PipelineStage, ...], + options: _Options, + ) -> None: + self._source = _SourceAdapter(source) + self._stages = stages + self._options = options + self._initialized = False + self._started = False + self._closed = False + self._advancing = False + self._cancel_event: asyncio.Event | None = None + self._delivery: asyncio.Queue[PipelineItemResult | object] | None = None + self._completion_future: asyncio.Future[PipelineSummary] | None = None + self._producer_task: asyncio.Task[None] | None = None + self._producer_cancel_requested = False + self._cancellation_task: asyncio.Task[None] | None = None + self._drain_task: asyncio.Task[None] | None = None + self._item_tasks: set[asyncio.Task[None]] = set() + self._credit: asyncio.Semaphore | None = None + self._stage_slots: tuple[asyncio.Semaphore, ...] = () + self._stage_queues: tuple[asyncio.Semaphore, ...] = () + self._queue_depths = [0 for _ in stages] + self._queue_maxima = [0 for _ in stages] + self._active_attempts = [0 for _ in stages] + self._active_maxima = [0 for _ in stages] + self._commit_lock: asyncio.Lock | None = None + self._reorder: dict[int, PipelineItemResult] = {} + self._next_input_result = 0 + self._accepted = 0 + self._emitted = 0 + self._max_observed_in_flight = 0 + self._status_counts = {status: 0 for status in PipelineItemStatus} + self._run_failure: PipelineRunFailure | None = None + self._intake_stopped = False + self._end_signalled = False + + def __aiter__(self) -> PipelineRun: + return self + + async def __aenter__(self) -> PipelineRun: + self._ensure_started() + return self + + async def __aexit__(self, *_: object) -> None: + await self.aclose() + + def _ensure_initialized(self) -> None: + if self._initialized: + return + loop = asyncio.get_running_loop() + self._initialized = True + self._cancel_event = asyncio.Event() + # One non-item slot is reserved for the end marker. Item records remain + # bounded by the independently enforced in-flight credit window. + self._delivery = asyncio.Queue(maxsize=self._options.max_in_flight + 1) + self._completion_future = loop.create_future() + self._credit = asyncio.Semaphore(self._options.max_in_flight) + self._stage_slots = tuple( + asyncio.Semaphore(cast(int, stage.concurrency)) for stage in self._stages + ) + self._stage_queues = tuple( + asyncio.Semaphore(self._options.buffer_capacity) for _ in self._stages + ) + self._commit_lock = asyncio.Lock() + + def _ensure_started(self) -> None: + self._ensure_initialized() + if self._started: + return + if self._cancel_event is None or self._delivery is None: + raise AssertionError("pipeline initialization omitted runtime primitives") + loop = asyncio.get_running_loop() + self._started = True + if ( + self._options.cancellation_signal is not None + and self._options.cancellation_signal.is_set() + ): + self._cancel_event.set() + self._end_signalled = True + self._delivery.put_nowait(_END) + return + try: + self._source.start() + except (Exception, asyncio.CancelledError) as exc: + cause_name = _exception_name(exc) + self._run_failure = PipelineRunFailure( + PipelineRunFailureCode.SOURCE_FAILED, + f"pipeline source failed to create its iterator: {_exception_message(exc)}", + cause_name, + ) + self._end_signalled = True + self._delivery.put_nowait(_END) + return + self._producer_task = loop.create_task(self._produce()) + if self._options.cancellation_signal is not None: + self._cancellation_task = loop.create_task(self._watch_cancellation()) + + async def __anext__(self) -> PipelineItemResult: + if self._advancing: + raise RuntimeError("concurrent pipeline iteration is not allowed") + if self._closed: + raise StopAsyncIteration + self._advancing = True + try: + self._ensure_started() + if self._delivery is None or self._credit is None: + raise AssertionError("pipeline delivery queue was not initialized") + delivered = await self._delivery.get() + if delivered is _END: + self._closed = True + await self._stop_cancellation_watcher() + self._finish_summary(PipelineRunStatus.CANCELLED if self._cancelled else None) + raise StopAsyncIteration + if not isinstance(delivered, PipelineItemResult): + raise AssertionError("pipeline delivered an invalid internal record") + self._emitted += 1 + self._credit.release() + await self._finish_summary_if_drained() + return delivered + finally: + self._advancing = False + + @property + def _cancelled(self) -> bool: + return bool( + (self._cancel_event is not None and self._cancel_event.is_set()) + or ( + self._options.cancellation_signal is not None + and self._options.cancellation_signal.is_set() + ) + ) + + async def completion(self) -> PipelineSummary: + self._ensure_initialized() + if self._completion_future is None: + raise AssertionError("pipeline completion future was not initialized") + return await asyncio.shield(self._completion_future) + + async def _watch_cancellation(self) -> None: + signal = self._options.cancellation_signal + if signal is None: + return + await signal.wait() + await self._cancel() + await self._drain_after_intake_stop() + + async def _stop_cancellation_watcher(self) -> None: + task = self._cancellation_task + if task is None or task is asyncio.current_task() or task.done(): + return + task.cancel() + await asyncio.gather(task, return_exceptions=True) + + async def _produce(self) -> None: + try: + while not self._intake_stopped and not self._cancelled: + if self._accepted >= self._options.max_items: + self._run_failure = PipelineRunFailure( + PipelineRunFailureCode.ITEM_LIMIT_REACHED, + f"pipeline accepted its maxItems limit of {self._options.max_items}", + ) + self._intake_stopped = True + break + if self._credit is None: + raise AssertionError("pipeline credit semaphore was not initialized") + await self._credit.acquire() + if self._intake_stopped or self._cancelled: + self._credit.release() + break + queue_index: int | None = None + if self._stages: + queue_index = 0 + await self._enter_stage_queue(queue_index) + if self._intake_stopped or self._cancelled: + self._leave_stage_queue(queue_index) + self._credit.release() + break + try: + present, raw_item = await self._source.next() + except (Exception, asyncio.CancelledError) as exc: + if queue_index is not None: + self._leave_stage_queue(queue_index) + self._credit.release() + if not self._producer_cancel_requested: + cause_name = _exception_name(exc) + self._run_failure = PipelineRunFailure( + PipelineRunFailureCode.SOURCE_FAILED, + f"pipeline source failed: {_exception_message(exc)}", + cause_name, + ) + self._intake_stopped = True + break + if self._intake_stopped or self._cancelled: + if queue_index is not None: + self._leave_stage_queue(queue_index) + self._credit.release() + break + if not present: + if queue_index is not None: + self._leave_stage_queue(queue_index) + self._credit.release() + break + item_index = self._accepted + self._accepted += 1 + self._max_observed_in_flight = max( + self._max_observed_in_flight, self._accepted - self._emitted + ) + try: + item_snapshot = portable_json_snapshot(raw_item) + except (Exception, asyncio.CancelledError) as exc: + task = asyncio.create_task( + self._settle_invalid_input( + item_index, + _exception_message(exc), + queued=queue_index is not None, + ) + ) + else: + task = asyncio.create_task( + self._process_item( + item_index, + item_snapshot, + queued=queue_index is not None, + ) + ) + self._item_tasks.add(task) + task.add_done_callback(self._item_tasks.discard) + await self._close_source_safely() + if self._item_tasks: + await asyncio.shield( + asyncio.gather(*tuple(self._item_tasks), return_exceptions=False) + ) + await self._signal_end() + except asyncio.CancelledError: + if ( + not self._producer_cancel_requested + and not self._cancelled + and not self._intake_stopped + ): + self._run_failure = PipelineRunFailure( + PipelineRunFailureCode.SOURCE_FAILED, + "pipeline source cancelled its own iteration", + "CancelledError", + ) + await self._close_source_safely() + if self._item_tasks: + await asyncio.shield( + asyncio.gather(*tuple(self._item_tasks), return_exceptions=False) + ) + await self._signal_end() + return + except BaseException as exc: + if self._completion_future is not None and not self._completion_future.done(): + self._completion_future.set_exception(exc) + + async def _settle_invalid_input( + self, + item_index: int, + message: str, + *, + queued: bool, + ) -> None: + if queued: + self._leave_stage_queue(0) + if self._cancelled: + await self._commit_result( + PipelineItemResult( + item_index=item_index, + status=PipelineItemStatus.CANCELLED, + input_bound=False, + completed_stages=0, + total_attempts=0, + failure=PipelineItemFailure( + PipelineFailureCode.ITEM_CANCELLED, + "pipeline cancellation won before invalid input committed", + item_index, + 0, + ), + ) + ) + return + await self._commit_result( + PipelineItemResult( + item_index=item_index, + status=PipelineItemStatus.FAILED, + input_bound=False, + completed_stages=0, + total_attempts=0, + failure=PipelineItemFailure( + PipelineFailureCode.INVALID_INPUT, + message, + item_index, + 0, + ), + ) + ) + + async def _process_item( + self, + item_index: int, + original: JsonValue, + *, + queued: bool, + ) -> None: + try: + if self._cancelled: + if queued: + self._leave_stage_queue(0) + await self._commit_result( + PipelineItemResult( + item_index=item_index, + status=PipelineItemStatus.CANCELLED, + input_bound=True, + input=portable_json_snapshot(original), + completed_stages=0, + total_attempts=0, + failure=PipelineItemFailure( + PipelineFailureCode.ITEM_CANCELLED, + "pipeline was cancelled after item admission", + item_index, + 0, + ), + ) + ) + return + current = original + total_attempts = 0 + completed_stages = 0 + for stage_index, stage in enumerate(self._stages): + if not queued: + entered = await self._enter_stage_queue(stage_index, cancel_on_run=True) + if not entered: + await self._commit_result( + self._cancelled_result( + item_index, + original, + completed_stages, + total_attempts, + stage_index, + ) + ) + return + queued = False + if self._cancelled: + self._leave_stage_queue(stage_index) + await self._commit_result( + self._cancelled_result( + item_index, original, completed_stages, total_attempts, stage_index + ) + ) + return + value, attempts, failure, output_slot = await self._execute_stage( + stage, stage_index, item_index, current + ) + total_attempts += attempts + if self._cancelled and ( + failure is None or failure.code is not PipelineFailureCode.ITEM_CANCELLED + ): + failure = PipelineItemFailure( + PipelineFailureCode.ITEM_CANCELLED, + "pipeline cancellation won before the stage outcome committed", + item_index, + max(1, attempts), + stage_id=stage.id, + stage_index=stage_index, + ) + if failure is not None: + if output_slot is not None: + output_slot.release() + if failure.code is PipelineFailureCode.ITEM_CANCELLED: + status = PipelineItemStatus.CANCELLED + elif stage.on_failure is PipelineFailurePolicy.DROP: + status = PipelineItemStatus.DROPPED + else: + status = PipelineItemStatus.FAILED + if ( + stage.on_failure is PipelineFailurePolicy.STOP + and failure.code is not PipelineFailureCode.ITEM_CANCELLED + ): + self._intake_stopped = True + self._schedule_intake_drain() + await self._commit_result( + PipelineItemResult( + item_index=item_index, + status=status, + input_bound=True, + input=portable_json_snapshot(original), + completed_stages=completed_stages, + total_attempts=total_attempts, + failure=failure, + ) + ) + return + current = value + completed_stages += 1 + if stage_index + 1 < len(self._stages): + try: + entered = await self._enter_stage_queue(stage_index + 1, cancel_on_run=True) + finally: + if output_slot is not None: + output_slot.release() + if not entered: + await self._commit_result( + self._cancelled_result( + item_index, + original, + completed_stages, + total_attempts, + stage_index + 1, + ) + ) + return + queued = True + elif output_slot is not None: + output_slot.release() + if self._cancelled: + final_result = PipelineItemResult( + item_index=item_index, + status=PipelineItemStatus.CANCELLED, + input_bound=True, + input=portable_json_snapshot(original), + completed_stages=completed_stages, + total_attempts=total_attempts, + failure=PipelineItemFailure( + PipelineFailureCode.ITEM_CANCELLED, + "pipeline cancellation won before item success committed", + item_index, + 0, + ), + ) + else: + final_result = PipelineItemResult( + item_index=item_index, + status=PipelineItemStatus.SUCCEEDED, + input_bound=True, + input=portable_json_snapshot(original), + output=portable_json_snapshot(current), + completed_stages=completed_stages, + total_attempts=total_attempts, + ) + await self._commit_result(final_result) + except asyncio.CancelledError: + if queued and self._stages: + self._leave_stage_queue(min(completed_stages, len(self._stages) - 1)) + raise + + async def _execute_stage( + self, + stage: PipelineStage, + stage_index: int, + item_index: int, + stage_input: JsonValue, + ) -> tuple[ + JsonValue, + int, + PipelineItemFailure | None, + asyncio.Semaphore | None, + ]: + slot = self._stage_slots[stage_index] + retry = stage.retry + if retry is None: + raise AssertionError("pipeline stage retry policy was not normalized") + last_failure: PipelineItemFailure | None = None + maximum_attempts = cast(int, retry.max_attempts) + for attempt in range(1, maximum_attempts + 1): + acquired = await self._acquire_unless_cancelled(slot) + # Acquiring the slot is an await boundary. Cancellation can become + # visible after the helper's final check but before control resumes + # here, so recheck before creating the handler task. + if not acquired or self._cancelled: + if acquired: + slot.release() + if attempt == 1: + self._leave_stage_queue(stage_index) + return ( + stage_input, + attempt - 1, + PipelineItemFailure( + PipelineFailureCode.ITEM_CANCELLED, + "pipeline was cancelled before the stage attempt", + item_index, + attempt - 1, + stage_id=stage.id, + stage_index=stage_index, + ), + None, + ) + if attempt == 1: + self._leave_stage_queue(stage_index) + self._active_attempts[stage_index] += 1 + self._active_maxima[stage_index] = max( + self._active_maxima[stage_index], self._active_attempts[stage_index] + ) + try: + outcome, failure = await self._run_attempt( + stage, stage_index, item_index, stage_input, attempt + ) + except BaseException: + self._active_attempts[stage_index] -= 1 + slot.release() + raise + self._active_attempts[stage_index] -= 1 + if failure is None: + # Retain this stage's worker permit until the validated output + # enters the downstream bounded queue. This is what propagates + # a full downstream buffer back into upstream admission. + return outcome, attempt, None, slot + slot.release() + last_failure = failure + if ( + failure.code + not in { + PipelineFailureCode.STAGE_EXECUTION_FAILED, + PipelineFailureCode.STAGE_TIMEOUT, + } + or attempt >= maximum_attempts + or self._cancelled + ): + return stage_input, attempt, failure, None + if not await self._retry_delay(retry, attempt): + return ( + stage_input, + attempt, + PipelineItemFailure( + PipelineFailureCode.ITEM_CANCELLED, + "pipeline cancellation interrupted retry delay", + item_index, + attempt, + stage_id=stage.id, + stage_index=stage_index, + ), + None, + ) + if last_failure is None: + raise AssertionError("bounded pipeline retry loop omitted its outcome") + return stage_input, maximum_attempts, last_failure, None + + async def _run_attempt( + self, + stage: PipelineStage, + stage_index: int, + item_index: int, + stage_input: JsonValue, + attempt: int, + ) -> tuple[JsonValue, PipelineItemFailure | None]: + if self._cancel_event is None: + raise AssertionError("pipeline cancellation signal was not initialized") + attempt_cancel = asyncio.Event() + context = PipelineHandlerContext( + input=portable_json_snapshot(stage_input), + item_index=item_index, + stage_id=stage.id, + stage_index=stage_index, + attempt=attempt, + cancel_signal=CancellationSignal(attempt_cancel), + ) + execution = asyncio.create_task(_invoke_and_snapshot(stage.handler, context)) + cancelled = asyncio.create_task(self._cancel_event.wait()) + timeout: asyncio.Task[None] | None = None + waiters: set[asyncio.Task[object]] = {execution, cancelled} + if stage.timeout_ms is not None: + timeout = asyncio.create_task(asyncio.sleep(stage.timeout_ms / 1000)) + waiters.add(timeout) + done, _ = await asyncio.wait(waiters, return_when=asyncio.FIRST_COMPLETED) + if execution in done: + cancelled.cancel() + if timeout is not None: + timeout.cancel() + await asyncio.gather(cancelled, timeout, return_exceptions=True) + else: + await asyncio.gather(cancelled, return_exceptions=True) + try: + raw_output = execution.result() + except asyncio.CancelledError: + return stage_input, PipelineItemFailure( + PipelineFailureCode.STAGE_EXECUTION_FAILED, + "stage handler cancelled itself", + item_index, + attempt, + stage_id=stage.id, + stage_index=stage_index, + cause_name="CancelledError", + ) + except PortableJsonError as exc: + return stage_input, PipelineItemFailure( + PipelineFailureCode.INVALID_OUTPUT, + _exception_message(exc), + item_index, + attempt, + stage_id=stage.id, + stage_index=stage_index, + cause_name="PortableJsonError", + ) + except Exception as exc: + cause_name = _exception_name(exc) + return stage_input, PipelineItemFailure( + PipelineFailureCode.STAGE_EXECUTION_FAILED, + f"stage {stage.id!r} failed: {_exception_message(exc)}", + item_index, + attempt, + stage_id=stage.id, + stage_index=stage_index, + cause_name=cause_name, + ) + return raw_output, None + + attempt_cancel.set() + execution.cancel() + execution.add_done_callback(_consume_future_outcome) + if timeout is not None and timeout not in done: + timeout.cancel() + if cancelled in done or self._cancelled: + if timeout is not None: + await asyncio.gather(cancelled, timeout, return_exceptions=True) + else: + await asyncio.gather(cancelled, return_exceptions=True) + return stage_input, PipelineItemFailure( + PipelineFailureCode.ITEM_CANCELLED, + "pipeline cancellation interrupted the stage", + item_index, + attempt, + stage_id=stage.id, + stage_index=stage_index, + ) + cancelled.cancel() + if timeout is not None: + await asyncio.gather(cancelled, timeout, return_exceptions=True) + else: + await asyncio.gather(cancelled, return_exceptions=True) + return stage_input, PipelineItemFailure( + PipelineFailureCode.STAGE_TIMEOUT, + f"stage {stage.id!r} timed out after {stage.timeout_ms} ms", + item_index, + attempt, + stage_id=stage.id, + stage_index=stage_index, + cause_name="TimeoutError", + ) + + async def _retry_delay(self, retry: PipelineRetryOptions, failed_attempt: int) -> bool: + maximum = retry.max_delay_ms + if maximum is None: + raise AssertionError("pipeline retry maximum delay was not normalized") + try: + computed = retry.initial_delay_ms * retry.backoff_multiplier ** (failed_attempt - 1) + except OverflowError: + computed = math.inf + delay_ms = min(maximum, computed) + if delay_ms <= 0: + return not self._cancelled + if self._cancel_event is None: + raise AssertionError("pipeline cancellation signal was not initialized") + delay = asyncio.create_task(asyncio.sleep(delay_ms / 1000)) + cancelled = asyncio.create_task(self._cancel_event.wait()) + done, _ = await asyncio.wait({delay, cancelled}, return_when=asyncio.FIRST_COMPLETED) + if cancelled in done: + delay.cancel() + await asyncio.gather(delay, cancelled, return_exceptions=True) + return False + cancelled.cancel() + await asyncio.gather(delay, cancelled, return_exceptions=True) + return True + + async def _acquire_unless_cancelled(self, semaphore: asyncio.Semaphore) -> bool: + if self._cancelled: + return False + if self._cancel_event is None: + raise AssertionError("pipeline cancellation signal was not initialized") + acquire = asyncio.create_task(semaphore.acquire()) + cancelled = asyncio.create_task(self._cancel_event.wait()) + done, _ = await asyncio.wait({acquire, cancelled}, return_when=asyncio.FIRST_COMPLETED) + if cancelled in done or self._cancelled: + if acquire in done: + acquire.result() + semaphore.release() + else: + acquire.cancel() + if cancelled not in done: + cancelled.cancel() + await asyncio.gather(acquire, cancelled, return_exceptions=True) + return False + cancelled.cancel() + await asyncio.gather(cancelled, return_exceptions=True) + acquire.result() + return True + + async def _enter_stage_queue(self, stage_index: int, *, cancel_on_run: bool = False) -> bool: + queue = self._stage_queues[stage_index] + if cancel_on_run: + if not await self._acquire_unless_cancelled(queue): + return False + else: + await queue.acquire() + self._queue_depths[stage_index] += 1 + self._queue_maxima[stage_index] = max( + self._queue_maxima[stage_index], self._queue_depths[stage_index] + ) + return True + + def _leave_stage_queue(self, stage_index: int) -> None: + if self._queue_depths[stage_index] <= 0: + return + self._queue_depths[stage_index] -= 1 + self._stage_queues[stage_index].release() + + async def _commit_result(self, result: PipelineItemResult) -> None: + self._status_counts[result.status] += 1 + if self._delivery is None or self._commit_lock is None: + raise AssertionError("pipeline result coordinator was not initialized") + if self._options.ordering is PipelineOrdering.COMPLETION: + await self._delivery.put(result) + return + async with self._commit_lock: + self._reorder[result.item_index] = result + while self._next_input_result in self._reorder: + await self._delivery.put(self._reorder.pop(self._next_input_result)) + self._next_input_result += 1 + + def _cancelled_result( + self, + item_index: int, + original: JsonValue, + completed_stages: int, + total_attempts: int, + stage_index: int, + ) -> PipelineItemResult: + stage = self._stages[stage_index] + return PipelineItemResult( + item_index=item_index, + status=PipelineItemStatus.CANCELLED, + input_bound=True, + input=portable_json_snapshot(original), + completed_stages=completed_stages, + total_attempts=total_attempts, + failure=PipelineItemFailure( + PipelineFailureCode.ITEM_CANCELLED, + "pipeline was cancelled before the next stage attempt", + item_index, + 0, + stage_id=stage.id, + stage_index=stage_index, + ), + ) + + async def _signal_end(self) -> None: + if not self._end_signalled: + self._end_signalled = True + if self._delivery is not None: + await self._delivery.put(_END) + await self._finish_summary_if_drained() + + async def _finish_summary_if_drained(self) -> None: + if not self._end_signalled or self._emitted != self._accepted: + return + await self._stop_cancellation_watcher() + self._finish_summary(PipelineRunStatus.CANCELLED if self._cancelled else None) + + def _schedule_intake_drain(self) -> None: + if self._drain_task is None: + self._drain_task = asyncio.create_task(self._drain_after_intake_stop()) + + async def _drain_after_intake_stop(self) -> None: + producer = self._producer_task + if producer is not None and producer is not asyncio.current_task() and not producer.done(): + await self._cancel_or_detach_producer(producer) + active_items = tuple(self._item_tasks) + if active_items: + await asyncio.gather(*active_items, return_exceptions=True) + await self._close_source_safely() + await self._signal_end() + + async def _close_source_safely(self) -> None: + close_task = asyncio.create_task(self._source.close()) + try: + await asyncio.shield(close_task) + except asyncio.CancelledError: + # Runtime cancellation of the producer must not cancel the + # isolated cleanup task before the source hook is invoked. + try: + await asyncio.shield(close_task) + except BaseException: + return + except BaseException: + # Cleanup failure is diagnostic-only under the standalone contract. + return + + async def _cancel_or_detach_producer(self, producer: asyncio.Task[None]) -> None: + self._producer_cancel_requested = True + producer.cancel() + # Cooperative sources finish in this turn. A source that suppresses + # cancellation is detached and its eventual outcome remains observed, + # so explicit close cannot be held hostage by arbitrary user code. + await asyncio.sleep(0) + if producer.done(): + await asyncio.gather(producer, return_exceptions=True) + else: + producer.add_done_callback(_consume_future_outcome) + + async def _cancel(self) -> None: + if self._cancel_event is None or self._cancel_event.is_set(): + return + self._cancel_event.set() + self._intake_stopped = True + + def _finish_summary(self, forced: PipelineRunStatus | None = None) -> PipelineSummary: + if self._completion_future is not None and self._completion_future.done(): + return self._completion_future.result() + status = forced + if status is None: + status = ( + PipelineRunStatus.FAILED + if self._run_failure is not None + or self._status_counts[PipelineItemStatus.FAILED] + or self._status_counts[PipelineItemStatus.DROPPED] + else PipelineRunStatus.SUCCEEDED + ) + summary = PipelineSummary( + status=status, + accepted=self._accepted, + emitted=self._emitted, + succeeded=self._status_counts[PipelineItemStatus.SUCCEEDED], + failed=self._status_counts[PipelineItemStatus.FAILED], + dropped=self._status_counts[PipelineItemStatus.DROPPED], + cancelled=self._status_counts[PipelineItemStatus.CANCELLED], + max_observed_in_flight=self._max_observed_in_flight, + stage_max_observed_concurrency=MappingProxyType( + {stage.id: self._active_maxima[index] for index, stage in enumerate(self._stages)} + ), + stage_max_observed_queue_depth=MappingProxyType( + {stage.id: self._queue_maxima[index] for index, stage in enumerate(self._stages)} + ), + run_failure=self._run_failure, + ) + if self._completion_future is not None and not self._completion_future.done(): + self._completion_future.set_result(summary) + return summary + + async def aclose(self) -> PipelineSummary: + self._ensure_initialized() + if self._closed and self._completion_future is not None and self._completion_future.done(): + return self._completion_future.result() + await self._cancel() + self._closed = True + if self._drain_task is not None and not self._drain_task.done(): + self._drain_task.cancel() + await asyncio.gather(self._drain_task, return_exceptions=True) + if self._producer_task is not None and not self._producer_task.done(): + await self._cancel_or_detach_producer(self._producer_task) + active_items = tuple(self._item_tasks) + if active_items: + await asyncio.gather(*active_items, return_exceptions=True) + await self._close_source_safely() + await self._stop_cancellation_watcher() + await self._signal_end() + while self._advancing: + await asyncio.sleep(0) + return self._finish_summary(PipelineRunStatus.CANCELLED) + + +def run_pipeline( + source: PipelineSource, + stages: Sequence[PipelineStage], + *, + buffer_capacity: int | float = _DEFAULT_BUFFER_CAPACITY, + max_in_flight: int | float = _DEFAULT_MAX_IN_FLIGHT, + max_items: int | float = _DEFAULT_MAX_ITEMS, + max_stages: int | float = _DEFAULT_MAX_STAGES, + ordering: PipelineOrdering | str = PipelineOrdering.INPUT, + cancellation_signal: asyncio.Event | None = None, +) -> PipelineRun: + """Construct a lazy bounded pipeline without advancing ``source``.""" + + if not isinstance(source, (Iterable, AsyncIterable)): + raise TypeError("source must be an iterable or async iterable") + validated_buffer = _positive_integer(buffer_capacity, "buffer_capacity") + validated_in_flight = _positive_integer(max_in_flight, "max_in_flight") + validated_max_items = _positive_integer(max_items, "max_items") + validated_max_stages = _max_stages(max_stages) + try: + validated_ordering = PipelineOrdering(ordering) + except ValueError: + raise ValueError("ordering must be 'input' or 'completion'") from None + if cancellation_signal is not None and not isinstance(cancellation_signal, asyncio.Event): + raise TypeError("cancellation_signal must be an asyncio.Event") + copied_stages = _snapshot_stages( + stages, + validated_max_items, + validated_max_stages, + ) + return PipelineRun( + source, + copied_stages, + _Options( + buffer_capacity=validated_buffer, + max_in_flight=validated_in_flight, + max_items=validated_max_items, + ordering=validated_ordering, + cancellation_signal=cancellation_signal, + ), + ) diff --git a/python/tests/test_compiler.py b/python/tests/test_compiler.py index b47009e..1311ced 100644 --- a/python/tests/test_compiler.py +++ b/python/tests/test_compiler.py @@ -42,6 +42,25 @@ def test_invalid_conformance_fixtures(fixture: str, code: DiagnosticCode) -> Non result.raise_for_errors() +@pytest.mark.parametrize( + "fixture", + [ + "invalid-null-metadata-description.graph.json", + "invalid-null-state-schema.graph.json", + "invalid-null-output-port.graph.json", + "invalid-null-node-retry.graph.json", + ], +) +def test_explicit_null_optional_field_fixtures_report_only_invalid_graph( + fixture: str, +) -> None: + result = try_compile_graph(load_fixture(fixture)) + + assert not result.valid + assert result.graph is None + assert [item.code for item in result.diagnostics] == [DiagnosticCode.INVALID_GRAPH] + + def test_missing_entrypoint_and_output_are_structured() -> None: document = load_fixture("diamond.graph.json") document["entrypoints"] = ["missing-entry"] diff --git a/python/tests/test_models.py b/python/tests/test_models.py index 43e4ebd..08364fc 100644 --- a/python/tests/test_models.py +++ b/python/tests/test_models.py @@ -3,14 +3,64 @@ import copy import json from pathlib import Path +from typing import Any import pytest -from pydantic import ValidationError - -from graph_engineering import GraphSpec, canonical_json +from pydantic import BaseModel, ValidationError + +from graph_engineering import ( + EdgeSpec, + Endpoint, + GraphPolicies, + GraphSpec, + Metadata, + NodeSpec, + RetryPolicy, + canonical_json, +) ROOT = Path(__file__).resolve().parents[2] +GRAPH_IR_MODELS: tuple[type[BaseModel], ...] = ( + Metadata, + Endpoint, + RetryPolicy, + NodeSpec, + EdgeSpec, + GraphPolicies, + GraphSpec, +) + +OPTIONAL_NULL_CASES: tuple[tuple[type[BaseModel], str, tuple[str | int, ...]], ...] = ( + (Metadata, "description", ("metadata",)), + (Metadata, "labels", ("metadata",)), + (Endpoint, "port", ("outputs", "result")), + (RetryPolicy, "maxAttempts", ("nodes", 0, "retry")), + (RetryPolicy, "initialDelayMs", ("nodes", 0, "retry")), + (RetryPolicy, "maxDelayMs", ("nodes", 0, "retry")), + (RetryPolicy, "backoffMultiplier", ("nodes", 0, "retry")), + (RetryPolicy, "jitter", ("nodes", 0, "retry")), + (NodeSpec, "retry", ("nodes", 0)), + (NodeSpec, "timeoutMs", ("nodes", 0)), + (NodeSpec, "cache", ("nodes", 0)), + (NodeSpec, "resources", ("nodes", 0)), + (NodeSpec, "isolation", ("nodes", 0)), + (NodeSpec, "sideEffects", ("nodes", 0)), + (EdgeSpec, "map", ("edges", 0)), + (EdgeSpec, "condition", ("edges", 0)), + (EdgeSpec, "mode", ("edges", 0)), + (EdgeSpec, "schema", ("edges", 0)), + (GraphPolicies, "maxConcurrency", ("policies",)), + (GraphPolicies, "maxDynamicNodes", ("policies",)), + (GraphPolicies, "maxDepth", ("policies",)), + (GraphPolicies, "maxFanOut", ("policies",)), + (GraphPolicies, "maxTotalAttempts", ("policies",)), + (GraphPolicies, "maxDurationMs", ("policies",)), + (GraphPolicies, "maxCostUsd", ("policies",)), + (GraphSpec, "stateSchema", ()), + (GraphSpec, "policies", ()), +) + def diamond() -> dict[str, object]: return json.loads((ROOT / "spec/conformance/diamond.graph.json").read_text()) @@ -62,6 +112,69 @@ def test_strict_policy_types_and_retry_bounds_are_enforced() -> None: GraphSpec.model_validate(retry) +def test_optional_null_case_matrix_covers_every_known_graph_ir_optional_field() -> None: + expected = { + (model, field.alias or field_name) + for model in GRAPH_IR_MODELS + for field_name, field in model.model_fields.items() + if not field.is_required() + } + expected.update((GraphPolicies, key) for key in GraphPolicies.known_keys()) + covered = {(model, alias) for model, alias, _ in OPTIONAL_NULL_CASES} + + assert covered == expected + + +@pytest.mark.parametrize( + ("model", "alias", "path"), + OPTIONAL_NULL_CASES, + ids=lambda value: value.__name__ if isinstance(value, type) else str(value), +) +def test_every_known_optional_graph_ir_field_rejects_explicit_null( + model: type[BaseModel], + alias: str, + path: tuple[str | int, ...], +) -> None: + document: Any = diamond() + target: Any = document + for part in path: + if isinstance(part, int): + target = target[part] + continue + if part not in target: + target[part] = {} + target = target[part] + target[alias] = None + + with pytest.raises(ValidationError): + GraphSpec.model_validate(document) + + +def test_unknown_policy_extension_may_retain_json_null() -> None: + document = diamond() + document["policies"] = {"futurePolicy": None} + + graph = GraphSpec.model_validate(document) + + assert graph.policies is not None + assert graph.policies.model_extra == {"futurePolicy": None} + + +@pytest.mark.parametrize("value", [None, 5]) +def test_policy_extension_colliding_with_python_field_name_is_lossless( + value: object, +) -> None: + document = diamond() + document["policies"] = {"max_concurrency": value} + + graph = GraphSpec.model_validate(document) + + assert graph.policies is not None + assert graph.policies.max_concurrency is None + assert graph.policies.model_extra == {"max_concurrency": value} + assert graph.model_dump(mode="json", by_alias=True, exclude_unset=True) == document + + def test_required_null_config_is_not_dropped_from_canonical_json() -> None: document = copy.deepcopy(diamond()) document["nodes"][0]["config"] = None # type: ignore[index] diff --git a/python/tests/test_pipeline.py b/python/tests/test_pipeline.py new file mode 100644 index 0000000..5892bcd --- /dev/null +++ b/python/tests/test_pipeline.py @@ -0,0 +1,1675 @@ +from __future__ import annotations + +import asyncio +from collections.abc import AsyncIterator, Sequence +from typing import Any + +import pytest + +from graph_engineering import ( + PipelineFailureCode, + PipelineFailurePolicy, + PipelineItemStatus, + PipelineRetryOptions, + PipelineRun, + PipelineRunFailureCode, + PipelineRunStatus, + PipelineStage, + run_pipeline, +) + + +def test_pipeline_is_lazy_and_identity_preserves_null() -> None: + class Source: + pulls = 0 + iterated = False + + def __iter__(self) -> Source: + self.iterated = True + return self + + def __next__(self) -> object: + self.pulls += 1 + if self.pulls == 1: + return None + raise StopIteration + + source = Source() + run = run_pipeline(source, [], max_items=2, max_in_flight=1) + assert source.pulls == 0 + + async def consume() -> None: + results = [item async for item in run] + assert len(results) == 1 + assert results[0].status is PipelineItemStatus.SUCCEEDED + assert results[0].input_bound + assert results[0].input is None + assert results[0].output is None + summary = await run.completion() + assert summary.status is PipelineRunStatus.SUCCEEDED + assert summary.accepted == summary.emitted == 1 + + asyncio.run(consume()) + + +def test_pipeline_retries_and_dead_letters_without_stopping_siblings() -> None: + attempts: dict[int, int] = {} + + async def handler(context: Any) -> object: + attempts[context.item_index] = attempts.get(context.item_index, 0) + 1 + if context.item_index == 0 and context.attempt == 1: + raise RuntimeError("transient") + if context.item_index == 2: + return float("inf") + return {"value": context.input} + + run = run_pipeline( + [1, 2, 3], + [ + PipelineStage( + "prepare", + handler, + concurrency=2, + retry=PipelineRetryOptions(max_attempts=2), + ) + ], + buffer_capacity=1, + max_in_flight=3, + max_items=4, + ) + + async def consume() -> None: + results = [item async for item in run] + assert [item.item_index for item in results] == [0, 1, 2] + assert [item.status for item in results] == [ + PipelineItemStatus.SUCCEEDED, + PipelineItemStatus.SUCCEEDED, + PipelineItemStatus.FAILED, + ] + assert results[0].total_attempts == 2 + assert results[2].failure is not None + assert results[2].failure.code is PipelineFailureCode.INVALID_OUTPUT + assert results[2].total_attempts == 1 + summary = await run.completion() + assert summary.status is PipelineRunStatus.FAILED + assert summary.succeeded == 2 + assert summary.failed == 1 + assert summary.max_observed_in_flight <= 3 + assert summary.stage_max_observed_concurrency["prepare"] <= 2 + assert summary.stage_max_observed_queue_depth["prepare"] <= 1 + + asyncio.run(consume()) + + +def test_hostile_exception_stringification_is_structured_and_releases_stage_slot() -> None: + class HostileError(Exception): + def __str__(self) -> str: + raise RuntimeError("exception stringification failed") + + calls: list[int] = [] + + async def handler(context: Any) -> object: + calls.append(context.item_index) + if context.item_index == 0: + raise HostileError + return context.input + + async def consume() -> None: + run = run_pipeline( + [0, 1], + [PipelineStage("work", handler, concurrency=1)], + buffer_capacity=2, + max_in_flight=2, + max_items=3, + ) + results = await asyncio.wait_for(_collect(run), timeout=1) + + assert calls == [0, 1] + assert [item.status for item in results] == [ + PipelineItemStatus.FAILED, + PipelineItemStatus.SUCCEEDED, + ] + first = results[0] + assert first.total_attempts == 1 + assert first.failure is not None + assert first.failure.code is PipelineFailureCode.STAGE_EXECUTION_FAILED + assert first.failure.attempt == 1 + assert first.failure.cause_name == "HostileError" + assert "HostileError" in first.failure.message + summary = await run.completion() + assert summary.accepted == summary.emitted == 2 + assert summary.failed == 1 + assert summary.succeeded == 1 + + asyncio.run(consume()) + + +def test_hostile_invalid_input_diagnostic_is_structured_without_hanging() -> None: + class HostileTypeName(type): + def __getattribute__(cls, name: str) -> object: + if name == "__name__": + raise RuntimeError("hostile type name") + return super().__getattribute__(name) + + class InvalidInput(metaclass=HostileTypeName): + pass + + async def consume() -> None: + run = run_pipeline([InvalidInput()], [], max_items=2) + results = await asyncio.wait_for(_collect(run), timeout=1) + + assert len(results) == 1 + result = results[0] + assert result.status is PipelineItemStatus.FAILED + assert not result.input_bound + assert result.total_attempts == 0 + assert result.failure is not None + assert result.failure.code is PipelineFailureCode.INVALID_INPUT + assert result.failure.message == "hostile type name" + summary = await run.completion() + assert summary.status is PipelineRunStatus.FAILED + assert summary.accepted == summary.emitted == 1 + assert summary.failed == 1 + + asyncio.run(consume()) + + +def test_hostile_invalid_output_diagnostic_remains_invalid_output() -> None: + class HostileTypeName(type): + def __getattribute__(cls, name: str) -> object: + if name == "__name__": + raise RuntimeError("hostile output type name") + return super().__getattribute__(name) + + class InvalidOutput(metaclass=HostileTypeName): + pass + + async def consume() -> None: + run = run_pipeline( + [1], + [PipelineStage("work", lambda _: InvalidOutput())], + max_items=2, + ) + result = await asyncio.wait_for(anext(run), timeout=1) + + assert result.status is PipelineItemStatus.FAILED + assert result.total_attempts == 1 + assert result.failure is not None + assert result.failure.code is PipelineFailureCode.INVALID_OUTPUT + assert result.failure.message == "hostile output type name" + assert result.failure.cause_name == "PortableJsonError" + with pytest.raises(StopAsyncIteration): + await anext(run) + + asyncio.run(consume()) + + +def test_retry_receives_a_fresh_input_snapshot() -> None: + async def consume() -> None: + seen: list[object] = [] + + def handler(context: Any) -> object: + seen.append(context.input) + context.input["values"].append(context.attempt) + if context.attempt == 1: + raise RuntimeError("retry") + return context.input + + run = run_pipeline( + [{"values": []}], + [ + PipelineStage( + "work", + handler, + retry=PipelineRetryOptions(max_attempts=2), + ) + ], + max_items=2, + ) + result = await anext(run) + assert seen == [{"values": [1]}, {"values": [2]}] + assert result.output == {"values": [2]} + assert [item async for item in run] == [] + + asyncio.run(consume()) + + +def test_stop_policy_closes_without_one_extra_pull() -> None: + class Source: + def __init__(self) -> None: + self.pulls = 0 + self.closes = 0 + + def __iter__(self) -> Source: + return self + + def __next__(self) -> str: + self.pulls += 1 + return "stop" if self.pulls == 1 else "must-not-pull" + + def close(self) -> None: + self.closes += 1 + + source = Source() + + def fail(_: Any) -> object: + raise RuntimeError("stop") + + run = run_pipeline( + source, + [PipelineStage("gate", fail, on_failure=PipelineFailurePolicy.STOP)], + buffer_capacity=1, + max_in_flight=1, + max_items=3, + ) + + async def consume() -> None: + results = [item async for item in run] + assert len(results) == 1 + assert results[0].status is PipelineItemStatus.FAILED + assert source.pulls == 1 + assert source.closes == 1 + + asyncio.run(consume()) + + +def test_stop_policy_drains_every_already_accepted_sibling() -> None: + async def consume() -> None: + sibling_started = asyncio.Event() + release_sibling = asyncio.Event() + + async def handler(context: Any) -> object: + if context.item_index == 0: + await sibling_started.wait() + raise RuntimeError("stop") + sibling_started.set() + await release_sibling.wait() + return "sibling-finished" + + run = run_pipeline( + ["stop", "sibling", "must-not-pull"], + [ + PipelineStage( + "work", + handler, + concurrency=2, + on_failure="stop", + ) + ], + buffer_capacity=2, + max_in_flight=2, + max_items=4, + ) + release_sibling.set() + results = [item async for item in run] + assert len(results) == 2 + assert results[0].status is PipelineItemStatus.FAILED + assert results[1].status is PipelineItemStatus.SUCCEEDED + summary = await run.completion() + assert summary.accepted == 2 + assert summary.failed == 1 + assert summary.succeeded == 1 + + asyncio.run(consume()) + + +def test_item_limit_stops_without_probing_source() -> None: + class Source: + def __init__(self) -> None: + self.pulls = 0 + + def __iter__(self) -> Source: + return self + + def __next__(self) -> int: + self.pulls += 1 + return self.pulls + + source = Source() + run = run_pipeline(source, [], max_items=2, max_in_flight=2) + + async def consume() -> None: + assert len([item async for item in run]) == 2 + summary = await run.completion() + assert source.pulls == 2 + assert summary.run_failure is not None + assert summary.run_failure.code is PipelineRunFailureCode.ITEM_LIMIT_REACHED + + asyncio.run(consume()) + + +def test_async_source_failure_drains_accepted_prefix() -> None: + async def source() -> AsyncIterator[int]: + yield 1 + yield 2 + raise RuntimeError("offline") + + run = run_pipeline(source(), [], max_items=4, max_in_flight=3) + + async def consume() -> None: + assert [item.output async for item in run] == [1, 2] + summary = await run.completion() + assert summary.status is PipelineRunStatus.FAILED + assert summary.run_failure is not None + assert summary.run_failure.code is PipelineRunFailureCode.SOURCE_FAILED + + asyncio.run(consume()) + + +def test_async_source_self_cancellation_is_a_source_failure() -> None: + async def source() -> AsyncIterator[int]: + yield 1 + raise asyncio.CancelledError + + run = run_pipeline(source(), [], max_items=3, max_in_flight=2) + + async def consume() -> None: + assert [item.output async for item in run] == [1] + summary = await run.completion() + assert summary.status is PipelineRunStatus.FAILED + assert summary.run_failure is not None + assert summary.run_failure.code is PipelineRunFailureCode.SOURCE_FAILED + assert summary.run_failure.cause_name == "CancelledError" + + asyncio.run(consume()) + + +def test_async_source_task_self_cancellation_is_a_source_failure() -> None: + class Source: + def __aiter__(self) -> Source: + return self + + async def __anext__(self) -> int: + current = asyncio.current_task() + assert current is not None + current.cancel() + await asyncio.sleep(0) + raise AssertionError("self-cancellation must interrupt the source pull") + + async def consume() -> None: + run = run_pipeline(Source(), [], max_items=2) + assert await asyncio.wait_for(_collect(run), timeout=1) == [] + summary = await run.completion() + assert summary.status is PipelineRunStatus.FAILED + assert summary.run_failure is not None + assert summary.run_failure.code is PipelineRunFailureCode.SOURCE_FAILED + assert summary.run_failure.cause_name == "CancelledError" + + asyncio.run(consume()) + + +def test_fast_item_enters_later_stage_without_whole_stage_barrier() -> None: + release_slow = asyncio.Event() + fast_reached_second = asyncio.Event() + + async def first(context: Any) -> str: + if context.item_index == 0: + await release_slow.wait() + return "slow-first" + return "fast-first" + + async def second(context: Any) -> str: + if context.item_index == 1: + fast_reached_second.set() + release_slow.set() + return "fast-done" + return "slow-done" + + run = run_pipeline( + ["slow", "fast"], + [ + PipelineStage("first", first, concurrency=2), + PipelineStage("second", second), + ], + buffer_capacity=1, + max_in_flight=2, + max_items=3, + ) + + async def consume() -> None: + results = [item async for item in run] + assert fast_reached_second.is_set() + assert [item.item_index for item in results] == [0, 1] + assert [item.output for item in results] == ["slow-done", "fast-done"] + + asyncio.run(consume()) + + +def test_completion_order_delivers_the_first_committed_terminal_item() -> None: + async def consume() -> None: + release_slow = asyncio.Event() + fast_finished = asyncio.Event() + + async def handler(context: Any) -> object: + if context.item_index == 0: + await release_slow.wait() + return "slow" + fast_finished.set() + return "fast" + + run = run_pipeline( + [0, 1], + [PipelineStage("work", handler, concurrency=2)], + max_items=3, + max_in_flight=2, + ordering="completion", + ) + first = await anext(run) + assert fast_finished.is_set() + assert first.item_index == 1 + release_slow.set() + second = await anext(run) + assert second.item_index == 0 + assert [item async for item in run] == [] + + asyncio.run(consume()) + + +def test_slow_consumer_bounds_source_pull_ahead() -> None: + class Source: + def __init__(self) -> None: + self.pulls = 0 + + def __iter__(self) -> Source: + return self + + def __next__(self) -> int: + if self.pulls >= 5: + raise StopIteration + value = self.pulls + self.pulls += 1 + return value + + source = Source() + run = run_pipeline( + source, + [], + buffer_capacity=1, + max_in_flight=2, + max_items=6, + ordering="completion", + ) + + async def consume() -> None: + first = await anext(run) + assert first.item_index == 0 + for _ in range(20): + if source.pulls == 3: + break + await asyncio.sleep(0) + assert source.pulls <= 3 + remainder = [item async for item in run] + assert {item.item_index for item in [first, *remainder]} == set(range(5)) + + asyncio.run(consume()) + + +def test_full_downstream_buffer_stops_additional_upstream_attempts() -> None: + async def consume() -> None: + downstream_started = asyncio.Event() + release_downstream = asyncio.Event() + upstream_calls = 0 + + async def upstream(context: Any) -> object: + nonlocal upstream_calls + upstream_calls += 1 + return context.input + + async def downstream(context: Any) -> object: + if context.item_index == 0: + downstream_started.set() + await release_downstream.wait() + return context.input + + run = run_pipeline( + list(range(5)), + [ + PipelineStage("upstream", upstream, concurrency=1), + PipelineStage("downstream", downstream, concurrency=1), + ], + buffer_capacity=1, + max_in_flight=5, + max_items=6, + ) + pending_read = asyncio.create_task(anext(run)) + await downstream_started.wait() + for _ in range(10): + await asyncio.sleep(0) + # One item is active downstream, one is buffered, and at most one + # upstream worker may be blocked trying to enqueue its completed output. + assert upstream_calls <= 3 + release_downstream.set() + first = await pending_read + assert len([first, *[item async for item in run]]) == 5 + + asyncio.run(consume()) + + +def test_large_run_keeps_fixed_in_flight_and_queue_high_water() -> None: + async def consume() -> None: + async def increment(context: Any) -> int: + await asyncio.sleep(0) + return context.input + 1 + + run = run_pipeline( + range(1_000), + [ + PipelineStage("one", increment, concurrency=4), + PipelineStage("two", increment, concurrency=4), + PipelineStage("three", increment, concurrency=4), + ], + buffer_capacity=2, + max_in_flight=8, + max_items=1_001, + ordering="completion", + ) + results = [item async for item in run] + assert len(results) == 1_000 + summary = await run.completion() + assert summary.max_observed_in_flight <= 8 + assert max(summary.stage_max_observed_queue_depth.values()) <= 2 + assert max(summary.stage_max_observed_concurrency.values()) <= 4 + + asyncio.run(consume()) + + +def test_stage_concurrency_metric_reaches_but_never_exceeds_the_limit() -> None: + async def consume() -> None: + active = 0 + observed = 0 + all_slots_used = asyncio.Event() + release = asyncio.Event() + + async def handler(context: Any) -> object: + nonlocal active, observed + active += 1 + observed = max(observed, active) + if active == 3: + all_slots_used.set() + try: + await release.wait() + return context.input + finally: + active -= 1 + + run = run_pipeline( + [0, 1, 2, 3], + [PipelineStage("work", handler, concurrency=3)], + buffer_capacity=4, + max_in_flight=4, + max_items=5, + ) + collecting = asyncio.create_task(_collect(run)) + await asyncio.wait_for(all_slots_used.wait(), timeout=1) + assert active == observed == 3 + release.set() + assert len(await collecting) == 4 + summary = await run.completion() + assert summary.stage_max_observed_concurrency == {"work": 3} + + asyncio.run(consume()) + + +def test_pre_cancelled_pipeline_never_pulls_source() -> None: + class Source: + pulls = 0 + iterated = False + + def __iter__(self) -> Source: + self.iterated = True + return self + + def __next__(self) -> int: + self.pulls += 1 + return 1 + + source = Source() + + async def consume() -> None: + cancelled = asyncio.Event() + cancelled.set() + run = run_pipeline(source, [], cancellation_signal=cancelled) + assert [item async for item in run] == [] + summary = await run.completion() + assert summary.status is PipelineRunStatus.CANCELLED + assert source.pulls == 0 + assert not source.iterated + + asyncio.run(consume()) + + +def test_cancellation_during_iterator_construction_prevents_first_pull() -> None: + class Source: + def __init__(self, cancellation: asyncio.Event) -> None: + self.cancellation = cancellation + self.pulls = 0 + self.closes = 0 + + def __iter__(self) -> Source: + self.cancellation.set() + return self + + def __next__(self) -> int: + self.pulls += 1 + return 1 + + def close(self) -> None: + self.closes += 1 + + async def consume() -> None: + cancellation = asyncio.Event() + source = Source(cancellation) + run = run_pipeline(source, [], cancellation_signal=cancellation) + assert [item async for item in run] == [] + summary = await run.completion() + assert summary.status is PipelineRunStatus.CANCELLED + assert summary.accepted == 0 + assert source.pulls == 0 + assert source.closes == 1 + + asyncio.run(consume()) + + +def test_source_iterator_construction_failure_is_structured() -> None: + class Source: + def __iter__(self) -> Source: + raise RuntimeError("cannot open") + + def __next__(self) -> int: + raise AssertionError("unreachable") + + run = run_pipeline(Source(), [], max_items=2) + + async def consume() -> None: + assert [item async for item in run] == [] + summary = await run.completion() + assert summary.status is PipelineRunStatus.FAILED + assert summary.run_failure is not None + assert summary.run_failure.code is PipelineRunFailureCode.SOURCE_FAILED + assert summary.run_failure.cause_name == "RuntimeError" + + asyncio.run(consume()) + + +def test_source_iterator_construction_self_cancellation_is_structured() -> None: + class Source: + def __iter__(self) -> Source: + raise asyncio.CancelledError + + async def consume() -> None: + run = run_pipeline(Source(), [], max_items=2) + assert await asyncio.wait_for(_collect(run), timeout=1) == [] + summary = await run.completion() + assert summary.status is PipelineRunStatus.FAILED + assert summary.run_failure is not None + assert summary.run_failure.code is PipelineRunFailureCode.SOURCE_FAILED + assert summary.run_failure.cause_name == "CancelledError" + + asyncio.run(consume()) + + +def test_running_cancellation_is_structured() -> None: + async def consume() -> None: + started = asyncio.Event() + cancelled = asyncio.Event() + + async def handler(context: Any) -> object: + started.set() + await context.cancel_signal.wait() + return "late-success" + + run = run_pipeline( + [1], + [PipelineStage("work", handler)], + max_items=2, + max_in_flight=1, + cancellation_signal=cancelled, + ) + next_result = asyncio.create_task(anext(run)) + await started.wait() + cancelled.set() + result = await asyncio.wait_for(next_result, timeout=1) + assert result.status is PipelineItemStatus.CANCELLED + assert result.failure is not None + assert result.failure.code is PipelineFailureCode.ITEM_CANCELLED + assert [item async for item in run] == [] + assert (await run.completion()).status is PipelineRunStatus.CANCELLED + + asyncio.run(consume()) + + +def test_stage_timeout_retries_to_exact_bound() -> None: + calls = 0 + + async def consume() -> None: + nonlocal calls + + async def handler(_: Any) -> object: + nonlocal calls + calls += 1 + await asyncio.Event().wait() + return "never" + + run = run_pipeline( + [1], + [ + PipelineStage( + "work", + handler, + timeout_ms=1, + retry=PipelineRetryOptions(max_attempts=2), + ) + ], + max_items=2, + max_in_flight=1, + ) + result = await anext(run) + assert result.status is PipelineItemStatus.FAILED + assert result.total_attempts == 2 + assert result.failure is not None + assert result.failure.code is PipelineFailureCode.STAGE_TIMEOUT + assert calls == 2 + assert [item async for item in run] == [] + + asyncio.run(consume()) + + +def test_cancellation_wakes_retry_delay_and_preserves_attempt_count() -> None: + async def consume() -> None: + first_failed = asyncio.Event() + cancellation = asyncio.Event() + + async def handler(_: Any) -> object: + first_failed.set() + raise RuntimeError("retry") + + run = run_pipeline( + [1], + [ + PipelineStage( + "work", + handler, + retry=PipelineRetryOptions(max_attempts=2, initial_delay_ms=10_000), + ) + ], + max_items=2, + cancellation_signal=cancellation, + ) + pending = asyncio.create_task(anext(run)) + await first_failed.wait() + await asyncio.sleep(0) + cancellation.set() + result = await asyncio.wait_for(pending, timeout=1) + assert result.status is PipelineItemStatus.CANCELLED + assert result.total_attempts == 1 + assert result.failure is not None + assert result.failure.attempt == 1 + assert [item async for item in run] == [] + + asyncio.run(consume()) + + +def test_cancellation_accounts_items_waiting_in_a_stage_queue() -> None: + async def consume() -> None: + started = asyncio.Event() + cancellation = asyncio.Event() + calls: list[int] = [] + + async def handler(context: Any) -> object: + calls.append(context.item_index) + if context.item_index == 0: + started.set() + await context.cancel_signal.wait() + return context.input + + run = run_pipeline( + [0, 1, 2], + [PipelineStage("work", handler, concurrency=1)], + buffer_capacity=1, + max_in_flight=3, + max_items=4, + cancellation_signal=cancellation, + ) + first_read = asyncio.create_task(anext(run)) + await started.wait() + cancellation.set() + first = await asyncio.wait_for(first_read, timeout=1) + rest = [item async for item in run] + results = [first, *rest] + assert results + assert all(item.status is PipelineItemStatus.CANCELLED for item in results) + assert calls == [0] + summary = await run.completion() + assert summary.accepted == len(results) + assert summary.cancelled == len(results) + + asyncio.run(consume()) + + +def test_cancellation_after_slot_acquire_prevents_handler_start( + monkeypatch: pytest.MonkeyPatch, +) -> None: + async def consume() -> None: + cancellation = asyncio.Event() + calls = 0 + original_acquire = PipelineRun._acquire_unless_cancelled + + async def acquire_then_cancel(pipeline: PipelineRun, semaphore: asyncio.Semaphore) -> bool: + acquired = await original_acquire(pipeline, semaphore) + if acquired: + cancellation.set() + return acquired + + monkeypatch.setattr( + PipelineRun, + "_acquire_unless_cancelled", + acquire_then_cancel, + ) + + async def handler(context: Any) -> object: + nonlocal calls + calls += 1 + return context.input + + run = run_pipeline( + [1], + [PipelineStage("work", handler)], + max_items=2, + cancellation_signal=cancellation, + ) + results = [item async for item in run] + + assert calls == 0 + assert len(results) == 1 + assert results[0].status is PipelineItemStatus.CANCELLED + assert results[0].total_attempts == 0 + assert results[0].failure is not None + assert results[0].failure.attempt == 0 + summary = await run.completion() + assert summary.status is PipelineRunStatus.CANCELLED + assert summary.stage_max_observed_concurrency == {"work": 0} + + asyncio.run(consume()) + + +def test_early_close_is_idempotent_and_accounts_accepted_items() -> None: + async def consume() -> None: + run = run_pipeline(range(10), [], max_items=11, max_in_flight=2) + first = await anext(run) + assert first.item_index == 0 + summary = await run.aclose() + assert summary.status is PipelineRunStatus.CANCELLED + assert summary.accepted == ( + summary.succeeded + summary.failed + summary.dropped + summary.cancelled + ) + assert await run.aclose() == summary + + asyncio.run(consume()) + + +def test_close_wakes_a_pending_read_and_summary_counts_any_delivery() -> None: + async def consume() -> None: + started = asyncio.Event() + + async def handler(context: Any) -> object: + started.set() + await context.cancel_signal.wait() + return "late" + + run = run_pipeline([1], [PipelineStage("work", handler)], max_items=2) + pending_read = asyncio.create_task(anext(run)) + await started.wait() + summary = await asyncio.wait_for(run.aclose(), timeout=1) + delivered = await asyncio.wait_for(pending_read, timeout=1) + assert delivered.status is PipelineItemStatus.CANCELLED + assert summary.emitted == 1 + assert summary.accepted == 1 + + asyncio.run(consume()) + + +def test_source_close_self_cancellation_is_diagnostic_only() -> None: + async def consume() -> None: + release_handler = asyncio.Event() + + class Source: + def __init__(self) -> None: + self.sent = False + self.close_called = asyncio.Event() + + def __iter__(self) -> Source: + return self + + def __next__(self) -> int: + if self.sent: + raise StopIteration + self.sent = True + return 1 + + def close(self) -> None: + self.close_called.set() + current = asyncio.current_task() + assert current is not None + current.cancel() + + async def handler(context: Any) -> object: + await release_handler.wait() + return context.input + + source = Source() + run = run_pipeline( + source, + [PipelineStage("work", handler)], + max_in_flight=2, + max_items=2, + ) + pending = asyncio.create_task(anext(run)) + await asyncio.wait_for(source.close_called.wait(), timeout=1) + release_handler.set() + + result = await asyncio.wait_for(pending, timeout=1) + assert result.status is PipelineItemStatus.SUCCEEDED + with pytest.raises(StopAsyncIteration): + await anext(run) + summary = await run.completion() + assert summary.status is PipelineRunStatus.SUCCEEDED + assert summary.run_failure is None + + asyncio.run(consume()) + + +def test_close_detaches_a_source_that_suppresses_cancellation() -> None: + class Source: + def __init__(self) -> None: + self.started = asyncio.Event() + self.release = asyncio.Event() + self.cancel_seen = asyncio.Event() + self.closes = 0 + + def __aiter__(self) -> Source: + return self + + async def __anext__(self) -> int: + self.started.set() + try: + await self.release.wait() + except asyncio.CancelledError: + self.cancel_seen.set() + await self.release.wait() + return 1 + + async def aclose(self) -> None: + self.closes += 1 + + async def consume() -> None: + source = Source() + run = run_pipeline(source, [], max_items=2, max_in_flight=1) + pending_read = asyncio.create_task(anext(run)) + await source.started.wait() + summary = await asyncio.wait_for(run.aclose(), timeout=1) + with pytest.raises(StopAsyncIteration): + await asyncio.wait_for(pending_read, timeout=1) + assert source.cancel_seen.is_set() + assert source.closes == 1 + assert summary.status is PipelineRunStatus.CANCELLED + assert summary.accepted == 0 + + source.release.set() + for _ in range(3): + await asyncio.sleep(0) + + asyncio.run(consume()) + + +def test_caller_cancellation_does_not_promote_a_late_source_failure() -> None: + class Source: + def __init__(self) -> None: + self.started = asyncio.Event() + + def __aiter__(self) -> Source: + return self + + async def __anext__(self) -> int: + self.started.set() + try: + await asyncio.Future() + except asyncio.CancelledError: + raise RuntimeError("late source failure after cancellation") from None + + async def consume() -> None: + cancellation = asyncio.Event() + source = Source() + run = run_pipeline( + source, + [], + max_items=2, + max_in_flight=1, + cancellation_signal=cancellation, + ) + pending_read = asyncio.create_task(anext(run)) + await source.started.wait() + cancellation.set() + + with pytest.raises(StopAsyncIteration): + await asyncio.wait_for(pending_read, timeout=1) + summary = await run.completion() + assert summary.status is PipelineRunStatus.CANCELLED + assert summary.run_failure is None + + asyncio.run(consume()) + + +def test_stop_policy_does_not_recancel_source_for_item_cancellation() -> None: + class Source: + def __init__(self) -> None: + self.calls = 0 + self.second_pull_started = asyncio.Event() + self.release = asyncio.Event() + self.cancel_count = 0 + + def __aiter__(self) -> Source: + return self + + async def __anext__(self) -> int: + self.calls += 1 + if self.calls == 1: + return 1 + self.second_pull_started.set() + while not self.release.is_set(): + try: + await self.release.wait() + except asyncio.CancelledError: + self.cancel_count += 1 + raise StopAsyncIteration + + async def consume() -> None: + cancellation = asyncio.Event() + source = Source() + + async def handler(context: Any) -> object: + await context.cancel_signal.wait() + return context.input + + run = run_pipeline( + source, + [PipelineStage("work", handler, on_failure=PipelineFailurePolicy.STOP)], + buffer_capacity=2, + max_in_flight=2, + max_items=3, + cancellation_signal=cancellation, + ) + pending_read = asyncio.create_task(anext(run)) + await source.second_pull_started.wait() + cancellation.set() + + result = await asyncio.wait_for(pending_read, timeout=1) + assert result.status is PipelineItemStatus.CANCELLED + assert [item async for item in run] == [] + summary = await run.completion() + assert summary.status is PipelineRunStatus.CANCELLED + assert source.cancel_count == 1 + + source.release.set() + for _ in range(3): + await asyncio.sleep(0) + assert source.cancel_count == 1 + + asyncio.run(consume()) + + +def test_async_context_closes_after_early_loop_break() -> None: + async def consume() -> None: + run = run_pipeline(range(10), [], max_items=11, max_in_flight=2) + async with run as active: + async for item in active: + assert item.item_index == 0 + break + summary = await run.completion() + assert summary.status is PipelineRunStatus.CANCELLED + assert summary.emitted == 1 + assert summary.accepted == ( + summary.succeeded + summary.failed + summary.dropped + summary.cancelled + ) + + asyncio.run(consume()) + + +def test_normal_completion_leaves_no_pipeline_owned_tasks() -> None: + async def consume() -> None: + cancellation = asyncio.Event() + run = run_pipeline( + [1, 2], + [PipelineStage("identity", lambda context: context.input)], + max_items=3, + cancellation_signal=cancellation, + ) + assert len([item async for item in run]) == 2 + await run.completion() + await asyncio.sleep(0) + current = asyncio.current_task() + assert {task for task in asyncio.all_tasks() if task is not current} == set() + + asyncio.run(consume()) + + +def test_wire_projection_distinguishes_null_from_absence() -> None: + async def consume() -> None: + success_run = run_pipeline([None], [], max_items=2) + success = await anext(success_run) + assert success.to_dict()["input"] is None + assert success.to_dict()["output"] is None + assert "failure" not in success.to_dict() + assert [item async for item in success_run] == [] + + failed_run = run_pipeline([object()], [], max_items=2) + failed = await anext(failed_run) + document = failed.to_dict() + assert document["inputBound"] is False + assert "input" not in document + assert "output" not in document + assert document["failure"]["code"] == "INVALID_INPUT" # type: ignore[index] + assert [item async for item in failed_run] == [] + + asyncio.run(consume()) + + +def test_mathematical_integers_normalize_and_invalid_config_is_eager() -> None: + class Source: + iterated = False + + def __iter__(self) -> Source: + self.iterated = True + return self + + def __next__(self) -> int: + raise StopIteration + + source = Source() + run = run_pipeline( + source, + [PipelineStage("stage", lambda context: context.input, concurrency=1.0)], + buffer_capacity=1.0, + max_in_flight=1.0, + max_items=2.0, + max_stages=1.0, + ) + assert not source.iterated + asyncio.run(run.aclose()) + assert not source.iterated + + with pytest.raises(TypeError): + run_pipeline(source, [], max_in_flight=True) + with pytest.raises(TypeError): + run_pipeline(source, [], max_items=1.5) + with pytest.raises(TypeError): + run_pipeline(source, [PipelineStage("bad", lambda _: None, timeout_ms=0.5)]) + with pytest.raises(ValueError, match="duplicate"): + run_pipeline( + source, + [ + PipelineStage("same", lambda context: context.input), + PipelineStage("same", lambda context: context.input), + ], + ) + with pytest.raises(ValueError, match="attempt"): + run_pipeline( + source, + [ + PipelineStage( + "unsafe", + lambda context: context.input, + retry=PipelineRetryOptions(max_attempts=2), + ) + ], + max_items=2**53 - 1, + ) + assert not source.iterated + + +@pytest.mark.parametrize("stage_count", [1, 2]) +def test_max_stages_accepts_under_and_exact_limit(stage_count: int) -> None: + async def consume() -> None: + stages = [ + PipelineStage(f"stage-{index}", lambda context: context.input) + for index in range(stage_count) + ] + run = run_pipeline( + [1], + stages, + max_items=2, + max_stages=2, + ) + + result = await anext(run) + assert result.status is PipelineItemStatus.SUCCEEDED + assert result.output == 1 + assert result.completed_stages == stage_count + assert [item async for item in run] == [] + + asyncio.run(consume()) + + +def test_stage_overflow_does_not_read_extra_stage_or_construct_source() -> None: + reads: list[str] = [] + + class Source: + iterated = False + + def __iter__(self) -> Source: + self.iterated = True + return self + + def __next__(self) -> int: + raise StopIteration + + class ExtraStage: + def __getattribute__(self, name: str) -> object: + reads.append(name) + raise AssertionError("overflow stage properties must not be read") + + source = Source() + stages: list[object] = [ + PipelineStage("first", lambda context: context.input), + PipelineStage("second", lambda context: context.input), + ExtraStage(), + ] + + with pytest.raises( + ValueError, + match=r"^pipeline stage count exceeds max_stages limit of 2$", + ): + run_pipeline(source, stages, max_stages=2) # type: ignore[arg-type] + + assert reads == [] + assert not source.iterated + + +def test_infinite_stage_sequence_stops_after_limit_plus_one_pull() -> None: + class InfiniteStages(Sequence[PipelineStage]): + def __init__(self) -> None: + self.pulls = 0 + + def __getitem__( + self, + index: int | slice, + ) -> PipelineStage | Sequence[PipelineStage]: + if isinstance(index, slice): + return () + self.pulls += 1 + return PipelineStage(f"stage-{index}", lambda context: context.input) + + def __len__(self) -> int: + return 0 + + stages = InfiniteStages() + + with pytest.raises( + ValueError, + match=r"^pipeline stage count exceeds max_stages limit of 2$", + ): + run_pipeline([], stages, max_stages=2) + + assert stages.pulls == 3 + + +def test_stage_overflow_closes_iterator_and_preserves_overflow_error() -> None: + class Stages(Sequence[PipelineStage]): + def __init__(self) -> None: + self.iterator: Any | None = None + + def __iter__(self) -> Any: + owner = self + + class TrackingIterator: + def __init__(self) -> None: + self.pulls = 0 + self.close_calls = 0 + + def __iter__(self) -> TrackingIterator: + return self + + def __next__(self) -> PipelineStage: + index = self.pulls + self.pulls += 1 + return PipelineStage(f"stage-{index}", lambda context: context.input) + + def close(self) -> None: + self.close_calls += 1 + raise RuntimeError("hostile close") + + owner.iterator = TrackingIterator() + return owner.iterator + + def __getitem__(self, index: int | slice) -> PipelineStage: + raise AssertionError("iteration must use __iter__") + + def __len__(self) -> int: + return 0 + + stages = Stages() + + with pytest.raises( + ValueError, + match=r"^pipeline stage count exceeds max_stages limit of 2$", + ): + run_pipeline([], stages, max_stages=2) + + assert stages.iterator is not None + assert stages.iterator.pulls == 3 + assert stages.iterator.close_calls == 1 + + +@pytest.mark.parametrize( + "value", + [0, -1, True, 1.5, float("nan"), float("inf"), 2049], +) +def test_invalid_max_stages_is_rejected_eagerly(value: object) -> None: + class Source: + iterated = False + + def __iter__(self) -> Source: + self.iterated = True + return self + + def __next__(self) -> int: + raise StopIteration + + source = Source() + + with pytest.raises(TypeError, match="max_stages"): + run_pipeline(source, [], max_stages=value) # type: ignore[arg-type] + + assert not source.iterated + + +def test_empty_source_succeeds_without_invoking_a_stage() -> None: + def forbidden(_: Any) -> object: + raise AssertionError("an empty source must not invoke handlers") + + run = run_pipeline([], [PipelineStage("unused", forbidden)], max_items=1) + + async def consume() -> None: + assert [item async for item in run] == [] + summary = await run.completion() + assert summary.status is PipelineRunStatus.SUCCEEDED + assert summary.accepted == summary.emitted == 0 + assert summary.stage_max_observed_concurrency == {"unused": 0} + + asyncio.run(consume()) + + +def test_completion_waiter_does_not_start_source_without_consumer_demand() -> None: + async def consume() -> None: + class Source: + def __init__(self) -> None: + self.pulls = 0 + + def __iter__(self) -> Source: + return self + + def __next__(self) -> int: + if self.pulls: + raise StopIteration + self.pulls += 1 + return 1 + + source = Source() + run = run_pipeline(source, [], max_items=2) + waiting = asyncio.create_task(run.completion()) + await asyncio.sleep(0) + assert source.pulls == 0 + assert not waiting.done() + assert (await anext(run)).output == 1 + assert [item async for item in run] == [] + assert (await waiting).status is PipelineRunStatus.SUCCEEDED + + asyncio.run(consume()) + + +def test_completion_settles_after_last_result_without_reading_end_marker() -> None: + async def consume() -> None: + run = run_pipeline([1], [], max_in_flight=1, max_items=2) + result = await anext(run) + assert result.output == 1 + + summary = await asyncio.wait_for(run.completion(), timeout=1) + assert summary.status is PipelineRunStatus.SUCCEEDED + assert summary.accepted == summary.emitted == 1 + + with pytest.raises(StopAsyncIteration): + await anext(run) + assert await run.completion() is summary + + asyncio.run(consume()) + + +def test_stop_cancels_an_outstanding_async_source_pull_and_closes_once() -> None: + async def consume() -> None: + class Source: + def __init__(self) -> None: + self.pulls = 0 + self.closes = 0 + self.second_pull_started = asyncio.Event() + + def __aiter__(self) -> Source: + return self + + async def __anext__(self) -> str: + self.pulls += 1 + if self.pulls == 1: + return "stop" + self.second_pull_started.set() + await asyncio.Event().wait() + raise AssertionError("unreachable") + + async def aclose(self) -> None: + self.closes += 1 + + source = Source() + + async def fail(_: Any) -> object: + await source.second_pull_started.wait() + raise RuntimeError("stop while pull is pending") + + run = run_pipeline( + source, + [PipelineStage("gate", fail, on_failure="stop")], + buffer_capacity=1, + max_in_flight=2, + max_items=3, + ) + results = await asyncio.wait_for( + _collect(run), + timeout=1, + ) + assert len(results) == 1 + assert results[0].status is PipelineItemStatus.FAILED + assert source.pulls == 2 + assert source.closes == 1 + + asyncio.run(consume()) + + +def test_stop_does_not_promote_a_late_source_failure() -> None: + class Source: + def __init__(self) -> None: + self.calls = 0 + self.second_pull_started = asyncio.Event() + + def __aiter__(self) -> Source: + return self + + async def __anext__(self) -> str: + self.calls += 1 + if self.calls == 1: + return "stop" + self.second_pull_started.set() + try: + await asyncio.Future() + except asyncio.CancelledError: + raise RuntimeError("late source failure after stop") from None + + async def consume() -> None: + source = Source() + + async def handler(_: Any) -> object: + await source.second_pull_started.wait() + raise ValueError("stop item") + + run = run_pipeline( + source, + [PipelineStage("work", handler, on_failure=PipelineFailurePolicy.STOP)], + buffer_capacity=2, + max_in_flight=2, + max_items=3, + ) + results = [item async for item in run] + + assert len(results) == 1 + assert results[0].status is PipelineItemStatus.FAILED + summary = await run.completion() + assert summary.status is PipelineRunStatus.FAILED + assert summary.run_failure is None + + asyncio.run(consume()) + + +def test_timeout_observes_a_late_non_cooperative_failure() -> None: + async def consume() -> None: + started = asyncio.Event() + release = asyncio.Event() + loop_errors: list[dict[str, Any]] = [] + loop = asyncio.get_running_loop() + old_handler = loop.get_exception_handler() + loop.set_exception_handler(lambda _loop, context: loop_errors.append(context)) + + async def handler(_: Any) -> object: + started.set() + try: + await asyncio.Event().wait() + except asyncio.CancelledError: + await release.wait() + raise RuntimeError("late failure") from None + + try: + run = run_pipeline( + [1], + [PipelineStage("work", handler, timeout_ms=1)], + max_items=2, + max_in_flight=1, + ) + result = await anext(run) + assert started.is_set() + assert result.failure is not None + assert result.failure.code is PipelineFailureCode.STAGE_TIMEOUT + assert [item async for item in run] == [] + release.set() + await asyncio.sleep(0) + await asyncio.sleep(0) + assert loop_errors == [] + finally: + loop.set_exception_handler(old_handler) + + asyncio.run(consume()) + + +def test_concurrent_next_is_rejected_without_stealing_a_result() -> None: + async def consume() -> None: + started = asyncio.Event() + release = asyncio.Event() + + async def handler(context: Any) -> object: + started.set() + await release.wait() + return context.input + + run = run_pipeline([1], [PipelineStage("work", handler)], max_items=2) + first_read = asyncio.create_task(anext(run)) + await started.wait() + with pytest.raises(RuntimeError, match="concurrent"): + await anext(run) + release.set() + assert (await first_read).output == 1 + assert [item async for item in run] == [] + + asyncio.run(consume()) + + +def test_drop_is_explicit_and_mutation_cannot_change_snapshots() -> None: + owned_output = {"value": [1]} + source_value = {"input": [1]} + stages = [ + PipelineStage( + "stage", + lambda context: ( + (_ for _ in ()).throw(RuntimeError("drop")) + if context.item_index == 0 + else owned_output + ), + on_failure="drop", + ) + ] + run = run_pipeline(["bad", source_value], stages, max_items=3) + stages.clear() + + async def consume() -> None: + results = [item async for item in run] + source_value["input"].append(2) + owned_output["value"].append(2) + assert results[0].status is PipelineItemStatus.DROPPED + assert results[0].failure is not None + assert results[0].failure.code is PipelineFailureCode.STAGE_EXECUTION_FAILED + assert results[1].status is PipelineItemStatus.SUCCEEDED + assert results[1].input == {"input": [1]} + assert results[1].output == {"value": [1]} + + asyncio.run(consume()) + + +def test_source_value_is_snapshotted_before_the_next_pull_can_mutate_it() -> None: + class Source: + def __init__(self) -> None: + self.shared = {"values": [1]} + self.pull = 0 + + def __iter__(self) -> Source: + return self + + def __next__(self) -> object: + self.pull += 1 + if self.pull == 1: + return self.shared + if self.pull == 2: + self.shared["values"].append(2) + return "second" + raise StopIteration + + run = run_pipeline(Source(), [], max_items=3, max_in_flight=2) + + async def consume() -> None: + results = [item async for item in run] + assert results[0].input == {"values": [1]} + assert results[0].output == {"values": [1]} + + asyncio.run(consume()) + + +def test_handler_output_is_snapshotted_in_the_attempt_completion_turn() -> None: + async def consume() -> None: + owned = {"values": [1]} + + async def handler(_: Any) -> object: + asyncio.get_running_loop().call_soon(owned["values"].append, 2) + return owned + + run = run_pipeline([0], [PipelineStage("work", handler)], max_items=2) + result = await anext(run) + assert result.output == {"values": [1]} + assert owned == {"values": [1, 2]} + assert [item async for item in run] == [] + + asyncio.run(consume()) + + +async def _collect(run: Any) -> list[Any]: + return [item async for item in run] diff --git a/scripts/check-packed-install.mjs b/scripts/check-packed-install.mjs index 99f45d5..46d1915 100644 --- a/scripts/check-packed-install.mjs +++ b/scripts/check-packed-install.mjs @@ -123,7 +123,21 @@ try { .map((name) => `await import(${JSON.stringify(name)});`) .join("\n"); const smokePath = join(consumerRoot, "smoke.mjs"); - writeFileSync(smokePath, `${importSmoke}\n`); + writeFileSync( + smokePath, + `${importSmoke} +const { runPipeline } = await import("@graph-engineering/runtime"); +if (typeof runPipeline !== "function") throw new Error("runtime package omits runPipeline"); +const pipelineRun = runPipeline([null], [], { maxItems: 2, maxStages: 1 }); +const pipelineItem = await pipelineRun.next(); +if (pipelineItem.done || pipelineItem.value.output !== null) { + throw new Error("installed runPipeline failed its null-presence smoke test"); +} +if (!(await pipelineRun.next()).done || (await pipelineRun.completion).status !== "succeeded") { + throw new Error("installed runPipeline failed to terminate successfully"); +} +`, + ); run(process.execPath, [smokePath], { cwd: consumerRoot }); const cliManifest = workspace.get("@graph-engineering/cli")?.manifest; diff --git a/scripts/check-python-artifacts.py b/scripts/check-python-artifacts.py index f645d4d..74ac2d2 100755 --- a/scripts/check-python-artifacts.py +++ b/scripts/check-python-artifacts.py @@ -46,6 +46,7 @@ def safe_archive_path(name: str) -> bool: raise SystemExit("wheel contains an unsafe or forbidden path") required_wheel = { "graph_engineering/__init__.py", + "graph_engineering/pipeline.py", "graph_engineering/py.typed", "graph_engineering/scheduler.py", "graph_engineering/persistence/__init__.py", @@ -97,6 +98,7 @@ def safe_archive_path(name: str) -> bool: f"{prefix}src/graph_engineering/__init__.py", f"{prefix}src/graph_engineering/_json.py", f"{prefix}src/graph_engineering/canonical.py", + f"{prefix}src/graph_engineering/pipeline.py", f"{prefix}src/graph_engineering/py.typed", f"{prefix}src/graph_engineering/scheduler.py", f"{prefix}src/graph_engineering/persistence/event_store.py", diff --git a/spec/README.md b/spec/README.md index dfeebd0..58f3049 100644 --- a/spec/README.md +++ b/spec/README.md @@ -6,7 +6,8 @@ `durable-json.schema.json` defines the tagged, checkpoint-safe encoding used by scheduler recovery for portable finite JSON, including exact binary64 values. `conformance/` contains inputs and expected results used by every native runtime, -including settled-barrier and deterministic route-selection decision corpora. +including settled-barrier, deterministic route-selection, and bounded-pipeline +decision and coordination corpora. Runtime scheduling is fixed by [runtime-semantics.md](runtime-semantics.md), and local event/checkpoint behavior is fixed by @@ -14,6 +15,10 @@ local event/checkpoint behavior is fixed by behavior is fixed by [primitives-semantics.md](primitives-semantics.md). Scheduler-integrated continuation for one immutable DAG is fixed by [durable-recovery-semantics.md](durable-recovery-semantics.md). +Standalone per-item streaming, bounded buffers, source backpressure, structured +failure policies, and cancellation are fixed by +[pipeline-semantics.md](pipeline-semantics.md). This standalone contract does +not activate Graph IR stream edges or durable item recovery. ## Canonical serialization v1alpha1 diff --git a/spec/conformance/expected.json b/spec/conformance/expected.json index be52db6..69cc879 100644 --- a/spec/conformance/expected.json +++ b/spec/conformance/expected.json @@ -18,6 +18,14 @@ ["merge"] ] }, + "policy-extension-python-name-null.graph.json": { + "valid": true, + "topologicalLayers": [["node"]] + }, + "policy-extension-python-name-number.graph.json": { + "valid": true, + "topologicalLayers": [["node"]] + }, "invalid-duplicate-node.graph.json": { "valid": false, "diagnosticCodes": ["GE1001_DUPLICATE_NODE"] @@ -42,6 +50,22 @@ "valid": false, "diagnosticCodes": ["GE1007_INVALID_GRAPH"] }, + "invalid-null-metadata-description.graph.json": { + "valid": false, + "diagnosticCodes": ["GE1007_INVALID_GRAPH"] + }, + "invalid-null-state-schema.graph.json": { + "valid": false, + "diagnosticCodes": ["GE1007_INVALID_GRAPH"] + }, + "invalid-null-output-port.graph.json": { + "valid": false, + "diagnosticCodes": ["GE1007_INVALID_GRAPH"] + }, + "invalid-null-node-retry.graph.json": { + "valid": false, + "diagnosticCodes": ["GE1007_INVALID_GRAPH"] + }, "invalid-entrypoint-incoming.graph.json": { "valid": false, "diagnosticCodes": ["GE1010_ENTRYPOINT_HAS_INCOMING", "GE1006_UNREACHABLE_NODE"] diff --git a/spec/conformance/invalid-null-metadata-description.graph.json b/spec/conformance/invalid-null-metadata-description.graph.json new file mode 100644 index 0000000..dcc7ed9 --- /dev/null +++ b/spec/conformance/invalid-null-metadata-description.graph.json @@ -0,0 +1,25 @@ +{ + "apiVersion": "graphengineering.reacher-z.github.io/v1alpha1", + "kind": "Graph", + "metadata": { + "name": "invalid-null-metadata-description", + "version": "1.0.0", + "description": null + }, + "inputSchema": {}, + "outputSchema": {}, + "entrypoints": ["only"], + "outputs": { + "result": { "node": "only" } + }, + "nodes": [ + { + "id": "only", + "kind": "transform", + "inputSchema": {}, + "outputSchema": {}, + "config": {} + } + ], + "edges": [] +} diff --git a/spec/conformance/invalid-null-node-retry.graph.json b/spec/conformance/invalid-null-node-retry.graph.json new file mode 100644 index 0000000..021dc1d --- /dev/null +++ b/spec/conformance/invalid-null-node-retry.graph.json @@ -0,0 +1,25 @@ +{ + "apiVersion": "graphengineering.reacher-z.github.io/v1alpha1", + "kind": "Graph", + "metadata": { + "name": "invalid-null-node-retry", + "version": "1.0.0" + }, + "inputSchema": {}, + "outputSchema": {}, + "entrypoints": ["only"], + "outputs": { + "result": { "node": "only" } + }, + "nodes": [ + { + "id": "only", + "kind": "transform", + "inputSchema": {}, + "outputSchema": {}, + "config": {}, + "retry": null + } + ], + "edges": [] +} diff --git a/spec/conformance/invalid-null-output-port.graph.json b/spec/conformance/invalid-null-output-port.graph.json new file mode 100644 index 0000000..9b525e9 --- /dev/null +++ b/spec/conformance/invalid-null-output-port.graph.json @@ -0,0 +1,24 @@ +{ + "apiVersion": "graphengineering.reacher-z.github.io/v1alpha1", + "kind": "Graph", + "metadata": { + "name": "invalid-null-output-port", + "version": "1.0.0" + }, + "inputSchema": {}, + "outputSchema": {}, + "entrypoints": ["only"], + "outputs": { + "result": { "node": "only", "port": null } + }, + "nodes": [ + { + "id": "only", + "kind": "transform", + "inputSchema": {}, + "outputSchema": {}, + "config": {} + } + ], + "edges": [] +} diff --git a/spec/conformance/invalid-null-state-schema.graph.json b/spec/conformance/invalid-null-state-schema.graph.json new file mode 100644 index 0000000..affbfed --- /dev/null +++ b/spec/conformance/invalid-null-state-schema.graph.json @@ -0,0 +1,25 @@ +{ + "apiVersion": "graphengineering.reacher-z.github.io/v1alpha1", + "kind": "Graph", + "metadata": { + "name": "invalid-null-state-schema", + "version": "1.0.0" + }, + "inputSchema": {}, + "outputSchema": {}, + "stateSchema": null, + "entrypoints": ["only"], + "outputs": { + "result": { "node": "only" } + }, + "nodes": [ + { + "id": "only", + "kind": "transform", + "inputSchema": {}, + "outputSchema": {}, + "config": {} + } + ], + "edges": [] +} diff --git a/spec/conformance/pipeline.case.json b/spec/conformance/pipeline.case.json new file mode 100644 index 0000000..9a39913 --- /dev/null +++ b/spec/conformance/pipeline.case.json @@ -0,0 +1,440 @@ +{ + "schemaVersion": 1, + "description": "Language-neutral behavioral cases for the bounded standalone pipeline contract.", + "defaults": { + "bufferCapacity": 16, + "maxInFlight": 16, + "maxItems": 1000, + "maxStages": 2048, + "ordering": "input", + "stageConcurrency": 1, + "maxAttempts": 1, + "onFailure": "dead-letter" + }, + "mockOutcomeKinds": [ + "return", + "throw", + "invalid-output", + "wait-for-gate" + ], + "mockGrammar": { + "outcomesByItem": "Each array position is the one-based attempt outcome for that item at that stage; after the final declared outcome, the last outcome repeats.", + "waitForGate": "wait-for-gate is one attempt action and must contain a terminal then action for the same attempt.", + "onStartByItem": "Coordination side effects happen immediately after an attempt starts and before its declared outcome." + }, + "cases": [ + { + "id": "identity-preserves-null-presence", + "source": { + "kind": "array", + "items": [null, { "value": 1 }, ["x", true]] + }, + "stages": [], + "options": { + "bufferCapacity": 1, + "maxInFlight": 2, + "maxItems": 4, + "ordering": "input" + }, + "expect": { + "deliveryOrder": [0, 1, 2], + "items": [ + { + "itemIndex": 0, + "status": "succeeded", + "inputBound": true, + "input": null, + "output": null, + "completedStages": 0, + "totalAttempts": 0 + }, + { + "itemIndex": 1, + "status": "succeeded", + "inputBound": true, + "input": { "value": 1 }, + "output": { "value": 1 }, + "completedStages": 0, + "totalAttempts": 0 + }, + { + "itemIndex": 2, + "status": "succeeded", + "inputBound": true, + "input": ["x", true], + "output": ["x", true], + "completedStages": 0, + "totalAttempts": 0 + } + ], + "summary": { + "status": "succeeded", + "accepted": 3, + "emitted": 3, + "succeeded": 3, + "failed": 0, + "dropped": 0, + "cancelled": 0, + "runFailure": null + } + } + }, + { + "id": "bounded-retry-and-dead-letter", + "source": { + "kind": "array", + "items": [{ "id": "a" }, { "id": "b" }, { "id": "c" }] + }, + "stages": [ + { + "id": "prepare", + "concurrency": 2, + "retry": { + "maxAttempts": 2, + "initialDelayMs": 0, + "backoffMultiplier": 1, + "maxDelayMs": 0 + }, + "onFailure": "dead-letter", + "outcomesByItem": { + "0": [{ "kind": "return", "value": { "n": 1 } }], + "1": [ + { "kind": "throw", "causeName": "TransientError", "message": "try again" }, + { "kind": "return", "value": { "n": 2 } } + ], + "2": [{ "kind": "invalid-output", "valueType": "non-finite-number" }] + } + }, + { + "id": "finish", + "concurrency": 1, + "onFailure": "dead-letter", + "outcomesByItem": { + "0": [{ "kind": "return", "value": { "answer": 10 } }], + "1": [{ "kind": "return", "value": { "answer": 20 } }] + } + } + ], + "options": { + "bufferCapacity": 1, + "maxInFlight": 3, + "maxItems": 4, + "maxStages": 2, + "ordering": "input" + }, + "expect": { + "deliveryOrder": [0, 1, 2], + "items": [ + { + "itemIndex": 0, + "status": "succeeded", + "inputBound": true, + "input": { "id": "a" }, + "output": { "answer": 10 }, + "completedStages": 2, + "totalAttempts": 2 + }, + { + "itemIndex": 1, + "status": "succeeded", + "inputBound": true, + "input": { "id": "b" }, + "output": { "answer": 20 }, + "completedStages": 2, + "totalAttempts": 3 + }, + { + "itemIndex": 2, + "status": "failed", + "inputBound": true, + "input": { "id": "c" }, + "completedStages": 0, + "totalAttempts": 1, + "failure": { + "code": "INVALID_OUTPUT", + "itemIndex": 2, + "stageId": "prepare", + "stageIndex": 0, + "attempt": 1, + "retryable": false + } + } + ], + "summary": { + "status": "failed", + "accepted": 3, + "emitted": 3, + "succeeded": 2, + "failed": 1, + "dropped": 0, + "cancelled": 0, + "runFailure": null + } + } + }, + { + "id": "drop-is-explicit", + "source": { + "kind": "array", + "items": ["bad", "good"] + }, + "stages": [ + { + "id": "validate", + "concurrency": 1, + "onFailure": "drop", + "outcomesByItem": { + "0": [{ "kind": "throw", "causeName": "Rejected", "message": "bad item" }], + "1": [{ "kind": "return", "value": "accepted" }] + } + } + ], + "options": { + "bufferCapacity": 1, + "maxInFlight": 2, + "maxItems": 3, + "ordering": "input" + }, + "expect": { + "deliveryOrder": [0, 1], + "items": [ + { + "itemIndex": 0, + "status": "dropped", + "inputBound": true, + "input": "bad", + "completedStages": 0, + "totalAttempts": 1, + "failure": { + "code": "STAGE_EXECUTION_FAILED", + "itemIndex": 0, + "stageId": "validate", + "stageIndex": 0, + "attempt": 1, + "retryable": false, + "causeName": "Rejected" + } + }, + { + "itemIndex": 1, + "status": "succeeded", + "inputBound": true, + "input": "good", + "output": "accepted", + "completedStages": 1, + "totalAttempts": 1 + } + ], + "summary": { + "status": "failed", + "accepted": 2, + "emitted": 2, + "succeeded": 1, + "failed": 0, + "dropped": 1, + "cancelled": 0, + "runFailure": null + } + } + }, + { + "id": "stop-closes-intake-and-drains-accepted", + "source": { + "kind": "counting-array", + "items": ["stop", "must-not-pull"] + }, + "stages": [ + { + "id": "gate", + "concurrency": 1, + "onFailure": "stop", + "outcomesByItem": { + "0": [{ "kind": "throw", "causeName": "StopRequested", "message": "stop" }] + } + } + ], + "options": { + "bufferCapacity": 1, + "maxInFlight": 1, + "maxItems": 3, + "ordering": "input" + }, + "expect": { + "sourcePullCount": 1, + "sourceCloseCount": 1, + "deliveryOrder": [0], + "items": [ + { + "itemIndex": 0, + "status": "failed", + "inputBound": true, + "input": "stop", + "completedStages": 0, + "totalAttempts": 1, + "failure": { + "code": "STAGE_EXECUTION_FAILED", + "itemIndex": 0, + "stageId": "gate", + "stageIndex": 0, + "attempt": 1, + "retryable": false, + "causeName": "StopRequested" + } + } + ], + "summary": { + "status": "failed", + "accepted": 1, + "emitted": 1, + "succeeded": 0, + "failed": 1, + "dropped": 0, + "cancelled": 0, + "runFailure": null + } + } + }, + { + "id": "source-failure-drains-prefix", + "source": { + "kind": "throwing-array", + "items": [1, 2], + "thenThrow": { + "causeName": "SourceOffline", + "message": "source failed" + } + }, + "stages": [], + "options": { + "bufferCapacity": 1, + "maxInFlight": 3, + "maxItems": 4, + "ordering": "input" + }, + "expect": { + "deliveryOrder": [0, 1], + "summary": { + "status": "failed", + "accepted": 2, + "emitted": 2, + "succeeded": 2, + "failed": 0, + "dropped": 0, + "cancelled": 0, + "runFailure": { + "code": "SOURCE_FAILED", + "causeName": "SourceOffline" + } + } + } + }, + { + "id": "item-limit-does-not-probe", + "source": { + "kind": "counting-array", + "items": [1, 2, 3] + }, + "stages": [], + "options": { + "bufferCapacity": 1, + "maxInFlight": 2, + "maxItems": 2, + "ordering": "input" + }, + "expect": { + "sourcePullCount": 2, + "deliveryOrder": [0, 1], + "summary": { + "status": "failed", + "accepted": 2, + "emitted": 2, + "succeeded": 2, + "failed": 0, + "dropped": 0, + "cancelled": 0, + "runFailure": { + "code": "ITEM_LIMIT_REACHED" + } + } + } + }, + { + "id": "fast-item-crosses-stage-without-barrier", + "source": { + "kind": "array", + "items": ["slow", "fast"] + }, + "gates": ["release-slow"], + "stages": [ + { + "id": "first", + "concurrency": 2, + "onFailure": "dead-letter", + "outcomesByItem": { + "0": [ + { + "kind": "wait-for-gate", + "gate": "release-slow", + "then": { "kind": "return", "value": "slow-first" } + } + ], + "1": [{ "kind": "return", "value": "fast-first" }] + } + }, + { + "id": "second", + "concurrency": 1, + "onStartByItem": { + "1": [{ "releaseGate": "release-slow" }] + }, + "outcomesByItem": { + "0": [{ "kind": "return", "value": "slow-done" }], + "1": [{ "kind": "return", "value": "fast-done" }] + } + } + ], + "options": { + "bufferCapacity": 1, + "maxInFlight": 2, + "maxItems": 3, + "ordering": "input" + }, + "expect": { + "mustOccurBefore": [ + ["stage:second:item:1:start", "stage:first:item:0:return"] + ], + "deliveryOrder": [0, 1], + "outputByItem": { + "0": "slow-done", + "1": "fast-done" + }, + "maxObservedInFlightAtMost": 2, + "maxObservedQueueDepthAtMost": 1 + } + }, + { + "id": "slow-consumer-backpressures-source", + "source": { + "kind": "counting-array", + "items": [0, 1, 2, 3, 4] + }, + "stages": [], + "options": { + "bufferCapacity": 1, + "maxInFlight": 2, + "maxItems": 6, + "ordering": "completion" + }, + "consumer": { + "readResults": 1, + "thenPauseUntil": "pipeline-idle" + }, + "expect": { + "sourcePullCountWhilePausedAtMost": 3, + "deliveryItemSet": [0, 1, 2, 3, 4], + "maxObservedInFlightAtMost": 2, + "maxObservedQueueDepthAtMost": 1 + } + } + ] +} diff --git a/spec/conformance/policy-extension-python-name-null.graph.json b/spec/conformance/policy-extension-python-name-null.graph.json new file mode 100644 index 0000000..b1012fc --- /dev/null +++ b/spec/conformance/policy-extension-python-name-null.graph.json @@ -0,0 +1,14 @@ +{ + "apiVersion": "graphengineering.reacher-z.github.io/v1alpha1", + "kind": "Graph", + "metadata": { "name": "policy-extension-python-name-null", "version": "1.0.0" }, + "inputSchema": {}, + "outputSchema": {}, + "entrypoints": ["node"], + "outputs": { "result": { "node": "node" } }, + "nodes": [ + { "id": "node", "kind": "transform", "inputSchema": {}, "outputSchema": {}, "config": {} } + ], + "edges": [], + "policies": { "max_concurrency": null } +} diff --git a/spec/conformance/policy-extension-python-name-number.graph.json b/spec/conformance/policy-extension-python-name-number.graph.json new file mode 100644 index 0000000..f85705d --- /dev/null +++ b/spec/conformance/policy-extension-python-name-number.graph.json @@ -0,0 +1,14 @@ +{ + "apiVersion": "graphengineering.reacher-z.github.io/v1alpha1", + "kind": "Graph", + "metadata": { "name": "policy-extension-python-name-number", "version": "1.0.0" }, + "inputSchema": {}, + "outputSchema": {}, + "entrypoints": ["node"], + "outputs": { "result": { "node": "node" } }, + "nodes": [ + { "id": "node", "kind": "transform", "inputSchema": {}, "outputSchema": {}, "config": {} } + ], + "edges": [], + "policies": { "max_concurrency": 5 } +} diff --git a/spec/pipeline-semantics.md b/spec/pipeline-semantics.md new file mode 100644 index 0000000..a16d8de --- /dev/null +++ b/spec/pipeline-semantics.md @@ -0,0 +1,515 @@ +# Bounded pipeline semantics v1alpha1 + +This document fixes the observable contract for the standalone TypeScript +`runPipeline` and Python `run_pipeline` APIs. A pipeline accepts a sequence of +portable JSON items and moves each item through the same ordered stages. Items +may occupy different stages at the same time; there is no implicit whole-stage +barrier. + +This API is deliberately independent from `runGraph`. It does **not** activate +Graph IR `edge.mode: "stream"`, change the one-result-per-node scheduler model, +or add item-level events to durable graph recovery. Those integrations require +a later protocol revision with item identities, offsets, acknowledgements, +stream fan-in rules, and crash-safe queue reconstruction. + +## Public concepts + +A pipeline has four bounded parts: + +1. a synchronous or asynchronous item source; +2. an immutable, ordered list of stages; +3. bounded queues between adjacent stages; and +4. a global in-flight window that connects consumer demand back to source + demand. + +Every accepted item reaches one structured terminal status. Failed items are +never represented by `null`, silently removed, or converted to an absent array +entry. JSON `null` remains a valid input and output value. + +Native APIs use their language naming conventions. The field names shown in +this document use the camel-case portable projection used by conformance +fixtures and documentation. + +### Native lifecycle surface + +TypeScript exposes a synchronous factory with this semantic surface: + +```ts +runPipeline(source, stages, options?): PipelineRun + +interface PipelineRun extends AsyncIterableIterator { + readonly completion: Promise; + close(reason?: unknown): Promise; +} +``` + +`PipelineRun[Symbol.asyncIterator]()` returns the same object. Its iterator +`return()` delegates to `close()` so breaking a `for await` loop initiates +cleanup. `close()` is idempotent and resolves to the same terminal summary as +`completion`. + +Python exposes the corresponding synchronous factory and async iterator: + +```python +run_pipeline(source, stages, **options) -> PipelineRun + +PipelineRun.__aiter__() -> PipelineRun +await PipelineRun.__anext__() -> PipelineItemResult +await PipelineRun.completion() -> PipelineSummary +await PipelineRun.aclose() -> PipelineSummary +async with PipelineRun: ... +``` + +`aclose()` is idempotent and returns the same terminal summary as +`completion()`. Because breaking an `async for` over an arbitrary custom Python +iterator does not portably call `aclose`, examples use the async context manager. + +Both factories validate and copy options and stages synchronously. They do not +advance the source until the first consumer read or context entry. Each run is +single-pass and not replayable. Repeated calls to the language's iterator-symbol +method return the same iterator object; concurrent attempts to advance it fail +explicitly instead of racing terminal delivery. + +## Stage contract + +A stage has the following logical shape: + +```text +PipelineStage { + id: string + handler: PipelineHandler + concurrency: positive integer = 1 + timeoutMs?: non-negative integer + retry?: { + maxAttempts: positive integer = 1 + initialDelayMs: non-negative number = 0 + backoffMultiplier: finite number >= 1 = 1 + maxDelayMs?: non-negative number + } + onFailure: "stop" | "drop" | "dead-letter" = "dead-letter" +} +``` + +Stage IDs are non-empty and unique by exact string comparison. The stage list, +configuration values, and handler references are copied before execution can +yield. Mutating the caller's list or stage objects after construction cannot +change a live pipeline. + +The handler receives a read-only context with: + +- `input`: a detached portable JSON snapshot of the preceding value; +- `itemIndex`: the zero-based admission index; +- `stageId` and `stageIndex`; +- `attempt`: the one-based attempt within this item and stage; and +- a cooperative cancellation signal. + +The handler returns one portable JSON value, synchronously or asynchronously. +Its result is detached and validated before the item can enter the next queue. +The pipeline does not infer mappings, flatten collections, or merge values. +Those are explicit stages. + +## Options and numeric bounds + +The common options are: + +```text +PipelineOptions { + bufferCapacity: positive integer = 16 + maxInFlight: positive integer = 16 + maxItems: positive integer = 1000 + maxStages: positive integer <= 2048 = 2048 + ordering: "input" | "completion" = "input" + cancellationSignal?: caller-owned signal +} +``` + +`bufferCapacity`, `maxInFlight`, `maxItems`, `maxStages`, every stage `concurrency`, and +`maxAttempts` must be mathematical integers in `[1, 2^53 - 1]`. Booleans are +not integers. Timer values must be finite, non-negative, no larger than +`2^31 - 1` milliseconds, and cannot be booleans. `backoffMultiplier` must be +finite and at least one. Invalid configuration fails before the source is +advanced or a handler is called. + +An option receives its default only when it is omitted (or is JavaScript +`undefined`). Explicit `null` is not an omission and is invalid for every +configuration field. This rule does not affect item values: JSON `null` remains +a valid pipeline input and output. + +`maxItems` is a hard admission budget, including invalid input items. Before +requesting another source value, the producer checks whether `maxItems` values +have already been accepted. At the limit it stops without probing the source +again and reports `ITEM_LIMIT_REACHED` after accepted work drains. Even a source +that is infinite or controlled by an adversary therefore cannot create +unbounded total dynamic work. + +The maximum possible handler-attempt count is statically bounded by: + +```text +maxItems * sum(stage.retry.maxAttempts or 1) +``` + +Construction rejects a configuration when that product is not a safe integer. +This derived attempt budget is not a usage estimate: no conforming execution +can exceed it. + +Construction copies at most `maxStages` declarations and rejects a stage +sequence that contains another value beyond that bound. `maxStages` itself may +not exceed 2048. This check is synchronous and occurs before the item source is +constructed, so an accidental infinite stage generator terminates with a +configuration error instead of hanging the factory forever. An implementation +may request the one overflow value needed to distinguish an exact-length +sequence from an over-limit sequence, but it never invokes a handler or advances +the item source during configuration validation. + +If stage enumeration exits abruptly because of overflow or another validation +error, the implementation requests synchronous iterator cleanup when the host +iterator exposes it. Cleanup is best-effort: a failing cleanup hook cannot mask +the deterministic configuration error that caused the unwind. + +This finite pull count cannot preempt one malicious synchronous iterator +`next()` call or property getter that never returns. Such caller code shares the +ordinary synchronous-host limitation and must be isolated by a process boundary +when it is not trusted. + +Implementations may reject capacities that cannot be represented safely by the +host runtime before allocating queues. They must not silently clamp an invalid +public value. + +## Admission and source backpressure + +An item is **accepted** after all of the following occur: + +1. the pipeline has acquired one global in-flight credit; +2. the source returns a non-terminal item; +3. the item receives the next monotonically increasing `itemIndex`; and +4. the pipeline takes, or attempts to take, its portable JSON snapshot. + +The producer must acquire credit **before** requesting the next source item. A +source returning end-of-stream consumes no credit. It also checks the item +budget before acquiring credit or calling the iterator. At all observable times: + +```text +accepted - emitted <= maxInFlight +``` + +Credit is released when the corresponding terminal result is returned to the +consumer, not merely when the last stage finishes. A slow consumer therefore +fills a bounded terminal/reorder buffer, exhausts the in-flight window, fills +upstream queues, and eventually stops the runtime from pulling the source. +This is end-to-end backpressure rather than a concurrency limit with an +unbounded result array behind it. + +Source-to-first-stage and stage-to-stage queue occupancy cannot exceed +`bufferCapacity`. A running handler does not count as queued. All queues and +the final reorder buffer are additionally bounded by `maxInFlight` because one +credit follows each accepted item until emission. + +A synchronous source or synchronous handler can block its event-loop thread. +The runtime cannot preempt arbitrary synchronous code; applications that need +responsive backpressure and cancellation must use cooperative asynchronous +sources and handlers or isolate blocking work. + +## Pipeline flow and absence of barriers + +Stages are ordered for one item, but different items are independent. After an +item succeeds at stage `N`, it may enter stage `N + 1` as soon as queue capacity +and a stage slot are available. It never waits for other items to finish stage +`N`. + +With stage concurrency greater than one, items may leave a stage in completion +order. The `ordering` option controls only terminal delivery to the pipeline +consumer; it does not serialize internal stage execution, downstream delivery, +or side effects. This distinction is required so a slow early item cannot +silently recreate a barrier for a fast later item. + +Stage concurrency counts active handler attempts. Waiting in a queue, waiting +for a retry delay, and waiting for terminal delivery do not count as active +handler attempts. The observed active attempts for a stage must never exceed +that stage's configured concurrency. + +An empty stage list is a valid identity pipeline. Each valid source item +succeeds with a detached output equal to its detached input and with zero +completed stages and zero attempts. An empty source succeeds without emitting +an item result. + +## Output ordering + +`ordering: "input"` is the default. Terminal results are delivered in ascending +`itemIndex`, even if later items finish first. Implementations use a bounded +reorder buffer; a missing early result can delay consumer delivery but cannot +cause unbounded admission. + +`ordering: "completion"` delivers terminal results in the order in which their +terminal outcomes commit to the pipeline coordinator. Exact order between +simultaneous completions is not a cross-language conformance promise. Every +result still retains its deterministic `itemIndex`. + +An implementation cannot switch ordering during a run. A single pipeline run +is one non-replayable iterator. Concurrent calls that attempt to advance it fail +explicitly. + +## Item result + +Every accepted item that can be delivered has one terminal result: + +```text +PipelineItemResult { + itemIndex: non-negative integer + status: "succeeded" | "failed" | "dropped" | "cancelled" + inputBound: boolean + input?: JsonValue + output?: JsonValue + completedStages: non-negative integer + totalAttempts: non-negative integer + failure?: PipelineItemFailure +} +``` + +Rules: + +- `inputBound` distinguishes an invalid input from valid JSON `null`. `input` + is present exactly when `inputBound` is true. +- `output` is present exactly for `succeeded`, including when the output is JSON + `null`. +- `failure` is absent exactly for `succeeded` and present for every other + status. +- `completedStages` counts stages whose validated output was accepted for this + item. A handler return that fails output validation does not increment it. +- `totalAttempts` is the sum of handler attempts across all stages for the + item. Queue waits and invalid input snapshots are not attempts. +- Result input, output, and failure data are detached from caller- and + handler-owned mutable containers. + +The stable item failure shape is: + +```text +PipelineItemFailure { + code: PipelineFailureCode + message: string + itemIndex: non-negative integer + stageId?: string + stageIndex?: non-negative integer + attempt: non-negative integer + retryable: boolean + causeName?: string +} +``` + +The stable codes are: + +| Code | Meaning | +|---|---| +| `INVALID_INPUT` | The accepted source value is not portable JSON. | +| `STAGE_EXECUTION_FAILED` | A handler threw, rejected, or cancelled itself without pipeline cancellation. | +| `STAGE_TIMEOUT` | The configured stage-attempt timer won. | +| `INVALID_OUTPUT` | A handler returned a value outside portable JSON. | +| `ITEM_CANCELLED` | Pipeline cancellation prevented the item from completing. | + +`message` and `causeName` are diagnostic and need not be byte-identical across +languages. Code, indices, stage identity, status, attempts, and presence rules +are conformance fields. + +An item result is terminal, so its `retryable` field is always `false`. A failed +attempt was retryable only when the runtime actually scheduled another attempt; +intermediate attempt records are not part of this API. + +## Retries and timeouts + +Each stage owns a bounded per-item attempt budget. `maxAttempts` includes the +first attempt. A failure may retry only for `STAGE_EXECUTION_FAILED` or +`STAGE_TIMEOUT`, while attempts remain and the pipeline is not cancelled. +`INVALID_INPUT`, `INVALID_OUTPUT`, and `ITEM_CANCELLED` never retry. + +The delay before attempt `k + 1`, after attempt `k` fails, is: + +```text +min(maxDelayMs, initialDelayMs * backoffMultiplier^(k - 1)) +``` + +When `maxDelayMs` is absent it equals `initialDelayMs`. The implementation must +calculate without producing a non-finite host timer and clamp only the computed +delay to the already validated configured maximum. Retry delay is cancellable +and does not occupy a stage concurrency slot. + +A timeout starts immediately before the handler is invoked, after the item has +obtained its stage concurrency slot. Queue time and retry delay do not consume +the timeout. Timeout or cancellation asks the handler to stop cooperatively. +If user code ignores that signal, the pipeline may detach and observe its late +outcome so the pipeline itself can terminate; the external side effect may +still occur. + +Retries are at-least-once attempts. A handler with external effects must be +idempotent or use an application-provided idempotency key derived from stable +item and stage identity. This standalone pipeline does not persist retry claims +and does not promise exactly-once effects. + +## Final failure policies + +The stage policy applies only after a failure cannot retry: + +- `dead-letter`: the item stops before downstream stages and emits `failed`. + Other items and source intake continue. +- `drop`: the item stops before downstream stages and emits `dropped` with its + failure. The explicit terminal result is the audit record; drop never means + silent disappearance. +- `stop`: the item emits `failed`, source intake is requested to stop, and no + later source item may be deliberately accepted. Items already accepted drain + to their own terminal outcomes. They are not discarded merely because a + sibling triggered stop. + +The accepted set at a concurrent stop boundary may include items pulled before +the final failure committed, up to `maxInFlight`. Every member of that set is +accounted. Implementations call the source iterator's close/return hook when it +exists. A source that ignores close is an application limitation and may be +detached after its eventual outcome is observed. + +An exception raised only by the source close/return hook is observed for cleanup +and diagnostics but does not replace the first run-level failure or turn an +otherwise explicit consumer close into a different terminal cause. + +An invalid accepted input produces a `failed` item and intake continues. Source +iteration failure is a run-level failure, not a fabricated item. + +## Cancellation and consumer close + +Caller cancellation has priority once observed. The runtime stops intentional +source intake, wakes queue and retry waiters, signals active handlers, and +settles every accepted non-terminal item as `cancelled`. No new handler attempt +starts after cancellation is observed. + +Closing a run early is explicit cancellation initiated by the consumer. The +runtime may be unable to deliver terminal results after the consumer refuses +further items, but it must account for accepted work in the final summary and +must not leak its own producer, worker, coordinator, timer, or listener tasks. +Late outcomes from non-cooperative user code are observed so they do not become +unhandled exceptions. + +Language-level cancellation of the consumer task remains language-level +cancellation. Cleanup APIs must be used in a `finally`/`using` or async context +manager. Merely abandoning a custom async iterator without closing it is not a +portable cleanup guarantee. + +## Run failures and summary + +A source iterator exception stops intake and is recorded as: + +```text +PipelineRunFailure { + code: "SOURCE_FAILED" | "ITEM_LIMIT_REACHED" + message: string + causeName?: string +} +``` + +Already accepted items drain unless caller cancellation or consumer close ends +the run. Source failure is not assigned an `itemIndex` because the source did +not produce an accepted item. + +Reaching `maxItems` records `ITEM_LIMIT_REACHED`, stops without one extra source +pull, and drains accepted work. Because the runtime deliberately does not peek, +it reports the limit whenever exactly `maxItems` items were accepted, even when +the caller believes the source would have ended next. Callers processing a +known finite collection should set a limit strictly above the expected length. + +After exhaustion or explicit close, completion exposes: + +```text +PipelineSummary { + status: "succeeded" | "failed" | "cancelled" + accepted: non-negative integer + emitted: non-negative integer + succeeded: non-negative integer + failed: non-negative integer + dropped: non-negative integer + cancelled: non-negative integer + maxObservedInFlight: non-negative integer + stageMaxObservedConcurrency: { [stageId]: non-negative integer } + stageMaxObservedQueueDepth: { [stageId]: non-negative integer } + runFailure?: PipelineRunFailure +} +``` + +`accepted` equals the sum of terminal status counts after normal drain or +explicit close. `emitted` can be smaller only when the consumer closes before +accepting all terminal records. Natural exhaustion has `emitted == accepted`. +`maxObservedInFlight <= maxInFlight`; observed queue depths cannot exceed +`bufferCapacity`. + +Summary status precedence is: + +1. caller cancellation or consumer close -> `cancelled`; +2. run failure or any failed/dropped item -> `failed`; +3. otherwise -> `succeeded`. + +A completion awaitable is allowed to remain pending while an open consumer has +not drained bounded results; silently collecting an unbounded output array to +make completion finish would violate this contract. Explicit close must always +initiate cleanup and make completion settle without requiring further reads. + +## Snapshot and mutation isolation + +Portable JSON uses the same finite, cycle-free, safe-integer boundary as the +ordinary runtimes. At minimum implementations detach: + +- each source value at admission; +- the input supplied to every attempt; +- every handler output before downstream release; +- result values retained for terminal delivery; and +- configuration and stage metadata before execution. + +Mutating the original source item after it is pulled, a previous handler input, +a returned handler object, the stage array, or option objects cannot alter +already admitted or future pipeline semantics. Handler functions themselves are +opaque application capabilities and are referenced, not serialized. + +## Durable and Graph IR boundary + +Calling a standalone pipeline inside a graph node treats the complete pipeline +as part of that one node attempt. The pipeline iterator is not portable JSON +and cannot be returned as a graph node output. An application may explicitly +materialize a bounded result, but a crash can replay the entire node attempt. + +Inner pipeline attempts do not consume the enclosing graph's +`maxTotalAttempts`. Applications must include their multiplicative retry cost +in budgets and must not claim item-level recovery. For a non-idempotent durable +node, a crash remains in doubt under the durable recovery rules. + +Until a later specification says otherwise: + +- `edge.mode: "stream"` remains declarative and is not lowered by `runGraph`; +- graph fan-in still waits for one terminal result per upstream node; +- durable `EdgeEmitted` remains a single value-edge marker; +- no queue contents, item offsets, or item acknowledgements are persisted; and +- stream joins, windows, materializing barriers, replay, and fork are outside + this standalone API. + +Documentation and status tables must preserve this boundary rather than imply +that standalone in-memory flow is crash-safe stream execution. + +## Minimum conformance obligations + +Both native runtimes must test: + +- a fast later item entering a downstream stage before a slow earlier item + finishes, proving the absence of a whole-stage barrier; +- source pull-ahead and every queue high-water staying within configured bounds; +- input-order delivery with internally completion-ordered flow; +- completion-order delivery without loss or duplicate indices; +- exact per-stage concurrency maxima; +- bounded retry counts, deterministic delays, timeout, and cancellation; +- `stop`, `drop`, and `dead-letter` with every accepted item accounted; +- invalid input versus valid JSON `null`, and invalid stage output; +- source failure after a prefix of accepted items; +- hard item-limit termination without an extra source pull, plus rejection of + an unsafe derived attempt bound; +- pre-cancellation, cancellation while queued/running/in retry delay, and + explicit early close; +- mutation isolation for source values, outputs, stages, and handler maps; +- empty source, empty stages, invalid numeric configuration, and duplicate IDs; +- a large run demonstrating fixed queue/in-flight high-water; and +- no runtime-owned task, timer, or listener leak after normal completion or + close. + +Timing-only sleeps are insufficient for the no-barrier and backpressure claims. +Tests use gates, probes, or deterministic coordination so overloaded CI hosts do +not turn semantic checks into flaky benchmarks. diff --git a/tools/conformance/python_pipeline_report.py b/tools/conformance/python_pipeline_report.py new file mode 100644 index 0000000..f1e38d6 --- /dev/null +++ b/tools/conformance/python_pipeline_report.py @@ -0,0 +1,217 @@ +"""Emit deterministic bounded-pipeline projections for cross-language comparison.""" + +from __future__ import annotations + +import asyncio +import json +from collections.abc import Iterator +from pathlib import Path +from typing import Any + +from graph_engineering import ( + PipelineRetryOptions, + PipelineStage, + run_pipeline, +) + +ROOT = Path(__file__).resolve().parents[2] +FIXTURE = json.loads( + (ROOT / "spec" / "conformance" / "pipeline.case.json").read_text(encoding="utf-8") +) + + +class _Source: + def __init__(self, document: dict[str, Any]) -> None: + self.document = document + self.items = list(document.get("items", [])) + self.index = 0 + self.pulls = 0 + self.closes = 0 + self.pull_changed = asyncio.Event() + + def __iter__(self) -> Iterator[object]: + return self + + def __next__(self) -> object: + kind = self.document["kind"] + if kind == "throwing-array" and self.index >= len(self.items): + failure = self.document["thenThrow"] + error_type = type(failure["causeName"], (RuntimeError,), {}) + raise error_type(failure["message"]) + if self.index >= len(self.items): + raise StopIteration + value = self.items[self.index] + self.index += 1 + self.pulls += 1 + self.pull_changed.set() + return value + + async def wait_for_pull_count(self, target: int) -> None: + while self.pulls < target: + self.pull_changed.clear() + if self.pulls >= target: + return + await self.pull_changed.wait() + + def close(self) -> None: + self.closes += 1 + + +def _compact_item(item: Any, expected: dict[str, Any] | None) -> dict[str, Any]: + document = dict(item.to_dict()) + if "failure" in document: + failure = dict(document["failure"]) + failure.pop("message", None) + expected_failure = None if expected is None else expected.get("failure") + if not isinstance(expected_failure, dict) or "causeName" not in expected_failure: + # causeName is diagnostic rather than byte-identical conformance + # data. Preserve it only where the fixture explicitly contracts its + # presence and value. + failure.pop("causeName", None) + document["failure"] = failure + return document + + +def _compact_summary(summary: Any, expected: dict[str, Any] | None) -> dict[str, Any]: + document = dict(summary.to_dict()) + document.pop("maxObservedInFlight", None) + document.pop("stageMaxObservedConcurrency", None) + document.pop("stageMaxObservedQueueDepth", None) + failure = document.get("runFailure") + if failure is None: + document["runFailure"] = None + else: + compact = dict(failure) + compact.pop("message", None) + expected_failure = None if expected is None else expected.get("runFailure") + if not isinstance(expected_failure, dict) or "causeName" not in expected_failure: + compact.pop("causeName", None) + document["runFailure"] = compact + return document + + +async def _run_case(case: dict[str, Any]) -> dict[str, Any]: + source_document = case["source"] + source = _Source(source_document) + gates = {name: asyncio.Event() for name in case.get("gates", [])} + trace: list[str] = [] + + async def action( + document: dict[str, Any], + *, + stage_id: str, + item_index: int, + ) -> object: + kind = document["kind"] + if kind == "wait-for-gate": + await gates[document["gate"]].wait() + return await action(document["then"], stage_id=stage_id, item_index=item_index) + if kind == "throw": + error_type = type(document["causeName"], (RuntimeError,), {}) + raise error_type(document["message"]) + if kind == "invalid-output": + return float("inf") + if kind == "return": + trace.append(f"stage:{stage_id}:item:{item_index}:return") + return document["value"] + raise AssertionError(f"unknown pipeline fixture action {kind!r}") + + stages: list[PipelineStage] = [] + for stage_index, stage_document in enumerate(case["stages"]): + async def handler(context: Any, document: dict[str, Any] = stage_document) -> object: + trace.append(f"stage:{document['id']}:item:{context.item_index}:start") + for effect in document.get("onStartByItem", {}).get(str(context.item_index), []): + gates[effect["releaseGate"]].set() + outcomes = document.get("outcomesByItem", {}).get(str(context.item_index)) + if not outcomes: + return context.input + selected = outcomes[min(context.attempt - 1, len(outcomes) - 1)] + return await action( + selected, + stage_id=document["id"], + item_index=context.item_index, + ) + + retry_document = stage_document.get("retry") + retry = None + if retry_document is not None: + retry = PipelineRetryOptions( + max_attempts=retry_document.get("maxAttempts", 1), + initial_delay_ms=retry_document.get("initialDelayMs", 0), + backoff_multiplier=retry_document.get("backoffMultiplier", 1), + max_delay_ms=retry_document.get("maxDelayMs"), + ) + stages.append( + PipelineStage( + id=stage_document["id"], + handler=handler, + concurrency=stage_document.get("concurrency", 1), + retry=retry, + on_failure=stage_document.get("onFailure", "dead-letter"), + ) + ) + + options = case["options"] + run = run_pipeline( + source, + stages, + buffer_capacity=options["bufferCapacity"], + max_in_flight=options["maxInFlight"], + max_items=options["maxItems"], + max_stages=options.get("maxStages", FIXTURE["defaults"]["maxStages"]), + ordering=options["ordering"], + ) + results: list[Any] = [] + pull_count_while_paused: int | None = None + consumer = case.get("consumer") + if consumer is not None: + for _ in range(consumer["readResults"]): + results.append(await anext(run)) + if consumer.get("thenPauseUntil") != "pipeline-idle" or stages: + raise AssertionError( + "the deterministic pause probe currently requires an identity pipeline" + ) + pause_target = min( + len(source.items), consumer["readResults"] + options["maxInFlight"] + ) + await source.wait_for_pull_count(pause_target) + pull_count_while_paused = source.pulls + results.extend([item async for item in run]) + summary = await run.completion() + observations: dict[str, int] = { + "maxObservedInFlight": summary.max_observed_in_flight, + "maxObservedQueueDepth": max( + summary.stage_max_observed_queue_depth.values(), default=0 + ), + } + if pull_count_while_paused is not None: + observations["sourcePullCountWhilePaused"] = pull_count_while_paused + expectation = case.get("expect", {}) + expected_items = { + item["itemIndex"]: item for item in expectation.get("items", []) + } + + report: dict[str, Any] = { + "deliveryOrder": [item.item_index for item in results], + "items": [ + _compact_item(item, expected_items.get(item.item_index)) for item in results + ], + "summary": _compact_summary(summary, expectation.get("summary")), + "sourcePullCount": source.pulls, + "sourceCloseCount": source.closes, + "observations": observations, + } + if case.get("expect", {}).get("mustOccurBefore"): + report["requiredTraceRelations"] = [ + trace.index(before) < trace.index(after) + for before, after in case["expect"]["mustOccurBefore"] + ] + return report + + +async def _main() -> None: + report = {case["id"]: await _run_case(case) for case in FIXTURE["cases"]} + print(json.dumps(report, sort_keys=True, separators=(",", ":"))) + + +asyncio.run(_main()) diff --git a/tools/conformance/run.mjs b/tools/conformance/run.mjs index 701c50d..a8f8052 100644 --- a/tools/conformance/run.mjs +++ b/tools/conformance/run.mjs @@ -703,3 +703,295 @@ for (const testCase of durableInteropCases) { process.stdout.write( `Cross-language terminal durable-history interop passed for ${durableInteropCases.length} cases in both directions.\n`, ); + +class PipelineFixtureSource { + constructor(document) { + this.document = document; + this.items = [...(document.items ?? [])]; + this.index = 0; + this.pulls = 0; + this.closes = 0; + this.pullWaiters = []; + } + + [Symbol.iterator]() { + return this; + } + + next() { + if (this.document.kind === "throwing-array" && this.index >= this.items.length) { + const failure = this.document.thenThrow; + const error = new Error(failure.message); + error.name = failure.causeName; + throw error; + } + if (this.index >= this.items.length) return { done: true, value: undefined }; + const value = this.items[this.index]; + this.index += 1; + this.pulls += 1; + for (const waiter of this.pullWaiters.splice(0)) waiter(); + return { done: false, value }; + } + + async waitForPullCount(target) { + while (this.pulls < target) { + await new Promise((resolve) => this.pullWaiters.push(resolve)); + } + } + + return() { + this.closes += 1; + return { done: true, value: undefined }; + } +} + +function pipelineFixtureGate() { + let open; + const promise = new Promise((resolve) => { + open = resolve; + }); + return { promise, open }; +} + +async function executePipelineFixtureAction(document, gates, trace, stageId, itemIndex) { + switch (document.kind) { + case "wait-for-gate": + await gates[document.gate].promise; + return await executePipelineFixtureAction(document.then, gates, trace, stageId, itemIndex); + case "throw": { + const error = new Error(document.message); + error.name = document.causeName; + throw error; + } + case "invalid-output": + return Number.POSITIVE_INFINITY; + case "return": + trace.push(`stage:${stageId}:item:${itemIndex}:return`); + return document.value; + default: + throw new Error(`unknown pipeline fixture action '${document.kind}'`); + } +} + +function compactPipelineItem(item) { + const document = { ...item }; + if (document.failure !== undefined) { + document.failure = { ...document.failure }; + delete document.failure.message; + } + return document; +} + +function compactPipelineSummary(summary) { + const document = { ...summary, runFailure: summary.runFailure ?? null }; + delete document.maxObservedInFlight; + delete document.stageMaxObservedConcurrency; + delete document.stageMaxObservedQueueDepth; + if (document.runFailure !== null) { + document.runFailure = { ...document.runFailure }; + delete document.runFailure.message; + } + return document; +} + +async function exercisePipelineFixture(testCase) { + const source = new PipelineFixtureSource(testCase.source); + const gates = Object.fromEntries( + (testCase.gates ?? []).map((name) => [name, pipelineFixtureGate()]), + ); + const trace = []; + const stages = testCase.stages.map((stage) => ({ + id: stage.id, + concurrency: stage.concurrency ?? 1, + onFailure: stage.onFailure ?? "dead-letter", + ...(stage.retry === undefined ? {} : { retry: stage.retry }), + handler: async (context) => { + trace.push(`stage:${stage.id}:item:${context.itemIndex}:start`); + for (const effect of stage.onStartByItem?.[String(context.itemIndex)] ?? []) { + gates[effect.releaseGate].open(); + } + const outcomes = stage.outcomesByItem?.[String(context.itemIndex)]; + if (outcomes === undefined || outcomes.length === 0) return context.input; + const selected = outcomes[Math.min(context.attempt - 1, outcomes.length - 1)]; + return await executePipelineFixtureAction( + selected, + gates, + trace, + stage.id, + context.itemIndex, + ); + }, + })); + const run = runtime.runPipeline(source, stages, testCase.options); + const results = []; + let pullCountWhilePaused; + if (testCase.consumer !== undefined) { + for (let index = 0; index < testCase.consumer.readResults; index += 1) { + const delivered = await run.next(); + assert.equal(delivered.done, false, `${testCase.id}: pipeline ended before consumer read`); + results.push(delivered.value); + } + assert.equal( + testCase.consumer.thenPauseUntil, + "pipeline-idle", + `${testCase.id}: unsupported deterministic pause condition`, + ); + assert.equal(stages.length, 0, `${testCase.id}: pause probe requires an identity pipeline`); + const pauseTarget = Math.min( + source.items.length, + testCase.consumer.readResults + testCase.options.maxInFlight, + ); + await source.waitForPullCount(pauseTarget); + pullCountWhilePaused = source.pulls; + } + for await (const result of run) results.push(result); + const summary = await run.completion; + const report = { + deliveryOrder: results.map(({ itemIndex }) => itemIndex), + items: results.map(compactPipelineItem), + summary: compactPipelineSummary(summary), + sourcePullCount: source.pulls, + sourceCloseCount: source.closes, + observations: { + maxObservedInFlight: summary.maxObservedInFlight, + maxObservedQueueDepth: Math.max( + 0, + ...Object.values(summary.stageMaxObservedQueueDepth), + ), + ...(pullCountWhilePaused === undefined + ? {} + : { sourcePullCountWhilePaused: pullCountWhilePaused }), + }, + }; + if (testCase.expect.mustOccurBefore !== undefined) { + report.requiredTraceRelations = testCase.expect.mustOccurBefore.map( + ([before, after]) => trace.indexOf(before) >= 0 && trace.indexOf(before) < trace.indexOf(after), + ); + } + return JSON.parse(JSON.stringify(report)); +} + +function assertPipelineSubset(actual, expectedValue, label) { + if (expectedValue === null || typeof expectedValue !== "object") { + assert.deepEqual(actual, expectedValue, label); + return; + } + if (Array.isArray(expectedValue)) { + assert.ok(Array.isArray(actual), `${label}: actual value is not an array`); + assert.equal(actual.length, expectedValue.length, `${label}: array length differs`); + expectedValue.forEach((value, index) => { + assertPipelineSubset(actual[index], value, `${label}[${index}]`); + }); + return; + } + assert.ok(actual !== null && typeof actual === "object", `${label}: actual value is not an object`); + for (const [key, value] of Object.entries(expectedValue)) { + assert.ok(Object.hasOwn(actual, key), `${label}: missing key '${key}'`); + assertPipelineSubset(actual[key], value, `${label}.${key}`); + } +} + +function pipelineSemanticProjection(report, testCase) { + const projected = JSON.parse(JSON.stringify(report)); + delete projected.observations; + const expectedItems = new Map( + (testCase.expect.items ?? []).map((item) => [item.itemIndex, item]), + ); + for (const item of projected.items) { + const expectedItem = expectedItems.get(item.itemIndex); + if (item.failure !== undefined && expectedItem?.failure?.causeName === undefined) { + delete item.failure.causeName; + } + } + if ( + projected.summary.runFailure !== null && + testCase.expect.summary?.runFailure?.causeName === undefined + ) { + delete projected.summary.runFailure.causeName; + } + return projected; +} + +function assertPipelineExpectations(report, testCase, language) { + const expectation = testCase.expect; + const prefix = `${testCase.id}: ${language}`; + if (expectation.deliveryOrder !== undefined) { + assert.deepEqual(report.deliveryOrder, expectation.deliveryOrder, `${prefix} delivery order`); + } + if (expectation.items !== undefined) { + assertPipelineSubset(report.items, expectation.items, `${prefix} items`); + } + if (expectation.summary !== undefined) { + assertPipelineSubset(report.summary, expectation.summary, `${prefix} summary`); + } + if (expectation.sourcePullCount !== undefined) { + assert.equal(report.sourcePullCount, expectation.sourcePullCount, `${prefix} source pulls`); + } + if (expectation.sourceCloseCount !== undefined) { + assert.equal(report.sourceCloseCount, expectation.sourceCloseCount, `${prefix} source closes`); + } + if (expectation.outputByItem !== undefined) { + const outputs = Object.fromEntries(report.items.map((item) => [item.itemIndex, item.output])); + assert.deepEqual(outputs, expectation.outputByItem, `${prefix} outputs by item`); + } + if (expectation.deliveryItemSet !== undefined) { + assert.deepEqual( + [...report.deliveryOrder].sort((left, right) => left - right), + [...expectation.deliveryItemSet].sort((left, right) => left - right), + `${prefix} delivery item set`, + ); + } + for (const relation of report.requiredTraceRelations ?? []) { + assert.equal(relation, true, `${prefix} required trace relation`); + } + if (expectation.maxObservedInFlightAtMost !== undefined) { + assert.ok( + report.observations.maxObservedInFlight <= expectation.maxObservedInFlightAtMost, + `${prefix} exceeded maxObservedInFlight bound`, + ); + } + if (expectation.maxObservedQueueDepthAtMost !== undefined) { + assert.ok( + report.observations.maxObservedQueueDepth <= expectation.maxObservedQueueDepthAtMost, + `${prefix} exceeded maxObservedQueueDepth bound`, + ); + } + if (expectation.sourcePullCountWhilePausedAtMost !== undefined) { + assert.ok( + report.observations.sourcePullCountWhilePaused <= + expectation.sourcePullCountWhilePausedAtMost, + `${prefix} source was not backpressured while consumer paused`, + ); + } +} + +const pipelineFixture = JSON.parse( + await readFile(join(fixtureRoot, "pipeline.case.json"), "utf8"), +); +const pythonPipeline = spawnSync( + "uv", + ["run", "--project", "python", "python", "tools/conformance/python_pipeline_report.py"], + { cwd: root, encoding: "utf8" }, +); +if (pythonPipeline.status !== 0) { + throw new Error( + `Python bounded-pipeline conformance failed:\n${pythonPipeline.stderr || pythonPipeline.stdout}`, + ); +} +const pyPipelineReport = JSON.parse(pythonPipeline.stdout); +for (const testCase of pipelineFixture.cases) { + const tsPipelineReport = await exercisePipelineFixture(testCase); + const pythonCaseReport = pyPipelineReport[testCase.id]; + assert.ok(pythonCaseReport !== undefined, `${testCase.id}: Python report is missing`); + assertPipelineExpectations(tsPipelineReport, testCase, "TypeScript"); + assertPipelineExpectations(pythonCaseReport, testCase, "Python"); + assert.deepEqual( + pipelineSemanticProjection(tsPipelineReport, testCase), + pipelineSemanticProjection(pythonCaseReport, testCase), + `${testCase.id}: bounded-pipeline semantic reports differ`, + ); +} + +process.stdout.write( + `Cross-language bounded-pipeline conformance passed for ${pipelineFixture.cases.length} cases.\n`, +); diff --git a/tools/progress-scanner/README.md b/tools/progress-scanner/README.md index 9b9573e..e283e9d 100644 --- a/tools/progress-scanner/README.md +++ b/tools/progress-scanner/README.md @@ -38,6 +38,42 @@ can be overridden with: } ``` +New work can require evidence before a terminal status is trusted: + +```json +{ + "evidence_policy": { + "required_for_assigned_at_or_after": "2026-07-26T16:20:00Z" + }, + "tasks": [{ + "id": "example", + "assigned_at": "2026-07-26T16:30:00Z", + "expected_tests": ["unit suite"], + "test_evidence": [{ + "requirement": "unit suite", + "result": "passed", + "recorded_at": "2026-07-26T17:00:00Z", + "reference": "command: pytest tests/unit" + }], + "completion_evidence": ["review: PR #42"] + }] +} +``` + +Each required expected-test string needs a matching passing record, and at least +one completion reference is required. A failed record is an integration risk +even before the task is marked complete. The timestamp cutoff is an explicit +legacy migration boundary; a task can override it with +`"evidence_required": true` or `false`. References are auditable descriptions, +not commands the scanner executes. + +Evidence records are append-only: for each exact requirement, the newest +`recorded_at` value wins (array order breaks equal-timestamp ties). This permits +a later passing rerun to supersede a recorded failure without deleting history, +while a later failure reopens the gate. Evidence cannot predate task assignment +or claim a future timestamp, and an evidence-required task must declare at least +one expected test. + Classification precedence is blocked, integration risk, waiting dependency, quiet window, completion, staleness, then healthy. A dependency is satisfied only when its registry status is one of `complete`, `completed`, `done`, `merged`, or @@ -45,6 +81,11 @@ when its registry status is one of `complete`, `completed`, `done`, `merged`, or risk. A registered `quiet_until` suppresses age warnings while long-running work is expected. +For evidence-required completed work, missing passing records or a missing +completion reference is also an integration risk. Scan snapshots preserve the +expected, passing, failed, and missing requirements so a healthy liveness result +cannot be mistaken for release acceptance. + Snapshots are written to `codex_logs/scans/.json` and atomically mirrored to `codex_logs/scans/latest.json`. Nudge and acknowledgement events are append-only records in `codex_logs/nudges/queue.jsonl`. The repository-level diff --git a/tools/progress-scanner/graph_progress.py b/tools/progress-scanner/graph_progress.py index 4e1f8a2..07c98b2 100644 --- a/tools/progress-scanner/graph_progress.py +++ b/tools/progress-scanner/graph_progress.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 """Local, zero-dependency progress scanner for Graph Engineering. The scanner deliberately does not contact agents or mutate the task registry. It @@ -21,10 +20,10 @@ import subprocess import sys import tempfile +from collections.abc import Iterable, Iterator, Mapping, Sequence from dataclasses import dataclass from pathlib import Path -from typing import Any, Iterable, Iterator, Mapping, Sequence - +from typing import Any SCHEMA_VERSION = 1 DEFAULT_WARNING_MINUTES = 60 @@ -52,7 +51,7 @@ class ScanPolicy: escalate_same_blocker_after_scans: int = DEFAULT_ESCALATE_SCANS @classmethod - def from_registry(cls, registry: Mapping[str, Any]) -> "ScanPolicy": + def from_registry(cls, registry: Mapping[str, Any]) -> ScanPolicy: value = registry.get("scan_policy", {}) if not isinstance(value, Mapping): raise ScannerError("task-registry.json: scan_policy must be an object") @@ -60,30 +59,61 @@ def from_registry(cls, registry: Mapping[str, Any]) -> "ScanPolicy": def positive_int(key: str, default: int) -> int: raw = value.get(key, default) if isinstance(raw, bool) or not isinstance(raw, int) or raw <= 0: - raise ScannerError(f"task-registry.json: scan_policy.{key} must be a positive integer") + raise ScannerError( + f"task-registry.json: scan_policy.{key} must be a positive integer" + ) return raw policy = cls( - warning_after_minutes=positive_int("warning_after_minutes", DEFAULT_WARNING_MINUTES), - stale_after_minutes=positive_int("stale_after_minutes", DEFAULT_STALE_MINUTES), - nudge_cooldown_minutes=positive_int("nudge_cooldown_minutes", DEFAULT_COOLDOWN_MINUTES), + warning_after_minutes=positive_int( + "warning_after_minutes", DEFAULT_WARNING_MINUTES + ), + stale_after_minutes=positive_int( + "stale_after_minutes", DEFAULT_STALE_MINUTES + ), + nudge_cooldown_minutes=positive_int( + "nudge_cooldown_minutes", DEFAULT_COOLDOWN_MINUTES + ), escalate_same_blocker_after_scans=positive_int( "escalate_same_blocker_after_scans", DEFAULT_ESCALATE_SCANS ), ) if policy.warning_after_minutes > policy.stale_after_minutes: raise ScannerError( - "task-registry.json: warning_after_minutes cannot exceed stale_after_minutes" + "task-registry.json: warning_after_minutes cannot exceed " + "stale_after_minutes" ) return policy +@dataclass(frozen=True) +class EvidencePolicy: + """Opt-in completion evidence policy with a migration cutoff for legacy tasks.""" + + required_for_assigned_at_or_after: dt.datetime | None = None + + @classmethod + def from_registry(cls, registry: Mapping[str, Any]) -> EvidencePolicy: + value = registry.get("evidence_policy", {}) + if not isinstance(value, Mapping): + raise ScannerError("task-registry.json: evidence_policy must be an object") + threshold = parse_time( + value.get("required_for_assigned_at_or_after"), + field="task-registry.json: evidence_policy.required_for_assigned_at_or_after", + ) + return cls(required_for_assigned_at_or_after=threshold) + + def utc_now() -> dt.datetime: return dt.datetime.now(dt.timezone.utc) def format_time(value: dt.datetime) -> str: - return value.astimezone(dt.timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z") + return ( + value.astimezone(dt.timezone.utc) + .isoformat(timespec="seconds") + .replace("+00:00", "Z") + ) def parse_time(value: Any, *, field: str) -> dt.datetime | None: @@ -97,7 +127,9 @@ def parse_time(value: Any, *, field: str) -> dt.datetime | None: try: parsed = dt.datetime.fromisoformat(candidate) except ValueError as exc: - raise ScannerError(f"{field} is not a valid ISO-8601 timestamp: {value!r}") from exc + raise ScannerError( + f"{field} is not a valid ISO-8601 timestamp: {value!r}" + ) from exc if parsed.tzinfo is None: parsed = parsed.replace(tzinfo=dt.timezone.utc) return parsed.astimezone(dt.timezone.utc) @@ -112,12 +144,16 @@ def json_load(path: Path, *, required: bool = True) -> Any: raise ScannerError(f"required file does not exist: {path}") from None return None except json.JSONDecodeError as exc: - raise ScannerError(f"invalid JSON in {path}: line {exc.lineno}, column {exc.colno}") from exc + raise ScannerError( + f"invalid JSON in {path}: line {exc.lineno}, column {exc.colno}" + ) from exc def atomic_write_json(path: Path, value: Any) -> None: path.parent.mkdir(parents=True, exist_ok=True) - fd, temporary = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=path.parent) + fd, temporary = tempfile.mkstemp( + prefix=f".{path.name}.", suffix=".tmp", dir=path.parent + ) try: with os.fdopen(fd, "w", encoding="utf-8") as handle: json.dump(value, handle, indent=2, ensure_ascii=False, sort_keys=True) @@ -138,7 +174,10 @@ def atomic_write_json(path: Path, value: Any) -> None: def append_jsonl(path: Path, value: Mapping[str, Any]) -> None: path.parent.mkdir(parents=True, exist_ok=True) - payload = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n" + payload = ( + json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + + "\n" + ) with path.open("a", encoding="utf-8") as handle: handle.write(payload) handle.flush() @@ -156,9 +195,13 @@ def read_jsonl(path: Path) -> list[dict[str, Any]]: try: value = json.loads(line) except json.JSONDecodeError as exc: - raise ScannerError(f"invalid JSONL in {path} at line {line_number}") from exc + raise ScannerError( + f"invalid JSONL in {path} at line {line_number}" + ) from exc if not isinstance(value, dict): - raise ScannerError(f"invalid JSONL in {path} at line {line_number}: expected object") + raise ScannerError( + f"invalid JSONL in {path} at line {line_number}: expected object" + ) events.append(value) return events @@ -174,7 +217,9 @@ def repository_lock(log_dir: Path) -> Iterator[None]: fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) except OSError as exc: if exc.errno in {errno.EACCES, errno.EAGAIN}: - raise ScannerBusy(f"another graph-progress process owns {lock_path}") from None + raise ScannerBusy( + f"another graph-progress process owns {lock_path}" + ) from None raise try: handle.seek(0) @@ -213,13 +258,20 @@ def validated_tasks(registry: Mapping[str, Any]) -> list[dict[str, Any]]: raise ScannerError(f"task-registry.json: tasks[{index}] must be an object") task_id = raw.get("id") if not isinstance(task_id, str) or not task_id.strip(): - raise ScannerError(f"task-registry.json: tasks[{index}].id must be a non-empty string") + raise ScannerError( + f"task-registry.json: tasks[{index}].id must be a non-empty string" + ) if task_id in identifiers: raise ScannerError(f"task-registry.json: duplicate task id {task_id!r}") identifiers.add(task_id) dependencies = raw.get("depends_on", raw.get("dependencies", [])) - if not isinstance(dependencies, list) or not all(isinstance(item, str) for item in dependencies): - raise ScannerError(f"task-registry.json: task {task_id!r} depends_on must be a string array") + if not isinstance(dependencies, list) or not all( + isinstance(item, str) for item in dependencies + ): + raise ScannerError( + f"task-registry.json: task {task_id!r} depends_on must be a " + "string array" + ) normalized = dict(raw) normalized["id"] = task_id normalized["depends_on"] = dependencies @@ -249,7 +301,13 @@ def safe_artifact_evidence(repo: Path, raw_paths: Any) -> dict[str, Any]: elif isinstance(raw_paths, list): paths = raw_paths else: - return {"present": [], "missing": [], "unsafe": [], "invalid": True, "latest_mtime": None} + return { + "present": [], + "missing": [], + "unsafe": [], + "invalid": True, + "latest_mtime": None, + } present: list[str] = [] missing: list[str] = [] @@ -259,7 +317,11 @@ def safe_artifact_evidence(repo: Path, raw_paths: Any) -> dict[str, Any]: if not isinstance(raw, str) or not raw.strip(): unsafe.append(repr(raw)) continue - candidate = (repo / raw).resolve() if not Path(raw).is_absolute() else Path(raw).resolve() + candidate = ( + (repo / raw).resolve() + if not Path(raw).is_absolute() + else Path(raw).resolve() + ) try: candidate.relative_to(repo) except ValueError: @@ -268,7 +330,11 @@ def safe_artifact_evidence(repo: Path, raw_paths: Any) -> dict[str, Any]: if candidate.exists(): present.append(raw) with contextlib.suppress(OSError): - mtimes.append(dt.datetime.fromtimestamp(candidate.stat().st_mtime, tz=dt.timezone.utc)) + mtimes.append( + dt.datetime.fromtimestamp( + candidate.stat().st_mtime, tz=dt.timezone.utc + ) + ) else: missing.append(raw) return { @@ -280,11 +346,161 @@ def safe_artifact_evidence(repo: Path, raw_paths: Any) -> dict[str, Any]: } +def task_completion_evidence( + task: Mapping[str, Any], policy: EvidencePolicy, *, now: dt.datetime +) -> dict[str, Any]: + explicit_required = task.get("evidence_required") + if explicit_required is not None and not isinstance(explicit_required, bool): + raise ScannerError(f"task {task['id']}.evidence_required must be a boolean") + assigned_at = parse_time( + task.get("assigned_at"), field=f"task {task['id']}.assigned_at" + ) + required = bool(explicit_required) + if ( + explicit_required is None + and policy.required_for_assigned_at_or_after is not None + ): + required = bool( + assigned_at and assigned_at >= policy.required_for_assigned_at_or_after + ) + + expected = task.get("expected_tests", []) + if not isinstance(expected, list) or not all( + isinstance(item, str) and item.strip() for item in expected + ): + raise ScannerError( + f"task {task['id']}.expected_tests must be a non-empty string array" + ) + expected_requirements = [item.strip() for item in expected] + if required and not expected_requirements: + raise ScannerError( + f"task {task['id']}.expected_tests must not be empty when " + "evidence is required" + ) + if len(set(expected_requirements)) != len(expected_requirements): + raise ScannerError(f"task {task['id']}.expected_tests contains duplicates") + + raw_records = task.get("test_evidence", []) + if not isinstance(raw_records, list): + raise ScannerError(f"task {task['id']}.test_evidence must be an array") + records: list[dict[str, Any]] = [] + latest_results: dict[str, tuple[dt.datetime, int, str]] = {} + for index, raw in enumerate(raw_records): + if not isinstance(raw, Mapping): + raise ScannerError( + f"task {task['id']}.test_evidence[{index}] must be an object" + ) + requirement = raw.get("requirement") + result = raw.get("result") + reference = raw.get("reference") + if not isinstance(requirement, str) or not requirement.strip(): + raise ScannerError( + f"task {task['id']}.test_evidence[{index}].requirement must be " + "a non-empty string" + ) + normalized_requirement = requirement.strip() + if normalized_requirement not in expected_requirements: + raise ScannerError( + f"task {task['id']}.test_evidence[{index}] names an unknown requirement" + ) + if not isinstance(result, str) or result.casefold() not in { + "passed", + "failed", + "skipped", + }: + raise ScannerError( + f"task {task['id']}.test_evidence[{index}].result must be " + "passed, failed, or skipped" + ) + if not isinstance(reference, str) or not reference.strip(): + raise ScannerError( + f"task {task['id']}.test_evidence[{index}].reference must be " + "a non-empty string" + ) + recorded_at = parse_time( + raw.get("recorded_at"), + field=f"task {task['id']}.test_evidence[{index}].recorded_at", + ) + if recorded_at is None: + raise ScannerError( + f"task {task['id']}.test_evidence[{index}].recorded_at is required" + ) + if assigned_at is not None and recorded_at < assigned_at: + raise ScannerError( + f"task {task['id']}.test_evidence[{index}].recorded_at " + "predates assigned_at" + ) + if recorded_at > now: + raise ScannerError( + f"task {task['id']}.test_evidence[{index}].recorded_at is in the future" + ) + normalized_result = result.casefold() + previous = latest_results.get(normalized_requirement) + if previous is None or (recorded_at, index) >= (previous[0], previous[1]): + latest_results[normalized_requirement] = ( + recorded_at, + index, + normalized_result, + ) + records.append( + { + "requirement": normalized_requirement, + "result": normalized_result, + "reference": reference.strip(), + "recorded_at": format_time(recorded_at), + } + ) + + passed = { + requirement + for requirement, (_, _, result) in latest_results.items() + if result == "passed" + } + failed = { + requirement + for requirement, (_, _, result) in latest_results.items() + if result == "failed" + } + + raw_completion = task.get("completion_evidence", []) + if not isinstance(raw_completion, list) or not all( + isinstance(item, str) and item.strip() for item in raw_completion + ): + raise ScannerError( + f"task {task['id']}.completion_evidence must be a string array" + ) + completion = [item.strip() for item in raw_completion] + missing = [ + requirement + for requirement in expected_requirements + if requirement not in passed + ] + satisfied = not required or (not missing and bool(completion) and not failed) + return { + "required": required, + "satisfied": satisfied, + "expected_requirements": expected_requirements, + "passing_requirements": sorted(passed), + "failed_requirements": sorted(failed), + "missing_requirements": missing, + "test_records": records, + "completion_references": completion, + } + + def latest_task_activity( - task: Mapping[str, Any], artifact_evidence: Mapping[str, Any], log_activity: dt.datetime | None + task: Mapping[str, Any], + artifact_evidence: Mapping[str, Any], + log_activity: dt.datetime | None, ) -> tuple[dt.datetime | None, str | None]: candidates: list[tuple[dt.datetime, str]] = [] - for key in ("last_heartbeat", "last_progress_at", "updated_at", "started_at", "assigned_at"): + for key in ( + "last_heartbeat", + "last_progress_at", + "updated_at", + "started_at", + "assigned_at", + ): timestamp = parse_time(task.get(key), field=f"task {task['id']}.{key}") if timestamp: candidates.append((timestamp, key)) @@ -295,7 +511,11 @@ def latest_task_activity( candidates.append((artifact_time, "artifact_mtime")) if log_activity: candidates.append((log_activity, "agent_log")) - return max(candidates, default=(None, None), key=lambda item: item[0] or dt.datetime.min.replace(tzinfo=dt.timezone.utc)) + return max( + candidates, + default=(None, None), + key=lambda item: item[0] or dt.datetime.min.replace(tzinfo=dt.timezone.utc), + ) def blocker_text(task: Mapping[str, Any]) -> str | None: @@ -306,7 +526,11 @@ def blocker_text(task: Mapping[str, Any]) -> str | None: return raw.strip() or None if isinstance(raw, Mapping): summary = raw.get("summary") or raw.get("reason") or raw.get("message") - return str(summary) if summary else json.dumps(raw, sort_keys=True, ensure_ascii=False) + return ( + str(summary) + if summary + else json.dumps(raw, sort_keys=True, ensure_ascii=False) + ) return str(raw) @@ -322,6 +546,7 @@ def classify_task( *, all_tasks: Mapping[str, Mapping[str, Any]], evidence: Mapping[str, Any], + completion_evidence: Mapping[str, Any], log_activity: dt.datetime | None, previous: Mapping[str, Any] | None, policy: ScanPolicy, @@ -337,17 +562,27 @@ def classify_task( item for item in dependencies if item in all_tasks - and str(all_tasks[item].get("status", "")).strip().casefold() not in TERMINAL_SUCCESS + and str(all_tasks[item].get("status", "")).strip().casefold() + not in TERMINAL_SUCCESS ] artifact_evidence = dict(evidence) - last_activity, activity_source = latest_task_activity(task, artifact_evidence, log_activity) + last_activity, activity_source = latest_task_activity( + task, artifact_evidence, log_activity + ) age_minutes = None if last_activity: age_minutes = max(0.0, (now - last_activity).total_seconds() / 60) - quiet_until = parse_time(task.get("quiet_until"), field=f"task {task_id}.quiet_until") + quiet_until = parse_time( + task.get("quiet_until"), field=f"task {task_id}.quiet_until" + ) in_quiet_window = bool(quiet_until and quiet_until > now) - failed_test = str(task.get("test_result", "")).casefold() in {"failed", "failure", "error"} + failed_test = str(task.get("test_result", "")).casefold() in { + "failed", + "failure", + "error", + } + failed_evidence = bool(completion_evidence["failed_requirements"]) if blocker or raw_status == "blocked": classification = "blocked" @@ -355,12 +590,25 @@ def classify_task( elif unknown_dependencies: classification = "integration-risk" reason = "unknown dependencies: " + ", ".join(unknown_dependencies) - elif raw_status in TERMINAL_FAILURE or failed_test: + elif raw_status in TERMINAL_FAILURE or failed_test or failed_evidence: classification = "integration-risk" - reason = "task or test result is failed" - elif raw_status in TERMINAL_SUCCESS and (artifact_evidence["missing"] or artifact_evidence["unsafe"]): + reason = "task or recorded test evidence is failed" + elif raw_status in TERMINAL_SUCCESS and ( + artifact_evidence["missing"] or artifact_evidence["unsafe"] + ): classification = "integration-risk" reason = "completed task is missing or references unsafe expected artifacts" + elif raw_status in TERMINAL_SUCCESS and not completion_evidence["satisfied"]: + classification = "integration-risk" + details = [] + if completion_evidence["missing_requirements"]: + details.append( + "missing passing test evidence: " + + ", ".join(completion_evidence["missing_requirements"]) + ) + if not completion_evidence["completion_references"]: + details.append("missing completion evidence reference") + reason = "; ".join(details) or "completion evidence is incomplete" elif unresolved_dependencies: classification = "waiting-dependency" reason = "waiting for: " + ", ".join(unresolved_dependencies) @@ -392,7 +640,11 @@ def classify_task( previous_fingerprint = previous.get("blocker_fingerprint") if previous else None previous_count = previous.get("consecutive_blocked_scans", 0) if previous else 0 - if classification == "blocked" and fingerprint and previous_fingerprint == fingerprint: + if ( + classification == "blocked" + and fingerprint + and previous_fingerprint == fingerprint + ): consecutive_blocked = int(previous_count) + 1 elif classification == "blocked": consecutive_blocked = 1 @@ -422,12 +674,15 @@ def classify_task( "escalated": escalated, "quiet_until": format_time(quiet_until) if quiet_until else None, "expected_artifacts": artifact_evidence, + "completion_evidence": completion_evidence, "risk": task.get("risk"), "next_action": task.get("next_action"), } -def queue_state(events: Sequence[Mapping[str, Any]]) -> tuple[dict[str, dt.datetime], set[str]]: +def queue_state( + events: Sequence[Mapping[str, Any]], +) -> tuple[dict[str, dt.datetime], set[str]]: last_created: dict[str, dt.datetime] = {} acknowledged: set[str] = set() for event in events: @@ -474,6 +729,7 @@ def scan_repository(repo: Path, *, now: dt.datetime | None = None) -> dict[str, if not isinstance(registry, dict): raise ScannerError("task-registry.json must contain a JSON object") policy = ScanPolicy.from_registry(registry) + evidence_policy = EvidencePolicy.from_registry(registry) tasks = validated_tasks(registry) task_by_id = {task["id"]: task for task in tasks} previous_snapshot = json_load(latest_path, required=False) @@ -489,11 +745,13 @@ def scan_repository(repo: Path, *, now: dt.datetime | None = None) -> dict[str, classified: list[dict[str, Any]] = [] for task in tasks: evidence = safe_artifact_evidence(repo, task.get("expected_artifacts")) + completion_evidence = task_completion_evidence(task, evidence_policy, now=now) classified.append( classify_task( task, all_tasks=task_by_id, evidence=evidence, + completion_evidence=completion_evidence, log_activity=activity.get(task["id"]), previous=previous_tasks.get(task["id"]), policy=policy, @@ -511,7 +769,9 @@ def scan_repository(repo: Path, *, now: dt.datetime | None = None) -> dict[str, cooldown_elapsed = last_nudge is None or now - last_nudge >= cooldown task["nudge_eligible"] = bool(needs_nudge and cooldown_elapsed) task["nudge_cooldown_until"] = ( - format_time(last_nudge + cooldown) if last_nudge and not cooldown_elapsed else None + format_time(last_nudge + cooldown) + if last_nudge and not cooldown_elapsed + else None ) if task["nudge_eligible"]: nudge = make_nudge(task, now) @@ -521,11 +781,33 @@ def scan_repository(repo: Path, *, now: dt.datetime | None = None) -> dict[str, else: task["nudge_id"] = None - counts = {key: 0 for key in ("healthy", "waiting-dependency", "stale", "blocked", "integration-risk")} + counts = { + key: 0 + for key in ( + "healthy", + "waiting-dependency", + "stale", + "blocked", + "integration-risk", + ) + } for task in classified: counts[task["classification"]] += 1 warning_count = sum(1 for task in classified if task["warning"]) - overall = "attention" if any(counts[key] for key in NUDGEABLE) or warning_count else "healthy" + evidence_required = sum( + 1 for task in classified if task["completion_evidence"]["required"] + ) + evidence_satisfied = sum( + 1 + for task in classified + if task["completion_evidence"]["required"] + and task["completion_evidence"]["satisfied"] + ) + overall = ( + "attention" + if any(counts[key] for key in NUDGEABLE) or warning_count + else "healthy" + ) snapshot = { "schema_version": SCHEMA_VERSION, "scan_id": now.strftime("scan-%Y%m%dT%H%M%S.%fZ"), @@ -537,9 +819,24 @@ def scan_repository(repo: Path, *, now: dt.datetime | None = None) -> dict[str, "warning_after_minutes": policy.warning_after_minutes, "stale_after_minutes": policy.stale_after_minutes, "nudge_cooldown_minutes": policy.nudge_cooldown_minutes, - "escalate_same_blocker_after_scans": policy.escalate_same_blocker_after_scans, + "escalate_same_blocker_after_scans": ( + policy.escalate_same_blocker_after_scans + ), + "evidence_required_for_assigned_at_or_after": ( + format_time(evidence_policy.required_for_assigned_at_or_after) + if evidence_policy.required_for_assigned_at_or_after + else None + ), + }, + "summary": { + **counts, + "warnings": warning_count, + "nudges_created": len(nudges), + "total": len(classified), + "evidence_required": evidence_required, + "evidence_satisfied": evidence_satisfied, + "evidence_open": evidence_required - evidence_satisfied, }, - "summary": {**counts, "warnings": warning_count, "nudges_created": len(nudges), "total": len(classified)}, "tasks": classified, "nudges_created": nudges, } @@ -552,14 +849,23 @@ def scan_repository(repo: Path, *, now: dt.datetime | None = None) -> dict[str, return snapshot -def acknowledge_nudge(repo: Path, nudge_id: str, *, now: dt.datetime | None = None) -> dict[str, Any]: +def acknowledge_nudge( + repo: Path, nudge_id: str, *, now: dt.datetime | None = None +) -> dict[str, Any]: now = (now or utc_now()).astimezone(dt.timezone.utc) queue_path = repo / "codex_logs" / "nudges" / "queue.jsonl" events = read_jsonl(queue_path) - created = [event for event in events if event.get("event") == "created" and event.get("nudge_id") == nudge_id] + created = [ + event + for event in events + if event.get("event") == "created" and event.get("nudge_id") == nudge_id + ] if not created: raise ScannerError(f"unknown nudge id: {nudge_id}") - if any(event.get("event") == "acknowledged" and event.get("nudge_id") == nudge_id for event in events): + if any( + event.get("event") == "acknowledged" and event.get("nudge_id") == nudge_id + for event in events + ): raise ScannerError(f"nudge is already acknowledged: {nudge_id}") event = { "schema_version": SCHEMA_VERSION, @@ -586,7 +892,9 @@ def timer_kind(requested: str) -> str: return "launchd" if sys.platform.startswith("linux"): return "systemd" - raise ScannerError("automatic timer installation supports Linux systemd and macOS launchd only") + raise ScannerError( + "automatic timer installation supports Linux systemd and macOS launchd only" + ) def scanner_command(repo: Path) -> list[str]: @@ -607,21 +915,18 @@ def systemd_units(repo: Path) -> tuple[str, str]: "", ] ) - timer = "\n".join( - [ - "[Unit]", - "Description=Run Graph Engineering progress scan every 30 minutes", - "", - "[Timer]", - "OnBootSec=5min", - "OnUnitActiveSec=30min", - "Persistent=true", - "Unit=graph-progress.service", - "", - "[Install]", - "WantedBy=timers.target", - "", - ] + timer = ( + "[Unit]\n" + "Description=Run Graph Engineering progress scan every 30 minutes\n" + "\n" + "[Timer]\n" + "OnBootSec=5min\n" + "OnUnitActiveSec=30min\n" + "Persistent=true\n" + "Unit=graph-progress.service\n" + "\n" + "[Install]\n" + "WantedBy=timers.target\n" ) return service, timer @@ -693,7 +998,12 @@ def uninstall_timer(*, kind: str, dry_run: bool = False) -> dict[str, Any]: ["systemctl", "--user", "daemon-reload"], ] else: - paths = [home / "Library" / "LaunchAgents" / "dev.graphengineering.progress-scanner.plist"] + paths = [ + home + / "Library" + / "LaunchAgents" + / "dev.graphengineering.progress-scanner.plist" + ] commands = [["launchctl", "bootout", f"gui/{os.getuid()}", str(paths[0])]] result = { "kind": selected, @@ -717,7 +1027,9 @@ def atomic_write_text(path: Path, value: str) -> None: def atomic_write_bytes(path: Path, value: bytes) -> None: path.parent.mkdir(parents=True, exist_ok=True) - fd, temporary = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=path.parent) + fd, temporary = tempfile.mkstemp( + prefix=f".{path.name}.", suffix=".tmp", dir=path.parent + ) try: with os.fdopen(fd, "wb") as handle: handle.write(value) @@ -729,13 +1041,17 @@ def atomic_write_bytes(path: Path, value: bytes) -> None: os.unlink(temporary) -def run_commands(commands: Iterable[Sequence[str]], *, tolerate_failure: bool = False) -> None: +def run_commands( + commands: Iterable[Sequence[str]], *, tolerate_failure: bool = False +) -> None: for command in commands: try: subprocess.run(command, check=True) except (FileNotFoundError, subprocess.CalledProcessError) as exc: if not tolerate_failure: - raise ScannerError(f"timer command failed: {shlex.join(command)}: {exc}") from exc + raise ScannerError( + f"timer command failed: {shlex.join(command)}: {exc}" + ) from exc def print_human_scan(snapshot: Mapping[str, Any]) -> None: @@ -744,13 +1060,22 @@ def print_human_scan(snapshot: Mapping[str, Any]) -> None: f"Graph progress: {snapshot['overall']} — {summary['total']} tasks, " f"{summary['healthy']} healthy, {summary['waiting-dependency']} waiting, " f"{summary['stale']} stale, {summary['blocked']} blocked, " - f"{summary['integration-risk']} integration risk, {summary['warnings']} warnings" + f"{summary['integration-risk']} integration risk, " + f"{summary['warnings']} warnings" + ) + print( + f"Evidence gates: {summary['evidence_satisfied']}/" + f"{summary['evidence_required']} satisfied, " + f"{summary['evidence_open']} open" ) for task in snapshot["tasks"]: marker = "!" if task["classification"] in NUDGEABLE or task["warning"] else "·" print(f"{marker} {task['id']} [{task['classification']}] {task['reason']}") if snapshot.get("nudges_created"): - print(f"Queued {len(snapshot['nudges_created'])} nudge(s) for supervisor delivery.") + print( + f"Queued {len(snapshot['nudges_created'])} nudge(s) for supervisor " + "delivery." + ) def build_parser() -> argparse.ArgumentParser: @@ -760,13 +1085,27 @@ def build_parser() -> argparse.ArgumentParser: subparsers = parser.add_subparsers(dest="command", required=True) subparsers.add_parser("scan", help="scan registry and append eligible nudges") subparsers.add_parser("status", help="show the most recent scan without rescanning") - install = subparsers.add_parser("install-timer", help="install a 30-minute user timer") - install.add_argument("--kind", choices=("auto", "systemd", "launchd"), default="auto") - install.add_argument("--dry-run", action="store_true", help="print files and commands without changing anything") + install = subparsers.add_parser( + "install-timer", help="install a 30-minute user timer" + ) + install.add_argument( + "--kind", choices=("auto", "systemd", "launchd"), default="auto" + ) + install.add_argument( + "--dry-run", + action="store_true", + help="print files and commands without changing anything", + ) uninstall = subparsers.add_parser("uninstall-timer", help="remove the user timer") - uninstall.add_argument("--kind", choices=("auto", "systemd", "launchd"), default="auto") - uninstall.add_argument("--dry-run", action="store_true", help="print actions without changing anything") - acknowledge = subparsers.add_parser("acknowledge", help="append an acknowledgement for a nudge") + uninstall.add_argument( + "--kind", choices=("auto", "systemd", "launchd"), default="auto" + ) + uninstall.add_argument( + "--dry-run", action="store_true", help="print actions without changing anything" + ) + acknowledge = subparsers.add_parser( + "acknowledge", help="append an acknowledgement for a nudge" + ) acknowledge.add_argument("nudge_id") return parser diff --git a/tools/progress-scanner/tests/test_graph_progress.py b/tools/progress-scanner/tests/test_graph_progress.py index 6b07efd..0571e46 100644 --- a/tools/progress-scanner/tests/test_graph_progress.py +++ b/tools/progress-scanner/tests/test_graph_progress.py @@ -11,7 +11,6 @@ from pathlib import Path from unittest import mock - MODULE_PATH = Path(__file__).parents[1] / "graph_progress.py" SPEC = importlib.util.spec_from_file_location("graph_progress", MODULE_PATH) assert SPEC and SPEC.loader @@ -35,7 +34,7 @@ def setUp(self) -> None: def tearDown(self) -> None: self.temporary.cleanup() - def write_registry(self, tasks, policy=None) -> None: + def write_registry(self, tasks, policy=None, evidence_policy=None) -> None: value = { "schema_version": 1, "updated_at": "2026-07-26T10:00:00Z", @@ -43,11 +42,15 @@ def write_registry(self, tasks, policy=None) -> None: } if policy: value["scan_policy"] = policy + if evidence_policy: + value["evidence_policy"] = evidence_policy (self.repo / "codex_logs" / "task-registry.json").write_text( json.dumps(value), encoding="utf-8" ) - def test_classifies_healthy_waiting_stale_blocked_and_integration_risk(self) -> None: + def test_classifies_healthy_waiting_stale_blocked_and_integration_risk( + self, + ) -> None: artifact = self.repo / "result.txt" artifact.write_text("done", encoding="utf-8") self.write_registry( @@ -172,7 +175,9 @@ def test_acknowledge_is_append_only_and_rejects_duplicate(self) -> None: events = graph_progress.read_jsonl( self.repo / "codex_logs" / "nudges" / "queue.jsonl" ) - self.assertEqual([event["event"] for event in events], ["created", "acknowledged"]) + self.assertEqual( + [event["event"] for event in events], ["created", "acknowledged"] + ) with self.assertRaises(graph_progress.ScannerError): graph_progress.acknowledge_nudge(self.repo, nudge_id, now=self.now) @@ -194,9 +199,223 @@ def test_completed_missing_and_unsafe_artifacts_are_integration_risk(self) -> No self.assertEqual(task["expected_artifacts"]["missing"], ["missing.txt"]) self.assertEqual(task["expected_artifacts"]["unsafe"], ["../outside.txt"]) + def test_required_completion_evidence_must_cover_every_expected_test(self) -> None: + artifact = self.repo / "result.txt" + artifact.write_text("done", encoding="utf-8") + self.write_registry( + [ + { + "id": "incomplete-evidence", + "status": "completed", + "assigned_at": "2026-07-26T11:00:00Z", + "expected_artifacts": ["result.txt"], + "expected_tests": ["unit suite", "integration suite"], + "test_evidence": [ + { + "requirement": "unit suite", + "result": "passed", + "recorded_at": "2026-07-26T11:30:00Z", + "reference": "command: unit", + } + ], + "completion_evidence": ["review: 17"], + } + ], + evidence_policy={ + "required_for_assigned_at_or_after": "2026-07-26T10:30:00Z" + }, + ) + + task = graph_progress.scan_repository(self.repo, now=self.now)["tasks"][0] + + self.assertEqual(task["classification"], "integration-risk") + self.assertEqual( + task["completion_evidence"]["missing_requirements"], ["integration suite"] + ) + self.assertIn("integration suite", task["reason"]) + + def test_required_completion_evidence_can_make_completed_task_healthy(self) -> None: + artifact = self.repo / "result.txt" + artifact.write_text("done", encoding="utf-8") + records = [ + { + "requirement": requirement, + "result": "passed", + "recorded_at": "2026-07-26T11:30:00Z", + "reference": f"command: {requirement}", + } + for requirement in ("unit suite", "integration suite") + ] + self.write_registry( + [ + { + "id": "evidenced", + "status": "completed", + "evidence_required": True, + "expected_artifacts": ["result.txt"], + "expected_tests": ["unit suite", "integration suite"], + "test_evidence": records, + "completion_evidence": ["review: 18", "commit: abc123"], + } + ] + ) + + task = graph_progress.scan_repository(self.repo, now=self.now)["tasks"][0] + + self.assertEqual(task["classification"], "healthy") + self.assertTrue(task["completion_evidence"]["satisfied"]) + self.assertEqual(task["completion_evidence"]["missing_requirements"], []) + snapshot = graph_progress.current_status(self.repo) + self.assertEqual(snapshot["summary"]["evidence_required"], 1) + self.assertEqual(snapshot["summary"]["evidence_satisfied"], 1) + self.assertEqual(snapshot["summary"]["evidence_open"], 0) + + def test_failed_recorded_evidence_is_integration_risk_before_completion( + self, + ) -> None: + self.write_registry( + [ + { + "id": "failed-test", + "status": "in_progress", + "last_heartbeat": "2026-07-26T11:59:00Z", + "expected_tests": ["red team"], + "test_evidence": [ + { + "requirement": "red team", + "result": "failed", + "recorded_at": "2026-07-26T11:58:00Z", + "reference": "log: failure-1", + } + ], + } + ] + ) + + task = graph_progress.scan_repository(self.repo, now=self.now)["tasks"][0] + + self.assertEqual(task["classification"], "integration-risk") + self.assertEqual( + task["completion_evidence"]["failed_requirements"], ["red team"] + ) + + def test_latest_evidence_record_supersedes_an_earlier_failure(self) -> None: + self.write_registry( + [ + { + "id": "retested", + "status": "completed", + "evidence_required": True, + "assigned_at": "2026-07-26T10:00:00Z", + "expected_tests": ["red team"], + "test_evidence": [ + { + "requirement": "red team", + "result": "failed", + "recorded_at": "2026-07-26T10:30:00Z", + "reference": "run: first", + }, + { + "requirement": "red team", + "result": "passed", + "recorded_at": "2026-07-26T11:30:00Z", + "reference": "run: fixed", + }, + ], + "completion_evidence": ["review: fixed"], + } + ] + ) + + task = graph_progress.scan_repository(self.repo, now=self.now)["tasks"][0] + + self.assertEqual(task["classification"], "healthy") + self.assertEqual( + task["completion_evidence"]["passing_requirements"], ["red team"] + ) + self.assertEqual(task["completion_evidence"]["failed_requirements"], []) + + def test_evidence_timestamps_cannot_predate_assignment_or_claim_the_future( + self, + ) -> None: + base = { + "id": "impossible-time", + "status": "completed", + "evidence_required": True, + "assigned_at": "2026-07-26T11:00:00Z", + "expected_tests": ["unit suite"], + "completion_evidence": ["review: time"], + } + for recorded_at, message in ( + ("2026-07-26T10:59:00Z", "predates assigned_at"), + ("2026-07-26T12:01:00Z", "is in the future"), + ): + with self.subTest(recorded_at=recorded_at): + self.write_registry( + [ + { + **base, + "test_evidence": [ + { + "requirement": "unit suite", + "result": "passed", + "recorded_at": recorded_at, + "reference": "run: impossible", + } + ], + } + ] + ) + with self.assertRaisesRegex(graph_progress.ScannerError, message): + graph_progress.scan_repository(self.repo, now=self.now) + + def test_evidence_required_task_needs_an_expected_test(self) -> None: + self.write_registry( + [ + { + "id": "empty-gate", + "status": "in_progress", + "evidence_required": True, + "last_heartbeat": "2026-07-26T11:59:00Z", + "expected_tests": [], + } + ] + ) + + with self.assertRaisesRegex(graph_progress.ScannerError, "must not be empty"): + graph_progress.scan_repository(self.repo, now=self.now) + + def test_invalid_test_evidence_is_rejected(self) -> None: + self.write_registry( + [ + { + "id": "invalid-evidence", + "status": "completed", + "expected_tests": ["unit suite"], + "test_evidence": [ + { + "requirement": "unknown suite", + "result": "passed", + "recorded_at": "2026-07-26T11:30:00Z", + "reference": "command: unit", + } + ], + } + ] + ) + + with self.assertRaisesRegex(graph_progress.ScannerError, "unknown requirement"): + graph_progress.scan_repository(self.repo, now=self.now) + def test_agent_jsonl_is_progress_evidence(self) -> None: self.write_registry( - [{"id": "active", "status": "in_progress", "last_heartbeat": "2026-07-26T08:00:00Z"}] + [ + { + "id": "active", + "status": "in_progress", + "last_heartbeat": "2026-07-26T08:00:00Z", + } + ] ) (self.repo / "codex_logs" / "agents" / "worker.jsonl").write_text( json.dumps( @@ -216,13 +435,23 @@ def test_agent_jsonl_is_progress_evidence(self) -> None: self.assertEqual(snapshot["tasks"][0]["activity_source"], "agent_log") def test_repository_lock_rejects_overlap(self) -> None: - with graph_progress.repository_lock(self.repo / "codex_logs"): - with self.assertRaises(graph_progress.ScannerBusy): - with graph_progress.repository_lock(self.repo / "codex_logs"): - pass + with ( + graph_progress.repository_lock(self.repo / "codex_logs"), + self.assertRaises(graph_progress.ScannerBusy), + graph_progress.repository_lock(self.repo / "codex_logs"), + ): + pass def test_snapshot_and_latest_are_complete_json(self) -> None: - self.write_registry([{"id": "active", "status": "in_progress", "last_heartbeat": "2026-07-26T11:59:00Z"}]) + self.write_registry( + [ + { + "id": "active", + "status": "in_progress", + "last_heartbeat": "2026-07-26T11:59:00Z", + } + ] + ) snapshot = graph_progress.scan_repository(self.repo, now=self.now) latest = graph_progress.current_status(self.repo) @@ -240,9 +469,7 @@ def test_timer_dry_run_does_not_write_or_execute(self, home) -> None: install = graph_progress.install_timer( self.repo, kind="systemd", dry_run=True ) - uninstall = graph_progress.uninstall_timer( - kind="systemd", dry_run=True - ) + uninstall = graph_progress.uninstall_timer(kind="systemd", dry_run=True) self.assertTrue(install["dry_run"]) self.assertTrue(uninstall["dry_run"]) @@ -253,7 +480,15 @@ def test_timer_dry_run_does_not_write_or_execute(self, home) -> None: run_commands.assert_not_called() def test_cli_json_scan(self) -> None: - self.write_registry([{"id": "active", "status": "in_progress", "last_heartbeat": "2099-01-01T00:00:00Z"}]) + self.write_registry( + [ + { + "id": "active", + "status": "in_progress", + "last_heartbeat": "2099-01-01T00:00:00Z", + } + ] + ) output = io.StringIO() with redirect_stdout(output):