diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d70b9f2..006a07a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,7 +32,7 @@ jobs: python3 -m unittest discover -s scripts/tests -p 'test_*.py' lean-proof: - name: Lean assurance laws + name: Lean assurance and SDLC design laws runs-on: ubuntu-latest timeout-minutes: 20 steps: @@ -63,9 +63,18 @@ jobs: echo "ELAN_HOME=$ELAN_HOME" >> "$GITHUB_ENV" echo "$ELAN_HOME/bin" >> "$GITHUB_PATH" - - name: Build assurance proof library + - name: Build assurance and SDLC proof libraries working-directory: proof/lean - run: lake build LambdaAitiaModuleAssuranceProof + run: lake build LambdaAitiaModuleAssuranceProof LambdaAitiaModuleSdlcProof + + - name: Reject sorry axioms in exported laws + working-directory: proof/lean + shell: bash + run: | + set -euo pipefail + output="$(lake env lean AxiomAudit.lean)" + printf '%s\n' "$output" + ! grep -q 'sorryAx' <<< "$output" - name: Save Lean cache if: steps.lean-cache.outputs.cache-hit != 'true' @@ -76,6 +85,34 @@ jobs: proof/lean/.lake key: lambda-aitia-lean-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('proof/lean/lean-toolchain', 'proof/lean/lakefile.lean', 'proof/lean/lake-manifest.json') }} + quint-model: + name: Quint lifecycle, retirement and ingress contracts + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout Lambda Aitia + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Set up Node for pinned Quint + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: '22' + + - name: Set up Java for Quint TLC backend + uses: actions/setup-java@d7793b545071e98d581d3bf084a51c3213318a07 # v4.9.0 + with: + distribution: temurin + java-version: '21' + + - name: Install locked Quint and check all 25 model contracts + env: + AITIA_QUINT_BIN: ${{ github.workspace }}/proof/quint/node_modules/.bin/quint + QUINT_HOME: ${{ runner.temp }}/lambda-aitia-quint + run: | + npm ci --prefix proof/quint + mkdir -p "$QUINT_HOME" + python3 proof/quint/check_contract.py + scheme-runtime: name: Scheme, integration and Python runtime runs-on: ubuntu-latest @@ -89,7 +126,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: repository: tao3k/poo-flow - ref: da492eea00a7dab502cfb4c7586edefbba96bbec + ref: 3a6330f9135b7e5d7a96f9c2de7b5fb90e9ad399 path: poo-flow submodules: false @@ -144,6 +181,35 @@ jobs: working-directory: poo-flow run: bazelisk run @local_gerbil//:native_scheme_env -- bash -lc 'cd "$BUILD_WORKSPACE_DIRECTORY/packages/lambda-aitia" && just check-semantic' + - name: Bind Quint and Lean sources to the retirement POO contract + working-directory: poo-flow + run: bazelisk run @local_gerbil//:native_scheme_env -- bash -lc 'cd "$BUILD_WORKSPACE_DIRECTORY/packages/lambda-aitia" && just test-scheme-atomic formal retirement-binding-test.ss' + + - name: Execute retirement ingress controls against Scheme Host + working-directory: poo-flow + run: bazelisk run @local_gerbil//:native_scheme_env -- bash -lc 'cd "$BUILD_WORKSPACE_DIRECTORY/packages/lambda-aitia" && just test-scheme-atomic formal retirement-execution-test.ss' + + - name: Set up Node for pinned Quint correspondence traces + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: '22' + + - name: Replay Quint states and unsafe-model controls through Scheme + working-directory: poo-flow + run: | + npm ci --prefix packages/lambda-aitia/proof/quint + bazelisk run @local_gerbil//:native_scheme_env -- bash -lc 'cd "$BUILD_WORKSPACE_DIRECTORY/packages/lambda-aitia" && just check-quint-scheme' + + - name: Set up Java for complete lifecycle state graph + uses: actions/setup-java@d7793b545071e98d581d3bf084a51c3213318a07 # v4.9.0 + with: + distribution: temurin + java-version: '21' + + - name: Replay every reachable lifecycle state and authorization delivery fault + working-directory: poo-flow + run: bazelisk run @local_gerbil//:native_scheme_env -- bash -lc 'cd "$BUILD_WORKSPACE_DIRECTORY/packages/lambda-aitia" && just check-lifecycle-scheme' + - name: Run SDLC and GitOps integration working-directory: poo-flow run: bazelisk run @local_gerbil//:native_scheme_env -- bash -lc 'cd "$BUILD_WORKSPACE_DIRECTORY/packages/lambda-aitia" && just test-integration' diff --git a/.gitignore b/.gitignore index aa2c3dd..524550e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ version.ss .cache/ .bin/ .lake/ +_apalache-out/ +proof/quint/node_modules/ __pycache__/ *.py[cod] *.so diff --git a/README.org b/README.org index b2db2b3..a143e52 100644 --- a/README.org +++ b/README.org @@ -1,14 +1,18 @@ # SPDX-FileCopyrightText: 2026 tao3k team and Contributors # SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later -#+title: Lambda Aitia — causal assurance as executable knowledge +#+title: Lambda Aitia — causal assurance across the software lifecycle #+startup: overview * Purpose -Lambda Aitia is the software and SDLC causal-assurance contribution for the POO -Flow ecosystem. It also owns the software Architecture Decision Record (ADR) -module migrated from Lambda Episteme. It answers five questions about an +Lambda Aitia organizes software engineering around the complete SDLC: +requirements, design, algorithms and implementation strategies, verification +and testing, delivery, operation, maintenance and retirement. Causal Assurance +connects the justification of engineering decisions across that lifecycle. +POO Flow supplies the reusable composition, temporal/causal and runtime +mechanisms. Aitia also owns the software Architecture Decision Record (ADR) +module migrated from Lambda Episteme. It answers five questions about an engineering conclusion: 1. What exactly is claimed? @@ -18,27 +22,30 @@ engineering conclusion: 5. Which decisions and real effects must remain blocked until the resulting obligations are discharged again? -The project manages the justification and invalidation of conclusions. It is -not a database of green verification results and it is not a generic knowledge -graph. +The project helps form, compare and revise design decisions, then retains the +evidence explaining why their guarantees remain applicable. Requirements lead +to explicit assumptions, design alternatives, responsibilities, state ownership +and conditional guarantees. Algorithm choices and code implement those +decisions; tests and runtime observations can challenge them and reopen design. + +System behavior trajectories and design/assurance evolution trajectories are +linked through evidence. Observed executions, model traces, hypotheses and +counterfactual alternatives preserve their distinct scope. Temporal order and +graph reachability alone cannot establish a causal explanation. +[[file:docs/rfcs/0004-sdlc-design-and-causal-trajectories.org][RFC 0004]] defines this lifecycle design and its pending acceptance gates. * Place in the ecosystem -#+begin_example -lambda-aitia - Software/SDLC, ADR, NASA and GitOps sources - claims, assumptions, typed relations, obligations, evidence, - invalidation and assurance planning - ^ - | -lambda-episteme - optional non-SDLC vertical knowledge and decision-kind/ontology sources - | - v -POO Flow - POO-native composition, functional flow, graph substrate, - verifier/runtime handoff and effect boundary -#+end_example +#+begin_export mermaid +flowchart TD + App["Application
explicit composition"] --> A["Lambda Aitia
SDLC, design and Causal Assurance"] + App --> E["Lambda Episteme
optional vertical knowledge"] + E -->|source facts when composed| A + A -->|consumes public library| P["POO Flow
composition, graph, temporal / causal mechanisms and Runtime"] + E -->|consumes public library| P + A --> Py["Aitia Python Runtime
owned-byte C ABI, tools and observations"] + Py -->|uses owned runtime boundaries| P +#+end_export This is a conceptual flow, not permission to collapse repository ownership. POO Flow core never imports a concrete Lambda contribution. Lambda Aitia may @@ -49,23 +56,26 @@ Flow. Mounting this Git submodule does not import a module, activate a Profile, run a verifier, grant a capability, authorize a decision or execute an effect. -The production Python SDLC Runtime is maintained entirely under +The production Aitia Python Runtime is maintained entirely under =bindings/python= in this repository; it is not an example. POO Flow's =packages/python-runtime= remains the generic graph execution and lifecycle mechanism and does not own Aitia semantics. Aitia Scheme is compiled AOT behind an owned-byte C ABI. The Python project validates typed lifecycle receipts and exposes them to downstream systems without reimplementing Scheme -decisions. +decisions. The Python Runtime serves Aitia capabilities throughout the SDLC; +the existing =SdlcRuntime.plan()= is one implemented assurance-flow projection. +AnyIO is declared as its runtime-wide concurrency foundation; async execution +and Host admission remain separate implementation gates. Generic Graph/DAG and +FunFlow execution mechanisms remain owned by POO Flow. MRR relation-query and +lineage integration is a later design direction, outside the current Python +implementation stage. * Core model The initial semantic vocabulary is deliberately small: -#+begin_example -Artifact Claim Assumption Observation -Event Action Obligation Evidence -Counterexample Finding Decision Effect -#+end_example +=Artifact=, =Claim=, =Assumption=, =Observation=, =Event=, =Action=, +=Obligation=, =Evidence=, =Counterexample=, =Finding=, =Decision= and =Effect=. Relations are typed by plane. A generic =edge(from, to)= is not part of the public model. @@ -87,15 +97,14 @@ authority. The first vertical slice is one fail-closed lifecycle: -#+begin_example -source revision changes - -> structural impact is computed - -> affected assumptions and claims are selected - -> dependent evidence becomes stale - -> new obligations are planned - -> the old decision loses authority - -> the associated effect remains blocked -#+end_example +#+begin_export mermaid +flowchart TD + C["Source revision
changed"] --> I["Structural impact
affected assumptions and claims identified"] + I --> S["Dependent support
stale"] + S --> O["New obligations
pending"] + S --> D["Old decision
invalid for the changed closure"] + D --> E["Associated effect
blocked"] +#+end_export The slice succeeds only when receipts bind the subject, source revision, assumption set, claim, obligation, producer identity, tool identity, artifact @@ -114,17 +123,18 @@ evaluate that model. ** Thirty-second compression -Spec-first Agent work asks an Agent to follow a better specification. Specula -uses Agents to derive and validate TLA+ models from system code so that deep -concurrency bugs can be found and reproduced. Lambda Aitia addresses the next -system-level question: after RFCs, code, formal models, proofs, policies, tests -and runtime observations all exist, which exact conclusion is still justified -for the current revision, what change invalidated it, which obligations must be -rerun, and why must an Agent effect remain blocked? +Aitia uses existing verification methods to help choose and revise software +designs throughout the SDLC. It connects requirements, assumptions, guarantees, +RFC/ADR decisions, implementation strategies and observed behavior. A new +finding can change the design, invalidate earlier support and create new +obligations with an explanation path. -Aitia therefore does not compete with Lean, TLA+, TLC, Cedar, Specula or POO -Flow. It binds their distinct artifacts into a causal-assurance lifecycle -without pretending that one tool's success proves the others' propositions. +Specula already derives and validates TLA+ models, reproduces bugs and supports +incremental verification. Aitia should reuse applicable tools, models, harnesses +and checks while evaluating its own contribution: better-founded design choices, +composition obligations and traceable changes in engineering justification. +The full Specula application is optional; an upstream component or script can +be reused without handing it ownership of Aitia's lifecycle. ** Related work and project-position matrix @@ -132,10 +142,11 @@ without pretending that one tool's success proves the others' propositions. |-----------------+----------------+---------------+----------------------------+--------------| | POO Flow Agentic Control Plane RFC suite | organization Bundle, Agent control plane, proof/evidence chain and effect token | establishes reusable mechanism, ownership and authority boundaries | intentionally does not own Aitia's Software/SDLC claims or assurance policy | POO Flow is the full public mechanism Library on which Aitia is built | | Agent RFC / specification-first Agent development | human-reviewed intent, constraints and implementation plan | gives Agents an explicit architecture and acceptance target instead of an ad hoc prompt | a document does not establish current evidence, invalidation closure, authorization or execution | Aitia turns RFC clauses and revisions into typed claims, threats and obligations | -| [[https://specula.info/][Specula]] ([[https://arxiv.org/abs/2607.25333][paper]]) | code-derived TLA+ specification, invariants, traces and reproduced bugs | autonomous specification generation, model checking, trace alignment and concrete bug reproduction | concentrates on discovering implementation/model violations; it is not a cross-tool evidence, policy and effect-authority ledger | a Specula run can satisfy Aitia TLA+/replay obligations when its artifacts are identity-bound and admitted | +| [[https://github.com/specula-org/Specula][Specula]] ([[https://arxiv.org/abs/2607.25333][paper]]) | code-derived models, invariants, traces and reproduced bugs | specification generation, model checking, replay and incremental verification | Aitia must demonstrate additional value in design choices, composition obligations and changing justification | reuse qualified tools and assets under POO composition; the complete application is optional | +| [[https://github.com/specula-org/specula-case-studies][Specula Case Studies]] | archived models, harnesses, traces and experiment records | concrete engineering assets and known-fault reproduction material | each selected record needs provenance, license and replay qualification | use selected assets as Recipe inputs; replay success is not a new discovery | | [[https://arxiv.org/abs/2605.02454][Causal Software Engineering]] | lifecycle-wide causal models, assumptions and counterfactual diagnosis | provides the broader research vision for causal reasoning throughout software engineering | a vision and roadmap do not by themselves define Aitia's POO objects, revision receipts or Runtime gate | Aitia is an executable contribution pursuing this direction with typed relation planes and invalidation | | Conventional CI/CD and test dashboards | jobs, statuses, logs and deployment stages | repeatable automation and immediate regression signals | a green result usually does not encode the complete subject, assumption, revision, causal support or downstream invalidation graph | Aitia consumes exact test/build receipts as evidence rather than treating green status as authority | -| Lean, TLA+/TLC and Cedar used independently | theorem, checked state space or policy decision | strong, specialized reasoning inside a clearly defined formal boundary | none alone proves model-code alignment, evidence freshness, current authorization and effect completion | Aitia keeps the propositions separate and joins only digest-bound admitted receipts | +| Lean, Quint/TLC and Cedar used independently | theorem, checked state space or policy decision | strong, specialized reasoning inside a clearly defined formal boundary | none alone proves model-code alignment, evidence freshness, current authorization and effect completion | Aitia keeps the propositions separate and joins only digest-bound admitted receipts | The comparison is about responsibility rather than feature count. Aitia's distinctive unit is the current, revision-bound justification of a conclusion @@ -148,7 +159,7 @@ The threat model covers failures that ordinary pass/fail CI cannot distinguish: - stale evidence is replayed after source, policy, assumption or tool changes; - a cached Agent decision survives invalidation or revocation; -- an RFC claim, Lean statement, TLA+ model and Cedar policy refer to different +- an RFC claim, Lean statement, Quint model and Cedar policy refer to different revisions of the supposed same subject; - tool exit success is admitted without exact input, environment and artifact bindings; @@ -169,32 +180,45 @@ different propositions and trusted boundaries: | Agent RFC and ADR | intent, vocabulary, trust boundary, decisions and required obligations | that an implementation, policy or runtime matches the documents | | Aitia Assurance Graph | exact subject/revision bindings, typed support, invalidation, unresolved frontier and verification plan | temporal safety, authorization or effect execution | | Lean | general laws such as freshness, monotone invalidation, modality preservation, no stale authority and composition obligations | that the current deployment supplied fresh facts or executed an effect | -| TLA+/TLC | bounded lifecycle transitions and adverse interleavings across change, invalidate, plan, verify, admit, decide, authorize and effect | universal theorem proof or production authorization | +| Quint/TLC | bounded lifecycle transitions and adverse interleavings across change, invalidate, plan, verify, admit, decide, authorize and effect | universal theorem proof or production authorization | | Cedar | an allow/deny decision for one identity-bound principal, action, resource, context, policy and entity snapshot | evidence freshness, proof construction, runtime conformance or external success | | Runtime Host | one bounded execution attempt and its typed execution receipt | that missing proof or authorization may be inferred | -The intended evidence flow is: - -#+begin_example -Agent RFC / ADR / source revisions - -> Aitia snapshot and threat/impact graph - -> invalidation and typed verification obligations - -> Lean proof receipts + TLA+/TLC model-check receipts + test/replay evidence - -> evidence admission against the same snapshot - -> Cedar projection and identity-bound authorization decision - -> one bounded POO Flow Runtime execution - -> execution receipt and post-effect evidence -#+end_example - -Every arrow is digest- and revision-bound. Lean cannot replace Cedar, Cedar -cannot replace TLA+, a TLC completion cannot replace runtime evidence, and a +The intended evidence workflow names semantic states, not implementation +completion. RFC 0004 contains the full SDLC workflow and its design feedback. + +#+begin_export mermaid +flowchart TD + D["RFC / ADR and sources
declared"] --> S["Snapshot / impact graph
bound"] + S --> O["Verification obligations
pending"] + O --> V["Proof, model check, test / replay
candidate results"] + V --> A{"Trusted admission valid?"} + A -->|yes| C["Scoped support
admitted and current"] + C --> G{"Effect gate allows?"} + G -->|yes| X["Bounded Runtime attempt
authorized"] + X --> R["Receipt and observations
outcome recorded"] + R --> F["Lifecycle reassessment
design review when challenged"] + F --> D + A -->|no / unknown| U["Unresolved / rejected
affected effect blocked"] + U --> O + G -->|no / unknown| B["Effect blocked
review / revalidation required"] + B --> O + C -->|changed / expired| B +#+end_export + +Admission checks the subject, revision and cut. The effect gate requires a +current decision, Cedar authorization and Runtime enforcement. Every arrow is +digest- and revision-bound. Lean cannot replace Cedar, Cedar +cannot replace Quint, a TLC completion cannot replace runtime evidence, and a Runtime success cannot retroactively prove the RFC. Unknown, conflicted, stale, mismatched or unavailable inputs fail closed and remain visible in the receipt graph. -The current repository has executable snapshot/invalidation and modular Lean -evidence. The complete TLA+ lifecycle, Cedar admission and Runtime effect gate -remain explicit roadmap phases; their absence is not represented as closure. +The current repository has executable snapshot/invalidation, pure planning, +modular Lean laws, bounded Quint lifecycle/retirement/ingress models and known-fault Python replay. +The connected design/trajectory experiment, upstream Recipe qualification, +Python-to-Host evidence admission and Runtime effect gate remain open. See +[[file:ROADMAP.org][ROADMAP]] for phase-specific evidence and limits. See [[file:docs/rfcs/0001-causal-assurance-core.org][RFC 0001]], [[file:docs/rfcs/0002-snapshot-and-invalidation-kernel.org][RFC 0002]], [[file:docs/rfcs/0003-composition-value-interface.org][RFC 0003]] and @@ -202,11 +226,11 @@ See [[file:docs/rfcs/0001-causal-assurance-core.org][RFC 0001]], * Project rules -- Public extension APIs are POO-native. The ordinary =aitia.ss= surface selects - the maintained =aitia= Composition and fills bounded Project slots; its macro - lowers directly to those POO values. Prototype, C3 and slot-transform - operators are exposed only when a user needs an advanced override. Neither - layer uses raw alist DSLs or ad hoc patch languages. +- Public extension APIs are POO-native. The maintained =aitia= Case now uses + POO Flow's =user-composition=, =compose profiles= and =use-module=. + The former no-import =aitia.ss= convenience is an open RFC 0003 UX gate, + not a supported loader in the current Profile algebra. Advanced overrides + use POO slots; neither path introduces an alist or patch-language DSL. - Functional transformations are values first. Side effects live behind an explicit POO Flow verifier or runtime handoff. - Native Scheme tests are never heap-unbounded: the Aitia Testing Interface @@ -238,6 +262,14 @@ The machine-readable declaration and complete texts are maintained in - [[file:docs/rfcs/0003-composition-value-interface.org][RFC 0003 — Composition Value Interface]] defines the Jsonnet-style single-value downstream file, the official =aitia= Composition and the advanced POO override boundary. +- [[file:docs/rfcs/0004-sdlc-design-and-causal-trajectories.org][RFC 0004 — SDLC Design, Causal Assurance and Trajectories]] defines the + complete lifecycle framework, design leverage, reuse contracts and future + MRR boundary, with implementation acceptance still pending. +- [[file:docs/rfcs/0005-org-native-engineering-workspace.org][RFC 0005 — Org-native engineering workspace]] defines source-bound Agent + review, POO Flow dependency ownership and the unresolved package cycle. + The [[file:user-interface/workspaces/job-executor.org][maintained job-executor workspace]] is the first native authoring surface. + Qualification must use Scheme-owned declarations and the Rowan/AOT consumer; + earlier legacy CLI lint/query results do not establish this integration. - [[file:docs/adrs/README.org][ADR contract and template]] defines standardized bounded decisions, lifecycle, RFC links and the non-authorizing assurance projection. - [[file:docs/adrs/0007-consume-poo-flow-public-library.org][ADR 0007]] establishes POO Flow as Aitia's reusable public mechanism library. @@ -245,8 +277,8 @@ The machine-readable declaration and complete texts are maintained in - [[file:docs/plans/software-sdlc-integration-inventory.org][Software/SDLC integration inventory]] classifies the existing implementation as keep, reference, generalize or later migrate. -The repository has Phase 0 and Phase 1 evidence-partial work plus an active -Phase 2 snapshot/invalidation slice. ABI revision 2 and =SdlcRuntime.plan()= -form the first production Python runtime contract for the complete eight-stage -SDLC DAG. That plan remains inert: executable stage receipts, release -authority and completed effects are not claimed yet. +Phase 2 snapshot/invalidation and Phase 3 pure planning have bounded completion +receipts; later phases retain their explicit gaps. ABI revision 2 and +=SdlcRuntime.plan()= expose the eight-stage assurance/execution DAG. That DAG +is a protocol used within the full SDLC, and its plan remains inert: executable +stage receipts, release authority and completed effects are not inferred. diff --git a/ROADMAP.org b/ROADMAP.org index df0eec8..fd61e92 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -4,6 +4,62 @@ #+title: Lambda Aitia Roadmap #+startup: overview +* Lifecycle framework and current priority + +The complete SDLC is the product framework: requirements, software design, +algorithm and implementation strategies, verification/testing, delivery, +operation, maintenance and retirement. These activities exchange observations +and reopen earlier decisions. The numbered phases below are implementation +milestones for that framework, not the lifecycle experienced by a project. + +[[file:docs/rfcs/0004-sdlc-design-and-causal-trajectories.org][RFC 0004]] makes software design an early leverage point and Causal Assurance +the connection across the lifecycle. The next product qualification must link +requirements, at least two design alternatives, assumptions and guarantees to +real execution trajectories, evidence, a revised ADR and renewed obligations. +Recipe execution and tool integration contribute to that connected experiment. + +The agreed lifecycle Impact design in RFC 0004 covers capability introduction, +change and retirement across historical assets/commitments, current code and +operation, and future work. POO Flow owns generic analysis; Aitia adds SDLC +meaning. A bounded job-executor retirement case is the next paired Impact +qualification, not an instruction to remove a production capability. Its +implementation and execution receipts remain pending. + +Reuse POO Flow Temporal Causality and existing upstream verification tools and +assets. AnyIO serves the entire Aitia Python Runtime. MRR relation queries, +derivation and lineage are deferred beyond the current Python implementation +stage; this queue introduces no MRR integration. + +This view tracks implementation progress; it is separate from the engineering +states in RFC 0004's lifecycle workflow. + +[[file:docs/rfcs/0005-org-native-engineering-workspace.org][RFC 0005]] adds the maintained [[file:user-interface/workspaces/job-executor.org][Org-native job-executor workspace]]. Its +Scheme-owned contract and Rowan consumer qualification remain pending; the +earlier legacy CLI checks do not close this gate. POO Flow owns the intended +Orgize dependency; direct Scheme package +installation is blocked by the Orgize → gerbil-parser → POO Flow dependency +cycle and missing Orgize package entrypoint. Existing generated Cargo artifacts +can be qualified independently of that source-package cycle. Rust/Rowan runtime +delivery is the later durable path, not an interim workaround or a prerequisite +for the current Python stage. The immediate implementation priority remains +Aitia's production Python Runtime: preserve frozen source observations through +AnyIO execution and reach the actual Scheme Host admission path. Python must +not reimplement Org parsing or contract evaluation to bypass the pending +Orgize boundary. Role projection, source-bound edits and the connected +lifecycle remain separate gates. + +#+begin_export mermaid +flowchart TD + B["Existing bounded foundations
invalidation, planning, formal laws and replay"] + B --> D["Pending
design alternatives and upstream qualification"] + B --> H["Pending
Python-to-Host admission and async transport"] + D --> T["Pending
connected behavior and design trajectories"] + H --> T + T --> R["Pending
revision, revalidation and comparative evidence"] + R --> O["Pending
contract-preserving optimization evidence"] + O -. later design .-> M["Deferred
MRR relation queries, derivation and lineage"] +#+end_export + * Completion discipline Each phase closes only when its source, negative contracts, executable checks @@ -36,7 +92,7 @@ Deliverables: applications compose contributions; POO Flow core never imports Aitia. 6. A checked integration inventory covering Lambda Episteme Software Engineering, SDLC, GitOps and decision knowledge plus POO Flow Graph, Lean, - TLA+, Cedar and Runtime Host artifacts. + Quint, Cedar and Runtime Host artifacts. Exit gates: @@ -48,13 +104,15 @@ Exit gates: parent state; - mounting the repository performs no activation or effect. -Current evidence: the production dependency closure, Phase 2 Assurance fixture, -modular Lean library, integration lane and native Scheme/C/Python package replay +Current evidence: the independent PR CI, production dependency closure, Phase 2 +Assurance fixture, modular Lean library, integration lane and native +Scheme/C/Python package replay from an isolated checkout at exact revisions. The Homebrew Darwin toolchain adaptation is child-process scoped and does not alter a Nix-selected toolchain or the parent environment. See =docs/receipts/2026-09-18-phase2-clean-replay.org=. The remaining Phase 0 work -is the independent CI/release lane and checked integration inventory. +is a release lane and executable integration-inventory acceptance. The PR CI +does not by itself constitute a published release or complete Phase 0. * Phase 1 — SDLC ownership migration and POO-native semantic kernel @@ -116,12 +174,20 @@ Deliverables: Core law: -#+begin_example -changed(dependency) -and reaches(dependency, claim) -=> not authorized(claim) -until current obligations are discharged by current evidence -#+end_example +#+begin_export mermaid +stateDiagram-v2 + state "Support: current" as Current + state "Support: stale; affected effect blocked" as Stale + state "Obligations: pending" as Pending + state "Support: renewed at a new cut" as Renewed + Current --> Stale: dependency changes and reaches claim + Stale --> Pending: derive current obligations + Pending --> Renewed: obligations discharged by current admitted evidence + Renewed --> Stale: another relevant change +#+end_export + +Renewed support does not itself authorize an effect; current decision and +authorization gates remain independently required. Exit gates: @@ -133,7 +199,56 @@ Exit gates: * Phase 3 — Obligation derivation and verification planning -Status: =planned= +Status: =complete= for pure planning at the exact source cut in +=docs/receipts/2026-09-23-phase3-planning-replay.org=. Verifier execution, +evidence promotion and effect authority remain Phase 4/6 work. + +The first executable slice reuses the Phase 2 invalidation receipt to select +snapshot-bound obligations. A POO-native verification policy declares +available capabilities, and the resulting POO-native plan records selected +obligations, capability requirements, separate unknown/conflicted blockers and +the underlying invalidation witnesses. It is deterministic under inventory +permutation and cannot claim verifier execution, release authority or runtime +execution. +Obligation evidence kinds are closed to proof, model checking, replay, +unit/integration/native tests, static analysis, authorization differential and +human review; an unknown kind is rejected at the model boundary. +Within the affected obligation set, structural =depends-on= relations are +projected into POO Flow Graph. Its topological order becomes the selected +request order; a blocked prerequisite blocks dependents, an unplanned +prerequisite is never assumed discharged, and a dependency cycle records its +path while blocking only its strongly connected component and downstream +dependents; an independent acyclic branch retains its own order. The plan +records every cyclic component as well as one deterministic path witness; it +still does not attest that any prerequisite has been verified. +An obligation bound to an older snapshot is blocked rather than silently +promoted to a replacement obligation. Selection compares snapshot identity, +revision and a non-circular context digest. The context digest covers the +canonical snapshot while excluding only obligation binding fields; the final +snapshot digest still covers those fields. Binding an unbound candidate is an +explicit pure step, and a previously bound obligation cannot be rebound. +The pure replacement-requirements receipt traces affected old obligations to +a changed target snapshot and records blockers for unresolved changes, stale +source bindings, revised claims or policy, and incomplete targets. It derives +the need for a new declaration without inventing a new obligation identity +or copying an old evidence requirement across a changed claim. The explicit +declaration step accepts a caller-authored, unbound candidate in a target draft +with a revised claim and support edge; it checks the source derivation and +canonical snapshots, binds only that fresh candidate through the existing +snapshot binder, reconstructs the final snapshot, and returns an inert receipt. +It does not copy evidence, invoke a verifier or grant release authority. +The explicit verifier-candidate catalog is a set of POO-native inert declarations +with evidence kinds, capability and priority. A pure explanation receipt +records the chosen declaration, every rejected candidate and unassigned +planned obligations; ties use candidate identity only after explicit priority. +This is declared suitability, not adapter installation, execution or evidence +admission. +An explicit structural =composes= relation connects a composite claim to its +component claims and projects through POO Flow Graph for invalidation. A pure +composition-requirements receipt demands an independent, claim-declared, +snapshot-bound obligation for the composite. Component status or evidence +never supplies it, and even a present direct obligation does not attest that +verification ran or evidence was admitted. Deliverables: @@ -157,27 +272,50 @@ Exit gates: * Phase 4 — Verifier adapters and evidence admission -Status: =planned= +Status: =in-progress= (typed reported outcomes, pure structural eligibility and +sealed-support checks reuse POO Flow's verification adapter. A configured +Scheme Host capability now owns adapter, clock, lease, current snapshot source, +revocation and a separate candidate admission ledger. An exact current +assurance-edge witness can consume that ledger without promoting the snapshot +evidence slot. A typed required-support receipt now checks all claim-declared +obligations and both assurance links at the current Host cut, with explicit +blockers and no release authority. Full Decision closure, trusted deployment, +Cedar policy and the Runtime effect gate remain open. A Host-issued preflight +can now bind an undecided Decision and its dependency edge to that support, +but cannot produce =allow= or an effect token). Deliverables: -1. Adapter contracts for Lean, TLA+/TLC, native tests, static analyzers, +1. Adapter contracts for Lean, Quint/TLC, native tests, static analyzers, runtime replay and human review. 2. Evidence envelopes binding producer, tool/version, input artifact digests, obligation, output digest, subject, revision, scope and validity interval. -3. Admission rules for successful, failed, timed-out, unavailable and - indeterminate verifier outcomes. +3. [~] Admission rules for successful, failed, timed-out, unavailable and + indeterminate verifier outcomes. The pure eligibility receipt remains + inert. A configured Host may now issue a separate admission for candidate + evidence after its verifier succeeds over the exact current snapshot; + changing =admission-state= in a node is not the promotion operation. + Admission remains subject to Host clock, source invalidation and revocation + and cannot authorize release. + The normal Host support entrypoint requires an exact POO Flow adapter-issued + seal over the current semantic subject; no structural-only public support + predicate remains. The low-level under-adapter check is explicitly + conditional, not an authorization path. A forged adapter or Host + presentation is rejected; proving that application configuration installed + the intended Host and that its clock and snapshot source are trusted remains + open. 4. Provenance-preserving counterexample and finding ingestion. 5. Replay checks proving presentation mutation cannot forge admitted evidence. 6. [X] An Aitia Scheme-native ABI contract following POO Flow's owned-byte, caller-release pattern; public Scheme and Python namespaces remain unversioned while ABI revision is explicit metadata. -7. [X] An Aitia-owned, uv-managed =bindings/python= production SDLC Runtime +7. [X] An Aitia-owned, uv-managed =bindings/python= production Python Runtime with an out-of-line CFFI package. POO Flow's =packages/python-runtime= remains the generic runtime mechanism, and Python contains no duplicate Aitia semantics. -8. [X] ABI revision 2 publishes the canonical eight-stage POO Flow DAG as an - inert typed =SdlcFlowPlan=; it does not claim authorization or execution. +8. [X] ABI revision 2 publishes the eight-stage assurance/execution protocol as + an inert typed =SdlcFlowPlan=; this is one protocol within the full SDLC and + does not claim authorization or execution. Exit gates: @@ -190,7 +328,7 @@ Exit gates: digests and typed failures; - the installed Python wheel consumes the AOT/native artifact without Gerbil. -* Phase 5 — Assurance laws in Lean and lifecycle model in TLA+ +* Phase 5 — Assurance laws in Lean and lifecycle models in Quint Status: =evidence-partial= @@ -201,14 +339,46 @@ Lean owns general laws, including: - no stale authority; - causal modality preservation; - counterfactual isolation; -- composition creates a new obligation; +- [X] composition requires a current, independent direct obligation; - unknown or conflicted support cannot grant authority. -TLA+ owns the dynamic lifecycle: - -#+begin_example -change -> invalidate -> plan -> verify -> admit -> decide -> authorize -> effect -#+end_example +Quint models the dynamic assurance/execution protocol within the SDLC: + +#+begin_export mermaid +stateDiagram-v2 + state "Change: admitted" as Changed + state "Support: invalidated" as Invalidated + state "Obligations: planned" as Planned + state "Verification: results produced" as Verified + state "Evidence: admitted" as Admitted + state "Decision: current" as Decided + state "Effect: authorized" as Authorized + state "Execution: receipt recorded" as Effect + Changed --> Invalidated + Invalidated --> Planned + Planned --> Verified: actual verifier execution + Verified --> Admitted: trusted evidence admission + Verified --> Planned: rejected or unresolved + Admitted --> Decided: decision obligations discharged + Decided --> Authorized: current authorization + Authorized --> Effect: Runtime gate and bounded attempt + Authorized --> Invalidated: revocation or expiry + Effect --> Changed: relevant new observation +#+end_export + +A bounded Quint model in =proof/quint/AssuranceLifecycle.qnt= now checks current +admission/decision/grant authority, expiry and revocation. Separate mutant +configurations demonstrate unsafe report-only effect start, stale admission, +grant before re-verification and effect start after revocation; a reachability +configuration demonstrates a valid effect path. This is local model evidence, +not proof that the Runtime Host enforces the contract. Clock/adapter trust, +counterfactual transition authority, unconditional liveness and source-bound CI replay +remain open. + +A separate stable-environment fairness configuration now checks eventual +effect start; removing fairness produces a stuttering counterexample. This +narrows the progress assumption but does not claim progress under source +changes, clock expiry, revocation or an unavailable Host. Required model-checking counterexamples include stale cached decisions, evidence admitted after a source change, release before re-verification, @@ -224,7 +394,16 @@ Exit gates: * Phase 6 — POO Flow integration and effect gate -Status: =planned= +Status: =active= (the Host Decision preflight can now project a current, +undecided candidate into POO Flow's POO-native Cedar request family. The +request is inert and editable; Cedar evaluation, independent Host source +admission and Runtime effect gating have not landed. The generic Runtime +boundary rejects its preflight-only marker before Cedar evaluation. The +Scheme Host can recheck an exact issued request against its current source +cut, but Runtime does not yet invoke that predicate. POO Flow can now declare +an action source-admission-required in the administrative Cedar capability; +request editing cannot remove that requirement. No Aitia admitted =allow= +Decision or trusted Runtime source-admission path exists yet). Deliverables: @@ -234,12 +413,14 @@ Deliverables: Aitia into the POO Flow core bootstrap. 3. Aitia AOT Scheme exports through a dedicated C ABI using the established POO Flow Scheme-native result ownership convention. -4. Aitia's own production Python SDLC Runtime consumes that ABI through CFFI +4. Aitia's own production Python Runtime consumes that ABI through CFFI and returns typed projections. POO Flow's Python runtime remains a separate generic runtime and lifecycle transport. -5. Cedar projection from a current admitted Decision, with snapshot and - invalidation bindings. +5. [~] Cedar request projection from a current Host preflight, with Decision, + claim, policy and snapshot bindings. This is not yet a projection from an + admitted =allow= Decision; the Runtime Host must independently validate + the source cut at use before any authority can be granted. 6. Runtime Host rejection of stale, unknown, conflicted, revoked or incomplete assurance closures. @@ -252,7 +433,46 @@ Exit gates: * Phase 7 — Software/SDLC reference qualification -Status: =planned= +Status: =active= for inert design-contract replacement and snapshot-bound +implementation-mapping slices; full reference qualification remains open. + +The target is the connected lifecycle experiment in RFC 0004: compare design +alternatives, qualify reusable verification methods, link behavior and design +evolution trajectories, and reopen obligations after a premise changes. +Existing bounded results below are foundations for that experiment. + +The [[file:docs/receipts/2026-09-24-retirement-impact.org][retirement Impact analysis receipt]] adds seven Scheme qualification cases +using the existing POO Flow Graph/Temporal Causality mechanisms. The synthetic +automatic-retry case preserves old dependents and historical cuts, distinguishes +explicit current activity from past occurrence, and retains unknown consumers +and future/out-of-horizon work. Candidate graph edits are not migration receipts. +Real persisted observations, derived migration duties and authorized execution +remain pending; the lifecycle retirement gate is not closed. + +The first job-executor regression now records an ADR-bound POO-native design +contract with assumptions, guarantees, state ownership, effects and variation +points. A retry-policy revision reopens the unique-effect guarantee; the +existing SDLC impact projection uses POO Flow Graph to select its verification +trajectory. Stronger assumptions, weakened guarantees, new effects, changed +decision and same-revision mutation have negative tests. This is a pure +declaration comparison, not a behavioral implication proof, admitted verifier +receipt or release decision. A follow-on mapping now binds each guarantee to +exact source artifact and Assurance snapshot revisions/digests. It fails closed +on source/snapshot changes and uncovered guarantees, but it does not establish +semantic model-to-code conformance or independent acceptance. A Host support +projection now requires the guarantee Claim, its declared source dependency, +an exact source input in a current Host admission, and the Claim's full support +set. This remains conditional on the configured Host and is not a proof of +verifier independence or deployment trust. + +The Python project's first installed-wheel qualification now runs a real +worker process twice against a persistent task ledger and loopback provider. +An intentionally regenerated retry key reproduces two commits after a lost +acknowledgement; a stable logical-operation key yields one commit and eventual +task completion. [[file:docs/adrs/0008-stable-operation-key-after-lost-ack.org][ADR 0008]] proposes widening the guarantee from +"per key" to "per logical operation" and the design-replacement test reopens +the affected verification. This is known-fault behavioral evidence, not a +Host-issued seal or proof of a production provider's idempotency. The first reference case is a software change crossing requirement, implementation, verification, release-decision and effect boundaries. It @@ -297,20 +517,74 @@ domain branch and the public API remains POO-native. * Immediate work queue -These are the next bounded changes after Phase 0 review: - -1. [X] Add the repository contribution policy under the confirmed license contract. -2. [X] Record the exact SDLC source/test/tool/profile inventory and current receipts. -3. Move the SDLC and GitOps owners into Aitia and close the published Episteme - deletion plus parent pin; Aitia-side source and parity are complete. -4. [X] Add the five-role =modules/assurance/= skeleton with declarations only. -5. [X] Define the versioned vocabulary manifest and schema identifiers. -6. [X] Implement relation-plane validation and its negative test matrix. -7. [X] Implement canonical identity/digest projection. -8. [X] Define the Aitia Scheme-native C ABI and pure-C harness with an unversioned - public namespace and explicit ABI revision metadata. -9. [X] Complete Aitia's =bindings/python= CFFI consumer and wheel receipt. -10. [X] Create the smallest change-to-invalidation fixture with no verifier calls. - -No Python semantic duplicate, Cedar policy, proof migration or effect execution -belongs in this immediate queue. +The earlier migration, nominal vocabulary, snapshot/invalidation and CFFI +foundation work is recorded in the phases above. The current queue is: + +Immediate priority (2026-10-10): defer new persistence integration. First +challenge the design with source-reviewed upstream failure reports and actual +formal checks. [[file:proof/quint/README.org][Quint design checks]] separate work/cancellation, retirement review, ingress and +the existing assurance lifecycle; [[file:docs/receipts/2026-09-24-retirement-formal-review.org][the formal review receipt]] records bounded +exploration and Lean laws, not Runtime closure. Historical +[[file:docs/receipts/2026-09-24-formal-source-qualification.org][TLA+ source qualification]] is retained as a receipt only. The maintained +Quint lane checks source syntax through Quint and declared POO Contract/source-cut +invalidation. The local retirement Host and its execution test now check a +bounded Scheme behavior cut, including a real callback and negative cases; +the source binding covers Lean, Quint and Scheme. Independent replay, broader +semantic correspondence and a refinement argument remain open. +The [[file:docs/receipts/2026-09-24-retirement-ingress-review.org][bounded ingress composition]] now admits post-retirement requests and +checks epoch fencing, with two unsafe mutants; deployed authority/Runtime +correspondence, new grant issuance and eventual progress remain unqualified. +Extract admission, +revocation and grant contracts from the existing lifecycle model with atomic +counterexamples; retain the whole-protocol regression. Do not resume storage +work merely because these design-level tests pass. + +1. [ ] Extend the job-executor design case with a common requirement set and + comparable alternatives, keeping completion and unique-effect obligations. + Historical-source applicability and the owner/coverage audit are documented + in [[file:docs/plans/engineering-case-source-inventory.org][the case-source inventory]] and ADR 0008 r2. Parameter mismatch, retention, + independent commit/crash and concurrent-attempt checks remain unqualified. + Reuse existing Cedar/Runtime mechanisms for authorization; keep provider + effect uniqueness separate. Add an executable source-correction/review case + before claiming historical-judgment invalidation is complete. +2. [ ] Qualify one asset-complete upstream bug case. Record files, exact + revisions, licenses, dependencies, resource bounds and required upstream + checks in the integration inventory. Do not add a TLA+/TLAPS execution lane. +3. [ ] Project experimental behavior and design/assurance evolution through + POO Flow temporal/causal mechanisms. Check modality, causal cuts, late + observations and missing parents without turning structural paths into + causal proof. + Pair the repair case with one capability-retirement experiment: bind old + persisted results, current/in-flight consumers and queued work to pre/post + snapshots. Preserve historical identity, distinguish old-but-active state + from past occurrence, and derive migration/drain obligations with witnesses. + Include an unrelated negative control and unknown external consumers; + disappearance from the new graph must not imply safe deletion. Close only + with the scoped qualification and effect receipts required by RFC 0004. + The [[file:docs/receipts/2026-09-24-retirement-impact.org][first bounded analysis slice]] passes; actual persistent-state and effect + qualification remains open and is deferred behind the formal design gates. +4. [ ] Connect real tool observations from the Aitia Python Runtime to the + configured Scheme Host, preserving source bytes and native-session ownership + through the AnyIO execution boundary. This is required before claiming + admitted support or progressing to the remaining Phase 6 effect gates. + The bounded frozen-pytest AnyIO worker now waits for process-group cleanup + under cancellation and never carries a thread-affine native session; it + remains an observation-only slice, not Host admission. The installed-wheel + qualification also exercises a real native session before and after that + worker, with the lost-ACK failure/fix and stale-source rejection. The next + gate is an owner-preserving Host operation and opaque capability transport, + followed by source-change, expiry, revocation and forged-receipt negatives. +5. [ ] Compose the qualified methods through existing POO Profiles and slots; + an override must preserve required checks or reopen corresponding obligations. +6. [ ] Replay failure, design revision, re-verification and a later assumption + change as one lifecycle case. Compare with the same upstream tools/assets + alone, and record explanation quality, missed obligations, unnecessary + reruns and execution costs. +7. [ ] Qualify one contract-preserving implementation optimization with both + conformance and performance evidence. + +Euclid/GCD is a candidate tool qualification, not evidence for an unrelated +job-executor claim. Known-fault replay is not novel bug discovery. Full Specula +CLI lifecycle mapping, new checker implementations and MRR integration are not +required deliverables in this queue. Phase 4/6/7 closure still requires actual +Host and Runtime receipts rather than a documentation or Recipe-only milestone. diff --git a/bindings/c/aitia-contract.ss b/bindings/c/aitia-contract.ss index 50f186a..dabed19 100644 --- a/bindings/c/aitia-contract.ss +++ b/bindings/c/aitia-contract.ss @@ -67,7 +67,9 @@ (def (json-check object repository revision) (github-check (json-symbol object "name") repository revision - (json-symbol object "conclusion"))) + (json-symbol object "conclusion") + standard-edition: (hash-get object "standardEdition") + source-lock-digest: (hash-get object "sourceLockDigest"))) (def (symbols->json values) (list->vector (map symbol->string values))) diff --git a/bindings/c/aitia-native.ss b/bindings/c/aitia-native.ss index 0062f96..0afb03b 100644 --- a/bindings/c/aitia-native.ss +++ b/bindings/c/aitia-native.ss @@ -7,6 +7,9 @@ (import (only-in :std/ffi C-declare C-ffi-macrology def-C-lambda def-C-type) + ;; Orgize owns the Scheme implementation and public C symbols. Keep + ;; this module in Aitia's native link closure without copying its ABI. + :orgize/bindings/c/orgize :poo-flow/lambda-aitia/bindings/c/aitia-contract) (export aitia-c-round-trip) diff --git a/bindings/c/aitia-runtime.c b/bindings/c/aitia-runtime.c index b51f114..0c5d1d4 100644 --- a/bindings/c/aitia-runtime.c +++ b/bindings/c/aitia-runtime.c @@ -6,6 +6,7 @@ #include "gambit.h" #include +#include #ifndef POO_FLOW_AITIA_LINKER #error "POO_FLOW_AITIA_LINKER must name the generated Gambit link-unit linker" @@ -28,7 +29,11 @@ int32_t poo_flow_aitia_runtime_init(void) { /* Embedded callers must report uncaught Scheme errors without a REPL. */ setup_params.debug_settings = ___DEBUG_SETTINGS_INITIAL; status = ___setup(&setup_params); - if (status != ___FIX(___NO_ERR)) return -1; + if (status != ___FIX(___NO_ERR)) { + (void)fprintf(stderr, "[lambda-aitia-native] setup-error=%ld\n", + (long)___INT(status)); + return -1; + } poo_flow_aitia_runtime_initialized = 1; return 0; } diff --git a/bindings/python/README.md b/bindings/python/README.md index 2aff181..b55f600 100644 --- a/bindings/python/README.md +++ b/bindings/python/README.md @@ -3,26 +3,119 @@ SPDX-FileCopyrightText: 2026 tao3k team and Contributors SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later --> -# Lambda Aitia Python SDLC Runtime +# Lambda Aitia Python Runtime This is Lambda Aitia's production Python project, not an example package. It -is the Python runtime surface for the complete SDLC lifecycle: +is the Python runtime surface for Aitia across the complete SDLC: requirements, +design, algorithms and implementation strategies, testing, delivery, operation +and evolution. The first planning API exposes the assurance/execution protocol +used within that lifecycle: -```text -change -> invalidate -> plan -> verify -> admit -> decide -> authorize -> effect +```mermaid +flowchart TD + C["change
identified"] --> I["invalidate
dependent support stale"] + I --> P["plan
obligations pending"] + P --> V["verify
candidate results produced"] + V --> A["admit
evidence accepted within scope"] + A --> D["decide
decision current"] + D --> U["authorize
bounded effect permitted"] + U --> E["effect
execution receipt recorded"] + V -->|failed or unresolved| P + A -->|relevant premise changes| I + U -->|revoked or expired| I ``` +These are semantic workflow states, not implementation-progress labels. +Feedback edges describe lifecycle reassessment, not edges returned by the +current API. The API projects only an inert, ordered eight-stage DAG, not +executed transitions. Admission, decision, authorization and execution each +require their own checked receipt. + Lambda Aitia Scheme owns the lifecycle vocabulary, assurance semantics, decisions and receipts. POO Flow owns the reusable Graph/DAG and runtime mechanisms. The Python package consumes those contracts through the stable C ABI and does not reimplement either owner's semantics. -The current production surface publishes and validates the canonical lifecycle +AnyIO is a production dependency for the runtime-wide asynchronous execution +boundary: structured task lifetimes, cancellation and bounded concurrency will +apply across all Aitia capabilities. The current native session is +thread-affine, so it must not be passed into `anyio.to_thread.run_sync`; an async +*native-session* entry point requires a qualified owner-preserving transport +before it can claim execution. + +`lambda_aitia.verification.run_pytest_frozen_async` now runs the bounded pytest +observer in an AnyIO worker, with an optional capacity limiter. Cancellation +waits for its timeout-bound process-group cleanup; it never abandons pytest or +passes an `AitiaNativeSession` to that worker. This returns a source-bound +observation only, not a Scheme Host admission or authorization. + +The current production surface publishes and validates the assurance-flow DAG through `SdlcRuntime.plan()`. The returned receipt is deliberately inert: it cannot claim release authorization or runtime execution. Stage execution is added only as each real Scheme operation and attributable receipt becomes available; the package will never simulate completion with Python callbacks. +[RFC 0004](../../docs/rfcs/0004-sdlc-design-and-causal-trajectories.org) defines +the connected design and trajectory qualification. POO Flow owns generic +temporal/causal mechanisms; this project integrates actual tool execution and +scoped observations through its native boundary. Reusable upstream checker, +model and harness qualification is pending. MRR integration is deferred beyond +the current Python stage. + +`lambda_aitia.verification.run_pytest` is a production subprocess observation +surface, not a Host-issued admission. It preserves exit status, selected test +count, JUnit digest and source-tree digest; zero collected tests, skipped-only +runs, timeouts, missing reports and source drift do not pass its obligation. +It captures a bounded, canonical source-byte payload and executes pytest only +from a separately materialized copy; selectors cannot escape that copy. +`capture_source_tree` and `run_pytest_frozen` expose the same payload boundary +for a future Host-owned source reader. This still does not prove repository +provenance, interpreter independence, or trusted external services. The +Assurance Host owns admission, revocation and execution authority. + +`prepare_org_edit_review` is the Aitia-owned preflight for an Org-native +engineering workspace. It accepts a node ID, byte span and expected old bytes +from an upstream Orgize projection, checks them against the exact projected +source digest, rejects stale, moved, duplicate or overlapping edits, and +returns an inert owner-review proposal. It does not parse Org, apply edits or +grant authorization. The existing Orgize PR #15 now supplies a Scheme-AOT +graph-backed source transformation that rechecks node ownership and produces +candidate text. Aitia-to-Orgize transport and owner-approved persistence are +still pending; this Python API does not claim those steps. + +```python +from lambda_aitia import OrgNodeEdit, prepare_org_edit_review + +review = prepare_org_edit_review( + source_path, + source_id="design/decision.org", + projected_source_digest=orgize_projection.source_digest, + edits=(OrgNodeEdit(node_id, start_byte, end_byte, expected_old, replacement),), +) +assert review.source_current(source_path) # freshness only; not approval +``` + +The POSIX verifier runs pytest in its own process group and stops that group +on timeout or output overflow. `max_output_bytes` defaults to 8 MiB; only the +last 8 KiB is retained in the observation. Cleanup uncertainty is reported as +`control-error`, never as a passing result. + +`AitiaNativeSession` keeps one Gambit transport runtime alive across calls to +`SdlcRuntime(session=...)`, `evaluate_gitops(..., session=...)`, and +`evaluate_org_contract(..., session=...)`. It is +process-exclusive and thread-affine; use it as a context manager. It does not +issue Host verification seals or admission capabilities. A separate Scheme +Host operation and opaque ABI handles are still required before Python +observations can be admitted. + +`evaluate_org_contract` calls Orgize's versioned C ABI from the same initialized +native link unit. The input is a sequence of `OrgElementFact` values already +projected by the source owner, not raw Org text. Orgize owns Element graph +queries and Contract evaluation; Aitia does not parse Org or treat a matching +contract as authenticated provenance, review, admission, or effect authority. +The binary row/result layout and revision are defined in Orgize's +`bindings/c/include/orgize.h`. Aitia checks the revision before each call. + ```python from lambda_aitia import SdlcRuntime @@ -36,22 +129,35 @@ Builds require `LAMBDA_AITIA_NATIVE_LIBRARY` to identify the qualified native library that will be bundled into the wheel. The binding is an independent uv project owned entirely by Lambda Aitia. From -this directory, synchronize and test the locked environment with: +the repository root, refresh the generated CFFI extension and check its current +Orgize row type with: ```sh -env -u SDKROOT uv sync --locked --extra test -env -u SDKROOT uv run --locked --extra test pytest +just dev-python ``` -Build the distributable wheel after producing the Scheme-native library: +To remove only stale generated CFFI modules and their temporary C objects: ```sh -LAMBDA_AITIA_NATIVE_LIBRARY=/absolute/path/to/libpoo_flow_aitia.dylib \ - env -u SDKROOT uv sync --locked --extra test -LAMBDA_AITIA_NATIVE_LIBRARY=/absolute/path/to/libpoo_flow_aitia.dylib \ - env -u SDKROOT uv build --wheel --offline --no-build-isolation +just clean-python-cffi ``` +`dev-python` requires the declared POO Flow/Gerbil dependencies, including +Orgize's C header, in the selected `GERBIL_PATH`. It cleans and rebuilds only +the in-place CFFI extension, without compiling POO Flow or replacing the native +library. Run `just check-native` against the qualified POO Flow version for +the complete Scheme/native/Python and wheel qualification. +The `justfile` removes `SDKROOT` only on Darwin; it does not alter that +environment variable on Linux. + +`just check-native` also installs that wheel in a fresh uv environment outside +the repository, runs the native plan, reproduces a lost-acknowledgement bug in +two real worker processes and a loopback HTTP service, checks the corrected +worker through the AnyIO frozen-source verifier while the main thread retains +one native session, then rejects the old observation after a source edit. This +is a known fault-injection qualification case, not a newly discovered +production defect or an Assurance Host seal. + On Darwin, the repository `justfile` selects `/usr/bin/cc`, unsets `SDKROOT` for Gerbil invocations, and keeps the deployment metadata consistent with the installed Gerbil/Gambit native objects. The large Gambit closure uses DWARF diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml index 897797b..e44a773 100644 --- a/bindings/python/pyproject.toml +++ b/bindings/python/pyproject.toml @@ -9,12 +9,12 @@ build-backend = "setuptools.build_meta" [project] name = "lambda-aitia" version = "0.1.0" -description = "Production Python runtime for Lambda Aitia's Scheme-owned SDLC" +description = "Production Python runtime for Lambda Aitia" readme = "README.md" requires-python = ">=3.11" license = "Apache-2.0 AND LGPL-2.1-or-later" license-files = ["LICENSE"] -dependencies = ["cffi>=2.0"] +dependencies = ["anyio>=4.14.1", "cffi>=2.0"] [project.optional-dependencies] test = ["pytest>=8.0", "setuptools>=77"] diff --git a/bindings/python/qualification/job_executor/README.org b/bindings/python/qualification/job_executor/README.org new file mode 100644 index 0000000..235cb97 --- /dev/null +++ b/bindings/python/qualification/job_executor/README.org @@ -0,0 +1,59 @@ +# SPDX-FileCopyrightText: 2026 tao3k team and Contributors +# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +#+title: Python job-executor qualification case + +This case belongs to the production =bindings/python= uv project. Its worker +is a real child process, its task ledger and provider effect log are SQLite +files, and the effect provider is a loopback HTTP server. The service commits +once, withholds the first acknowledgement, and lets a restarted worker retry. + +The =buggy= strategy generates a new provider idempotency key after restart: +the test must fail with two commits. The =fixed= strategy retains a key derived +from the logical operation: the same test must pass with one commit and a +completed task. This is a deliberate known-fault replay, not a novel bug. + +The installed-wheel project test copies the scenario outside the checkout, +installs the built wheel and locked dependencies in a new uv environment, and +runs both strategies through =lambda_aitia.verification.run_pytest=. It also +changes one source file after the passing observation and requires that +=source_current= reject the old observation. The observation is not a Scheme +Host admission. Pytest runs from captured source bytes in a fresh directory, +but repository provenance, Scheme Host admission, independent provider trust, +and a Runtime effect gate remain open. + +The case challenges the original guarantee's *per key* scope: a provider can +honor that scope while a logical operation is duplicated with two keys. +[[file:../../../../docs/adrs/0008-stable-operation-key-after-lost-ack.org][ADR 0008]] +proposes a revised logical-operation guarantee and keeps its acceptance and +re-verification separate from this test result. + +* Coverage boundary + +The worker has one fixed job and constant payload; the provider's single SQLite +row represents both the deduplication key and the effect. This is not a model +of multi-tenant identity, changed intent, retention expiry, concurrent attempts +or atomicity across a ledger and an independent external service. Fixed mode +repairs the injected identity fault, not every idempotency failure mode. + +ADR 0008's r2 coverage matrix assigns those remaining qualifications to their +owners. Existing POO Flow Cedar authorization and Runtime effect mechanisms +must be reused, not reimplemented in this fixture. A Cedar permit answers an +authorization question; it does not establish provider commit uniqueness or +resolve an unknown outcome after a lost acknowledgement. + +* Focused replay receipt — 2026-09-24 + +The unchanged worker/test sources at Aitia commit +=f3438241780e863db6c0b518d86628b540f96cd1= were rerun during the ADR r2 audit. +From =bindings/python=, =uv run --locked --no-sync= ran +=pytest qualification/job_executor/test_unique_effect.py= with a process-scoped +=AITIA_QUALIFICATION_MODE=: + +- =fixed=: one test passed; one commit and task completion were asserted. +- =buggy=: the expected negative test failed specifically at =committed == 1=, + observing two commits, rather than failing to start the worker/provider. + +This was a focused source-fixture replay in the existing uv environment, not a +new installed-wheel qualification or Cedar/Runtime execution receipt. The new +owner matrix does not promote untested boundaries to passing evidence. diff --git a/bindings/python/qualification/job_executor/test_unique_effect.py b/bindings/python/qualification/job_executor/test_unique_effect.py new file mode 100644 index 0000000..77fedc6 --- /dev/null +++ b/bindings/python/qualification/job_executor/test_unique_effect.py @@ -0,0 +1,72 @@ +# SPDX-FileCopyrightText: 2026 tao3k team and Contributors +# +# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +"""Project qualification of acknowledgement loss in real worker processes.""" + +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +import os +from pathlib import Path +import sqlite3 +import subprocess +import sys +import tempfile +from threading import Thread + + +def test_unique_effect_after_lost_ack_and_restart() -> None: + mode = os.environ["AITIA_QUALIFICATION_MODE"] + with tempfile.TemporaryDirectory(prefix="aitia-worker-state-") as directory: + state = Path(directory) + effects = state / "effects.sqlite" + ledger = state / "tasks.sqlite" + + class EffectHandler(BaseHTTPRequestHandler): + def do_POST(self) -> None: + key = self.headers.get("Idempotency-Key") + assert key + with sqlite3.connect(effects) as database: + database.execute( + "create table if not exists effects " + "(key text primary key, payload blob not null)" + ) + previous = database.execute( + "select count(*) from effects" + ).fetchone()[0] + database.execute( + "insert or ignore into effects values (?, ?)", + (key, self.rfile.read(int(self.headers["Content-Length"]))), + ) + if previous == 0: + # The service committed; the worker never receives its ACK. + self.close_connection = True + return + self.send_response(200) + self.end_headers() + + def log_message(self, format: str, *args: object) -> None: + pass + + server = ThreadingHTTPServer(("127.0.0.1", 0), EffectHandler) + thread = Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + endpoint = f"http://127.0.0.1:{server.server_port}/effect" + command = (sys.executable, str(Path(__file__).with_name("worker.py")), + str(ledger), endpoint, mode) + first = subprocess.run(command, check=False, timeout=10) + second = subprocess.run(command, check=False, timeout=10) + assert first.returncode == 2 + assert second.returncode == 0 + with sqlite3.connect(effects) as database: + committed = database.execute("select count(*) from effects").fetchone()[0] + with sqlite3.connect(ledger) as database: + final_state = database.execute( + "select state from tasks where job = 'job-42'" + ).fetchone()[0] + assert committed == 1 + assert final_state == "complete" + finally: + server.shutdown() + server.server_close() + thread.join(timeout=5) diff --git a/bindings/python/qualification/job_executor/worker.py b/bindings/python/qualification/job_executor/worker.py new file mode 100644 index 0000000..dc8ab9d --- /dev/null +++ b/bindings/python/qualification/job_executor/worker.py @@ -0,0 +1,41 @@ +# SPDX-FileCopyrightText: 2026 tao3k team and Contributors +# +# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +"""Qualification worker with a persistent task ledger and HTTP side effect.""" + +import sqlite3 +import sys +from urllib.request import ProxyHandler, Request, build_opener +from uuid import uuid4 + + +def execute(ledger_path: str, endpoint: str, mode: str) -> int: + if mode not in ("buggy", "fixed"): + raise ValueError("unknown worker mode") + with sqlite3.connect(ledger_path) as ledger: + ledger.execute( + "create table if not exists tasks (job text primary key, state text not null)" + ) + ledger.execute("insert or ignore into tasks values ('job-42', 'pending')") + state = ledger.execute( + "select state from tasks where job = 'job-42'" + ).fetchone()[0] + if state == "complete": + return 0 + # Bug: regenerating the provider key after restart creates a second effect. + key = "job-42" if mode == "fixed" else uuid4().hex + request = Request(endpoint, data=b"commit", headers={"Idempotency-Key": key}) + try: + with build_opener(ProxyHandler({})).open(request, timeout=3) as response: + if response.status != 200: + return 2 + except Exception: + return 2 + with sqlite3.connect(ledger_path) as ledger: + ledger.execute("update tasks set state = 'complete' where job = 'job-42'") + return 0 + + +if __name__ == "__main__": + raise SystemExit(execute(sys.argv[1], sys.argv[2], sys.argv[3])) diff --git a/bindings/python/src/lambda_aitia/__init__.py b/bindings/python/src/lambda_aitia/__init__.py index 93da0ed..05c02b0 100644 --- a/bindings/python/src/lambda_aitia/__init__.py +++ b/bindings/python/src/lambda_aitia/__init__.py @@ -2,17 +2,26 @@ # # SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later -"""Production Python runtime for Lambda Aitia's Scheme-owned SDLC.""" +"""Production Python runtime for Lambda Aitia.""" from .gitops import GitOpsDecision, evaluate_gitops -from .native import AitiaNativeError, descriptor +from .native import AitiaNativeError, AitiaNativeSession, descriptor +from .org_contract import OrgContractResult, OrgElementFact, evaluate_org_contract +from .org_workspace import OrgEditReview, OrgNodeEdit, prepare_org_edit_review from .runtime import SdlcFlowPlan, SdlcRuntime __all__ = [ "AitiaNativeError", + "AitiaNativeSession", "GitOpsDecision", + "OrgContractResult", + "OrgElementFact", + "OrgEditReview", + "OrgNodeEdit", "SdlcFlowPlan", "SdlcRuntime", "descriptor", "evaluate_gitops", + "evaluate_org_contract", + "prepare_org_edit_review", ] diff --git a/bindings/python/src/lambda_aitia/_native/_build.py b/bindings/python/src/lambda_aitia/_native/_build.py index 96aed4a..d150683 100644 --- a/bindings/python/src/lambda_aitia/_native/_build.py +++ b/bindings/python/src/lambda_aitia/_native/_build.py @@ -5,12 +5,26 @@ from __future__ import annotations from pathlib import Path +import os from cffi import FFI ffibuilder = FFI() ffibuilder.cdef( """ + typedef struct lambda_aitia_python_session lambda_aitia_python_session; + lambda_aitia_python_session *lambda_aitia_python_open( + const char *library_path, char *error, size_t error_capacity); + void lambda_aitia_python_close(lambda_aitia_python_session *session); + int lambda_aitia_python_session_call( + lambda_aitia_python_session *session, + const char *operation, + const uint8_t *payload, + size_t payload_length, + uint8_t **output, + size_t *output_length, + char *error, + size_t error_capacity); int lambda_aitia_python_call( const char *library_path, const char *operation, @@ -21,19 +35,47 @@ char *error, size_t error_capacity); void lambda_aitia_python_release(uint8_t *output); + typedef struct { + int64_t id; + int64_t parent_id; + const char *kind; + const char *field_name; + const char *field_value; + } orgize_element_row; + typedef struct { + int32_t status; + uint32_t matched_count; + int32_t passed; + } orgize_contract_result; + int lambda_aitia_python_session_org_contract( + lambda_aitia_python_session *session, const orgize_element_row *rows, + uint32_t row_count, int64_t scope_id, const char *query_kind, + const char *field_name, const char *field_value, + uint32_t expectation, uint32_t expected_count, + orgize_contract_result *result, char *error, size_t error_capacity); """ ) native_dir = Path(__file__).resolve().parent c_binding_dir = native_dir.parents[3] / "c" +gerbil_path = os.environ.get("GERBIL_PATH") +if gerbil_path is None: + raise RuntimeError("GERBIL_PATH must identify the installed Orgize dependency") +orgize_c_dir = ( + Path(gerbil_path).resolve() + / "pkg/github.com/tao3k/orgize/bindings/c/include" +) +if not (orgize_c_dir / "orgize.h").is_file(): + raise RuntimeError("Orgize's public C header is absent; install POO Flow dependencies") ffibuilder.set_source( "lambda_aitia._native._aitia_cffi", '#include "aitia_shim.c"', - include_dirs=[str(native_dir), str(c_binding_dir / "include")], + include_dirs=[str(native_dir), str(c_binding_dir / "include"), str(orgize_c_dir)], ) if __name__ == "__main__": import sys + import sysconfig python_project = Path(__file__).resolve().parents[3] if str(python_project) not in sys.path: @@ -43,6 +85,8 @@ sanitize_python_linker_config() ffibuilder.compile( tmpdir=str(native_dir / "_build_temp"), - target=str(native_dir / "_aitia_cffi.*"), + target=str( + native_dir / ("_aitia_cffi" + sysconfig.get_config_var("EXT_SUFFIX")) + ), verbose=True, ) diff --git a/bindings/python/src/lambda_aitia/_native/aitia_shim.c b/bindings/python/src/lambda_aitia/_native/aitia_shim.c index e0b80b3..c517c3b 100644 --- a/bindings/python/src/lambda_aitia/_native/aitia_shim.c +++ b/bindings/python/src/lambda_aitia/_native/aitia_shim.c @@ -3,7 +3,9 @@ // SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later #include "lambda_aitia/aitia.h" +#include "orgize.h" +#include #include #include #include @@ -31,6 +33,24 @@ typedef void (*lambda_aitia_result_init_fn)(poo_flow_aitia_result *); typedef void (*lambda_aitia_result_release_fn)(poo_flow_aitia_result *); typedef int32_t (*lambda_aitia_descriptor_fn)(poo_flow_aitia_result *); typedef int32_t (*lambda_aitia_evaluate_fn)(char *, poo_flow_aitia_result *); +typedef uint32_t (*orgize_revision_fn)(void); +typedef int32_t (*orgize_evaluate_fn)(const orgize_element_row *, uint32_t, + int64_t, const char *, const char *, + const char *, uint32_t, uint32_t, + orgize_contract_result *); + +typedef struct lambda_aitia_python_session { + lambda_aitia_library library; + lambda_aitia_runtime_shutdown_fn runtime_shutdown; + lambda_aitia_result_init_fn result_init; + lambda_aitia_result_release_fn result_release; + lambda_aitia_descriptor_fn descriptor; + lambda_aitia_evaluate_fn sdlc_flow_plan; + lambda_aitia_evaluate_fn evaluate; +} lambda_aitia_python_session; + +/* Gambit has one process runtime. A live session excludes transient calls. */ +static atomic_flag lambda_aitia_runtime_lease = ATOMIC_FLAG_INIT; static int lambda_aitia_load_symbol(lambda_aitia_library library, const char *name, void *destination, @@ -57,121 +77,187 @@ static const char *lambda_aitia_loader_error(void) { #endif } -void lambda_aitia_python_release(uint8_t *output) { free(output); } - -int lambda_aitia_python_call(const char *library_path, const char *operation, - const uint8_t *payload, size_t payload_length, - uint8_t **output, size_t *output_length, - char *error, size_t error_capacity) { - lambda_aitia_library library; +lambda_aitia_python_session *lambda_aitia_python_open( + const char *library_path, char *error, size_t error_capacity) { + lambda_aitia_python_session *session; lambda_aitia_revision_fn revision; lambda_aitia_runtime_init_fn runtime_init; - lambda_aitia_runtime_shutdown_fn runtime_shutdown; - lambda_aitia_result_init_fn result_init; - lambda_aitia_result_release_fn result_release; - lambda_aitia_descriptor_fn descriptor; - lambda_aitia_evaluate_fn sdlc_flow_plan; - lambda_aitia_evaluate_fn evaluate; - poo_flow_aitia_result result; - char *input = NULL; - int32_t status; - if (output == NULL || output_length == NULL || library_path == NULL || - operation == NULL) { - lambda_aitia_write_error(error, error_capacity, "invalid Aitia call"); - return -100; + if (library_path == NULL || library_path[0] == '\0') { + lambda_aitia_write_error(error, error_capacity, "invalid Aitia library path"); + return NULL; } - *output = NULL; - *output_length = 0; - library = LAMBDA_AITIA_OPEN(library_path); - if (library == NULL) { + if (atomic_flag_test_and_set(&lambda_aitia_runtime_lease)) { + lambda_aitia_write_error(error, error_capacity, + "Lambda Aitia runtime already has an active session"); + return NULL; + } + session = (lambda_aitia_python_session *)calloc(1, sizeof(*session)); + if (session == NULL) { + lambda_aitia_write_error(error, error_capacity, "session allocation failed"); + goto fail_lease; + } + session->library = LAMBDA_AITIA_OPEN(library_path); + if (session->library == NULL) { lambda_aitia_write_error(error, error_capacity, lambda_aitia_loader_error()); - return -101; + goto fail_session; } - if (!lambda_aitia_load_symbol(library, "poo_flow_aitia_abi_revision", + if (!lambda_aitia_load_symbol(session->library, "poo_flow_aitia_abi_revision", &revision, sizeof(revision)) || - !lambda_aitia_load_symbol(library, "poo_flow_aitia_runtime_init", + !lambda_aitia_load_symbol(session->library, "poo_flow_aitia_runtime_init", &runtime_init, sizeof(runtime_init)) || - !lambda_aitia_load_symbol(library, "poo_flow_aitia_runtime_shutdown", - &runtime_shutdown, - sizeof(runtime_shutdown)) || - !lambda_aitia_load_symbol(library, "poo_flow_aitia_result_init", - &result_init, sizeof(result_init)) || - !lambda_aitia_load_symbol(library, "poo_flow_aitia_result_release", - &result_release, sizeof(result_release)) || - !lambda_aitia_load_symbol(library, "poo_flow_aitia_descriptor", - &descriptor, sizeof(descriptor)) || - !lambda_aitia_load_symbol(library, "poo_flow_aitia_sdlc_flow_plan", - &sdlc_flow_plan, sizeof(sdlc_flow_plan)) || - !lambda_aitia_load_symbol(library, "poo_flow_aitia_gitops_evaluate", - &evaluate, sizeof(evaluate))) { + !lambda_aitia_load_symbol(session->library, "poo_flow_aitia_runtime_shutdown", + &session->runtime_shutdown, + sizeof(session->runtime_shutdown)) || + !lambda_aitia_load_symbol(session->library, "poo_flow_aitia_result_init", + &session->result_init, sizeof(session->result_init)) || + !lambda_aitia_load_symbol(session->library, "poo_flow_aitia_result_release", + &session->result_release, + sizeof(session->result_release)) || + !lambda_aitia_load_symbol(session->library, "poo_flow_aitia_descriptor", + &session->descriptor, sizeof(session->descriptor)) || + !lambda_aitia_load_symbol(session->library, "poo_flow_aitia_sdlc_flow_plan", + &session->sdlc_flow_plan, + sizeof(session->sdlc_flow_plan)) || + !lambda_aitia_load_symbol(session->library, "poo_flow_aitia_gitops_evaluate", + &session->evaluate, sizeof(session->evaluate))) { lambda_aitia_write_error(error, error_capacity, "Lambda Aitia ABI symbol is absent"); - LAMBDA_AITIA_CLOSE(library); - return -102; + goto fail_library; } if (runtime_init() != 0) { lambda_aitia_write_error(error, error_capacity, "Lambda Aitia runtime initialization failed"); - LAMBDA_AITIA_CLOSE(library); - return -103; + goto fail_library; } if (revision() != POO_FLOW_AITIA_ABI_REVISION) { lambda_aitia_write_error(error, error_capacity, "Lambda Aitia ABI revision mismatch"); - runtime_shutdown(); - LAMBDA_AITIA_CLOSE(library); - return -104; + session->runtime_shutdown(); + goto fail_library; + } + return session; + +fail_library: + LAMBDA_AITIA_CLOSE(session->library); +fail_session: + free(session); +fail_lease: + atomic_flag_clear(&lambda_aitia_runtime_lease); + return NULL; +} + +void lambda_aitia_python_close(lambda_aitia_python_session *session) { + if (session == NULL) return; + session->runtime_shutdown(); + LAMBDA_AITIA_CLOSE(session->library); + free(session); + atomic_flag_clear(&lambda_aitia_runtime_lease); +} + +void lambda_aitia_python_release(uint8_t *output) { free(output); } + +int lambda_aitia_python_session_call( + lambda_aitia_python_session *session, const char *operation, + const uint8_t *payload, size_t payload_length, uint8_t **output, + size_t *output_length, char *error, size_t error_capacity) { + poo_flow_aitia_result result; + char *input = NULL; + int32_t status; + + if (session == NULL || output == NULL || output_length == NULL || + operation == NULL) { + lambda_aitia_write_error(error, error_capacity, "invalid Aitia call"); + return -100; } - result_init(&result); + *output = NULL; + *output_length = 0; + session->result_init(&result); if (strcmp(operation, "descriptor") == 0) { - status = descriptor(&result); + status = session->descriptor(&result); } else if (strcmp(operation, "sdlc-flow-plan") == 0 || strcmp(operation, "gitops-evaluate") == 0) { if (payload == NULL || payload_length == 0 || payload_length == SIZE_MAX) { lambda_aitia_write_error(error, error_capacity, "Aitia operation requires JSON input"); - result_release(&result); - runtime_shutdown(); - LAMBDA_AITIA_CLOSE(library); - return -105; + status = -105; + goto done; } input = (char *)malloc(payload_length + 1u); if (input == NULL) { lambda_aitia_write_error(error, error_capacity, "input allocation failed"); - result_release(&result); - runtime_shutdown(); - LAMBDA_AITIA_CLOSE(library); - return -106; + status = -106; + goto done; } memcpy(input, payload, payload_length); input[payload_length] = '\0'; status = strcmp(operation, "sdlc-flow-plan") == 0 - ? sdlc_flow_plan(input, &result) - : evaluate(input, &result); - free(input); + ? session->sdlc_flow_plan(input, &result) + : session->evaluate(input, &result); } else { lambda_aitia_write_error(error, error_capacity, "unknown Aitia operation"); - result_release(&result); - runtime_shutdown(); - LAMBDA_AITIA_CLOSE(library); - return -107; + status = -107; + goto done; } if (result.payload != NULL && result.length > 0) { *output = (uint8_t *)malloc(result.length); if (*output == NULL) { - result_release(&result); lambda_aitia_write_error(error, error_capacity, "output allocation failed"); - runtime_shutdown(); - LAMBDA_AITIA_CLOSE(library); - return -108; + status = -108; + goto done; } memcpy(*output, result.payload, result.length); *output_length = result.length; } - result_release(&result); - runtime_shutdown(); - LAMBDA_AITIA_CLOSE(library); + +done: + free(input); + session->result_release(&result); + return status; +} + +int lambda_aitia_python_session_org_contract( + lambda_aitia_python_session *session, const orgize_element_row *rows, + uint32_t row_count, int64_t scope_id, const char *query_kind, + const char *field_name, const char *field_value, uint32_t expectation, + uint32_t expected_count, orgize_contract_result *result, + char *error, size_t error_capacity) { + orgize_revision_fn revision; + orgize_evaluate_fn evaluate; + + if (session == NULL || result == NULL) { + lambda_aitia_write_error(error, error_capacity, "invalid Orgize call"); + return -100; + } + if (!lambda_aitia_load_symbol(session->library, "orgize_abi_revision", + &revision, sizeof(revision)) || + !lambda_aitia_load_symbol(session->library, "orgize_contract_evaluate", + &evaluate, sizeof(evaluate))) { + lambda_aitia_write_error(error, error_capacity, + "Orgize ABI symbols are absent"); + return -101; + } + if (revision() != 1u) { + lambda_aitia_write_error(error, error_capacity, + "Orgize ABI revision mismatch"); + return -102; + } + return evaluate(rows, row_count, scope_id, query_kind, field_name, + field_value, expectation, expected_count, result); +} + +int lambda_aitia_python_call(const char *library_path, const char *operation, + const uint8_t *payload, size_t payload_length, + uint8_t **output, size_t *output_length, + char *error, size_t error_capacity) { + lambda_aitia_python_session *session = + lambda_aitia_python_open(library_path, error, error_capacity); + int status; + if (session == NULL) return -101; + status = lambda_aitia_python_session_call( + session, operation, payload, payload_length, output, output_length, + error, error_capacity); + lambda_aitia_python_close(session); return status; } diff --git a/bindings/python/src/lambda_aitia/gitops.py b/bindings/python/src/lambda_aitia/gitops.py index 6114711..cc66230 100644 --- a/bindings/python/src/lambda_aitia/gitops.py +++ b/bindings/python/src/lambda_aitia/gitops.py @@ -9,7 +9,7 @@ from dataclasses import dataclass from typing import Any, Mapping -from .native import AitiaNativeError, _evaluate_gitops_payload +from .native import AitiaNativeError, AitiaNativeSession, _evaluate_gitops_payload from .runtime import _boolean, _string, _strings @@ -67,9 +67,13 @@ def from_payload(cls, payload: Mapping[str, Any]) -> GitOpsDecision: return decision -def evaluate_gitops(change: Mapping[str, Any]) -> GitOpsDecision: +def evaluate_gitops( + change: Mapping[str, Any], *, session: AitiaNativeSession | None = None +) -> GitOpsDecision: """Evaluate check evidence without implying release authorization or execution.""" if not isinstance(change, Mapping): raise TypeError("change must be a mapping") - return GitOpsDecision.from_payload(_evaluate_gitops_payload(change)) + return GitOpsDecision.from_payload( + _evaluate_gitops_payload(change, session=session) + ) diff --git a/bindings/python/src/lambda_aitia/native.py b/bindings/python/src/lambda_aitia/native.py index 5ba9b88..d41b36e 100644 --- a/bindings/python/src/lambda_aitia/native.py +++ b/bindings/python/src/lambda_aitia/native.py @@ -10,6 +10,8 @@ import os from pathlib import Path import platform +from threading import get_ident +from types import TracebackType from typing import Any, Mapping from ._native._aitia_cffi import ffi, lib @@ -36,21 +38,9 @@ def _library_path() -> Path: return candidate -def _call(operation: str, payload: bytes = b"") -> dict[str, Any]: - output = ffi.new("uint8_t **") - output_length = ffi.new("size_t *") - error = ffi.new("char[512]") - payload_pointer = ffi.from_buffer(payload) if payload else ffi.NULL - status = lib.lambda_aitia_python_call( - os.fsencode(_library_path()), - operation.encode("ascii"), - payload_pointer, - len(payload), - output, - output_length, - error, - 512, - ) +def _decode_call( + status: int, output: Any, output_length: Any, error: Any +) -> dict[str, Any]: try: response_bytes = ( bytes(ffi.buffer(output[0], output_length[0])) @@ -73,23 +63,119 @@ def _call(operation: str, payload: bytes = b"") -> dict[str, Any]: return value +def _call(operation: str, payload: bytes = b"") -> dict[str, Any]: + output = ffi.new("uint8_t **") + output_length = ffi.new("size_t *") + error = ffi.new("char[512]") + payload_pointer = ffi.from_buffer(payload) if payload else ffi.NULL + status = lib.lambda_aitia_python_call( + os.fsencode(_library_path()), + operation.encode("ascii"), + payload_pointer, + len(payload), + output, + output_length, + error, + 512, + ) + return _decode_call(status, output, output_length, error) + + +class AitiaNativeSession: + """Own one process-exclusive Gambit transport session, not Host authority. + + Calls and close must run on the thread that opened the session. Its lifetime + keeps Scheme state alive, but only Scheme-issued opaque capabilities can + establish verification/admission authority in a later ABI revision. + """ + + def __init__(self) -> None: + error = ffi.new("char[512]") + handle = lib.lambda_aitia_python_open( + os.fsencode(_library_path()), error, 512 + ) + if handle == ffi.NULL: + raise AitiaNativeError(ffi.string(error).decode("utf-8", errors="replace")) + self._handle = handle + self._owner_thread = get_ident() + + @property + def closed(self) -> bool: + return self._handle == ffi.NULL + + def _require_owner(self) -> None: + if get_ident() != self._owner_thread: + raise AitiaNativeError("Aitia native session belongs to another thread") + + def close(self) -> None: + self._require_owner() + if not self.closed: + handle = self._handle + self._handle = ffi.NULL + lib.lambda_aitia_python_close(handle) + + def _call(self, operation: str, payload: bytes = b"") -> dict[str, Any]: + self._require_owner() + if self.closed: + raise AitiaNativeError("Aitia native session is closed") + output = ffi.new("uint8_t **") + output_length = ffi.new("size_t *") + error = ffi.new("char[512]") + payload_pointer = ffi.from_buffer(payload) if payload else ffi.NULL + status = lib.lambda_aitia_python_session_call( + self._handle, + operation.encode("ascii"), + payload_pointer, + len(payload), + output, + output_length, + error, + 512, + ) + return _decode_call(status, output, output_length, error) + + def __enter__(self) -> AitiaNativeSession: + self._require_owner() + if self.closed: + raise AitiaNativeError("Aitia native session is closed") + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + traceback: TracebackType | None, + ) -> None: + self.close() + + def __del__(self) -> None: + if getattr(self, "_handle", ffi.NULL) != ffi.NULL: + # Gambit cleanup is thread-affine. Refuse silent cross-thread cleanup. + if get_ident() == self._owner_thread: + self.close() + + def descriptor() -> dict[str, Any]: """Return the native ABI descriptor published by Scheme.""" return _call("descriptor") -def sdlc_flow_plan(lifecycle_id: str) -> dict[str, Any]: +def sdlc_flow_plan( + lifecycle_id: str, *, session: AitiaNativeSession | None = None +) -> dict[str, Any]: """Ask Scheme to project an SDLC lifecycle onto the POO Flow DAG.""" payload = json.dumps( {"lifecycle-id": lifecycle_id}, separators=(",", ":"), ensure_ascii=False ).encode() - return _call("sdlc-flow-plan", payload) + return (session._call if session is not None else _call)("sdlc-flow-plan", payload) -def _evaluate_gitops_payload(change: Mapping[str, Any]) -> dict[str, Any]: +def _evaluate_gitops_payload( + change: Mapping[str, Any], *, session: AitiaNativeSession | None = None +) -> dict[str, Any]: """Ask Scheme to evaluate one GitOps change and its check evidence.""" payload = json.dumps(change, separators=(",", ":"), ensure_ascii=False).encode() - return _call("gitops-evaluate", payload) + return (session._call if session is not None else _call)("gitops-evaluate", payload) diff --git a/bindings/python/src/lambda_aitia/org_contract.py b/bindings/python/src/lambda_aitia/org_contract.py new file mode 100644 index 0000000..bd9227c --- /dev/null +++ b/bindings/python/src/lambda_aitia/org_contract.py @@ -0,0 +1,145 @@ +# SPDX-FileCopyrightText: 2026 tao3k team and Contributors +# +# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +"""Call Orgize's Scheme-owned Element/Contract C ABI from Aitia's runtime. + +Rows are already-admitted Org graph facts. Org parsing and contract semantics +remain in Orgize; this module only owns Python transport and lifetime checks. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Literal, Sequence + +from ._native._aitia_cffi import ffi, lib +from .native import AitiaNativeError, AitiaNativeSession + + +Expectation = Literal["exactly", "at-least", "at-most"] +_EXPECTATION_CODE = {"exactly": 0, "at-least": 1, "at-most": 2} +_INT64_MAX = (1 << 63) - 1 + + +@dataclass(frozen=True, slots=True) +class OrgElementFact: + """One Orgize-projected graph node with at most one projected field.""" + + id: int + parent_id: int + kind: str + field_name: str = "" + field_value: str = "" + + +@dataclass(frozen=True, slots=True) +class OrgContractResult: + matched_count: int + passed: bool + + +def _integer(value: int, *, name: str, minimum: int, maximum: int) -> int: + if type(value) is not int or not minimum <= value <= maximum: + raise ValueError(f"{name} must be an integer between {minimum} and {maximum}") + return value + + +def _utf8(value: str, *, name: str, required: bool = False) -> bytes: + if not isinstance(value, str) or (required and not value): + raise ValueError(f"{name} must be {'nonempty ' if required else ''}text") + if "\0" in value: + raise ValueError(f"{name} cannot contain NUL") + return value.encode("utf-8") + + +def _evaluate( + rows: Sequence[OrgElementFact], + scope_id: int, + query_kind: str, + field_name: str, + field_value: str, + expectation: Expectation, + expected_count: int, + session: AitiaNativeSession, +) -> OrgContractResult: + session._require_owner() + if session.closed: + raise AitiaNativeError("Aitia native session is closed") + + # Keep the same library handle and process-exclusive Gambit session used + # by Aitia. Opening a second dynamic handle can break cleanup/reinit. + buffers = [] + + def pointer(value: str, name: str, required: bool = False): + buffer = ffi.new("char[]", _utf8(value, name=name, required=required)) + buffers.append(buffer) + return buffer + + native_rows = ffi.new("orgize_element_row[]", len(rows)) + for index, row in enumerate(rows): + native_rows[index].id = _integer( + row.id, name="row id", minimum=0, maximum=_INT64_MAX + ) + native_rows[index].parent_id = _integer( + row.parent_id, name="parent id", minimum=-1, maximum=_INT64_MAX + ) + native_rows[index].kind = pointer(row.kind, "row kind", required=True) + native_rows[index].field_name = pointer(row.field_name, "field name") + native_rows[index].field_value = pointer(row.field_value, "field value") + result = ffi.new("orgize_contract_result *") + error = ffi.new("char[512]") + status = lib.lambda_aitia_python_session_org_contract( + session._handle, + native_rows if rows else ffi.NULL, + len(rows), + scope_id, + pointer(query_kind, "query kind", required=True), + pointer(field_name, "field name"), + pointer(field_value, "field value"), + _EXPECTATION_CODE[expectation], + expected_count, + result, + error, + 512, + ) + if status != 0 or result.status != 0: + detail = ffi.string(error).decode("utf-8", errors="replace") + raise AitiaNativeError(detail or "Orgize rejected graph contract evaluation") + return OrgContractResult(result.matched_count, result.passed == 1) + + +def evaluate_org_contract( + rows: Sequence[OrgElementFact], + *, + scope_id: int, + query_kind: str, + field_name: str = "", + field_value: str = "", + expectation: Expectation = "exactly", + expected_count: int = 1, + session: AitiaNativeSession | None = None, +) -> OrgContractResult: + """Evaluate an Orgize Contract on source-owner-projected graph facts. + + This result is an observation, not source authenticity, Host admission, + review approval, or runtime-effect authority. + """ + if not isinstance(rows, Sequence) or isinstance(rows, (str, bytes)): + raise TypeError("rows must be a sequence of OrgElementFact values") + if len(rows) > 10000 or any(not isinstance(row, OrgElementFact) for row in rows): + raise ValueError("rows must contain at most 10000 OrgElementFact values") + _integer(scope_id, name="scope id", minimum=0, maximum=_INT64_MAX) + _integer(expected_count, name="expected count", minimum=0, maximum=10000) + if expectation not in _EXPECTATION_CODE: + raise ValueError("expectation must be exactly, at-least, or at-most") + if session is None: + with AitiaNativeSession() as owned: + return _evaluate( + rows, scope_id, query_kind, field_name, field_value, + expectation, expected_count, owned, + ) + return _evaluate( + rows, scope_id, query_kind, field_name, field_value, + expectation, expected_count, session, + ) diff --git a/bindings/python/src/lambda_aitia/org_workspace.py b/bindings/python/src/lambda_aitia/org_workspace.py new file mode 100644 index 0000000..7da207e --- /dev/null +++ b/bindings/python/src/lambda_aitia/org_workspace.py @@ -0,0 +1,169 @@ +# SPDX-FileCopyrightText: 2026 tao3k team and Contributors +# +# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +"""Source-bound Org edit proposals; Orgize owns parsing and application. + +The caller supplies node identities and byte ranges from an Orgize projection. +This module checks that those ranges still refer to the projected source. It +does not authenticate the projection, interpret Org syntax, or write a file. +""" + +from dataclasses import dataclass +import hashlib +import os +from pathlib import Path +import re +import stat +from typing import Sequence + + +MAX_ORG_SOURCE_BYTES = 16 * 1024 * 1024 +MAX_ORG_EDITS = 128 +_DIGEST_PATTERN = re.compile(r"sha256:[0-9a-f]{64}\Z") + + +def _read_source(source: Path) -> bytes: + if not hasattr(os, "O_NOFOLLOW"): + raise OSError("Org source preflight requires O_NOFOLLOW") + flags = os.O_RDONLY | os.O_NOFOLLOW + if hasattr(os, "O_CLOEXEC"): + flags |= os.O_CLOEXEC + descriptor = os.open(source, flags) + try: + with os.fdopen(descriptor, "rb") as stream: + descriptor = -1 + info = os.fstat(stream.fileno()) + if not stat.S_ISREG(info.st_mode): + raise ValueError("Org source must be a regular file") + if info.st_size > MAX_ORG_SOURCE_BYTES: + raise ValueError("Org source exceeds byte limit") + payload = stream.read(MAX_ORG_SOURCE_BYTES + 1) + if len(payload) > MAX_ORG_SOURCE_BYTES: + raise ValueError("Org source exceeds byte limit") + return payload + finally: + if descriptor >= 0: + os.close(descriptor) + + +def org_source_digest(payload: bytes) -> str: + """Digest exact Org source bytes, not a parsed or canonicalized tree.""" + return "sha256:" + hashlib.sha256(payload).hexdigest() + + +@dataclass(frozen=True, slots=True) +class OrgNodeEdit: + """An Orgize-projected node's original byte span and proposed replacement.""" + + node_id: str + start_byte: int + end_byte: int + expected_old: bytes + replacement: bytes + + +@dataclass(frozen=True, slots=True) +class OrgEditReview: + """Inert proposal for owner review and source-owned application.""" + + source_id: str + source_digest: str + source_size_bytes: int + edits: tuple[OrgNodeEdit, ...] + + def source_current(self, source: Path) -> bool: + """Recheck bytes before review; application must check again itself.""" + try: + payload = _read_source(Path(source)) + except (OSError, ValueError): + return False + return ( + len(payload) == self.source_size_bytes + and org_source_digest(payload) == self.source_digest + and all( + payload[edit.start_byte : edit.end_byte] == edit.expected_old + for edit in self.edits + ) + ) + + +def prepare_org_edit_review( + source: Path, + *, + source_id: str, + projected_source_digest: str, + edits: Sequence[OrgNodeEdit], +) -> OrgEditReview: + """Reject stale/ambiguous Orgize anchors without modifying the source. + + ``projected_source_digest`` must describe the exact bytes used to produce + the upstream node IDs and byte ranges. A matching digest is a freshness + check, not proof that the caller's projection is trusted or authorized. + """ + if not isinstance(source_id, str) or not source_id.strip(): + raise ValueError("source_id must be nonempty") + if not isinstance(projected_source_digest, str) or not _DIGEST_PATTERN.fullmatch( + projected_source_digest + ): + raise ValueError("projected_source_digest must be a SHA-256 digest") + payload = _read_source(Path(source)) + try: + payload.decode("utf-8") + except UnicodeDecodeError as error: + raise ValueError("Org source must be UTF-8") from error + digest = org_source_digest(payload) + if digest != projected_source_digest: + raise ValueError("Org source changed since projection") + if not 1 <= len(edits) <= MAX_ORG_EDITS: + raise ValueError("Org edit count must be between 1 and 128") + if any(not isinstance(edit, OrgNodeEdit) for edit in edits): + raise TypeError("edits must be OrgNodeEdit values") + if any( + type(edit.start_byte) is not int or type(edit.end_byte) is not int + for edit in edits + ): + raise ValueError("Org node edit has an invalid byte span") + + ordered = sorted(edits, key=lambda edit: (edit.start_byte, edit.end_byte)) + seen_ids: set[str] = set() + prior_end = 0 + replacement_bytes = 0 + for edit in ordered: + if not isinstance(edit.node_id, str) or not edit.node_id.strip(): + raise ValueError("node_id must be nonempty") + if edit.node_id in seen_ids: + raise ValueError("duplicate Org node ID in edit proposal") + seen_ids.add(edit.node_id) + if ( + not 0 <= edit.start_byte < edit.end_byte <= len(payload) + ): + raise ValueError("Org node edit has an invalid byte span") + if ( + edit.start_byte < len(payload) + and payload[edit.start_byte] & 0xC0 == 0x80 + ) or ( + edit.end_byte < len(payload) + and payload[edit.end_byte] & 0xC0 == 0x80 + ): + raise ValueError("Org node edit splits a UTF-8 codepoint") + if edit.start_byte < prior_end: + raise ValueError("Org node edits overlap") + if not isinstance(edit.expected_old, bytes) or not isinstance( + edit.replacement, bytes + ): + raise TypeError("Org node edits require exact source bytes") + try: + edit.node_id.encode("utf-8") + edit.expected_old.decode("utf-8") + edit.replacement.decode("utf-8") + except UnicodeError as error: + raise ValueError("Org node edit must use UTF-8") from error + replacement_bytes += len(edit.replacement) + if replacement_bytes > MAX_ORG_SOURCE_BYTES: + raise ValueError("Org replacements exceed byte limit") + if payload[edit.start_byte : edit.end_byte] != edit.expected_old: + raise ValueError("Org node moved or its expected content changed") + prior_end = edit.end_byte + + return OrgEditReview(source_id, digest, len(payload), tuple(ordered)) diff --git a/bindings/python/src/lambda_aitia/runtime.py b/bindings/python/src/lambda_aitia/runtime.py index d7d007b..2bc6312 100644 --- a/bindings/python/src/lambda_aitia/runtime.py +++ b/bindings/python/src/lambda_aitia/runtime.py @@ -9,7 +9,7 @@ from dataclasses import dataclass from typing import Any, Mapping -from .native import AitiaNativeError, sdlc_flow_plan +from .native import AitiaNativeError, AitiaNativeSession, sdlc_flow_plan def _string(value: Any, field: str) -> str: @@ -87,9 +87,14 @@ class SdlcRuntime: complete until every Scheme-owned stage publishes an attributable receipt. """ + def __init__(self, *, session: AitiaNativeSession | None = None) -> None: + self._session = session + def plan(self, lifecycle_id: str) -> SdlcFlowPlan: """Build and validate the canonical POO Flow DAG for a lifecycle.""" if not isinstance(lifecycle_id, str) or not lifecycle_id: raise ValueError("lifecycle_id must be a non-empty string") - return SdlcFlowPlan.from_payload(sdlc_flow_plan(lifecycle_id)) + return SdlcFlowPlan.from_payload( + sdlc_flow_plan(lifecycle_id, session=self._session) + ) diff --git a/bindings/python/src/lambda_aitia/verification.py b/bindings/python/src/lambda_aitia/verification.py new file mode 100644 index 0000000..13e94fe --- /dev/null +++ b/bindings/python/src/lambda_aitia/verification.py @@ -0,0 +1,422 @@ +# SPDX-FileCopyrightText: 2026 tao3k team and Contributors +# +# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +"""Observable pytest execution, not an Assurance Host admission.""" + +from __future__ import annotations + +from dataclasses import dataclass, replace +import hashlib +import os +from pathlib import Path, PurePosixPath, PureWindowsPath +import selectors +import signal +import subprocess +import tempfile +import time +from uuid import uuid4 +import xml.etree.ElementTree as ET + +import anyio + + +_SOURCE_MAGIC = b"lambda-aitia.source-tree.v1\0" +_MAX_SOURCE_BYTES = 64 * 1024 * 1024 +_MAX_SOURCE_FILES = 10_000 + + +def _source_payload_digest(payload: bytes) -> str: + return "sha256:" + hashlib.sha256(payload).hexdigest() + + +def capture_source_tree(root: Path) -> bytes: + """Freeze a regular-file tree into canonical, source-lock-compatible bytes. + + Source paths and contents are length-framed, ordered, and bounded. The + returned bytes can be independently rehashed by the Scheme Host's source + reader; their digest is not a verification seal. + """ + + root = root.resolve(strict=True) + if not root.is_dir(): + raise ValueError("source root must be a directory") + entries: list[tuple[bytes, bytes]] = [] + size = len(_SOURCE_MAGIC) + 8 + for path in root.rglob("*"): + if path.is_symlink(): + raise ValueError(f"source tree contains a symlink: {path}") + if path.is_dir(): + continue + if not path.is_file(): + raise ValueError(f"source tree contains a non-file: {path}") + relative = path.relative_to(root).as_posix().encode("utf-8") + remaining = _MAX_SOURCE_BYTES - size - 4 - len(relative) - 8 + if remaining < 0: + raise ValueError("source tree exceeds verifier capture limit") + with path.open("rb") as source: + contents = source.read(remaining + 1) + size += 4 + len(relative) + 8 + len(contents) + if size > _MAX_SOURCE_BYTES or len(entries) >= _MAX_SOURCE_FILES: + raise ValueError("source tree exceeds verifier capture limit") + entries.append((relative, contents)) + entries.sort(key=lambda entry: entry[0]) + framed = bytearray(_SOURCE_MAGIC) + framed.extend(len(entries).to_bytes(8, "big")) + for relative, contents in entries: + framed.extend(len(relative).to_bytes(4, "big")) + framed.extend(relative) + framed.extend(len(contents).to_bytes(8, "big")) + framed.extend(contents) + return bytes(framed) + + +def _source_entries(payload: bytes) -> tuple[tuple[Path, bytes], ...]: + if len(payload) > _MAX_SOURCE_BYTES or not payload.startswith(_SOURCE_MAGIC): + raise ValueError("invalid frozen source payload") + position = len(_SOURCE_MAGIC) + + def take(length: int) -> bytes: + nonlocal position + if length < 0 or position + length > len(payload): + raise ValueError("truncated frozen source payload") + result = payload[position : position + length] + position += length + return result + + count = int.from_bytes(take(8), "big") + if count > _MAX_SOURCE_FILES: + raise ValueError("frozen source file count exceeds limit") + entries: list[tuple[Path, bytes]] = [] + previous = b"" + for _ in range(count): + relative = take(int.from_bytes(take(4), "big")) + if not relative or relative <= previous or b"\\" in relative or b"\0" in relative: + raise ValueError("invalid frozen source path ordering") + previous = relative + try: + decoded = relative.decode("utf-8") + except UnicodeDecodeError as error: + raise ValueError("invalid frozen source path encoding") from error + path = PurePosixPath(decoded) + if ( + path.is_absolute() + or PureWindowsPath(decoded).drive + or any(part in ("", ".", "..") for part in decoded.split("/")) + ): + raise ValueError("frozen source path escapes root") + contents = take(int.from_bytes(take(8), "big")) + entries.append((Path(*path.parts), contents)) + if position != len(payload): + raise ValueError("trailing frozen source payload bytes") + return tuple(entries) + + +def source_digest(root: Path) -> str: + """Hash the canonical frozen bytes of a regular-file tree.""" + + return _source_payload_digest(capture_source_tree(root)) + + +@dataclass(frozen=True, slots=True) +class PytestObservation: + """One subprocess observation; this value carries no issuing authority.""" + + run_id: str + source_digest: str + argv: tuple[str, ...] + exit_code: int | None + status: str + collected: int + passed: int + skipped: int + report_digest: str | None + output: str + + @property + def passed_obligation(self) -> bool: + return self.status == "passed" + + def source_current(self, root: Path) -> bool: + try: + return source_digest(root) == self.source_digest + except (OSError, ValueError): + return False + + +def _validated_selectors( + selectors: tuple[str, ...], entries: tuple[tuple[Path, bytes], ...] +) -> None: + selected_files = {path.as_posix() for path, _ in entries} + if not selectors: + raise ValueError("at least one explicit pytest selector is required") + for selector in selectors: + filename = selector.split("::", 1)[0] + if ( + not filename + or selector.startswith("-") + or "\\" in filename + or "\0" in filename + or any(part in ("", ".", "..") for part in filename.split("/")) + or filename not in selected_files + ): + raise ValueError("pytest selector must name a file in frozen source") + + +def _stop_process_group(process: subprocess.Popen[bytes]) -> bool: + """Stop pytest's group; return false if descendant cleanup is uncertain.""" + + controlled = True + try: + os.killpg(process.pid, signal.SIGTERM) + except ProcessLookupError: + pass + except PermissionError: + controlled = False + try: + process.wait(timeout=0.2) + except subprocess.TimeoutExpired: + pass + try: + os.killpg(process.pid, signal.SIGKILL) + except ProcessLookupError: + pass + except PermissionError: + controlled = False + try: + process.wait(timeout=1) + except subprocess.TimeoutExpired: + controlled = False + process.kill() + process.wait() + return controlled + + +def _run_bounded_verifier( + argv: tuple[str, ...], + root: Path, + environment: dict[str, str], + timeout_seconds: int, + max_output_bytes: int, +) -> tuple[int | None, str, str | None]: + """Drain bounded output and terminate the whole verifier group on failure. + + Non-POSIX process trees are rejected until an equivalent descendant-kill + contract is qualified; silently killing only pytest would leave effects. + """ + + if os.name != "posix": + raise OSError("controlled pytest verification requires POSIX process groups") + tail = bytearray() + observed_bytes = 0 + deadline = time.monotonic() + timeout_seconds + stop_reason = None + process = subprocess.Popen( + argv, + cwd=root, + env=environment, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + start_new_session=True, + ) + try: + assert process.stdout is not None + with selectors.DefaultSelector() as selector: + selector.register(process.stdout, selectors.EVENT_READ) + output_complete = False + while not (output_complete and process.poll() is not None): + remaining = deadline - time.monotonic() + if remaining <= 0: + stop_reason = "timeout" + break + for key, _ in selector.select(timeout=min(remaining, 0.1)): + chunk = os.read(key.fileobj.fileno(), 65536) + if not chunk: + selector.unregister(key.fileobj) + output_complete = True + continue + observed_bytes += len(chunk) + tail.extend(chunk) + del tail[:-8192] + if observed_bytes > max_output_bytes: + stop_reason = "output-exceeded" + break + if stop_reason: + break + if stop_reason: + if not _stop_process_group(process): + stop_reason = "control-error" + exit_code = None + else: + exit_code = process.wait() + return exit_code, tail.decode("utf-8", "replace"), stop_reason + finally: + if process.poll() is None: + _stop_process_group(process) + if process.stdout is not None: + process.stdout.close() + + +def run_pytest_frozen( + python: Path, + source_payload: bytes, + selectors: tuple[str, ...], + *, + timeout_seconds: int = 30, + max_output_bytes: int = 8 * 1024 * 1024, + environment: dict[str, str] | None = None, +) -> PytestObservation: + """Run selected tests only from a validated frozen source payload. + + The caller owns the interpreter, dependencies and external services. The + payload is observable process input, not a Host-issued verification seal. + """ + + entries = _source_entries(source_payload) + _validated_selectors(selectors, entries) + if timeout_seconds <= 0: + raise ValueError("timeout_seconds must be positive") + if max_output_bytes <= 0: + raise ValueError("max_output_bytes must be positive") + before = _source_payload_digest(source_payload) + run_id = uuid4().hex + interpreter = python.absolute() + if not interpreter.is_file(): + raise ValueError("Python interpreter is absent") + safe_environment = os.environ.copy() + for name in ("PYTHONPATH", "PYTEST_ADDOPTS", "PYTEST_PLUGINS"): + safe_environment.pop(name, None) + safe_environment["PYTEST_DISABLE_PLUGIN_AUTOLOAD"] = "1" + safe_environment["PYTHONDONTWRITEBYTECODE"] = "1" + if environment: + if any(not key.startswith("AITIA_QUALIFICATION_") for key in environment): + raise ValueError("only AITIA_QUALIFICATION_ environment values are allowed") + safe_environment.update(environment) + with tempfile.TemporaryDirectory(prefix="aitia-pytest-") as workspace: + root = Path(workspace) / "source" + root.mkdir() + for relative, contents in entries: + target = root / relative + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(contents) + report = Path(workspace) / f"{run_id}.xml" + argv = ( + str(interpreter), "-m", "pytest", "-q", "-s", + "-p", "no:cacheprovider", f"--junitxml={report}", *selectors, + ) + exit_code, output, stop_reason = _run_bounded_verifier( + argv, root, safe_environment, timeout_seconds, max_output_bytes + ) + try: + after = source_digest(root) + except (OSError, ValueError): + after = None + collected = passed = skipped = 0 + report_digest = None + report_valid = False + if report.is_file(): + report_bytes = report.read_bytes() + report_digest = "sha256:" + hashlib.sha256(report_bytes).hexdigest() + try: + suites = ET.fromstring(report_bytes) + cases = suites.findall(".//testcase") + collected = len(cases) + skipped = sum(case.find("skipped") is not None for case in cases) + failed = sum( + case.find("failure") is not None or case.find("error") is not None + for case in cases + ) + passed = collected - skipped - failed + report_valid = suites.tag in ("testsuite", "testsuites") + except ET.ParseError: + pass + if before != after: + status = "source-changed" + elif stop_reason: + status = stop_reason + elif exit_code == 2: + status = "interrupted" + elif exit_code == 3: + status = "internal-error" + elif exit_code == 4: + status = "usage-error" + elif exit_code == 6: + status = "warnings-exceeded" + elif not report_valid: + status = "report-invalid" + elif collected == 0: + status = "no-tests" + elif exit_code != 0 or passed + skipped != collected: + status = "failed" + elif passed == 0: + status = "no-passing-tests" + elif skipped: + status = "skipped" + else: + status = "passed" + return PytestObservation( + run_id, before, argv, exit_code, status, collected, passed, + skipped, report_digest, output[-8192:], + ) + + +async def run_pytest_frozen_async( + python: Path, + source_payload: bytes, + selectors: tuple[str, ...], + *, + timeout_seconds: int = 30, + max_output_bytes: int = 8 * 1024 * 1024, + environment: dict[str, str] | None = None, + limiter: anyio.CapacityLimiter | None = None, +) -> PytestObservation: + """Run a frozen verifier through AnyIO without abandoning its process. + + Cancellation waits for the bounded worker to terminate and clean up the + pytest process group. No thread-affine native session enters this worker; + the result remains an observation, not a Host admission. + """ + + frozen_payload = bytes(source_payload) + frozen_selectors = tuple(selectors) + frozen_environment = dict(environment) if environment is not None else None + + def execute() -> PytestObservation: + return run_pytest_frozen( + python, + frozen_payload, + frozen_selectors, + timeout_seconds=timeout_seconds, + max_output_bytes=max_output_bytes, + environment=frozen_environment, + ) + + return await anyio.to_thread.run_sync( + execute, abandon_on_cancel=False, limiter=limiter + ) + + +def run_pytest( + python: Path, + source_root: Path, + selectors: tuple[str, ...], + *, + timeout_seconds: int = 30, + max_output_bytes: int = 8 * 1024 * 1024, + environment: dict[str, str] | None = None, +) -> PytestObservation: + """Capture source, execute its frozen copy, and detect caller-tree drift. + + A changing caller tree invalidates the observation, but even a passing + observation is not a Scheme Assurance Host admission. + """ + + payload = capture_source_tree(source_root) + observation = run_pytest_frozen( + python, payload, selectors, + timeout_seconds=timeout_seconds, max_output_bytes=max_output_bytes, + environment=environment, + ) + if not observation.source_current(source_root): + return replace(observation, status="source-changed") + return observation diff --git a/bindings/python/tests/installed_wheel_qualification.py b/bindings/python/tests/installed_wheel_qualification.py new file mode 100644 index 0000000..205344b --- /dev/null +++ b/bindings/python/tests/installed_wheel_qualification.py @@ -0,0 +1,107 @@ +# SPDX-FileCopyrightText: 2026 tao3k team and Contributors +# +# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +"""Project qualification of an installed wheel outside the checkout.""" + +from __future__ import annotations + +import os +from pathlib import Path +import shutil +import subprocess +import tempfile + + +PROJECT = Path(__file__).resolve().parents[3] +PYTHON = PROJECT / "bindings/python/.venv/bin/python" +WHEELS = PROJECT / "bindings/python/dist" +SCENARIO = PROJECT / "bindings/python/qualification/job_executor" + + +def run( + *argv: str, + cwd: Path, + environment: dict[str, str] | None = None, + quiet: bool = False, +) -> None: + subprocess.run( + argv, cwd=cwd, env=environment, check=True, timeout=90, + stdout=subprocess.DEVNULL if quiet else None, + ) + + +def test_installed_wheel_qualification() -> None: + wheels = list(WHEELS.glob("lambda_aitia-*.whl")) + if not wheels: + raise RuntimeError("build the Aitia wheel before qualification") + wheel = max(wheels, key=lambda path: path.stat().st_mtime_ns) + with tempfile.TemporaryDirectory(prefix="aitia-installed-wheel-") as directory: + root = Path(directory) + venv = root / "venv" + scenario = root / "scenario" + requirements = root / "requirements.txt" + shutil.copytree(SCENARIO, scenario) + run("uv", "export", "--offline", "--locked", "--extra", "test", + "--no-emit-project", "--output-file", str(requirements), + cwd=PROJECT / "bindings/python", quiet=True) + run("uv", "venv", "--python", str(PYTHON), str(venv), cwd=root) + interpreter = venv / "bin/python" + # A fresh CI cache can lack a locked wheel even when the source-tree + # environment already has it. Resolve only hashes from uv.lock; + # reuse cached wheels and fetch missing ones when needed. + run("uv", "pip", "install", "--python", str(interpreter), + "-r", str(requirements), cwd=root) + run("uv", "pip", "install", "--offline", "--no-deps", "--python", + str(interpreter), str(wheel), cwd=root) + environment = os.environ.copy() + for name in ("PYTHONPATH", "LAMBDA_AITIA_NATIVE_LIBRARY", "VIRTUAL_ENV"): + environment.pop(name, None) + # Import, native invocation, verifier and stale-source test all occur + # with cwd outside the checkout and no development path override. + code = """ +import anyio +from pathlib import Path +from lambda_aitia import (AitiaNativeSession, OrgElementFact, SdlcRuntime, + evaluate_org_contract) +from lambda_aitia.verification import capture_source_tree, run_pytest, run_pytest_frozen_async +root = Path('scenario').resolve() +assert SdlcRuntime().plan('qualification/job-42').lifecycle_id == 'qualification/job-42' +with AitiaNativeSession() as session: + runtime = SdlcRuntime(session=session) + assert runtime.plan('qualification/job-42/first').lifecycle_id == 'qualification/job-42/first' + assert runtime.plan('qualification/job-42/second').lifecycle_id == 'qualification/job-42/second' + graph = (OrgElementFact(0, -1, 'org-data'), + OrgElementFact(1, 0, 'headline', 'title', 'Evidence')) + org_result = evaluate_org_contract(graph, scope_id=0, query_kind='headline', + field_name='title', field_value='Evidence', + session=session) + assert org_result.passed and org_result.matched_count == 1 +assert session.closed +selector = ('test_unique_effect.py::test_unique_effect_after_lost_ack_and_restart',) +buggy = run_pytest(Path(__import__('sys').executable), root, selector, + environment={'AITIA_QUALIFICATION_MODE': 'buggy'}) +assert buggy.status == 'failed', buggy +assert 'assert 2 == 1' in buggy.output, buggy.output +source_payload = capture_source_tree(root) + +async def verify_fixed(): + return await run_pytest_frozen_async(Path(__import__('sys').executable), + source_payload, selector, + environment={'AITIA_QUALIFICATION_MODE': 'fixed'}, + limiter=anyio.CapacityLimiter(1)) + +with AitiaNativeSession() as session: + runtime = SdlcRuntime(session=session) + assert runtime.plan('qualification/job-42/first').lifecycle_id == 'qualification/job-42/first' + fixed = anyio.run(verify_fixed) + assert fixed.passed_obligation and fixed.collected == 1, fixed + assert runtime.plan('qualification/job-42/second').lifecycle_id == 'qualification/job-42/second' +assert session.closed +assert fixed.source_current(root) +worker = root / 'worker.py' +worker.write_bytes(worker.read_bytes() + b'\\n# changed after verification\\n') +assert not fixed.source_current(root) +print('installed-wheel-qualification: buggy=reproduced fixed=passed stale=rejected') +""" + run(str(interpreter), "-c", code, cwd=root, environment=environment) diff --git a/bindings/python/tests/test_native.py b/bindings/python/tests/test_native.py index d9fd888..235be48 100644 --- a/bindings/python/tests/test_native.py +++ b/bindings/python/tests/test_native.py @@ -4,7 +4,13 @@ import pytest -from lambda_aitia import GitOpsDecision, SdlcRuntime, descriptor, evaluate_gitops +from lambda_aitia import ( + AitiaNativeSession, + GitOpsDecision, + SdlcRuntime, + descriptor, + evaluate_gitops, +) from lambda_aitia.native import AitiaNativeError from lambda_aitia.runtime import SdlcFlowPlan @@ -42,6 +48,33 @@ def test_runtime_rejects_invalid_lifecycle_before_native_dispatch() -> None: SdlcRuntime().plan("") +def test_session_keeps_one_scheme_runtime_for_distinct_calls() -> None: + with AitiaNativeSession() as session: + runtime = SdlcRuntime(session=session) + assert runtime.plan("session/first").lifecycle_id == "session/first" + assert runtime.plan("session/second").lifecycle_id == "session/second" + assert evaluate_gitops(_gitops_change(), session=session).accepted is True + with pytest.raises(AitiaNativeError, match="active session"): + AitiaNativeSession() + with pytest.raises(AitiaNativeError, match="active session"): + descriptor() + assert session.closed + session.close() + with pytest.raises(AitiaNativeError, match="closed"): + runtime.plan("session/after-close") + assert descriptor()["semanticOwner"] == "lambda-aitia" + + +def test_session_rejects_cross_thread_calls() -> None: + from concurrent.futures import ThreadPoolExecutor + + with AitiaNativeSession() as session: + with ThreadPoolExecutor(max_workers=1) as pool: + failure = pool.submit(SdlcRuntime(session=session).plan, "other/thread") + with pytest.raises(AitiaNativeError, match="another thread"): + failure.result() + + @pytest.mark.parametrize("field", ["releaseAuthorized", "runtimeExecuted"]) def test_runtime_rejects_inert_plan_claiming_effect(field: str) -> None: payload = { @@ -62,23 +95,30 @@ def test_runtime_rejects_inert_plan_claiming_effect(field: str) -> None: SdlcFlowPlan.from_payload(payload) +def _gitops_change() -> dict[str, object]: + return { + "event": "pull-request", + "repository": "tao3k/poo-flow", + "revision": "0123456789abcdef", + "source-ref": "feature/aitia", + "target-ref": "develop", + "pull-request": 42, + "checks": [ + {"name": "commit-policy", "conclusion": "success"}, + {"name": "build", "conclusion": "success"}, + {"name": "unit-test", "conclusion": "success"}, + { + "name": "nasa/sdlc/npr-7150.2", + "conclusion": "success", + "standardEdition": "D", + "sourceLockDigest": "sha256:cfb963b8cd81fd8e22e9b47251c1dc7927b85fcdf9ec9366057cd38cb94f338c", + }, + ], + } + + def test_python_delegates_gitops_decision_to_scheme() -> None: - decision = evaluate_gitops( - { - "event": "pull-request", - "repository": "tao3k/poo-flow", - "revision": "0123456789abcdef", - "source-ref": "feature/aitia", - "target-ref": "develop", - "pull-request": 42, - "checks": [ - {"name": "commit-policy", "conclusion": "success"}, - {"name": "build", "conclusion": "success"}, - {"name": "unit-test", "conclusion": "success"}, - {"name": "nasa-7150-2d", "conclusion": "success"}, - ], - } - ) + decision = evaluate_gitops(_gitops_change()) assert isinstance(decision, GitOpsDecision) assert decision.accepted is True assert decision.profile == "dev" @@ -86,3 +126,28 @@ def test_python_delegates_gitops_decision_to_scheme() -> None: assert decision.authority_status == "not-evaluated" assert decision.release_authorized is False assert decision.runtime_executed is False + + +@pytest.mark.parametrize( + ("field", "value"), + [ + ("standardEdition", "C"), + ("sourceLockDigest", "sha256:wrong-lock"), + ("sourceLockDigest", None), + ], +) +def test_python_rejects_stale_standard_assessment(field: str, value: str | None) -> None: + change = _gitops_change() + checks = change["checks"] + assert isinstance(checks, list) + standard_check = checks[-1] + assert isinstance(standard_check, dict) + if value is None: + standard_check.pop(field) + else: + standard_check[field] = value + + decision = evaluate_gitops(change) + assert decision.accepted is False + assert decision.stale_checks == ("nasa/sdlc/npr-7150.2",) + assert decision.reasons == ("standard-assessment-mismatch",) diff --git a/bindings/python/tests/test_org_contract.py b/bindings/python/tests/test_org_contract.py new file mode 100644 index 0000000..413131b --- /dev/null +++ b/bindings/python/tests/test_org_contract.py @@ -0,0 +1,58 @@ +# SPDX-FileCopyrightText: 2026 tao3k team and Contributors +# +# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +"""The delivered Aitia library calls Orgize's Scheme-owned C ABI.""" + +import pytest + +from lambda_aitia import ( + AitiaNativeSession, + OrgElementFact, + evaluate_org_contract, +) +from lambda_aitia.native import AitiaNativeError + + +ROWS = ( + OrgElementFact(0, -1, "org-data"), + OrgElementFact(1, 0, "headline", "title", "Evidence"), +) + + +def test_orgize_contract_through_bundled_native_library() -> None: + result = evaluate_org_contract( + ROWS, scope_id=0, query_kind="headline", field_name="title", + field_value="Evidence", + ) + assert result.matched_count == 1 + assert result.passed is True + assert evaluate_org_contract( + ROWS, scope_id=0, query_kind="headline", field_name="title", + field_value="Missing", + ).passed is False + + +def test_orgize_contract_uses_existing_native_session() -> None: + with AitiaNativeSession() as session: + result = evaluate_org_contract( + ROWS, scope_id=0, query_kind="headline", session=session, + ) + assert result.matched_count == 1 + with pytest.raises(AitiaNativeError, match="closed"): + evaluate_org_contract( + ROWS, scope_id=0, query_kind="headline", session=session, + ) + + +@pytest.mark.parametrize( + ("rows", "kind", "error"), + [ + ((OrgElementFact(0, -1, "head\x00line"),), "headline", "NUL"), + ((OrgElementFact(-1, -1, "headline"),), "headline", "row id"), + (ROWS, "head\x00line", "NUL"), + ], +) +def test_orgize_transport_rejects_unsafe_inputs(rows, kind, error) -> None: + with pytest.raises(ValueError, match=error): + evaluate_org_contract(rows, scope_id=0, query_kind=kind) diff --git a/bindings/python/tests/test_org_workspace.py b/bindings/python/tests/test_org_workspace.py new file mode 100644 index 0000000..19ec0e5 --- /dev/null +++ b/bindings/python/tests/test_org_workspace.py @@ -0,0 +1,178 @@ +# SPDX-FileCopyrightText: 2026 tao3k team and Contributors +# +# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +"""Aitia preflight uses Orgize anchors; it never parses or mutates Org.""" + +import hashlib +from pathlib import Path + +import pytest + +from lambda_aitia import OrgNodeEdit, prepare_org_edit_review + + +SOURCE = b"* Design\n:PROPERTIES:\n:ID: design-1\n:END:\nOld guarantee\n* Notes\nKeep me\n" + + +def _digest(payload: bytes) -> str: + return "sha256:" + hashlib.sha256(payload).hexdigest() + + +def _edit(payload: bytes = SOURCE, target: bytes = b"Old guarantee") -> OrgNodeEdit: + start = payload.index(target) + return OrgNodeEdit("design-1", start, start + len(target), target, b"New guarantee") + + +def test_prepares_source_bound_review_without_writing(tmp_path: Path) -> None: + source = tmp_path / "design.org" + source.write_bytes(SOURCE) + review = prepare_org_edit_review( + source, + source_id="design.org", + projected_source_digest=_digest(SOURCE), + edits=(_edit(),), + ) + assert review.source_id == "design.org" + assert review.source_digest == _digest(SOURCE) + assert review.edits == (_edit(),) + assert review.source_current(source) + assert source.read_bytes() == SOURCE + + +def test_preflights_aitia_maintained_workspace_without_mutating_it() -> None: + source = Path(__file__).resolve().parents[3] / "user-interface/workspaces/job-executor.org" + payload = source.read_bytes() + expected = b"This subtree is the review context for ADR 0008." + start = payload.index(expected) + review = prepare_org_edit_review( + source, + source_id="job-executor", + projected_source_digest=_digest(payload), + edits=( + OrgNodeEdit( + "aitia-job-executor-design-decision", + start, + start + len(expected), + expected, + b"This subtree requires renewed review for ADR 0008.", + ), + ), + ) + assert review.source_current(source) + assert source.read_bytes() == payload + + +def test_rejects_stale_projection_even_when_old_text_remains(tmp_path: Path) -> None: + source = tmp_path / "design.org" + source.write_bytes(SOURCE + b"* Later\n") + with pytest.raises(ValueError, match="changed since projection"): + prepare_org_edit_review( + source, + source_id="design.org", + projected_source_digest=_digest(SOURCE), + edits=(_edit(),), + ) + + +def test_rejects_moved_span_with_fresh_digest(tmp_path: Path) -> None: + source = tmp_path / "design.org" + moved = SOURCE.replace(b"Old guarantee", b"prefix Old guarantee") + source.write_bytes(moved) + with pytest.raises(ValueError, match="moved or its expected content changed"): + prepare_org_edit_review( + source, + source_id="design.org", + projected_source_digest=_digest(moved), + edits=(_edit(),), + ) + + +def test_rejects_overlapping_edits_and_duplicate_node(tmp_path: Path) -> None: + source = tmp_path / "design.org" + source.write_bytes(SOURCE) + first = _edit() + second = OrgNodeEdit( + "design-2", + first.start_byte + 1, + first.end_byte, + SOURCE[first.start_byte + 1 : first.end_byte], + b"changed", + ) + with pytest.raises(ValueError, match="overlap"): + prepare_org_edit_review( + source, source_id="design.org", projected_source_digest=_digest(SOURCE), + edits=(first, second), + ) + with pytest.raises(ValueError, match="duplicate Org node ID"): + prepare_org_edit_review( + source, source_id="design.org", projected_source_digest=_digest(SOURCE), + edits=(first, first), + ) + + +def test_review_goes_stale_after_preflight(tmp_path: Path) -> None: + source = tmp_path / "design.org" + source.write_bytes(SOURCE) + review = prepare_org_edit_review( + source, source_id="design.org", projected_source_digest=_digest(SOURCE), + edits=(_edit(),), + ) + source.write_bytes(SOURCE.replace(b"Keep me", b"Changed")) + assert not review.source_current(source) + + +def test_refuses_symlink_and_invalid_byte_span(tmp_path: Path) -> None: + source = tmp_path / "design.org" + source.write_bytes(SOURCE) + alias = tmp_path / "alias.org" + alias.symlink_to(source) + with pytest.raises(OSError): + prepare_org_edit_review( + alias, source_id="design.org", projected_source_digest=_digest(SOURCE), + edits=(_edit(),), + ) + bad = OrgNodeEdit("design-1", True, 3, b"* D", b"new") + with pytest.raises(ValueError, match="invalid byte span"): + prepare_org_edit_review( + source, source_id="design.org", projected_source_digest=_digest(SOURCE), + edits=(bad,), + ) + + +def test_refuses_non_utf8_source_and_split_codepoint(tmp_path: Path) -> None: + source = tmp_path / "design.org" + source.write_bytes(SOURCE + b"\xff") + with pytest.raises(ValueError, match="source must be UTF-8"): + prepare_org_edit_review( + source, source_id="design.org", + projected_source_digest=_digest(source.read_bytes()), edits=(_edit(),), + ) + unicode_source = SOURCE + "说明\n".encode() + source.write_bytes(unicode_source) + start = unicode_source.index("说明".encode()) + 1 + split = OrgNodeEdit("design-1", start, start + 1, b"\x01", b"x") + with pytest.raises(ValueError, match="splits a UTF-8 codepoint"): + prepare_org_edit_review( + source, source_id="design.org", + projected_source_digest=_digest(unicode_source), edits=(split,), + ) + + +def test_refuses_non_utf8_replacement_and_excess_edit_count(tmp_path: Path) -> None: + source = tmp_path / "design.org" + source.write_bytes(SOURCE) + edit = _edit() + invalid = OrgNodeEdit( + edit.node_id, edit.start_byte, edit.end_byte, edit.expected_old, b"\xff", + ) + with pytest.raises(ValueError, match="edit must use UTF-8"): + prepare_org_edit_review( + source, source_id="design.org", projected_source_digest=_digest(SOURCE), + edits=(invalid,), + ) + with pytest.raises(ValueError, match="edit count"): + prepare_org_edit_review( + source, source_id="design.org", projected_source_digest=_digest(SOURCE), + edits=(edit,) * 129, + ) diff --git a/bindings/python/tests/test_verification.py b/bindings/python/tests/test_verification.py new file mode 100644 index 0000000..817ee82 --- /dev/null +++ b/bindings/python/tests/test_verification.py @@ -0,0 +1,221 @@ +# SPDX-FileCopyrightText: 2026 tao3k team and Contributors +# +# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +from pathlib import Path +import hashlib +import os +import sys +import time + +import anyio +import pytest + +from lambda_aitia.verification import ( + capture_source_tree, + run_pytest, + run_pytest_frozen, + run_pytest_frozen_async, + source_digest, +) + + +@pytest.mark.parametrize( + ("body", "status"), + [ + ("def test_ok():\n assert True\n", "passed"), + ("def test_no():\n assert False\n", "failed"), + ("import pytest\n@pytest.mark.skip\ndef test_skip():\n pass\n", "no-passing-tests"), + ], +) +def test_pytest_observation_classifies_execution( + tmp_path: Path, body: str, status: str +) -> None: + (tmp_path / "test_case.py").write_text(body) + observed = run_pytest(Path(sys.executable), tmp_path, ("test_case.py",)) + assert observed.status == status + assert observed.passed_obligation is (status == "passed") + assert observed.collected == 1 + assert observed.report_digest is not None + assert observed.source_current(tmp_path) + + +def test_pytest_observation_rejects_no_collection(tmp_path: Path) -> None: + (tmp_path / "test_case.py").write_text("x = 1\n") + observed = run_pytest(Path(sys.executable), tmp_path, ("test_case.py",)) + assert observed.status == "no-tests" + assert observed.exit_code == 5 + + +def test_pytest_observation_detects_source_drift(tmp_path: Path) -> None: + source = tmp_path / "test_case.py" + source.write_text( + "from pathlib import Path\n" + "def test_change():\n" + " Path(__file__).write_text('changed')\n" + ) + observed = run_pytest(Path(sys.executable), tmp_path, ("test_case.py",)) + assert observed.status == "source-changed" + assert not observed.passed_obligation + # The verifier mutated only the frozen execution tree, not caller source. + assert observed.source_current(tmp_path) + assert source.read_text().startswith("from pathlib import Path") + + +def test_frozen_execution_uses_captured_bytes_not_later_source(tmp_path: Path) -> None: + source = tmp_path / "test_case.py" + source.write_text("def test_ok():\n assert True\n") + payload = capture_source_tree(tmp_path) + assert source_digest(tmp_path) == "sha256:" + hashlib.sha256(payload).hexdigest() + source.write_text("def test_no():\n assert False\n") + observed = run_pytest_frozen(Path(sys.executable), payload, ("test_case.py",)) + assert observed.status == "passed" + assert not observed.source_current(tmp_path) + assert observed.source_digest != source_digest(tmp_path) + + +def test_anyio_frozen_execution_keeps_source_and_authority_separate( + tmp_path: Path, +) -> None: + source = tmp_path / "test_case.py" + source.write_text("def test_ok():\n assert True\n") + payload = capture_source_tree(tmp_path) + + async def execute(): + return await run_pytest_frozen_async( + Path(sys.executable), payload, ("test_case.py",), + limiter=anyio.CapacityLimiter(1), + ) + + observed = anyio.run(execute) + assert observed.status == "passed" + assert observed.source_digest == "sha256:" + hashlib.sha256(payload).hexdigest() + assert observed.source_current(tmp_path) + + +def test_anyio_cancellation_waits_for_bounded_verifier_cleanup( + tmp_path: Path, +) -> None: + if os.name != "posix": + pytest.skip("process-group termination is qualified on POSIX") + marker = tmp_path.parent / "cancelled-verifier-marker" + started = tmp_path.parent / "cancelled-verifier-started" + (tmp_path / "test_case.py").write_text( + "import subprocess, sys, time\n" + "from pathlib import Path\n" + "def test_child():\n" + " subprocess.Popen([sys.executable, '-c', " + f"\"import time; from pathlib import Path; time.sleep(2); " + f"Path({str(marker)!r}).write_text('orphan')\"])\n" + f" Path({str(started)!r}).write_text('started')\n" + " time.sleep(5)\n" + ) + payload = capture_source_tree(tmp_path) + + async def observe() -> None: + await run_pytest_frozen_async( + Path(sys.executable), payload, ("test_case.py",), + timeout_seconds=1, + ) + + async def execute() -> None: + async with anyio.create_task_group() as tasks: + tasks.start_soon(observe) + with anyio.fail_after(3): + while not started.exists(): + await anyio.sleep(0.01) + tasks.cancel_scope.cancel() + + anyio.run(execute) + time.sleep(2.2) + assert not marker.exists() + + +def test_frozen_source_rejects_escaping_selectors_and_malformed_bytes( + tmp_path: Path, +) -> None: + (tmp_path / "test_case.py").write_text("def test_ok():\n assert True\n") + payload = capture_source_tree(tmp_path) + with pytest.raises(ValueError, match="selector"): + run_pytest_frozen(Path(sys.executable), payload, ("../test_case.py",)) + with pytest.raises(ValueError, match="trailing"): + run_pytest_frozen(Path(sys.executable), payload + b"forged", ("test_case.py",)) + + +def test_frozen_source_orders_paths_by_canonical_bytes(tmp_path: Path) -> None: + nested = tmp_path / "a" + nested.mkdir() + (nested / "test_case.py").write_text("def test_ok():\n assert True\n") + (tmp_path / "a.txt").write_text("sibling") + payload = capture_source_tree(tmp_path) + observed = run_pytest_frozen(Path(sys.executable), payload, ("a/test_case.py",)) + assert observed.status == "passed" + + +def test_caller_tree_change_during_frozen_run_invalidates_observation( + tmp_path: Path, +) -> None: + source = tmp_path / "test_case.py" + source.write_text( + "from pathlib import Path\n" + "def test_mutate_caller():\n" + f" Path({str(source)!r}).write_text('changed')\n" + ) + observed = run_pytest(Path(sys.executable), tmp_path, ("test_case.py",)) + assert observed.status == "source-changed" + assert not observed.source_current(tmp_path) + + +def test_source_capture_refuses_symlink(tmp_path: Path) -> None: + (tmp_path / "real.py").write_text("x = 1\n") + (tmp_path / "alias.py").symlink_to(tmp_path / "real.py") + with pytest.raises(ValueError, match="symlink"): + capture_source_tree(tmp_path) + + +def test_pytest_observation_rejects_timeout(tmp_path: Path) -> None: + (tmp_path / "test_case.py").write_text( + "from time import sleep\n" + "def test_slow():\n" + " sleep(5)\n" + ) + observed = run_pytest( + Path(sys.executable), tmp_path, ("test_case.py",), timeout_seconds=1 + ) + assert observed.status == "timeout" + assert observed.exit_code is None + assert not observed.passed_obligation + + +def test_pytest_observation_stops_child_process_on_timeout(tmp_path: Path) -> None: + if os.name != "posix": + pytest.skip("process-group termination is qualified on POSIX") + marker = tmp_path.parent / "orphan-marker" + (tmp_path / "test_case.py").write_text( + "import subprocess, sys, time\n" + "def test_child():\n" + " subprocess.Popen([sys.executable, '-c', " + f"\"import time; from pathlib import Path; time.sleep(2); " + f"Path({str(marker)!r}).write_text('orphan')\"])\n" + " time.sleep(5)\n" + ) + observed = run_pytest( + Path(sys.executable), tmp_path, ("test_case.py",), timeout_seconds=1 + ) + assert observed.status == "timeout" + time.sleep(2.2) + assert not marker.exists() + + +def test_pytest_observation_caps_output(tmp_path: Path) -> None: + (tmp_path / "test_case.py").write_text( + "def test_loud():\n print('x' * 500_000)\n" + ) + observed = run_pytest( + Path(sys.executable), tmp_path, ("test_case.py",), + max_output_bytes=4096, + ) + assert observed.status == "output-exceeded" + assert observed.exit_code is None + assert len(observed.output) <= 8192 + assert not observed.passed_obligation diff --git a/bindings/python/uv.lock b/bindings/python/uv.lock index dac486f..e655752 100644 --- a/bindings/python/uv.lock +++ b/bindings/python/uv.lock @@ -2,6 +2,19 @@ version = 1 revision = 3 requires-python = ">=3.11" +[[package]] +name = "anyio" +version = "4.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.15'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" }, +] + [[package]] name = "cffi" version = "2.1.1" @@ -109,6 +122,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "idna" +version = "3.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -123,6 +145,7 @@ name = "lambda-aitia" version = "0.1.0" source = { editable = "." } dependencies = [ + { name = "anyio" }, { name = "cffi" }, ] @@ -134,6 +157,7 @@ test = [ [package.metadata] requires-dist = [ + { name = "anyio", specifier = ">=4.14.1" }, { name = "cffi", specifier = ">=2.0" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.0" }, { name = "setuptools", marker = "extra == 'test'", specifier = ">=77" }, @@ -200,3 +224,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d3691 wheels = [ { url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670", size = 818216, upload-time = "2026-08-08T18:27:56.719Z" }, ] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] diff --git a/build.ss b/build.ss index fb9f466..fbbeffd 100755 --- a/build.ss +++ b/build.ss @@ -20,6 +20,7 @@ "modules/formal-methods/interface.ss" "modules/gitops/interface.ss" "modules/sdlc/interface.ss" + "modules/sdlc/retirement-host.ss" "modules/sdlc/standards/interface.ss" "user-interface/compositions/aitia.ss" "user-interface/profiles/software-engineering/project.ss" @@ -27,7 +28,7 @@ "user-interface/profiles/gitops/dev.ss" "user-interface/profiles/gitops/staging.ss" "user-interface/profiles/gitops/production.ss" - "user-interface/profiles/sdlc/nasa-7150-2d.ss")) + "user-interface/profiles/nasa/sdlc.ss")) (asp-gerbil-scheme-package-spec! (lambda-aitia-package @ asp-gerbil-scheme-library-package-prototype) @@ -37,4 +38,10 @@ '((gxc: "testing-interface") (gxc: "testing-observer")))) -(defbuild-script (lambda-aitia-native-spec)) +(cond-expand + (darwin + ;; Gerbil v0.19's cross-module keyword specialization on Darwin can + ;; reference private __% procedures from POO CLOS .ssxi metadata. + (defbuild-script (lambda-aitia-native-spec) optimize: #f)) + (else + (defbuild-script (lambda-aitia-native-spec)))) diff --git a/docs/adrs/0004-own-assurance-proofs-in-aitia.org b/docs/adrs/0004-own-assurance-proofs-in-aitia.org index 7c6c188..efc6acd 100644 --- a/docs/adrs/0004-own-assurance-proofs-in-aitia.org +++ b/docs/adrs/0004-own-assurance-proofs-in-aitia.org @@ -7,7 +7,7 @@ :ID: lambda-aitia-adr-0004 :STATUS: accepted :DATE: 2026-09-17 -:REVISION: r1 +:REVISION: r2 :RFC_ID: lambda-aitia-rfc-0001 :DECISION_MAKERS: tao3k-maintainers :CONSULTED: poo-flow-proof-maintainers @@ -22,15 +22,19 @@ become owners of Aitia claim, evidence or invalidation semantics. * Decision Outcome -Lambda Aitia owns Lean statements and TLA+ models whose vocabulary is defined by +Lambda Aitia owns Lean statements and Quint models whose vocabulary is defined by Aitia, including evidence freshness, no stale authority, invalidation, counterfactual isolation and the assurance lifecycle. It may consume a pinned, independently versioned POO Flow proof package for generic composition laws. POO Flow proof sources are referenced, not copied or silently moved. -Theorems and TLC receipts remain evidence inputs. Neither grants a current +Theorems and Quint/TLC receipts remain evidence inputs. Neither grants a current Decision or Effect authority. +The 2026-10-10 r2 amendment replaces the maintained TLA+ source and TLAPS +roadmap lane with Quint. TLC remains the bounded checker behind Quint; generated +TLA+ for fairness is a temporary checker artifact, not a maintained model. + * Consequences - Good: proof statements evolve with their semantic owner. diff --git a/docs/adrs/0006-production-python-sdlc-runtime.org b/docs/adrs/0006-production-python-sdlc-runtime.org index cfd2131..7cea091 100644 --- a/docs/adrs/0006-production-python-sdlc-runtime.org +++ b/docs/adrs/0006-production-python-sdlc-runtime.org @@ -1,13 +1,14 @@ # SPDX-FileCopyrightText: 2026 tao3k team and Contributors # SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later -#+title: ADR 0006 — Maintain Python as the production SDLC runtime +#+title: ADR 0006 — Maintain Aitia Python Runtime as a production project :PROPERTIES: :ID: lambda-aitia-adr-0006 :STATUS: accepted :DATE: 2026-09-18 -:REVISION: r1 +:REVISION: r3 +:UPDATED: 2026-09-24 :RFC_ID: lambda-aitia-rfc-0001 :DECISION_MAKERS: tao3k-maintainers :CONSULTED: poo-flow-maintainers @@ -23,12 +24,30 @@ implementations to drift. * Decision -=bindings/python= is Lambda Aitia's production Python project. Its public -runtime covers the complete lifecycle vocabulary: +=bindings/python= is Lambda Aitia's production Python Runtime serving all its +capabilities within the complete SDLC: requirements, design, algorithms and +implementation strategies, testing, delivery, operation and evolution. Its +first lifecycle-planning API projects this assurance/execution protocol: -#+begin_example -change -> invalidate -> plan -> verify -> admit -> decide -> authorize -> effect -#+end_example +#+begin_export mermaid +flowchart TD + C["change
identified"] --> I["invalidate
dependent support stale"] + I --> P["plan
obligations pending"] + P --> V["verify
candidate results produced"] + V --> A["admit
evidence accepted within scope"] + A --> D["decide
decision current"] + D --> U["authorize
bounded effect permitted"] + U --> E["effect
execution receipt recorded"] + V -->|failed or unresolved| P + A -->|relevant premise changes| I + U -->|revoked or expired| I +#+end_export + +Node labels describe the states a completed stage would establish. Feedback +edges describe lifecycle reassessment, not edges returned by the current API. +The API projects only an inert, ordered eight-stage DAG, not executed +transitions. Admission, decision, authorization and execution each require +their own checked receipt. The responsibilities remain layered: @@ -38,9 +57,12 @@ The responsibilities remain layered: - The stable C ABI transfers owned JSON request and receipt bytes. - Python validates and materializes those receipts for production consumers; it does not reproduce the semantic algorithms. +- AnyIO is the runtime-wide asynchronous foundation for structured task + lifetimes, cancellation and bounded concurrency across Aitia capabilities. + It does not grant Host authority or make an inert plan executable. -The first admitted runtime operation is =SdlcRuntime.plan()=. It obtains the -canonical eight-stage DAG from Scheme through ABI revision 2. The plan is +The first exposed planning operation is =SdlcRuntime.plan()=. It obtains the +eight-stage assurance DAG from Scheme through ABI revision 2. The plan is inert and explicitly reports that it neither authorizes release nor proves runtime execution. @@ -50,8 +72,12 @@ runtime execution. - Python API stability is a release concern, not example-code maintenance. - No stage may be presented as executed until its Scheme operation publishes a source-bound receipt and the POO Flow runtime actually executes it. -- The complete lifecycle remains the product boundary even while individual - executable stages are delivered incrementally. +- The full SDLC organizes the Python project; the planning API is one capability + within it. Executable capabilities are delivered only with owned operations + and attributable receipts. +- Generic temporal/causal trajectories and execution mechanisms are reused + from POO Flow. Upstream tools, checkers and assets are qualified at their + usable boundaries; MRR integration is deferred beyond this Python stage. * Confirmation @@ -60,3 +86,11 @@ runtime execution. - Python tests construct the typed plan through the bundled native library. - Wheel verification imports and calls the installed production package without a Gerbil runtime in the Python process. + +* Scope clarification + +Revision r3 clarifies that the complete SDLC is the framework and the existing +eight-stage plan is its assurance/execution protocol. It preserves the Python +project and dependency ownership decision. Async execution, connected causal +trajectories and real tool-to-Host admission remain pending gates under +[[file:../rfcs/0004-sdlc-design-and-causal-trajectories.org][RFC 0004]]. diff --git a/docs/adrs/0008-stable-operation-key-after-lost-ack.org b/docs/adrs/0008-stable-operation-key-after-lost-ack.org new file mode 100644 index 0000000..5897f13 --- /dev/null +++ b/docs/adrs/0008-stable-operation-key-after-lost-ack.org @@ -0,0 +1,125 @@ +# SPDX-FileCopyrightText: 2026 tao3k team and Contributors +# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +#+title: ADR 0008 — Bind retry identity to the logical operation + +:PROPERTIES: +:ID: lambda-aitia-adr-0008 +:STATUS: proposed +:DATE: 2026-09-24 +:REVISION: r2 +:UPDATED: 2026-09-24 +:RFC_ID: lambda-aitia-rfc-0001 +:DECISION_MAKERS: tao3k-maintainers +:CONSULTED: poo-flow-maintainers +:INFORMED: lambda-aitia-consumers +:END: + +* Context and Problem Statement + +The original job-executor declaration says that the provider deduplicates +=tenant/job/operation= keys and guarantees at most one commit *per key*. A +real-process qualification case exposes the gap: after a provider commits but +its acknowledgement is lost, a restarted worker can generate a new key. The +provider correctly deduplicates each key while committing the same logical +job operation twice. This is a known fault-injection case, not a newly found +defect in a deployed application. + +* Decision Drivers + +- Keep a single logical identity across retries and process restarts. +- Preserve completion after a transient acknowledgement loss. +- Do not claim exactly-once effects without provider idempotency and durable + task-state assumptions. +- Keep a revised guarantee and its evidence separate from the old declaration. + +* Considered Options + +1. Keep the per-key guarantee and accept duplicate logical operations. +2. Turn off retries, avoiding duplicates by sacrificing completion. +3. Persist or deterministically derive a stable provider key from the logical + operation identity and revise the guarantee scope. + +* Decision Outcome + +Propose option 3. The job ledger owns the logical operation identity and retry +state. The same tenant/job/operation key reaches an idempotent provider after +restart. The revised =unique-effect= guarantee is at most one external commit +per logical job operation across retries and restarts, conditional on the +provider retaining its deduplication record. A changed guarantee, retry +variation and ADR identity reopen verification; old evidence is not admitted +by declaration-level compatibility. + +* Consequences + +- Good: acknowledgement loss does not create a second effect in the bounded + qualification case, and the worker can still complete after restart. +- Bad: key retention, scoping and provider deduplication are new operational + dependencies to qualify before any production assurance claim. +- Neutral: this proposed ADR neither issues a Host seal nor authorizes an + effect. Acceptance requires independent review of the actual provider and + persistence mechanism. + +* Confirmation + +- The Python project's installed-wheel qualification runs two worker processes + against a persistent SQLite ledger and a loopback HTTP provider. The buggy + key strategy fails with two commits; the stable strategy passes with one. +- The Scheme design-replacement test treats the changed guarantee and decision + as requiring fresh review, with no evidence admission or release authority. +- Full source-bound Host admission and production provider qualification remain + open. + +* Historical input and current coverage + +Revision r2 retains the proposed decision and adds an applicability and owner +audit. It does not accept the ADR or certify a production provider. +[[file:../plans/engineering-case-source-inventory.org][The case-source inventory]] records the AWS article used as design inspiration, +alongside candidate history/observability and composition cases. Its treatment +of late retries and changed intent motivates questions about retention and +payload binding; it is not verification evidence for this project. + +Source inspection on 2026-09-24 found that the fixture worker uses one literal +=job-42= key in fixed mode and always sends =commit=. The provider stores the +effect and key as one SQLite row, uses =insert or ignore= and withholds the +first ACK. This covers a bounded lost-ACK/restart scenario; it has no separate +external effect, expiry, parameter-conflict check or tenant namespace. The +=fixed= mode name denotes the repair to that specific injected fault only. +The inspected sources are the [[file:../../bindings/python/qualification/job_executor/worker.py][worker]], +[[file:../../bindings/python/qualification/job_executor/test_unique_effect.py][provider/test fixture]] and [[file:../../modules/assurance/host.ss][Host preflight projection]]. The reused POO Flow boundary is +=src/modules/authorization/providers/cedar/interface.ss= and its existing +decision/effect-token projection, not a new Aitia-owned authorization module. + +| Contract question | Existing mechanism / evidence boundary | Remaining obligation and owner | +|-------------------+----------------------------------------+--------------------------------| +| Is this attempt currently permitted? | POO Flow Cedar Provider and effect-token machinery exist; Aitia projects preflight requests and rechecks their currentness | Host supplies trusted facts; existing Cedar evaluates policy; Runtime enforces at use. The Aitia-to-Runtime path remains unqualified; no new local authorization engine | +| Does retry preserve logical identity? | fixed mode uses one job identity across two sequential worker processes | Aitia task-state contract and actual provider must qualify tenant/job/operation scope, persistence and concurrent attempts | +| Can the same identity change its intent? | payload is constant; provider does not compare an incoming payload with the stored payload | provider rejects inconsistent reuse; Host/Runtime bind authorized intent to dispatched bytes. Test mismatch rejection without an additional effect | +| What if the deduplication record expires? | no expiry or cleanup is modeled | provider owns retention guarantee; Aitia records retry horizon and review conditions. Test boundary/late requests and explicit reconciliation or rejection outside the guarantee | +| Is effect commit atomic with deduplication? | one SQLite row stands for both key and effect; no failure between independent commits is modeled | actual provider owns atomicity or its recovery protocol. Qualify crashes before/after commit and lost response; policy permission alone is insufficient | +| What if two attempts are allowed concurrently? | existing scenario restarts workers sequentially | Runtime/provider qualify atomic reservation or deduplication and outcome reconciliation; two Cedar permits must not be mistaken for unique execution | + +Do not copy these checks into a second Python policy layer. Trusted +identity/intent, current authority and applicable retry-window conditions may +be inputs to the existing Cedar policy boundary. Their truth must come from +the responsible Host/provider, not caller-supplied booleans. Policy evaluation +does not persist a deduplication record, make a network request atomic, or +prove whether an unacknowledged attempt committed. + +Retry identity and authorization identity are distinct: a logical operation +keeps its identity after restart, but an old permit need not remain valid. +An unknown result must retain that uncertainty until the provider's qualified +retry/reconciliation contract resolves it. Generic POO Flow reconciliation +cannot be assumed to establish uniqueness of an external provider effect. + +The desired design remains stable logical identity with current authorization +and a qualified provider contract. The added obligations refine its conditions; +they do not silently turn the guarantee into unconditional exactly-once delivery. + +* More Information + +- [[file:../../bindings/python/qualification/job_executor/README.org][Job-executor qualification case]] +- [[file:../rfcs/0001-causal-assurance-core.org][RFC 0001 — Causal Assurance Core]] +- [[file:../rfcs/0004-sdlc-design-and-causal-trajectories.org][RFC 0004 — Historical cases and lifecycle design]] +- [[https://aws.amazon.com/builders-library/making-retries-safe-with-idempotent-APIs/][AWS Builders' Library — Idempotent API design]] +- [[https://docs.cedarpolicy.com/auth/authorization.html][Cedar authorization semantics]] diff --git a/docs/adrs/README.org b/docs/adrs/README.org index ed73d1a..322d260 100644 --- a/docs/adrs/README.org +++ b/docs/adrs/README.org @@ -65,8 +65,9 @@ assurance closure and authority receipt is required before any effect. - [[file:0003-review-poo-flow-contract-allowlist.org][ADR 0003 — Superseded POO Flow contract allowlist]] - [[file:0004-own-assurance-proofs-in-aitia.org][ADR 0004 — Own assurance proofs in Aitia]] - [[file:0005-canonical-snapshot-persistence.org][ADR 0005 — Canonical snapshot persistence]] -- [[file:0006-production-python-sdlc-runtime.org][ADR 0006 — Maintain Python as the production SDLC runtime]] +- [[file:0006-production-python-sdlc-runtime.org][ADR 0006 — Maintain Aitia Python Runtime as a production project]] - [[file:0007-consume-poo-flow-public-library.org][ADR 0007 — Consume POO Flow as the mechanism library]] +- [[file:0008-stable-operation-key-after-lost-ack.org][ADR 0008 — Bind retry identity to the logical operation (proposed)]] * Standardization Sources diff --git a/docs/plans/engineering-case-source-inventory.org b/docs/plans/engineering-case-source-inventory.org new file mode 100644 index 0000000..01a1d3b --- /dev/null +++ b/docs/plans/engineering-case-source-inventory.org @@ -0,0 +1,77 @@ +# SPDX-FileCopyrightText: 2026 tao3k team and Contributors +# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +#+title: Engineering case sources and transfer judgments +#+startup: overview + +:PROPERTIES: +:ID: lambda-aitia-engineering-case-source-inventory +:STATUS: draft +:DATE: 2026-09-24 +:END: + +* Scope + +This is a reviewed reading inventory, not an imported dataset or a qualified +Recipe catalog. The links below were inspected on 2026-09-24. No upstream +source bytes, executable revision or license grant are pinned by this document. +No article or Issue is reproduced here. Asset reuse requires a separate version, +attribution/license and execution audit under +[[file:../rfcs/0004-sdlc-design-and-causal-trajectories.org][RFC 0004]]. + +* Source claims and our proposed transfer + +| Source | Source scope | Aitia design question / proposed transfer | Status and limit | +|--------+--------------+------------------------------------------+------------------| +| [[https://aws.amazon.com/builders-library/making-retries-safe-with-idempotent-APIs/][Malcolm Featonby: Making retries safe with idempotent APIs]]; sections on late requests and changed intent | design account of request identity, retention and parameter mismatch | which component owns identity, trusted retry context and durable effect uniqueness? | inspiration for ADR 0008; no qualification of AWS services or our provider | +| [[https://github.com/etcd-io/etcd/issues/19179][etcd #19179]]; original report by serathius, 2025-01-13 | reports a resumable-watch violation involving deletion, compaction, crash and resumed watch; original author had no local reproduction | does our assessment distinguish an absent event from incomplete history, and an author's interpretation from a checked cause? | external report only; linked CI artifacts, later resolution and affected-version range not qualified here | +| [[https://github.blog/news-insights/company-news/oct21-post-incident-analysis/][GitHub: October 21 post-incident analysis]]; Jason Warner, incident in 2018 | describes failover into a database topology incompatible with application expectations | do composition obligations cover recovery topology and latency assumptions across owners? | historical account for applicability review, not a replayed Aitia case or claim about present GitHub architecture | + +The last two rows are candidate questions, not findings about Aitia. They are +not additional implementation workstreams in this slice. Search results, +Issue closure or repeated citations cannot promote their status. + +* Retirement design challenges (2026-09-24) + +| Primary source | Observation status | Design transfer and executable check | Remaining limit | +|----------------+--------------------+--------------------------------------+-----------------| +| [[https://github.com/temporalio/sdk-python#asyncio-cancellation][Temporal Python SDK cancellation documentation]] | documented behavior, not a reported bug: cancellation can be caught or shielded; cancelling a child-start task is not equivalent to cancelling an already started child | =WorkLifecycle= keeps request separate from completion; =RetirementReviewCancellation= must produce a counterexample | no Temporal integration or reproduction claimed | +| [[https://github.com/etcd-io/etcd/issues/19179][etcd #19179]] | original report links CI evidence; local reproduction and affected versions are not qualified here | discovered history/inventory gaps reopen completeness; =RetirementReviewUnknown= must fail | the Boolean inventory abstraction is not a model or reproduction of etcd compaction | +| [[https://github.com/langchain-ai/langgraph/issues/8039][LangGraph #8039]] | author reports checkpoint/write ordering and repeated side effects on recovery, with a reproducer; not independently reproduced here | challenge the assumption that a checkpoint proves all effects are recorded; retain operation identity and replay-epoch obligations | no persistence implementation added; crash/replay model and upstream qualification remain pending | + +The stale-review mutant adds a separate Aitia design challenge: work observed +after review must be rechecked at the final cut. It is not presented as the +root cause of any of these upstream reports. Source reports motivate tests; +their conclusions are not admitted as local evidence. See the +[[file:../receipts/2026-09-24-retirement-formal-review.org][formal review receipt]] for what was actually executed. + +* First bounded transfer judgment + +The current job-executor shares the first source's response-loss uncertainty +and logical retry identity question. It differs in workload and provider: one +fixed job, constant payload, local SQLite and a loopback HTTP endpoint. It has +no multi-tenant identity, retention policy or independently committed remote +side effect. Those differences prevent a claim that it implements the source's +complete service contract. + +The source motivates questions; source inspection answers what the fixture +currently models. [[file:../adrs/0008-stable-operation-key-after-lost-ack.org][ADR 0008]] assigns pending checks to their actual owners, +including existing Cedar authorization and Runtime handoff mechanisms. The +judgment is provisional: provider changes, conflicting observations or source +corrections reopen it. A future executable assessment must bind the consulted +source revision and affected ADR/claim identities before testing invalidation. + +* Acceptance evidence still required + +- [ ] Bind the selected source snapshot/claims and accountable applicability + review without conflating external reports with local observations. +- [ ] Record which previously omitted assumption or design option changed, + compared with the same design review without the historical source. +- [ ] Qualify the pending ADR 0008 contracts with actual positive/negative + receipts; preserve Cedar, Runtime and provider ownership. +- [ ] Introduce a corrected source claim and show a new review cut and reopened + obligations while preserving the earlier judgment and its evidence. + +This inventory adds neither a new public object family nor an inference engine. +Successful designs and rejected alternatives should be included when selecting +further cases, so the corpus does not reduce to incident-driven prescriptions. diff --git a/docs/plans/software-sdlc-integration-inventory.org b/docs/plans/software-sdlc-integration-inventory.org index fedddda..78a91b8 100644 --- a/docs/plans/software-sdlc-integration-inventory.org +++ b/docs/plans/software-sdlc-integration-inventory.org @@ -2,10 +2,15 @@ # SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later #+title: Software/SDLC-to-Aitia Integration Inventory -#+status: draft -#+date: 2026-09-17 #+startup: overview +:PROPERTIES: +:ID: lambda-aitia-software-sdlc-integration-inventory +:STATUS: draft +:DATE: 2026-09-17 +:UPDATED: 2026-09-24 +:END: + * Purpose Lambda Aitia owns causal assurance for software engineering and the SDLC, @@ -17,12 +22,12 @@ ontology surfaces. The software Architecture Decision Record module is also Aitia-owned and is published as =ADR=. No lowercase compatibility module is retained. -The Aitia-side source migration and parity inventory are complete. Publication -cutover remains evidence-partial until Lambda Episteme publishes the deletion -of ADR, GitOps and SDLC and POO Flow pins that clean child commit. This -inventory governs the intended post-migration boundary and prevents either -repository from reintroducing compatibility aliases or duplicate semantic -owners. +The Aitia-side source migration and parity inventory are complete. The +Episteme deletion is published, and the POO Flow main gitlink already pins a +clean Episteme cut. The Aitia PR head is newer than the POO Flow main gitlink +and requires a separate pin update after acceptance. This inventory governs +the post-migration boundary and prevents either repository from reintroducing +compatibility aliases or duplicate semantic owners. Classification: @@ -34,25 +39,26 @@ Classification: * Normative layering -#+begin_example -Lambda Aitia - Software Engineering and SDLC modules - NASA standards and GitOps Profiles - claim and assumption support - typed provenance/dependency/causal/assurance/authority relations - change impact, evidence invalidation and obligation planning - | - | inert plans, decisions and receipts - v -POO Flow - POO composition, functional execution, graph substrate, - verifier/runtime adapters and effect handoff -#+end_example +#+begin_export mermaid +flowchart TD + A["Lambda Aitia
Software Engineering, SDLC, NASA standards and GitOps"] + A --> S["Aitia assurance semantics
claims, assumptions and typed relations"] + S --> I["Impact, invalidation and obligation planning
inert plans and receipts"] + I -->|consumes public mechanisms| P["POO Flow
composition, graph, temporal / causal algorithms and Runtime"] + A --> Py["Aitia Python Runtime
tools and observations"] + Py -->|owned runtime boundaries| P + Py -->|candidate evidence, not authority| S +#+end_export Lambda Episteme composes beside Aitia when an application needs non-SDLC vertical knowledge; it is not an upstream owner for Aitia's SDLC semantics. POO Flow execution does not redefine Aitia reasoning. +The complete SDLC organizes requirements, design, algorithm/implementation +strategies, testing, delivery, operation and evolution. RFC 0004 specifies how +behavior and design/assurance trajectories connect these activities. The +eight-step assurance/execution protocol is one mechanism within this framework. + * Inventory | Current source | Current owner | Classification | Aitia use | Boundary | @@ -66,14 +72,37 @@ POO Flow execution does not redefine Aitia reasoning. | =modules/gitops/=, GitOps/GitHub Profiles and scenario adapter | Lambda Aitia | migrated | repository revision, check and delivery-decision facts | stable public namespace has no version suffix; revision is evidence metadata | | =modules/decision-kind/= | Lambda Episteme | keep, reference | accountable software decision kinds and conflicts | Aitia links decisions but does not redefine their domain meaning | | =src/graph/= | POO Flow | reference | graph substrate and typed traversal | Aitia creates no second graph engine | +| =src/modules/temporal-causality/= | POO Flow | reference | event graphs, cuts, trajectory contracts and temporal classification | Aitia supplies software meaning; complete behavior/design trajectory integration remains pending | | POO Flow verification contribution contracts | POO Flow | reference | verifier capability and receipt handoff | a plan never invokes a verifier directly | | =bindings/runtime-c/include/poo_flow/scheme_native_v1.h= pattern | POO Flow | reference | owned-byte AOT Scheme ABI convention | Aitia uses stable unversioned language namespaces; ABI revision is descriptor metadata | -| =bindings/python= | Lambda Aitia | new | Aitia-owned CFFI consumer and wheel for the Scheme-native ABI | Python transports JSON and never duplicates Scheme decisions | +| =bindings/python= | Lambda Aitia | maintained | production Runtime across Aitia's SDLC capabilities, CFFI, tool observations and AnyIO dependency | async execution and tool-to-Host admission remain pending; Scheme retains semantic ownership | | =packages/python-runtime= | POO Flow | keep, separate | generic runtime and lifecycle binding | never owns Aitia or SDLC semantics | | =packages/proof/lean/PooFlowProof/Enterprise/EvidenceFreshness.lean= | POO Flow proof package | reference | seed for general freshness/no-stale-authority laws | theorem ownership decision must close before any move | -| SDLC/change lifecycle TLA+ model to be added | Aitia | new | invalidate, plan, reverify, decide and gate lifecycle | must be generic, with NASA supplied only as external facts | +| =proof/quint/AssuranceLifecycle.qnt= | Aitia | bounded candidate | invalidate, plan, reverify, admit, decide, authorize and effect lifecycle | generic model; stable-environment progress is conditional, and Host enforcement remains open | | Cedar adapter and Runtime Host | POO Flow | reference | current release/effect authority and final execution gate | no fallback or locally manufactured authority | +* Candidate reuse qualification + +Historical design sources have a separate +[[file:engineering-case-source-inventory.org][source and applicability inventory]]. Inspiration and judgment do not require +an executable asset, and do not qualify a Recipe or establish local evidence. +ADR 0008 records the first source-inspected coverage and mechanism-owner audit. + +The following are candidates, not installed or qualified dependencies. Before +selection, bind exact files and revision/digests, original attribution/license, +dependencies, applicability assumptions, mandatory checks and replay receipt. + +| Candidate | Reuse target | Qualification required | Current status | +|-----------+--------------+------------------------+----------------| +| Specula tools and Case Studies | models, harnesses, traces, reproduction programs and applicable checker code | select an asset-complete record; audit attribution, version and replay conditions | unqualified; no archive-wide import or complete-app backend requirement | +| MRR | future relation queries, derivation and lineage | a later ownership and snapshot-admission contract against its then-current capabilities | deferred; no current Python integration | + +The production code follows upstream reuse only after qualification. No new +proof scorer, Agent kernel or Specula workflow state machine is authorized by +this inventory. Use existing POO composition and POO Flow runtime mechanisms +to organize the selected operations. See +[[file:../rfcs/0004-sdlc-design-and-causal-trajectories.org][RFC 0004]] and the [[file:../../ROADMAP.org][current work queue]]. + * Existing semantics to preserve The current SDLC implementation already enforces boundaries Aitia must not @@ -109,19 +138,19 @@ weaken: The first fixture is purely software engineering: -#+begin_example -Requirement R@1 constrains Implementation I@1 -Verification V@1 checks I@1 against R@1 -Evidence E@1 discharges Obligation O@1 for ReleaseClaim C@1 -Decision D@1 permits deployment Effect X under Snapshot G@1 - -Change R@1 -> R@2 or I@1 -> I@2 - => dependent assumptions and C@1 impacted - => E@1 stale for the changed closure - => O@2 derived for the exact impacted verification set - => D@1 invalid - => X blocked -#+end_example +#+begin_export mermaid +flowchart TD + R["Requirement R@1
scoped"] -->|constrains| I["Implementation I@1
candidate"] + I --> V["Verification V@1
checks I@1 against R@1"] + V -->|bound admission| E["Evidence E@1
current"] + E --> O["Obligation O@1
discharged for ReleaseClaim C@1"] + O --> D["Decision D@1
current under Snapshot G@1"] + D --> X["Deployment Effect X
requires separate authorization / Runtime gate"] + C["R@2 or I@2
changed"] --> A["Dependent assumptions and C@1
impacted"] + A --> S["E@1
stale for changed closure"] + S --> N["O@2
pending for impacted verification set"] + S --> B["D@1 invalid
X blocked"] +#+end_export Required negative cases: diff --git a/docs/receipts/2026-09-23-phase3-planning-replay.org b/docs/receipts/2026-09-23-phase3-planning-replay.org new file mode 100644 index 0000000..8edd1e3 --- /dev/null +++ b/docs/receipts/2026-09-23-phase3-planning-replay.org @@ -0,0 +1,53 @@ +# SPDX-FileCopyrightText: 2026 tao3k team and Contributors +# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +#+title: Phase 3 pure planning replay receipt +#+startup: overview + +:PROPERTIES: +:ID: lambda-aitia-phase3-planning-replay-2026-09-23 +:STATUS: admitted +:DATE: 2026-09-23 +:RFC: lambda-aitia-rfc-0001 +:END: + +* Exact source cut + +- POO Flow mechanism library: =8aa14eafe87ed63d32295f3eac5e5a4eda6658f5= +- Lambda Aitia: =6b1476b38888080c03e514bc36a6d5789056ac64= +- Aitia PR CI: =35847725375= (license, Lean, Scheme/integration/Python passed) +- Local host: Darwin arm64, Gerbil v0.19, =GERBIL_BUILD_CORES=12=, + child-process =env -u SDKROOT=. + +The source cut is an open-PR candidate, not a merged release. The local replay +used isolated checkouts rather than the dirty shared parent worktree. Its +=just check= completed, including the native C ABI harness and Python wheel. + +* Source and negative-contract map + +The public planner in =modules/assurance/funs.ss= consumes a typed snapshot, +changed identities and a matching POO-native verification policy. It uses the +POO Flow Graph dependency order and loop analysis; the plan and verifier-choice +receipts cannot claim verifier execution, release authority or Runtime work. + +=t/assurance/unit/assurance-kernel-test.ss= directly checks: + +- deterministic plan and choice digests under inventory permutation; +- affected prerequisite order, unplanned dependencies, blocked propagation, + SCC cycle witnesses and retention of an independent acyclic branch; +- missing capability, unresolved input and conflicted snapshot blockers; +- same-identity revision/content changes rejecting old obligation bindings; +- independent composition obligations and explicit replacement declaration; +- chosen and rejected verifier candidates, including unavailable capability. + +The verification policy has no suppression or authorization operation. It can +declare capabilities but cannot convert a current failed or blocked request into +an authority decision; =release-authorized?= and =runtime-executed?= are fixed +false in the plan model. Authority remains a separate relation and Host gate. + +* Admission decision + +Phase 3's pure planning and explanation contract is closed for this exact +source cut. This receipt does not attest that any candidate adapter is installed +or trusted, that a verifier ran, that evidence was admitted, or that an effect +was authorized or executed. Those remain Phase 4 and Phase 6 exit gates. diff --git a/docs/receipts/2026-09-23-phase4-admission-ledger.org b/docs/receipts/2026-09-23-phase4-admission-ledger.org new file mode 100644 index 0000000..e0fe9f6 --- /dev/null +++ b/docs/receipts/2026-09-23-phase4-admission-ledger.org @@ -0,0 +1,52 @@ +# SPDX-FileCopyrightText: 2026 tao3k team and Contributors +# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +#+title: Phase 4 Host admission ledger candidate +#+startup: overview + +:PROPERTIES: +:ID: lambda-aitia-phase4-admission-ledger-2026-09-23 +:STATUS: evidence-partial +:DATE: 2026-09-23 +:RFC: lambda-aitia-rfc-0001 +:END: + +* Source cut + +- Aitia base: =9ea6385de909bcde784132532e1e3db7a99a53e4=. +- POO Flow verification owner: =90d1c79f474f89a4f975b282611de55662301b74=. +- Isolated Darwin arm64 checkouts; the Aitia load path selected that exact + POO Flow source before the older installed library. Child tests retained + the runner's =-:max-heap=512M= limit; no global environment was modified. + +* Evidence and negative contracts + +=assurance-host-admit= reads the Host-configured current snapshot and clock, +checks candidate evidence and an exactly bound successful outcome, executes +the configured POO Flow verifier, then issues a private-indexed POO admission. +It does not alter =AssuranceEvidence.admission-state= or the source snapshot. +Currentness rechecks the full semantic snapshot, Host epoch, verifier seal, +candidate evidence, outcome binding and lease. The Host also reconstructs each +snapshot through =assurance-snapshot= before use, rejecting a POO presentation +whose displayed digest, context, state or conflict fields disagree with the +canonical inventory. + +=t/assurance/unit/evidence-admission-test.ss= checks that copied or fabricated +admissions fail; changed source, same displayed digest with changed semantic +content, invalid snapshot source, expiry, clock rollback, explicit revocation +and verifier refusal cannot provide current admission. A source cut returning +to an older value does not revive its earlier admission or Host seal. + +Local =just build-scheme= and =just check-semantic= passed, including the +assurance, ADR, SDLC, GitOps and composition test groups. POO Flow's focused +verification test passed with its weak-key issued-receipt registry and a GC +case proving a live indexed admission retains its receipt. + +* Limits + +This is Host-local issuance and validity, not proof that a deployment installed +a trusted verifier, clock or snapshot source. It is not yet consumed by an +assurance Decision, Cedar projection or Runtime effect gate. The older +snapshot-node =admission-state= remains metadata; no release authority follows +from a Host admission object alone. CI for this candidate cut is separate +from the local replay and must be reported independently. diff --git a/docs/receipts/2026-09-23-phase4-host-capability.org b/docs/receipts/2026-09-23-phase4-host-capability.org new file mode 100644 index 0000000..21f36c7 --- /dev/null +++ b/docs/receipts/2026-09-23-phase4-host-capability.org @@ -0,0 +1,45 @@ +# SPDX-FileCopyrightText: 2026 tao3k team and Contributors +# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +#+title: Phase 4 configured verification Host candidate +#+startup: overview + +:PROPERTIES: +:ID: lambda-aitia-phase4-host-capability-2026-09-23 +:STATUS: evidence-partial +:DATE: 2026-09-23 +:RFC: lambda-aitia-rfc-0001 +:END: + +* Source cut and boundary + +- Aitia base: =742291f3c0bbef5df8a7e6c3fcaba93c0c8afe8c=. +- POO Flow verification mechanism: =8aa14eafe87ed63d32295f3eac5e5a4eda6658f5=. +- The application-configured =assurance-verification-host= keeps the exact + POO Flow adapter, clock and lease in private weak-key storage. Its POO + presentation cannot be copied or edited into a capability. +- Verification refuses an ineligible outcome or evidence not bound to the + current snapshot/outcome before invoking the configured operation. The + operation must still return true to receive a seal. +- Support checks read the Host clock internally; expiry, receipt revocation + and clock rollback fail closed. The low-level under-adapter function is + explicitly conditional, not a Host admission decision. + +* Replay + +The isolated local checkout used the current POO Flow source before the older +installed library in =GERBIL_LOADPATH=. The first attempted test with only the +older installed POO Flow failed an existing fake-adapter rejection assertion; +that run is not accepted as evidence for this source cut. With the pinned +source in the load path, the atomic evidence-admission and assurance-kernel +tests passed, as did =just build-scheme=, the complete assurance test group, +and =just check-semantic= across assurance, ADR, SDLC, GitOps and modules. +The test runner capped each child at =-:max-heap=512M=. + +* Limits + +The Host constructor is a configuration capability, not proof that a trusted +deployment installed it or that its clock is secure. =AssuranceEvidence='s +caller-supplied =admission-state= is still metadata; there is no Host-owned +promotion transition, Cedar projection, or Runtime effect gate in this slice. +No seal or green test outcome is release authority. diff --git a/docs/receipts/2026-09-23-phase5-lifecycle-model.org b/docs/receipts/2026-09-23-phase5-lifecycle-model.org new file mode 100644 index 0000000..8718e0b --- /dev/null +++ b/docs/receipts/2026-09-23-phase5-lifecycle-model.org @@ -0,0 +1,60 @@ +# SPDX-FileCopyrightText: 2026 tao3k team and Contributors +# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +#+title: Phase 5 bounded lifecycle model replay receipt +#+startup: overview + +:PROPERTIES: +:ID: lambda-aitia-phase5-lifecycle-model-2026-09-23 +:STATUS: evidence-partial +:DATE: 2026-09-23 +:RFC: lambda-aitia-rfc-0001 +:END: + +This is a historical TLA+ receipt. Its =proof/tla/= paths refer to the source +at commit =49cbc5d5fd4ec983c98dca8070a0b99bc3fc68b4=; the maintained model +and checks are now in [[file:../../proof/quint/README.org][Quint]]. + +* Exact model cut + +- Aitia base commit: =608525c84e853ed5f5b156693c8f72fe58cfcf97=; + the progress extension and this updated + receipt are the candidate change on top of that commit. +- =AssuranceLifecycle.tla= SHA-256: + =a2a12f2fe4723fe76a248a1eaeebdfd8130590e150bda82116f854f179dde9ab=. +- Safe TLC configuration SHA-256: + =984c59bc517d3ae0a4477e7cb578f4a5a553bd966aed577d202e513679df21e9=. +- Progress and no-fairness configuration SHA-256: + =4940784d1a627de1642cef9f4df7714780345a66aa70b1421923e360558db21f= and + =67ed10ce6eb65ef8eb2998f8f3bfce0cb0984fea1747fd9cd867b15fb31de439=. +- TLC2 2.19, single worker, =JAVA_TOOL_OPTIONS=-Xmx512m=, Darwin arm64. +- Replay commands and the expected invariant for every configuration are in + =proof/tla/README.org=. Each run used a separate =-metadir= under + =/private/tmp=; no global Gerbil/Nix environment was changed. + +* Result + +| Configuration | Expected | Observed | +|---------------+----------+----------| +| Safe | exit 0 | 7,468 generated; 2,202 distinct; queue empty | +| Stable progress with weak fairness | exit 0 | 38 generated; 18 distinct; temporal property passed | +| Stable progress without fairness | =EventuallyEffect= counterexample | exit 12; stuttering after authorization | +| Reachable | =NoEffectStarted= counterexample | exit 12; valid effect trace | +| Unsafe report-only effect | =NoUnsafeEffectStart= counterexample | exit 12 | +| Late admission after change | =AdmissionIsCurrent= counterexample | exit 12 | +| Grant before re-verification | =GrantRequiresCurrentDecision= counterexample | exit 12 | +| Effect after revocation | =NoUnsafeEffectStart= counterexample | exit 12 | + +These are local bounded-model results, not CI results. A deliberate mutant's +exit 12 is a successful *negative contract* only when TLC names the expected +invariant. The safe run's queue was empty, so the finite state space for the +declared bounds was exhausted. + +* Admission boundary + +This receipt supports a candidate dynamic lifecycle contract. It does not +promote a reported result to admitted evidence, establish a trusted Host +adapter/clock, cryptographically bind a Runtime grant, or close Phase 5. +Unconditional progress, counterfactual transition authority, source-bound CI +replay, and the executable Host handoff remain open. The existing POO Flow Cedar +=governance_admitted= field is not an Aitia evidence-admission decision. diff --git a/docs/receipts/2026-09-24-formal-source-qualification.org b/docs/receipts/2026-09-24-formal-source-qualification.org new file mode 100644 index 0000000..55e126d --- /dev/null +++ b/docs/receipts/2026-09-24-formal-source-qualification.org @@ -0,0 +1,124 @@ +# SPDX-FileCopyrightText: 2026 tao3k team and Contributors +# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +#+title: Formal source qualification — local joint gate +#+startup: overview + +:PROPERTIES: +:ID: lambda-aitia-formal-source-qualification-2026-09-24 +:STATUS: locally-qualified-source-boundary +:DATE: 2026-09-24 +:RFC_ID: lambda-aitia-rfc-0004 +:END: + +This is a historical TLA+ receipt. Its =proof/tla/= paths refer to the source +at commit =49cbc5d5fd4ec983c98dca8070a0b99bc3fc68b4=; the maintained model +and checks are now in [[file:../../proof/quint/README.org][Quint]]. + +* What closes here + +This follows the [[file:2026-09-24-retirement-ingress-review.org][ingress review]]. The previously rejected multiline and modular +TLA+ forms now parse in the parser owner without changing the Aitia models. +All five maintained models pass syntax success, artifact validity and exact +source-byte roundtrip. TLC and Lean remain independent gates. + +The native repair lives in the existing linked gerbil-parser checkout, not a +new Aitia parser. Its grammar now treats newlines as lossless whitespace rather +than declaration terminators. Named instances, qualified names and flat leading +junction expressions have explicit CST nodes. The scope does not include +nested indentation-sensitive junction lists, full TLA+ syntax, configuration +parsing or rebuilt Rust/Rowan AOT products. TLC still performs its own parsing +and semantic checks for the exact models/configurations. + +An initial optional-newline production introduced a shift ambiguity at the +next definition and exceeded a 60-second probe. It was replaced, not accepted +as qualified. The stale diagnostic process was interrupted. Gerbil probes +kept a 512 MiB heap; one cold generation diagnostic used a 180-second wall limit. +The final native regression processes completed within the 60-second gate. + +* Maintained POO declaration and source binding + +[[file:../../user-interface/scenarios/job-retirement-contract.ss][The retirement contract]] declares identity =job-executor/retirement=, +revision =r1=, guarantee =retirement-safety/g1= and four explicit assumptions: +atomic epoch fencing, known inventory, no new grant after closure, and the +single-client bound. It grants no allowed runtime effects. + +[[file:../../t/formal/retirement-binding-test.ss][Binding qualification]] uses existing =sdlc-design-implementation-link=, +=sdlc-design-implementation-review=, Assurance snapshots and source digests. +The TLA source group contains four model/helper files and fourteen configs; +the Lean group contains both retirement proof modules. Every member's content +digest participates in its source-group identity. The links bind both groups +to the same contract identity, revision, contract digest and snapshot cut. + +Checks confirm that either source-group mutation changes the snapshot context +and invalidates both old links, even if nominal revisions are unchanged. A +changed design assumption also rejects both links as =contract-changed=. +An initial test incorrectly expected =source-changed=; the existing owner +correctly diagnoses =snapshot-changed= first. The test was corrected without +weakening that owner or changing invalidation semantics. + +These are declared source bindings. They do not prove that the formal formulas +are faithful projections of the natural-language contract, that a Scheme +implementation refines the models, or that the Host admitted verifier evidence. +Review values continue to report =implementation-conforms?=, +=evidence-admitted?= and =release-authorized?= as false. A source-group manifest +is not a complete verifier/toolchain attestation. The explicit joint gate runs +every verifier; source mapping alone is not evidence-coverage admission. + +* Executed gates and replay + +| Gate | Result | +|------+--------| +| parser-owned native suite | 12 test cases, including new CST/roundtrip and incomplete-form rejection tests; =MODULE-OK= / =HARNESS-OK= | +| Aitia native syntax suite | 2 cases; five actual model files plus a lossless rejected-source control | +| Aitia POO binding suite | 4 cases; current binding and source/premise invalidation | +| TLC | 22 configurations match positive/expected-counterexample outcomes | +| Lean | both libraries build; all 20 audited laws have no axioms and no =sorryAx= | +| repository hygiene | license contract and its 7 tests pass; both changed repositories pass whitespace checks | + +=just check-formal= runs the two native suites, TLC and Lean in order. It is an +explicit qualification lane while the parser change remains uncommitted; +default CI is not silently changed to require an unpublished dependency. +With the current package layout, from the Aitia root: + +#+begin_src sh +GERBIL_PATH="$PWD/../../.gerbil" \ +GERBIL_LOADPATH="$PWD/../../.gerbil/pkg/github.com/tao3k/gerbil-parser:$PWD:$PWD/../..:$PWD/../../.gerbil/lib" \ +TLA_TOOLS_JAR=/path/to/verified/tla2tools.jar just check-formal +#+end_src + +No new root variable, global tool installation or source flattening is needed. +The existing atomic test recipe bounds its observer at 1 GiB and its native +test child at 512 MiB; direct parser probes used 512 MiB for the observer too. +TLC uses its checksum-pinned jar, one worker and 512 MiB Java heap. Its local +RMI listener required sandbox execution permission. No remote CI run is claimed. + +* Source cut + +Aitia remains at =de65fa2033ce62569147ee5411c05c50051206dd= with local changes. +The linked parser branch advanced independently during the work from +=f42393559de4aa78620a836777bbfd91ee6751ca= to +=f0df5a11d8014bdfe8be30be5a1f3ad9203ce0bf=. Its unrelated artifact-filename +change is preserved. Our parser edits remain confined to the two files below. +The qualified parser source resolves through the parent's existing package +link to =/private/tmp/gerbil-parser-rowan-scanned-tokens=. + +| Source | SHA-256 | +|--------+---------| +| parser =languages/tla-plus/v1/grammar.ss= | =a419e55d490afbdd7c8e31cf36fa17b2e598aac0b470dd6d8e2a5a12089299e6= | +| parser =languages/tla-plus/v1/parser-test.ss= | =44fcd73fd6e9f736ec8b736db0f92fc83f635a0ef04f1bfca16a0874ee5a56da= | +| Aitia =user-interface/scenarios/job-retirement-contract.ss= | =e333a91bf0964fb2762ddcf54e7bd5fa7c5e8071e721a45b50479617c27b5965= | +| Aitia =t/formal/tla-syntax-test.ss= | =2f95897254ac97067eba5f728fd01be2b3b7f49ac1f2be2aef8fe1c25a8ff982= | +| Aitia =t/formal/retirement-binding-test.ss= | =93438d52f8e71242615b6ef8feeb5f600d83da7a0344de6ad33916f1c3679f79= | + +* Remaining exit gates + +- Publish the parser-owned repair, update the actual dependency authority and + independently replay without relying on this dirty linked checkout. Neither + commits, pushes nor dependency-pin changes were performed in this slice. +- Establish semantic correspondence or generation of the formal projections + from the owning POO contract; source identity tests cannot substitute for it. +- Refine atomic fencing to actual authority/Runtime behavior, then evaluate + repeated clients and fairness under explicit assumptions. +- Keep persistence and actual effect execution deferred. This is local closure + of the source/verification plumbing, not production or SDLC-wide closure. diff --git a/docs/receipts/2026-09-24-retirement-formal-review.org b/docs/receipts/2026-09-24-retirement-formal-review.org new file mode 100644 index 0000000..8b811da --- /dev/null +++ b/docs/receipts/2026-09-24-retirement-formal-review.org @@ -0,0 +1,162 @@ +# SPDX-FileCopyrightText: 2026 tao3k team and Contributors +# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +#+title: Retirement design — modular formal review +#+startup: overview + +:PROPERTIES: +:ID: lambda-aitia-retirement-formal-review-2026-09-24 +:STATUS: evidence-partial +:DATE: 2026-09-24 +:RFC_ID: lambda-aitia-rfc-0004 +:END: + +This is a historical TLA+ receipt. Its =proof/tla/= paths refer to the source +at commit =49cbc5d5fd4ec983c98dca8070a0b99bc3fc68b4=; the maintained model +and checks are now in [[file:../../proof/quint/README.org][Quint]]. + +* Decision and evidence boundary + +New persistence integration is deferred. The prior +[[file:2026-09-24-retirement-impact.org][Scheme Impact checks]] exercise structural/temporal analysis, not a proof of +retirement safety. Existing Lean laws covered fixed-snapshot impact and design +substitution; the existing TLA+ model covered assurance admission/authorization. +Neither covered the new retirement review. This slice adds design experiments, +not deployed deletion, a new public DSL, or a second generic Impact engine. + +[[file:../plans/engineering-case-source-inventory.org][The source inventory]] distinguishes Temporal's documented cancellation +semantics, the etcd watch report and a LangGraph recovery report from our own +proposed transfers. None was replayed locally in this slice. Known upstream +reports are not new bug discoveries, and these models do not reproduce their +implementations. + +* Executed checks + +Run from the Aitia root with the existing pinned =TLA_TOOLS_JAR=: + +#+begin_src sh +just check-tla work +just check-tla retirement +just check-tla +just check-proof +just check-license-contract +#+end_src + +| Lane / configuration | Generated / distinct states | Observed result | +|----------------------+-----------------------------+-----------------| +| =WorkLifecycle= | 14 / 8 | exit 0; queue empty | +| =WorkCancellation= | 3 / 2 | exit 0; work remains after request | +| =WorkCancellationReachable= | 2 / 2 | exit 12; =NeverCancelled= violated | +| =WorkQuiescenceReachable= | 8 / 7 | exit 12; =NeverQuiescent= violated | +| =RetirementReview= | 84 / 42 | exit 0; queue empty; deadlock checks enabled | +| =RetirementReviewCancellation= | 24 / 15 | exit 12; =NoOutstandingWork= violated | +| =RetirementReviewStale= | 46 / 27 | exit 12; =NoOutstandingWork= violated | +| =RetirementReviewUnknown= | 49 / 28 | exit 12; =InventoryKnown= violated | +| =RetirementReviewReachable= | 39 / 21 | exit 12; =NeverRetired= violated | + +All nine new configurations and the eight unchanged assurance configurations +passed their expected-outcome contracts. This means 17 configurations within +three unittest methods, not 17 successful safety theorems. The harness checks +the exact counterexample property, exit code, nonempty exploration and trace. +Negative checks stop at a counterexample and need not exhaust their queues. + +One local work-only run took 2.595 s; retirement-only took 3.321 s; the final +all-module run took 11.390 s. These are single-run unittest wall times including +JVM startup, not a repeated benchmark or a general speedup claim. Atomic +selection avoids unrelated models; changing the shared work module still +requires its retirement consumer check. + +Lean 4.31.0 built both assurance/SDLC libraries. Six new exported laws prove +initial safety, one-step preservation, reachable-state safety and three unsafe +state witnesses. All 16 audited laws reported no axioms; no =sorryAx= was used. +The observation relation overapproximates all nonterminal observations, while +the finalize action checks current work/inventory. This is an abstract-model +proof, not an established refinement from TLA+ or Scheme. + +The invalid module selector =not-a-module= was also checked with the project's +existing Python interpreter: argparse exited 2 rather than running zero tests. +The same unittest suite remains discoverable through the existing CI command. +No remote CI run or clean-checkout validation is claimed. + +* Unclosed native syntax gate + +The parent dependency resolves to gerbil-parser commit +=562dcd428f00b05ca87407fd54b1e04dcf076ae9= at +=/private/tmp/gerbil-parser-rowan-scanned-tokens=, with a clean source worktree +at inspection. Importing its public =parse-tla-plus-v1= and runtime artifact +checks under a 512 MiB Gerbil heap / 60-second timeout rejected both sources: + +| Source | First diagnostic | +|--------+------------------| +| =WorkLifecycle.tla= | =GERBIL-PARSER-TLA-PLUS-V1=, =lr-no-action=, state 209, byte 472: newline after the =RequestCancellation= definition header | +| =RetirementReview.tla= | same code/kind, state 205, byte 541: newline within the =VARIABLES= declaration | + +The inspected grammar requires these expressions/declarations on one line; +TLC's parser accepts the native multiline forms. These are first-failure +locations, not an exhaustive parser capability audit: named =INSTANCE= and +qualified operators also require qualification after the newline gap is fixed. +No alternate parser, flattened-source workaround or false roundtrip receipt +was introduced. Native parser acceptance/byte-roundtrip remains failed. + +The executable reproduction uses the parent =GERBIL_PATH= and its existing +=GERBIL_LOADPATH= dependency source/library paths, then for each model: + +#+begin_src scheme +(import (only-in :std/misc/ports read-all-as-string) + (only-in :gerbil-parser/languages/tla-plus/v1/parser parse-tla-plus-v1) + (only-in :gerbil-parser/src/runtime/artifact + parse-artifact-success? parse-artifact-valid? + parse-artifact-roundtrip parse-artifact-ref)) +(let* ((source (call-with-input-file "proof/tla/WorkLifecycle.tla" read-all-as-string)) + (artifact (parse-tla-plus-v1 source))) + (displayln (parse-artifact-ref artifact 'diagnostics)) + (exit (if (and (parse-artifact-success? artifact) + (parse-artifact-valid? artifact) + (equal? source (parse-artifact-roundtrip artifact))) 0 1))) +#+end_src + +* Reproducibility and source cut + +Aitia HEAD is =de65fa2033ce62569147ee5411c05c50051206dd= with uncommitted +changes; this is a shared-worktree receipt. TLC release v1.7.2 reports TLC2 +2.17 (2022-02-02, revision 3c7caa5); its SHA-256 is +=fa18543e44ed5974a85bd2c60c0dc16620ae117680ea8e693d2691999ed90b22=. +Each execution used one worker, =-Xmx512m=, a fresh temporary state directory +and a 60-second process timeout. The sandbox first blocked Java's local RMI +listener; the same bounded checks passed with local execution permission. + +| Source (relative to Aitia) | SHA-256 | +|---------------------------+---------| +| =proof/tla/WorkLifecycle.tla= | =079452f967fc2f1d21a1e962dd0d38a5e62fa6b36a4055d2c42358fb006fbb23= | +| =proof/tla/RetirementReview.tla= | =4edbe49d59f663a007ad986108d1e374e3408c4593fb4c4c53a34c02232ed01a= | +| =proof/lean/LambdaAitiaProof/Modules/Sdlc/Retirement.lean= | =86f899c87cfd51ed2daa23dbc0c4b3bb88914851b42afeed28dc51e067eb45d3= | +| =bindings/python/tests/tla_contract.py= | =788e97254d0923104357654b59e6ad2902a9ec483eb72f35c75d8e18657c13a3= | +| =proof/tla/WorkCancellation.cfg= | =503131d547fa46de7555b245156dc072ebb04c4a32f3fc6f4672ff600f751198= | +| =proof/tla/WorkCancellationReachable.cfg= | =a79ba63517e17e4fd7ca1eb7d2964455f8c40e58a2b5a87aef6f29d565d8f5b1= | +| =proof/tla/WorkLifecycle.cfg= | =1084116ad37aeb2bbb511ce9fd7bded6381c946f0ba39f927116ff722a8910bf= | +| =proof/tla/WorkQuiescenceReachable.cfg= | =a4467507d11ce289101a29644dd84d45c57fb72c534d25d096b2276d46d31ed7= | +| =proof/tla/RetirementReview.cfg= | =9a433fe46ec346fe6d180ca0483b415cdf0f6f4fc9650cb739c6ce4e72dfd2df= | +| =proof/tla/RetirementReviewCancellation.cfg= | =641e05f7356e568494e2dc7fbc340635031706593a07971c612c6891ada31969= | +| =proof/tla/RetirementReviewReachable.cfg= | =1ec1a31f4e3d25c0d7dba39abdc1f79300a4ef7a80e683565cdf8a2d96231093= | +| =proof/tla/RetirementReviewStale.cfg= | =28e8245f7bb71691d7d0b274a06b6458d6a81f932520b74bc5d228f6d9e8442f= | +| =proof/tla/RetirementReviewUnknown.cfg= | =e935c67af6280fb68fddb1819ed0364ddbb5cd21aae6fb4f428df48d9027d61a= | + +* Next gates, not claimed complete + +Follow-up: [[file:2026-09-24-retirement-ingress-review.org][the ingress review]] adds a bounded challenge to item 3 and extracts the +existing grant predicate. The source hashes and 17-case results above remain +the earlier cut; the follow-up records its changed runner/model hashes and +22-case run. Items 1, 2 and live authority correspondence remain open. + +1. Qualify the native parser for modular/multiline TLA+ without weakening the + source-byte identity or introducing a downstream parsing authority. +2. Bind admitted Scheme Contract identity/revision and projection digests to + the TLA+ and Lean views, with an executable correspondence check. +3. Challenge the terminal assumption by composing retirement with ingress and + revocation: a late request after finalization must not be hidden by a + frozen state. Reuse the current authority owners; do not add a policy engine. +4. State fairness assumptions before proving eventual drain/retirement. + Unknown inventory, replay, multiple consumers and repeated late arrivals + remain outside this finite model's complete guarantees. +5. Keep persistence and real effect execution deferred. Neither abstract proof + nor the synthetic Scheme case constitutes production readiness. diff --git a/docs/receipts/2026-09-24-retirement-impact.org b/docs/receipts/2026-09-24-retirement-impact.org new file mode 100644 index 0000000..e9e147b --- /dev/null +++ b/docs/receipts/2026-09-24-retirement-impact.org @@ -0,0 +1,102 @@ +# SPDX-FileCopyrightText: 2026 tao3k team and Contributors +# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +#+title: Job-executor retirement Impact — bounded analysis receipt +#+startup: overview + +:PROPERTIES: +:ID: lambda-aitia-retirement-impact-2026-09-24 +:STATUS: evidence-partial +:DATE: 2026-09-24 +:RFC_ID: lambda-aitia-rfc-0004 +:END: + +* Subject and source boundary + +The selected capability is automatic retry in a synthetic job-executor case. +The candidate successor is reviewed recovery. These are fixture identities, +not deployed features or evidence that a migration occurred. The case reuses +POO Flow Graph and its public Temporal Causality interface; no new traversal, +time classifier, policy evaluator or Runtime is implemented. + +The [[file:../../user-interface/scenarios/job-executor-retirement.ss][maintained scenario]] contains pre-removal and candidate post-removal graphs, +plus an explicitly identified synthetic event history. The [[file:../../t/sdlc/unit/job-retirement-impact-test.ss][Scheme qualification]] +checks inert analysis receipts. It does not read the Python fixture's SQLite +ledger; historical results and current activity here are modeled facts. + +The shared worktrees were dirty. The parent checkout reported +=a9a2b331d42fda409ea142fb67137f395721397e= and Aitia reported +=de65fa2033ce62569147ee5411c05c50051206dd=; the new files below were uncommitted. +This is not an isolated clean-build or release receipt. + +| Source | SHA-256 | +|--------+---------| +| Aitia =user-interface/scenarios/job-executor-retirement.ss= | =1977be963e74ef6aabd48ff7b1649f423b1cb2a9b047d273bbfbf98e42c43044= | +| Aitia =t/sdlc/unit/job-retirement-impact-test.ss= | =8bc2fab67c2249a1433038f7c957e94189a8d65f1b47067ffc849eefc94887a6= | +| POO Flow =src/modules/temporal-causality/funs.ss= | =4ffc3517b48bba51abafb28f91c3e0a2e0994a1904911509b222e2780c0df00b= | +| POO Flow =src/modules/temporal-causality/objects.ss= | =19ada4187ad1f380c7dc95f58be97c24681f01b9d8d0ac21afaa582583893454= | +| POO Flow =src/modules/temporal-causality/types.ss= | =78499931958750bbfb458c14ba92beb3d44a4d1e6f0298f8c87adf1a85fd6a53= | + +These are inspected source hashes, not an attestation of all transitive binary +dependencies. Tests use the existing compiled dependency library and source +load paths below. No claim of fresh whole-package compilation is made. + +* Seven retirement checks + +1. The old graph retains the historical result, active job, queued job and its + verification dependency; an unrelated export is excluded. The verification + witness is =automatic-retry → queued-job → verify-queued-retry=. +2. Selecting the removed identity in the candidate graph raises an error + instead of reporting no impact; the before graph remains available. +3. An incomplete external-consumer inventory yields =partial-impact=. +4. The cut at logical position 4 separates past completion/start, explicit + current activity, declared future retry, counterfactual retirement and an + out-of-horizon retry. No activity is inferred merely from an old start. +5. Creating the later cut retains the earlier historical cut and its original + observed result/payload. +6. A missing causal parent remains =unknown-consumer-origin= in the unknown + frontier and yields partial temporal classification. +7. The retirement proposal cannot be selected as an observed trigger in the + current cut. Neither structural nor temporal analysis grants execution. + +* Local replay + +From the Aitia root, against the existing parent dependency installation: + +#+begin_src sh +GERBIL_LOADPATH="$PWD:$PWD/../..:$PWD/../../.gerbil/lib" \ + just test-scheme-atomic sdlc unit/job-retirement-impact-test.ss +GERBIL_LOADPATH="$PWD:$PWD/../..:$PWD/../../.gerbil/lib" \ + just test-scheme-atomic sdlc unit/design-contract-test.ss +env -u SDKROOT GERBIL_PATH="$PWD/.gerbil" \ + GERBIL_LOADPATH="$PWD:$PWD/../..:$PWD/../../.gerbil/lib" \ + timeout --foreground --signal=TERM --kill-after=3s 60s \ + gxi -:max-heap=512M ./run-test.ss ../../t/temporal-causality-core-test.ss +#+end_src + +All three focused modules returned =MODULE-OK= and =HARNESS-OK= with exit 0. +They contain seven retirement, eight design-replacement and eight core cases. +Gerbil reported =2591dcd on Gambit dcd677c=. The existing Testing Interface +selects a 512 MiB isolated test process; the existing Justfile's observer +process has a separate 1 GiB bound. The direct core observer used 512 MiB. + +The initial run with only the child default load path failed to resolve +=asp-gerbil-scheme/testing-api=. Supplying the parent dependency library via +=GERBIL_LOADPATH= fixed module discovery; no new root variable, global package +installation or user-environment mutation was introduced. + +* Remaining closure + +- [ ] Derive current activity and historical results from actual persisted + task/provider observations, rather than this synthetic inventory. +- [ ] Bind a source-level change and both graph revisions to admitted SDLC + facts, then derive reviewed migration/drain obligations with accountable owners. +- [ ] Exercise actual cancellation/migration through existing authorization and + Runtime boundaries, including races and failure/recovery receipts. +- [ ] Demonstrate replay/interpretation of an old persisted result and prevent + a successor from inheriting incompatible historical support. +- [ ] Qualify the Scheme-owned Org Contract / Rowan presentation separately; + these tests do not exercise Org parsing or the old CLI. + +The lifecycle retirement gate in RFC 0004 remains open. This receipt qualifies +one bounded analysis scenario, not safe production deletion or completed SDLC. diff --git a/docs/receipts/2026-09-24-retirement-ingress-review.org b/docs/receipts/2026-09-24-retirement-ingress-review.org new file mode 100644 index 0000000..9b00403 --- /dev/null +++ b/docs/receipts/2026-09-24-retirement-ingress-review.org @@ -0,0 +1,147 @@ +# SPDX-FileCopyrightText: 2026 tao3k team and Contributors +# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +#+title: Retirement ingress — terminal assumptions challenged +#+startup: overview + +:PROPERTIES: +:ID: lambda-aitia-retirement-ingress-review-2026-09-24 +:STATUS: evidence-partial +:DATE: 2026-09-24 +:RFC_ID: lambda-aitia-rfc-0004 +:END: + +This is a historical TLA+ receipt. Its =proof/tla/= paths refer to the source +at commit =49cbc5d5fd4ec983c98dca8070a0b99bc3fc68b4=; the maintained model +and checks are now in [[file:../../proof/quint/README.org][Quint]]. + +* Design finding + +The [[file:2026-09-24-retirement-formal-review.org][isolated review model]] prevents observations after its terminal state. +That does not prove that external callers stop sending requests. The new +[[file:../../proof/tla/RetirementIngress.tla][ingress composition]] permits a request before or after retirement, including +a client retaining a cached grant. Merely removing a capability from a graph, +or marking review complete, does not fence these callers. + +The modeled correction is ordered: close grant issuance and advance the +authority epoch, drain known work, then finalize. At effect start, recheck the +grant epoch. =EffectAdmission.CurrentGrant= is extracted without changing the +existing =AssuranceLifecycle.StartEffect= predicate; both consumers use that +same operator. This is reuse of a formal authorization contract, not a new +policy evaluator or a claim that Cedar/Runtime already implements the mapping. + +* Executed model checks + +#+begin_src sh +just check-tla ingress +just check-tla +just check-proof +#+end_src + +The same pinned TLC jar and 512 MiB / one-worker / 60-second-per-run limits as +the earlier receipt were used. Local RMI required sandbox execution permission. + +| Configuration | Generated / distinct | Expected and observed outcome | +|---------------+----------------------+-------------------------------| +| =RetirementIngress= | 741 / 248 | exit 0, queue empty; no post-retirement start; retired work drained, inventory known, epoch fenced | +| =RetirementIngressUnfenced= | 413 / 150 | exit 12, =NoPostRetirementStart= violated when finalization omits the fence | +| =RetirementIngressCachedGrant= | 524 / 179 | exit 12, same invariant violated when effect start skips grant validation | +| =RetirementIngressRejected= | 499 / 178 | exit 12, =NeverRejectAfterRetirement= violated: a later request really reaches rejection | +| =RetirementIngressStarted= | 38 / 23 | exit 12, =NeverStarted= violated: the model also allows an ordinary pre-retirement start | + +The last two are non-vacuity controls, not defects. The safe model includes +explicit terminal self-loops and keeps deadlock checking enabled. Negative +configurations require an actual trace and the named invariant violation, not +an arbitrary nonzero process result. + +All 22 configurations across four unittest methods matched their contracts. +The eight existing assurance checks retained exactly their preceding generated +and distinct state counts after extraction of =CurrentGrant=. This is a +regression observation, not a formal equivalence proof of the extraction. +The ingress-only run took 3.407 s and the complete run 14.813 s; these are +single-run wall times including JVM startup, not general performance claims. + +Four additional Lean laws prove initial safety, incompatibility of a current +cached epoch with safe retirement, preservation across abstract boundary steps, +and reachable-state safety. They reuse =Retirement.stepPreservesSafety=. +Requests/rejections can stutter after retirement; a current-grant start is not +excluded by a hard-coded retired guard but by the proved fence invariant. +All 20 exported audited laws reported no axioms and no =sorryAx=. + +* Explicit abstraction limits + +- One pending request and one attempt are modeled, with epochs 0 and 1 and a + cached epoch 0. Revision, admission, decision and time are fixed to isolate + revocation. New grant issuance, grant expiry and repeated retries are not + explored by this composition; the existing lifecycle regression is separate. +- Closing issuance and advancing the epoch is one atomic action. Actual + distributed ordering, concurrent authorities and restart behavior remain + obligations, not facts inferred from TLC. +- =known= is an assumption about inventory, not proof of actual completeness. + Post-retirement discovery of previously unknown work is not covered. +- No fairness or eventual retirement is claimed. Lean's abstract relation is + not yet a proved refinement of the TLA+ actions or a Scheme implementation. +- Persistence, runtime effects, package publication and remote CI were not + performed. The worktree remains uncommitted. + +* Native parser: four independent capability gaps + +At parser commit =562dcd428f00b05ca87407fd54b1e04dcf076ae9=, the public native +=parse-tla-plus-v1= API was probed with this wrapper: + +#+begin_example +---- MODULE Probe ---- + +==== +#+end_example + +| Body | Parse success | First native diagnostic | +|------+---------------+-------------------------| +| =Value == TRUE= | true | none; valid artifact and exact byte roundtrip | +| =Work == INSTANCE WorkLifecycle= | false | =lr-no-action=, state 137, byte 40, =WorkLifecycle= | +| =Value == Work!Quiescent= | false | =lr-no-action=, state 137, byte 36, =!= | +| =VARIABLES active,= then newline and = queued= | false | =lr-no-action=, state 205, byte 40, newline | +| =Value= definition header, then newline and =TRUE= | false | =lr-no-action=, state 209, byte 31, newline | + +These are syntax probes, not standalone resolved modules or TLC checks. +Grammar inspection agrees: only unnamed instance declarations exist, qualified +operator selection has no production, and newlines terminate declarations. +This expands the previous first-failure diagnosis; it does not repair the +parser. No parser-owner source was modified and no downstream fallback added. + +Notably the four rejected probes still reported a valid artifact and lossless +roundtrip. Those properties describe a well-formed diagnostic/lossless token +artifact; they do NOT mean successful syntax admission. Qualification must +continue requiring =success AND valid AND exact roundtrip=. + +The probe used a bounded =gxi -:max-heap=512M= process with 60-second timeout +and parent source/dependency paths in =GERBIL_LOADPATH=. An initial attempt +omitting the parent source path failed at an unbound observability import; +the corrected path resolved that import without changing dependencies. + +* Source cut and next obligations + +This continues the uncommitted Aitia source cut at +=de65fa2033ce62569147ee5411c05c50051206dd=. + +| Source | SHA-256 | +|--------+---------| +| =proof/tla/EffectAdmission.tla= | =94ab242a5b2610dff134511dd9e4431e9e831d863938bfa30dadea7718a2deb5= | +| =proof/tla/AssuranceLifecycle.tla= | =8b63b1b334cf31d490630c594aec7de0c30b0a5d4dcc4728d4f9d9c5fbc5bcfc= | +| =proof/tla/RetirementIngress.tla= | =7227aa8deea008de87e3204c659254d72e2bb153889f5baf2d2993b979067af5= | +| =proof/tla/RetirementIngress.cfg= | =df8d25edb57d96fc1756167e07b93f95eac717ab6741cbc5a8b0e5149a2c217a= | +| =proof/tla/RetirementIngressCachedGrant.cfg= | =9c9330666e2bed4cddb2fad63020b9c8432ad573722ca8d18a751e7da253acd0= | +| =proof/tla/RetirementIngressRejected.cfg= | =7c5725cf1ab1269da358a2366bba7609da0f704787df710dc8cff0a7299d1a51= | +| =proof/tla/RetirementIngressStarted.cfg= | =b2645bd555d48a306e6a232ce0ecd2a534d3a5ae27b6f8a1d36f15855dcc5b8e= | +| =proof/tla/RetirementIngressUnfenced.cfg= | =ec3c2181c8714a0408ce4da1430bdb79cc23d30623f6963e99d84673ea5064b0= | +| =proof/lean/LambdaAitiaProof/Modules/Sdlc/RetirementIngress.lean= | =cc1b7609613461a61ef6825edd39ae055860d99c1691895b437af099c3e27dd0= | +| =bindings/python/tests/tla_contract.py= | =2dfb943c0dabfe3e97ed4225926f3ce88e60e4f3ce6ddfba5e749ddbd3d1a548= | + +1. Repair modular/multiline syntax in the parser owner and require exact + source-byte roundtrip. Do not flatten source to disguise missing grammar. +2. Bind the formal projections to admitted Scheme Contract identity/revision + and prove or test the claimed correspondence; hashes alone do not do that. +3. Challenge atomic fencing/new issuance and repeated/concurrent requests, + then state fairness premises for progress. Keep using the existing authority + owner rather than adding another authorization implementation. +4. Keep persistence deferred while these design/verification gates are open. diff --git a/docs/rfcs/0001-causal-assurance-core.org b/docs/rfcs/0001-causal-assurance-core.org index 3b49a47..788a0c7 100644 --- a/docs/rfcs/0001-causal-assurance-core.org +++ b/docs/rfcs/0001-causal-assurance-core.org @@ -8,23 +8,31 @@ :ID: lambda-aitia-rfc-0001 :STATUS: draft :DATE: 2026-09-17 +:UPDATED: 2026-09-24 :END: * Decision summary -Lambda Aitia owns a domain-neutral, POO-native model of why an engineering -claim is justified, what defeats or invalidates that justification, which -verification obligations follow, and which decisions/effects must remain -blocked. It does not own domain knowledge, a second graph engine, verifier -implementations, runtime execution or effect authority. +Lambda Aitia organizes its software-engineering capabilities around the +complete SDLC, including requirements, design, algorithms and implementation +strategies, verification/testing, delivery, operation, maintenance and +retirement. Its POO-native assurance kernel models why an engineering claim is +justified, what defeats or invalidates that justification, which obligations +follow, and which decisions/effects must remain blocked. Software semantics +belong to Aitia's modules; generic graph, temporal/causal and runtime mechanisms +remain POO Flow-owned, and existing verifiers retain their checking semantics. The core abstraction is an executable causal assurance case, not a collection of verification result flags: -#+begin_example -claims + assumptions + typed relations + obligations + evidence -+ invalidation + decisions + effect guards -#+end_example +claims, assumptions, typed relations, obligations and evidence, together with +invalidation, decisions and effect guards. + +[[file:0004-sdlc-design-and-causal-trajectories.org][RFC 0004]] refines this architecture with design alternatives, conditional +guarantees, behavior and design-evolution trajectories, reusable Recipes and +the deferred MRR direction. Software design is an early leverage point within +the full lifecycle. The eight-stage assurance/execution flow is a protocol +used throughout that lifecycle, not a complete SDLC phase list. * Problem @@ -51,9 +59,9 @@ through named, checked functions that emit receipts. | Lambda Aitia | Software/SDLC, ADR and standards modules; claims, assumptions, relation semantics, obligations, evidence admission, invalidation and planning | non-software vertical truth, graph runtime, verifier semantics or effect execution | | POO Flow | POO composition, functional flow, graph substrate, adapter/runtime handoff | concrete contribution semantics or silent assurance promotion | | POO Flow Python runtime | generic runtime and lifecycle transport | Aitia bindings, Aitia Scheme semantics or a second SDLC implementation | -| Aitia Python SDLC Runtime | production SDLC API, Aitia-owned CFFI transport, ABI negotiation and typed receipt projection | reimplementing Scheme decisions or POO Flow's generic Runtime mechanisms | +| Aitia Python Runtime | production APIs across Aitia's SDLC capabilities, CFFI transport, tool execution integration and typed observations/receipts | reimplementing Scheme decisions or POO Flow's generic Runtime mechanisms | | Lean | general proof terms and assurance laws | current runtime state or action authority | -| TLA+/TLC | bounded temporal lifecycle behavior and counterexamples | theorem proving or production execution | +| Quint/TLC | bounded temporal lifecycle behavior and counterexamples | theorem proving or production execution | | Cedar | authorization evaluation over admitted input | evidence freshness, proof construction or effect completion | | Runtime Host | final typed boundary for authorized execution | manufacturing missing evidence or guessing transport/runtime state | | Application | explicit selection, composition and policy | redefining source-owned semantics | @@ -74,7 +82,9 @@ explicit descriptor metadata rather than part of those names. The production Python package validates and materializes lifecycle requests and receipts; Scheme remains the decision owner. POO Flow's =packages/python-runtime= remains the separate generic execution/lifecycle mechanism consumed by the -Aitia SDLC runtime rather than becoming a second SDLC semantic API. +Aitia Python Runtime rather than becoming a second SDLC semantic API. AnyIO +provides the runtime-wide concurrency foundation; thread-affine native sessions +and Host-issued authority must retain their owner at asynchronous boundaries. * Semantic objects @@ -106,7 +116,23 @@ commitment state and declared causal parents. A question that must be discharged for a claim or decision at an exact snapshot. The obligation states the required evidence kind and capability; -it does not select or invoke a tool by itself. +it does not select or invoke a tool by itself. A candidate obligation is +explicitly bound to the snapshot identity, revision and canonical context +digest before selection. That context digest excludes only the obligations' +own binding fields to avoid a self-referential hash; the final snapshot digest +includes the complete bound obligations. Rebinding an old obligation is not +replacement-obligation derivation and is rejected. +The first derivation step emits an inert replacement-requirements receipt +from an old snapshot, a target snapshot and a change set. It names affected +old obligations and their structural witnesses, and blocks automatic carryover +when the source binding, target frontier, claim or policy is not current. +It does not assign a new obligation identity or claim a replacement exists. +An explicit second step may bind a caller-authored fresh obligation already +declared in a target draft with the revised claim and =supports= edge. It +replays the derivation, rejects non-canonical or unresolved snapshots, and +returns a final snapshot plus an inert declaration receipt. A changed claim +blocks automatic carryover but not this explicit declaration; a policy change +still blocks it. Neither step admits evidence or authorizes an effect. ** Evidence @@ -147,6 +173,13 @@ that an assurance obligation is discharged, or that an effect is authorized. =unknown=, and deprecated/superseded records to =stale=. Only a separately admitted assurance closure and authority receipt can cross the effect boundary. +Requirements, assumptions, alternatives and conditional guarantees form the +design context for these choices. A finding can reopen that context before +implementation changes. Algorithm optimization is evaluated against the chosen +contract; changing its assumptions or guarantees reopens the design decision. +The existing design-contract review checks declarations, not arbitrary logical +implication. RFC 0004 specifies the corresponding qualification work. + * Relation planes ** Provenance plane @@ -157,7 +190,9 @@ itself manufacture a derivation or causal claim. ** Structural dependency plane -Representative relations: =depends-on=, =implements=, =tests= and =refines=. +Representative relations: =depends-on=, =composes=, =implements=, =tests= and +=refines=. =composes= points from a composite claim to a component claim; +its reverse impact projection uses the shared POO Flow Graph. This plane supports reverse impact and obligation selection. Its output is explicitly structural and has no release or runtime authority. @@ -168,6 +203,13 @@ Representative relations: =precedes=, =causal-parent=, =enables= and counterfactual. Missing parents and temporal contradictions create an unknown frontier. Earlier-than never automatically promotes to caused-by. +POO Flow's Temporal Causality owns generic event graphs, causal cuts, trajectory +assessment and witness paths. Aitia connects scoped system behavior to the +evolution of design decisions and their assurance using checked projections. +Late observations create new assessment cuts; observed failures, model traces +and counterfactual branches retain their modality and environment. Full Aitia +trajectory integration is a pending RFC 0004 gate, beyond structural impact. + ** Assurance plane Representative relations: =supports=, =discharges=, =defeats= and @@ -211,15 +253,15 @@ An assurance snapshot binds: Change processing is pure: -#+begin_example -admit change - -> compute structural witnesses - -> select affected claims and assumptions - -> invalidate dependent support - -> derive obligations - -> invalidate decisions - -> emit effect guards -#+end_example +#+begin_export mermaid +flowchart TD + C["Change
admitted"] --> W["Structural witnesses
computed"] + W --> A["Claims and assumptions
affected set selected"] + A --> S["Dependent support
invalidated"] + S --> O["New obligations
pending"] + S --> D["Dependent decisions
invalidated"] + D --> E["Effect guards
blocked"] +#+end_export An invalidation receipt reports affected and unresolved identities. Its slots for temporal assessment, assurance closure, release authorization and runtime @@ -228,21 +270,101 @@ receipt. Presentation values cannot mutate the admitted snapshot. * Functional verification planning -The planner consumes values and returns a value: - -#+begin_example -Plan : Change -> Snapshot -> Policy -> PlanningResult -#+end_example +The planner consumes =Change=, =Snapshot= and =Policy= values and returns a +=PlanningResult= value. PlanningResult contains ordered obligations, capability requirements, dependency constraints, rejected alternatives and blockers. Tool invocation is outside this function. An adapter later consumes one obligation and returns a typed execution outcome; evidence admission is another separate function. - -The first verifier kinds are Lean proof, TLA+/TLC model check, runtime replay, +The first reported-outcome contract records exact input identity, revision and +digest bindings and emits an inert structural eligibility receipt. A reported +success, even when structurally eligible, is not a trusted execution +attestation. Host-owned adapter identity and effectful admission are required +before an =AssuranceEvidence= may become authoritative. Its caller-supplied +=admission-state= is metadata, not proof of that transition. Changing that +slot after verification would alter the snapshot context and invalidate the +outcome's original binding. A configured Host therefore issues a separate +admission object over candidate evidence without editing the snapshot. Its +private issuance record binds the verifier seal, obligation, evidence, outcome, +full semantic snapshot, validity interval and source-invalidation epoch. +Currentness is checked against the Host's configured snapshot source and clock; +the Host reconstructs the source snapshot to reject stale displayed digests, +and source changes invalidate earlier seals, including if the old cut reappears. +This is a Host-local admission candidate, not a decision or effect grant. +=assurance-host-admitted-support?= consumes that exact current admission with +one current =supports=/=discharges= assurance relation to its bound obligation. +It requires the relation to be present in the same Host snapshot and rejects +copied admissions, speculative modalities, expiration and source invalidation. +This is one edge witness; it does not close all claim requirements or produce +an =allow= Decision. Full closure, Cedar policy and Runtime effect gating remain +separate steps. +=assurance-host-required-support= now evaluates the claim-declared obligation +inventory against the Host's current cut. Its typed POO receipt reports one +blocker per required obligation, rejects missing obligation-to-claim and +evidence-to-obligation links, and leaves empty/duplicate requirements, +assumptions, defeaters, unresolved frontiers and challenges visibly blocked. +Even =complete?= means only that the declared support inventory is witnessed; +=release-authorized?= remains false. Validity intervals, policy evaluation and +the actual Decision authority boundary are still separate work. +The Host may now issue a weak-key-indexed Decision preflight for an exact +*undecided* Decision node already present in that cut. It checks the +=depends-on= edge to the supported claim, policy/snapshot/subject bindings and +current required support; currentness rechecks the Host source epoch and the +admissions. Copies, revocation, source changes and rollback cannot reuse it. +This preflight carries =release-authorized? #f= and is not a Cedar result, +an =allow= Decision or an effect token. The POO Flow Cedar Provider constructs +inert inputs; the independent Runtime Host still owns policy evaluation and +effect admission. +=assurance-host-cedar-request= now rechecks that exact preflight and projects +its Decision authority, claim subject, policy identity/revision and full +semantic snapshot digest into POO Flow's POO-native Cedar request family. +The application supplies the action, intent and POO Flow runtime handoff. +The resulting request and its context are editable input, not a second Host +seal; copying or changing them cannot establish Aitia admission. The Runtime +Host rejects the projected =poo_flow_preflight_only= marker before policy +evaluation, regardless of its value. This fail-closed check blocks accidental +promotion of the official preflight path; it does not attest an edited request +with the marker removed. A future Runtime-owned admission must independently +bind the source cut at use, evaluate Cedar through both engines and gate the +effect. The Scheme Host now keeps a weak identity ledger for projected Cedar +requests. =assurance-host-cedar-request-current?= rechecks the exact issued +request and the current source cut, rejecting copied or edited presentations, +revocation, source change and rollback. This is a source-owned predicate, not +Runtime admission: no Runtime path calls it yet, and its truth cannot authorize +an effect. The Scheme projection does not evaluate Cedar or gate an effect. +The POO Flow Cedar capability may require Runtime-owned source admission for +an action independently of caller context. Aitia's eventual release action +must use that administrative requirement, and the unmarked request path must +remain closed until an admitted =allow= Decision and exact current source are +checked at issue and effect consumption. The present preflight is undecided +and cannot satisfy that requirement. +=assurance-host-sealed-support?= is the configured Host seal predicate, not a +promotion transition. +The lower-level =assurance-support-sealed-under-adapter?= is conditional on a +caller-supplied adapter and time; it is not an authority or effect gate. +It checks structural relationships, the current snapshot, exact +evidence/outcome bindings and a POO Flow verification seal over the complete +semantic subject. POO Flow rejects copied or forged adapter presentations as +well as copied, modified, expired or revoked receipts. This proves issuance by +the exact adapter instance, not that a caller-supplied adapter was installed +by a trusted Host. Adapter selection and the evaluation clock must both be +controlled at the execution boundary; caller-provided values cannot authorize +an effect. Cedar and Runtime Host remain separate. +Verifier choice explanation consumes the inert plan and an explicit catalog of +POO-native candidate declarations. Evidence-kind/capability compatibility and +declared numeric priority determine a deterministic choice; every rejected +candidate and unassigned planned obligation remains visible. A chosen +declaration is not proof that an adapter is installed, executable or trusted. + +Verifier integrations include Lean proof, Quint/TLC model check, runtime replay, unit test, integration test, Cedar differential, static analysis and human review. This list is extensible through named POO-native helpers, not raw compute hooks. +Tool-specific producers qualify against the existing evidence-kind vocabulary; +the maintained lane does not include TLAPS. +Reuse the necessary upstream checker and trusted execution boundary before +adding a new implementation, as specified by RFC 0004. * Assurance laws @@ -258,13 +380,24 @@ The architecture requires executable formal statements of at least these laws: history without an explicit authorized transition. 5. Composition obligation: independently supported A and B do not imply that compose(A,B) is supported. + An explicit =composes= relation identifies the component claims, while a + separate obligation must be declared on the composite claim, named in that + claim's support requirements, linked by =supports= and bound to the exact + snapshot. The composition-requirements receipt only checks this structural + declaration; it never admits evidence or grants authority. 6. Unknown blocks closure: an unresolved required frontier cannot grant authority. 7. Effect separation: authorization neither proves effect start nor effect completion. -Lean proves general laws. TLA+/TLC checks lifecycle interleavings such as a +Lean proves general laws. Quint/TLC checks lifecycle interleavings such as a source change racing a cached decision or an effect beginning after revocation. +The bounded lifecycle model additionally separates safety from progress: +eventual effect start is required only when the source and clock remain stable, +no revocation intervenes, and the Host weakly fairly schedules every enabled +stage. Without that scheduling assumption, a stuttering behavior after a grant +is a valid counterexample. Neither result substitutes for a trusted Host clock, +adapter identity, or Runtime effect gate. * Public API direction @@ -320,7 +453,7 @@ standards compliance, release certification or unrestricted causal inference. The repository license is closed as =Apache-2.0 AND LGPL-2.1-or-later=, matching POO Flow. The former open decisions are closed by: -1. ADR 0004: Aitia owns assurance-specific Lean/TLA+ sources and may consume a +1. ADR 0004 r2: Aitia owns assurance-specific Lean/Quint sources and may consume a pinned POO Flow proof package for generic laws. 2. ADR 0007 supersedes ADR 0003: POO Flow is Aitia's public mechanism library; additions require a stable public owner, not an artificially small diff --git a/docs/rfcs/0002-snapshot-and-invalidation-kernel.org b/docs/rfcs/0002-snapshot-and-invalidation-kernel.org index 61a288a..59555d8 100644 --- a/docs/rfcs/0002-snapshot-and-invalidation-kernel.org +++ b/docs/rfcs/0002-snapshot-and-invalidation-kernel.org @@ -19,6 +19,10 @@ source, claim, policy and evidence revisions. A change produces an inert =InvalidationReceipt= through pure traversal of typed relations. The receipt selects affected current obligations, evidence, decisions and effects; it does not run a verifier, derive replacement obligations or authorize an effect. +The snapshot also exposes a non-circular context digest used by Phase 3 +obligation binding. Unlike the final snapshot digest, it omits only each +obligation's own snapshot-binding slots; it retains all other node, relation, +revision, policy and frontier content. * Input contracts diff --git a/docs/rfcs/0003-composition-value-interface.org b/docs/rfcs/0003-composition-value-interface.org index 92664db..4c5ab34 100644 --- a/docs/rfcs/0003-composition-value-interface.org +++ b/docs/rfcs/0003-composition-value-interface.org @@ -6,215 +6,135 @@ :PROPERTIES: :ID: lambda-aitia-rfc-0003 -:STATUS: active +:STATUS: draft :DATE: 2026-09-18 +:UPDATED: 2026-09-24 :PARENT_RFC: lambda-aitia-rfc-0001 :IMPLEMENTS: lambda-aitia-rfc-0001 :END: -* Decision summary +* Decision and present boundary -Lambda Aitia configuration has Jsonnet-style file-value semantics. A downstream -=aitia.ss= contains one root =use-composition= expression. Evaluating the file -returns one closed POO-native Case; the file is not a conventional Gerbil library -module and the user does not write =import=, =export= or a result binding. +Lambda Aitia owns Software Engineering Profile values and the maintained +=aitia= Case. POO Flow owns semantic Module identities, Profile exports, +=use-module=, =compose profiles=, =user-composition=, Case lowering, admission +and Session behavior. The ordinary value is POO-native; Aitia does not own a +parallel selector, catalog, graph or runtime. -The official root Composition is named =aitia=: +POO Flow's current public expression surface is: #+begin_src scheme -(use-composition aitia) +(user-composition project + (compose profiles + (use-module AitiaModule default))) #+end_src -The official Composition carries a default Software Engineering Project Profile. -That Profile composes the Aitia-owned SDLC, NASA NPR 7150.2D knowledge, GitOps, -ADR/RFC, assurance and formal-method obligations, including TLA+ model checking. -These Modules and Profiles are implicit in the ordinary user surface. Selecting -=aitia= does not require the downstream project to restate or import them. +The maintained Aitia owner provides =AitiaModule= and its already composed +=aitia= value in =user-interface/compositions/aitia.ss=. The default Project +declares GitOps, SDLC, NASA, ADR/RFC, assurance and formal-method selections. +Declaration alone neither executes a verifier nor establishes compliance or +release authority. -* User experience +The former =(use-composition aitia)=, implicit catalog and single-expression +file loader were removed from POO Flow's public surface by its native Profile +algebra migration. Their earlier tests no longer qualify the current API. +The no-import, one-expression downstream convenience requested for ordinary +users remains open. Aitia will not recreate the removed loader locally or +present an untested downstream snippet as supported. -The common path exposes Project facts and named Composition slots rather than the -POO meta-object protocol: +* Namespace and edition -#+begin_src scheme -(use-composition aitia - (project - repository: "tao3k/example" - languages: '(rust python)) - (delivery - provider: github)) -#+end_src - -The clauses are hygienic syntax projected onto ordinary POO-native objects and -slot operations. They are not an alist, JSON or directory-shaped runtime DSL. -The resulting value remains a normal POO Flow Scenario Case. +The canonical NASA standard-family identity is =nasa/sdlc/npr-7150.2=: +organization =nasa=, domain Module =sdlc=, Profile =npr-7150.2=. The selected +standard object explicitly carries =edition: D=, effective date and source +URLs. Its versioned Source Lock uses exact edition, source digests and byte +counts. A future edition changes the bound source and assessment evidence, +not the public family namespace or Scheme/Python import path. -The default UI does not require knowledge of prototypes, C3 linearization, -=.mix=, =super=, ExecutionPlan or Session. A user who needs a non-default model -may explicitly extend the default Project Profile with the reviewed POO-native -override surface: +The maintained GitOps Scenario selects this typed Module Profile with: #+begin_src scheme -(use-composition aitia - (.def (project @ default-software-engineering-project) - (reviewers =>.+ '(security release)))) +(use-module NasaSdlcModule npr-7150.2) #+end_src -This is progressive disclosure: POO remains the implementation and extension -model, but it is not prerequisite knowledge for selecting the maintained -Composition and supplying ordinary Project facts. - -* File-value contract - -An =aitia.ss= input MUST contain exactly one root value expression. The POO -Flow Composition value loader expands that expression in a bounded prelude -which supplies only the public Composition syntax, an explicitly provided -official Composition catalog and the reviewed POO override operators. Aitia -contributes the =aitia= catalog entry; it does not own or wrap the loader. -Loading MUST NOT depend on a user-written import or export and MUST NOT discover -semantics by recursively importing a directory. - -The POO Flow loader MUST: - -1. read and expand the single root expression with source locations preserved; -2. resolve its Composition identity from the Composition catalog; -3. apply ordinary slot clauses and then explicit advanced POO overrides; -4. recompute derived slots and reject unresolved conflicts; -5. validate the closed object graph; and -6. return exactly one Case without starting a Runtime Session or executing an - effect. - -=aitia doctor= consumes the same value path. It may present effective values, -linearization and per-slot provenance, but it does not create a second -configuration representation. - -* Composition and Module namespaces - -Composition identities and Module identities are distinct typed namespaces. -The public official Composition identity =aitia= is reserved to the Lambda Aitia -project. A user-authored Composition MUST declare a user-owned namespace and -MUST NOT: - -- use =aitia= as its namespace; -- equal any registered Module identity; -- shadow an official Composition identity; or -- rely on import order to select between a Composition and a Module. - -The expander rejects these conflicts before Case construction. Catalog lookup -never falls back from the Composition namespace to the Module namespace. - -The exact textual spelling of user-owned qualified identities is owned by the -implementation slice, but the semantic identity MUST contain a namespace and a -local name. An unqualified identity is permitted only for an official reserved -Composition such as =aitia=. - -* Official =aitia= Composition - -The official Composition is a maintained object graph, not an empty template. -Its default Project Profile owns at least these slots: - -| Slot | Default owner | Meaning | -|------+---------------+---------| -| =project= | Lambda Aitia | Software Engineering Project facts and lifecycle | -| =sdlc= | Lambda Aitia | SDLC structure and review obligations | -| =standards= | Lambda Aitia | NASA NPR 7150.2D applicability knowledge | -| =delivery= | Lambda Aitia | GitOps development, staging and production flow | -| =decisions= | Lambda Aitia | ADR and RFC relationships | -| =assurance= | Lambda Aitia | Claims, obligations, evidence and invalidation | -| =formal-methods= | Lambda Aitia | TLA+ and Lean obligation projections | -| =graph-policy= | POO Flow | Execution, impact and temporal graph projection | -| =runtime-policy= | POO Flow | Case admission and fresh Session construction | - -The default does not claim universal NASA compliance, successful model checking, -proof completion or release authority. It installs the corresponding knowledge, -obligations and gates. Receipts establish what was actually evaluated. - -* Ownership boundaries - -Lambda Aitia owns the Software Engineering domain model, the official =aitia= -Composition catalog entry, its default Project Profile and its qualification -Scenarios. - -POO Flow owns =use-composition=, the single-expression value loader, typed -Composition identities and catalog resolution, generic POO composition -mechanisms, C3 behavior, Graph and ExecutionPlan projection, impact and temporal -algorithms, Case admission, Session state and runtime receipts. Aitia MUST -reuse those public mechanisms and MUST NOT implement a second selector, loader, -graph, planner or runtime engine. - -The Python project under =bindings/python= is a production SDLC Runtime consumer. -It consumes the closed Case projections and C ABI receipts; it does not parse a -parallel Python configuration model or redefine Composition semantics. - -* Scenario boundary - -=user-interface/scenarios/= contains Aitia-owned reference, qualification and -regression assets. Each Scenario invokes the same public Composition value path -as downstream users and adds fixed inputs plus expected projections and receipts. -It is not a directory contract copied into downstream projects. - -Downstream projects normally contain only =aitia.ss=. Advanced users may split -ordinary Scheme implementation files for their own maintenance, but the root -configuration still evaluates to one value and directory shape does not acquire -configuration semantics. - -* Slot resolution - -C3 determines deterministic prototype precedence. Each public slot also owns a -declared resolution policy; linearization alone does not imply merge behavior. -The initial policies are: - -- scalar facts replace the inherited value; -- ordered collections append with identity-based de-duplication; -- keyed objects merge only through their owning POO protocol; -- assurance obligations are monotone unless an explicit authority-governed - waiver object is admitted; -- derived slots are recomputed after overrides and are not directly writable; -- incompatible Profile selections fail closed. - -The resolved Case retains origin metadata sufficient for =aitia doctor= to show -which official Profile or downstream override contributed an effective slot. - -* Implementation sequence - -1. POO Flow supplies a source-located single-expression value loader and changes - the downstream =use-composition= surface from declaration/export semantics to - expression semantics. Existing internal Case builders remain the lowering - target. -2. Lambda Aitia publishes the official =aitia= Composition and default Software - Engineering Project Profile with the implicit Module closure described above. -3. Aitia qualification Scenarios exercise zero-override, ordinary-slot and - advanced-POO paths through the same loader. -4. =aitia doctor= projects value, provenance, obligations and Plan summaries from - the closed Case. -5. Python Runtime and C ABI consume the same canonical Case projections without - introducing another configuration source. - -There is no compatibility layer preserving directory discovery or explicit -downstream export syntax. Migration is direct because the former surface had -not established the desired downstream contract. +Its check name is a workflow gate keyed to the standard family. A green +check with only that name now fails closed: the check must also match the +selected Standard object's edition and Source Lock digest. These check +fields are still caller-supplied workflow data, not an Assurance seal or +proof of actual verifier inputs. Accountable Host admission and source-byte +binding remain separate acceptance gates. + +When an application configures an Assurance Host source reader, issuance and +currentness recheck all declared verifier inputs against current source bytes. +The configured verification operation receives Host-materialized POO input +values as a fourth argument, including a copy of each validated payload; the +Host rechecks identities, revisions, digests and payloads after the operation. +Without a reader, the operation keeps its three-argument contract. +Observed byte drift invalidates prior admissions, so returning to the old bytes +requires fresh verification. This strengthens the Host boundary but does not +authenticate the source repository or prove what a verifier process actually +consumed; those remain application Host responsibilities. + +* Composition semantics + +The default path composes maintained POO Profile values; advanced users may +extend those values with ordinary POO slot operations before composition. +Profile selection preserves Module definition, instance, Profile identity, +revision and generation in POO Flow selection proofs. The resulting Case owns +one execution-plan projection and fresh Sessions. Importing or composing +values has no Runtime effect. + +The official =aitia= Case is a maintained object graph, not an empty template. +Its Project Profile declares Software Engineering responsibilities and +selection of GitOps, SDLC, NASA, ADR/RFC, assurance and formal-method +obligations. The Project declaration does not yet prove that all downstream +slots, verifier bindings and no-import UX are complete. + +[[file:0004-sdlc-design-and-causal-trajectories.org][RFC 0004]] places these selections inside the complete SDLC. Design alternatives, +implementation strategies, testing, operation and revision use the same +maintained composition boundary. A Recipe composes existing POO declarations +for applicability, source-bound assets, operations and required checks. An +advanced replacement must preserve those checks or open explicit obligations; +changing a Profile cannot silently weaken a requirement or reuse stale support. +Recipe qualification and behavior/design trajectory projection remain pending. + +* Ownership and trust + +| Owner | Responsibility | +|-------+----------------| +| Aitia | Software Engineering/NASA semantics, maintained Profiles and Case, qualification scenarios | +| POO Flow | Module/Profile algebra, composition, Graph, Case admission, Session and Runtime handoff | +| Application Host | source reads, verifier execution, authority and effects | + +The Python project under =bindings/python= consumes Aitia's native C ABI; it +does not parse a second configuration model or recreate SDLC semantics. +Versioned NASA source identities are provenance facts, not namespace segments +that ordinary users must type. * Acceptance gates -- [ ] An =aitia.ss= containing only =(use-composition aitia)= evaluates to one - admitted Case without explicit import, export or binding forms. -- [ ] The zero-override Case includes GitOps, NASA SDLC, ADR/RFC, assurance and - formal-method obligation slots through the default Project Profile. -- [ ] An ordinary Project clause lowers to POO-native slot operations. -- [ ] An advanced POO override extends the same default Project Profile. -- [ ] Composition/Module namespace collision and use of the reserved =aitia= - namespace by a user Composition fail at expansion. -- [ ] Loading and =doctor= perform no Runtime effect. -- [ ] Reference, qualification and regression Scenarios use the public value - path rather than private constructors. -- [ ] Direct compilation, atomic behavior tests, a real Aitia Scenario and the - existing composition macro scale gate pass with exact receipts. - -* Relationship to other decisions - -- RFC 0001 defines the causal assurance vocabulary carried by the default - Composition. -- RFC 0002 defines the immutable AssuranceSnapshot and invalidation boundary. -- ADR 0007 keeps POO Flow as the public mechanism library consumed here. -- ADR 0006 keeps Python as the production SDLC Runtime rather than a second - configuration authority. +- [X] Official Aitia and NASA identities lower through POO Flow's public + =user-composition=, =use-module= and Profile algebra. +- [X] The maintained Aitia Case and GitOps/NASA Scenario construct without + Runtime execution and have focused tests. +- [ ] A zero-override downstream file can select =aitia= without imports or + exposing POO internals, through a POO Flow-owned public mechanism. +- [ ] An advanced override extends the same maintained Project Profile and + preserves selection/provenance receipts. +- [X] The pure GitOps decision rejects a same-name green check lacking the + selected edition or Source Lock digest, including a check for another edition. +- [ ] A Host-issued NASA assessment binds actual verifier inputs, source bytes, + exact edition and Source Lock digest before it can count as Assurance support. +- [ ] Case, C ABI and Python project consume one canonical composition cut in + a clean checkout across both repositories. +- [ ] Source-located diagnostics, doctor presentation and scale receipts pass + for the current POO-native expression path. + +* Related decisions + +- RFC 0001 defines the Assurance vocabulary carried by the Project Profile. +- RFC 0002 defines canonical snapshots and invalidation. +- RFC 0004 defines lifecycle design, trajectory scope and reusable Recipes. +- ADR 0007 keeps POO Flow as the mechanism library. +- ADR 0006 keeps Python as a production Runtime consumer, not a second + semantic owner. diff --git a/docs/rfcs/0004-sdlc-design-and-causal-trajectories.org b/docs/rfcs/0004-sdlc-design-and-causal-trajectories.org new file mode 100644 index 0000000..fc3516c --- /dev/null +++ b/docs/rfcs/0004-sdlc-design-and-causal-trajectories.org @@ -0,0 +1,612 @@ +# SPDX-FileCopyrightText: 2026 tao3k team and Contributors +# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +#+title: RFC 0004 — SDLC Design, Causal Assurance and Trajectories +#+startup: overview + +:PROPERTIES: +:ID: lambda-aitia-rfc-0004 +:STATUS: draft +:DATE: 2026-09-24 +:UPDATED: 2026-09-24 +:PARENT_RFC: lambda-aitia-rfc-0001 +:IMPLEMENTS: lambda-aitia-rfc-0001 +:END: + +* Decision summary and scope + +The complete Software Development Life Cycle (SDLC) is Aitia's organizing +framework. Requirements, engineering design, algorithm and implementation +strategies, verification and testing, delivery, operation, maintenance and +retirement all participate in that lifecycle. Causal Assurance maintains the +justification of engineering conclusions as these activities change the system. + +Software design receives early engineering investment because cheaper code +generation increases the value of deciding what to build, under which +assumptions, with which responsibilities and guarantees. This is a product +direction to evaluate through qualification cases, not a claim that code cost +or design quality has already been measured. + +The eight-step =change -> invalidate -> plan -> verify -> admit -> decide -> +authorize -> effect= flow is the existing assurance/execution protocol. It can +be used at multiple lifecycle points; it is not an enumeration of the entire +SDLC. The overall lifecycle contains feedback, concurrent activities and +repeated revisions. A bounded execution plan may be a DAG without turning the +whole lifecycle into one fixed pipeline. + +This RFC records the agreed direction and pending executable acceptance work. +Its draft status does not promote the capabilities below to implemented APIs. + +[[file:0005-org-native-engineering-workspace.org][RFC 0005]] specifies the native Org engineering work surface and source-bound +Agent review, with generic Org integration owned by POO Flow. Its initial +authoring checks are distinct from the pending connected execution loop. + +The lifecycle workflow below names engineering states, not implementation +milestones. Transitions require attributable revisions and receipts; an arrow +does not itself grant assurance or authority. Feedback can reopen earlier +activities, and independent obligations may proceed concurrently. + +#+begin_export mermaid +stateDiagram-v2 + state "Requirements: scoped" as Requirements + state "Design: alternatives proposed" as Design + state "ADR: option selected" as Selected + state "Implementation: candidate" as Implementation + state "Verification: obligations pending" as Verification + state "Assurance: supported at a bound cut" as Supported + state "Delivery: effect authorized" as Authorized + state "Operation: behavior observed" as Operation + state "Maintenance: reassessment pending" as Reassessment + state "Retirement: obligations pending" as Retirement + state "Retired: closure recorded" as Retired + [*] --> Requirements + Requirements --> Design: scope and constraints recorded + Design --> Selected: alternatives reviewed + Selected --> Implementation: strategy chosen + Implementation --> Verification: candidate and checks bound + Verification --> Supported: scoped evidence admitted + Verification --> Design: finding challenges design + Supported --> Authorized: current decision and effect gate + Authorized --> Operation: execution receipt and observations + Authorized --> Reassessment: revoked, expired or premise changed + Supported --> Reassessment: premise changes or late evidence + Operation --> Reassessment + Reassessment --> Requirements: requirement revision + Reassessment --> Design: decision revision + Reassessment --> Verification: renewed obligations + Operation --> Retirement + Retirement --> Retired: residual obligations discharged +#+end_export + +These states summarize activity, not a single global status enum. Multiple +claims may have different support states at the same cut. Pending or stale +support blocks the affected effect; an accepted ADR alone is not authorization. +Operational findings or changed context trigger reassessment; an accountable +retirement decision opens retirement obligations before closure is recorded. + +* Formal challenge gates before persistence integration + +The immediate design-validation priority is to challenge lifecycle assumptions +using reviewed upstream failure reports, not to add storage. Keep reported +behavior, our proposed transfer and locally executed evidence distinct in the +[[file:../plans/engineering-case-source-inventory.org][source inventory]]. An Issue being closed does not qualify our design. + +Quint is organized into reusable work and grant predicates with independently +runnable finite models and a separate ingress composition lane. Each safety boundary +needs positive exploration, a reachable-state control and a deliberately +weakened guard that produces the expected counterexample. Do not duplicate a +shared action in every scenario, discard environment interference to obtain a +green atomic test, or infer composite correctness from leaf checks alone. + +The [[file:../../proof/quint/README.org][maintained Quint slice]] separates cancellation/work observations from retirement +review. It requires final-cut quiescence and known inventory: a cancellation +request, an earlier accepted review, or missing history is insufficient. +Lean proves safety preservation for an abstract retirement transition relation; +it does not yet prove Scheme or Quint refinement. The +[[file:../receipts/2026-09-24-retirement-formal-review.org][historical TLA+ receipt]] records the earlier checks and parser boundary. + +The historical [[file:../receipts/2026-09-24-formal-source-qualification.org][source qualification]] checked TLA+ syntax/byte-roundtrip. The +maintained Quint CLI checks Quint syntax and binds both current source views +to one POO design declaration, revision and +source cut using existing SDLC/Assurance links. A model, proof or premise +change invalidates old bindings. This is source correspondence, not a proof of +semantic refinement or Host evidence admission. Independent replay and +semantic correspondence remain required for closure. +The isolated review terminal state assumes no later +ingress; the [[file:../receipts/2026-09-24-retirement-ingress-review.org][bounded ingress composition]] challenges this by permitting requests +after retirement. It requires closing grant issuance and advancing the epoch +before finalization, and rechecking the cached grant at effect start. The +effect-admission predicate is extracted from the existing assurance model. +This does not establish a deployed Cedar/Runtime binding, new grant issuance, +distributed atomicity, repeated retries or eventual progress. Authorization, +ingress exclusion and fairness still need their actual owner correspondences +before any Runtime or eventual-retirement guarantee. Persistence remains +deferred; passing these abstract checks does not authorize its implementation. + +** Correspondence gate across design, formal views and execution + +A source digest proves which bytes were checked, not that the checked formula +means the declared guarantee. Each claimed correspondence therefore needs a +reviewable interpretation, an owner and an adversarial check: + +| Boundary | Required correspondence | Falsifying control | +|----------+-------------------------+--------------------| +| POO design contract → Quint | Map each relevant assumption and guarantee to named state predicates/actions, including the environment and modeled client bound | weaken a fence, inventory or ingress premise and require the expected reachable counterexample | +| POO design contract → Lean | State theorem premises and conclusion against the same declaration; audit imported axioms and excluded behaviors | remove a needed premise and demonstrate that the claim is no longer established | +| Quint/Lean → Scheme and Runtime | Identify the concrete state/effect transitions represented by the abstract ones and document omitted transitions | replay a concrete behavior outside the mapping and keep conformance unknown or violated | +| Tool result → Assurance Host | Bind actual tool/source/configuration identities and the exact proposition assessed | change an input or tool identity and reject reuse of the earlier admission | + +The current source-group links qualify identity and invalidation only. A +matching name, digest, passing model configuration or axiom-free theorem does +not fill an interpretation row. A counterexample in the abstract model is a +model finding until its concrete correspondence is checked; an observed +Runtime trace is not retroactively turned into a model execution. If a +concrete transition has no modeled counterpart, record that coverage gap and +reopen the design or model rather than narrowing the tested environment to +make the check pass. No implementation-conformance or effect-authority flag +may become true from these source mappings alone. + +For the maintained =job-executor/retirement= declaration, the first +correspondence review has these concrete boundaries: + +| Declaration | Current formal representation | Remaining gap | +|-------------+-------------------------------+---------------| +| =atomic-epoch-fence= | Quint =CloseIngress= closes issuance and advances =epoch= in one action; Lean =Step.close= changes both fields | No concrete Host/Cedar/Runtime atomic transition is bound | +| =known-inventory= and quiescence | Quint =Retire= requires =known= and =quiescent(work)=; Lean =Retirement.Step.finalize= requires =inventory=, =idle= and =drained= | Neither formal view proves the inventory is complete in production | +| =no-new-grant= | The Quint composition has one fixed cached grant and no grant-issuance action; Lean has no issuance transition | This is an excluded environment behavior, not a proved revocation protocol | +| =bounded-client= | Quint =pending= and =attempted= model one request/attempt; Lean's =requestOrReject= stutters | Multiple clients, responses and retry progress are outside the Lean theorem | +| =retirement-safety/g1= | Quint checks =NoOutstandingWork= and =NoPostRetirementStart=; Lean proves =Retirement.Safe= and =RetirementIngress.Safe= for its own =Step= relation | These are two abstract checks, not a proof that either is a faithful refinement of the Scheme contract or deployed effects | + +The current source binding tracks changes to these files, but none of the +remaining gaps can be closed by adding another digest link. In particular, +the absent grant-issuance transition must be modeled and challenged before +claiming that real new grants are excluded after retirement. + +* Design and implementation within the lifecycle + +| Activity | Engineering question | Attributable output | +|----------+----------------------+---------------------| +| Requirements and context | What outcome is needed, for whom and under what constraints? | revisioned requirements, scope and acceptance conditions | +| Software and engineering design | Who owns state, responsibility, interfaces, failure handling and effects? | alternatives, assumptions, guarantees and RFC/ADR decisions | +| Algorithms and implementation strategies | How can the chosen guarantees be realized within cost and resource constraints? | strategy choice, implementation mapping and measurements | +| Verification and testing | Which precise propositions have supporting or contradicting evidence? | scoped proofs, model checks, tests, replay and review | +| Delivery and operation | Which action is currently permitted, and what actually happened? | authority, execution receipts and observed trajectories | +| Maintenance and retirement | Which assumptions changed, and what must be revised, migrated or stopped? | invalidation, new obligations and accountable successor decisions | + +Activities share one lifecycle subject and revision history. An algorithm +change that alters consistency or failure guarantees is also a design +decision. A performance optimization within the same contract still needs +conformance and performance evidence. Estimated cost, human judgment and +measured behavior retain distinct evidence kinds and scopes. + +Design work relates existing POO values through: + +#+begin_export mermaid +flowchart TD + R["Requirements and constraints
scoped"] --> A["Assumptions and alternatives
proposed"] + A --> D["Responsibilities, state, interfaces and effects
selected in RFC / ADR"] + D --> G["Conditional guarantees
declared, not yet supported"] + G --> I["Implementation strategy
candidate"] + G --> O["Verification obligations
open"] + I --> E["Tool results and observations
candidate evidence"] + O --> E + E --> C{"Scoped admission and assessment"} + C -->|support established| S["Guarantee supported
at this revision and cut"] + C -->|failed or unresolved| F["Finding / unknown frontier
requires review"] + F --> A + S -->|premise changes| X["Prior support stale
reassessment required"] + X --> O +#+end_export + +A guarantee states its subject, predicate and scope under named assumptions. +An assumption states a premise, its owner and the conditions requiring review. +An ADR records considered options and their consequences; acceptance records +the design choice, while current assurance requires separately admitted +evidence. Relaxing a requirement is an explicit requirement revision and +review, not a repair technique that silently turns a failed check green. + +The existing =SdlcDesignContract= supplies responsibilities, owned state, +allowed effects, assumptions, guarantees, variations and a decision identity. +Its replacement review admits exact declaration comparisons only. Logical +implication between different statements, model/code correspondence and +cross-component guarantees require additional obligations with attributable +checks. Neither name equality nor two successful component checks establishes +the safety of their composition. + +* System behavior and design evolution trajectories + +POO Flow owns Temporal Causality, event graphs, causal cuts, trajectory +contracts, witness paths and traversal. Aitia supplies software event meaning +and the links from behavior to requirements, assumptions, design decisions and +assurance. Python transports observations and invokes tools through the owned +runtime boundary. + +Two related trajectories are required: + +- system behavior: requests, state transitions, retries, effects, failures and + recovery in an explicitly identified environment; +- design and assurance evolution: hypotheses, RFC/ADR choices, checks, + findings, invalidation, revision and renewed acceptance. + +Evidence and checked projections connect these trajectories. A runtime event +can challenge an assumption; a reviewed explanation can motivate an ADR +revision. Structural impact, temporal precedence and graph reachability do not +by themselves establish a causal explanation. Agent-proposed explanations +remain hypotheses until their supporting checks are admitted. + +#+begin_export mermaid +flowchart TD + subgraph Behavior["System behavior trajectory"] + O["Execution events
observed in a bound environment"] --> R["Replay / checks
scoped results"] + end + subgraph Design["Design and assurance trajectory"] + G["Guarantee
declared"] --> F["Finding
open"] + F --> H["Causal explanation
hypothesized"] + H --> C["Explanation checks
pending"] + C --> A["ADR revision
proposed for review"] + A --> N["Revalidation obligations
pending after selection"] + end + R -->|challenges within scope| F + R -->|evidence for assessment| C + N -->|requires a new bound run| O +#+end_export + +The cross-trajectory links are evidence and obligation relations, not an +automatic causal inference from event order. A proposed ADR retains its +assumptions even when the explanation remains unresolved. + +| Trajectory source | Scope that must remain visible | +|-------------------+--------------------------------| +| Production or experimental execution | observed events in the named system or harness, with source and environment bindings | +| TLC/model-check counterexample | behavior permitted by the exact model and checked configuration; implementation correspondence is a separate obligation | +| Proposed intervention or alternative design | counterfactual branch under explicit assumptions and a bounded horizon | +| RFC/ADR and assurance evolution | recorded choices and changes in justification, without rewriting earlier facts or granting runtime authority | + +An observed failure remains observed in its original environment. The current +POO Flow trajectory contract reserves =error-event-paths= for uncommitted +counterfactuals; actual bug replays must not be relabeled to fit that slot. +Use the existing observed event graph, and place any missing generic trajectory +mechanism in its POO Flow owner before exposing an Aitia projection. + +An assessment binds the event graph, immutable causal cut, explicit time +observation, source/design/policy revisions and horizon. Occurrence order, +ingress order and revision order must not be conflated. A late observation +creates a new assessment cut and may invalidate current support while leaving +the earlier cut intact. Missing causal parents, incompatible clock domains +and incomplete coverage retain an unknown frontier. Cross-clock mapping or +additional time axes require a qualified POO Flow contract; this RFC does not +claim that every such mapping already exists. + +* Lifecycle Impact of introduction, change and retirement + +Design direction agreed on 2026-09-24; implementation qualification remains +pending. Impact is a lifecycle view of a change, not only a list of files to +rebuild. It asks which historical interpretations and obligations, current +conditions, and future options are changed by introducing, modifying or +retiring a capability. POO Flow retains generic Impact and Temporal Causality +ownership; Aitia supplies Software/SDLC relations and domain obligations. + +| Perspective | Introduce or modify a capability | Retire a capability | +|-------------+---------------------------------+---------------------| +| Past assets and commitments | check old data against new assumptions; reassess the applicability of earlier evidence | preserve interpretation/replay of old results and outstanding commitments | +| Current code and operation | inspect consumers, interfaces, deployment conditions, authority and resource interactions | identify remaining consumers, running instances, in-flight work and configuration references | +| Future work and obligations | open verification, migration, monitoring and maintenance obligations | migrate consumers, prevent new use where required, drain/cancel queued work and verify residual duties | + +Impact on the past never changes an observed historical fact. Distinguish +evidence that was valid under its original conditions but is no longer +applicable from evidence whose original justification is now challenged. +The latter opens correction and retrospective review, not silent deletion of +the original receipt. Record when a condition applied separately from when +it was learned or corrected. + +** Inputs, meaning and receipt boundary + +Assessments bind the proposed change and before/after source/design snapshots, +affected capability identities, deployment scope, selected typed relations, +explicit causal cut/time observation, horizon and inventory completeness. +An unexecuted change remains proposed or counterfactual; it is not an observed +production event merely because its impact has been computed. + +Relevant relations include implementation dependencies, data/protocol usage, +assumption/guarantee support, evidence applicability and accountable ownership. +Every relation needs a source and declared meaning. Org ancestry, organization +hierarchy, code references and causal relations are not interchangeable. +An unchanged function signature does not imply an unchanged guarantee: a +retention, permission or consistency change can alter assurance without an +API change. + +For each affected subject, present its identity, witness path, applicable +versions/environment/time bounds, evidence modality, unresolved conditions, +recommended obligation and responsible role. This is an analysis/proposal, +not execution authority. Existing Cedar/Runtime admission remains necessary +before a proposed migration, cancellation or other external effect. + +The existing POO Flow classifier partitions events by logical position. +That alone does not establish current exposure: an old task, authorization or +stored object can still be active now. Current applicability needs an explicit +state or validity-interval projection, including its source and uncertainty. +Any missing generic mechanism belongs in POO Flow, not a parallel Aitia engine. + +Dynamic callers, unregistered deployments, external consumers and missing +history remain an unknown frontier. No discovered dependency is not evidence +of no dependency. Completeness is relative to the declared inventory and scope. + +** Retirement preserves history and discharges remaining obligations + +Deletion is not the inverse of introduction. Analyze the old snapshot as well +as the candidate one: deleting an object and its edges before analysis can +hide precisely the consumers that need migration. Preserve the capability's +historical identity and provenance after removal from the active surface; +record retirement and any successor explicitly. A semantically different +replacement does not inherit identity merely by keeping the same display name. + +The maintained Composition selects applicable retirement stages. The following +workflow is a qualification target, not an implemented global state machine: + +#+begin_export mermaid +flowchart TD + P["Retirement
proposed"] --> I["Impact
scoped; unknowns retained"] + I --> R["Migration and residual duties
owner review pending"] + R --> A["Selected actions
authorization pending"] + A --> E["Migration / drain / cancellation
execution receipts required"] + E --> V["Residual obligations
verification pending"] + V -->|unresolved| R + V -->|closure conditions satisfied| C["Retired
historical identity retained"] +#+end_export + +An accepted retirement proposal neither cancels queued work nor rewrites +historical evidence. Required operations use the existing effect boundary; +irreversible effects may require explicit compensation or unresolved duties +rather than a claim that rollback erased them. + +** Next qualification: retire one job-executor capability + +Pair the existing repair case with a bounded retirement experiment using the +same job-executor subject. Select one concrete capability and bind its consumers +before implementing the experiment; this RFC does not order a production +removal. Include an old persisted job/result, a current consumer or in-flight +job, and queued future work. Preserve one unrelated consumer as a negative +control and one deliberately unknown external-consumer boundary. + +The experiment must show pre/post snapshot witnesses, explicit handling of +still-active old work, proposed migration/drain obligations and actual receipts +where execution is claimed. Replaying an old result must retain its original +version/scope; a replacement cannot silently acquire the old evidence. + +The [[file:../receipts/2026-09-24-retirement-impact.org][first bounded analysis receipt]] exercises the automatic-retry fixture through +POO Flow's existing public mechanisms. It uses synthetic facts and does not +close the persisted-state, obligation derivation or execution requirements above. + +* Historical engineering cases as design inputs + +Issues, design discussions, incident reports and engineering articles can +contribute before implementation begins. Preserve the reasoning that connects +context, requirements, assumptions, alternatives and consequences, rather than +only a summary or a recommended pattern. Include successful designs, rejected +proposals and withdrawn fixes as well as failures. + +| Use | Contribution | Admission boundary | +|-----+--------------+--------------------| +| Inspiration | proposes a question, assumption, alternative or failure scenario | similarity does not establish applicability or a local defect | +| Judgment | supports an accountable choice under explicit conditions and tradeoffs | record matching conditions, differences, counterevidence, owner and review triggers | +| Verification asset | supplies a model, checker, harness or regression | qualify the exact asset and proposition before admitting support for the current subject | + +These are uses, not automatic stages of increasing trust. A useful article +need not contain a replay program. A human cost or maintainability judgment +need not become a mechanical theorem, but must retain attribution, scope and +reconsideration conditions. Neither judgment nor author reputation grants an +effect capability. + +The reusable case records context/version/workload, intended guarantee, +explicit and inferred assumptions, reported events, proposed explanations, +considered alternatives, consequences and transfer conditions. Every extracted +claim points to its source passage or comment and available revision. Keep +original URL, author, publication/update and retrieval times; record digest and +reuse permissions when retaining source bytes or executable assets. A mutable +URL is not a pinned artifact. This phase references sources and records our +analysis; it does not vendor articles or assume all public material is licensed +for code/data redistribution. + +Before applying a case, compare state/effect ownership, failure model, +identity, timing/retention, workload and recovery requirements against the +current design. Record the correspondence as a reviewable proposal with +supporting and contradicting observations. Search similarity, citation count, +Issue closure and Agent confidence cannot discharge that applicability review. +Multiple articles repeating the same incident are not independent evidence. + +#+begin_export mermaid +flowchart TD + S["External material
reported and attributed"] --> Q["Design question / alternative
proposed"] + Q --> M["Applicability mapping
pending review"] + M --> J["Design judgment
scoped, with explicit unknowns"] + J --> D["RFC / ADR choice
proposed for accountable review"] + D --> O["Verification obligations
open"] + S --> A["Reusable assets
unqualified"] + A --> V["Actual checks
candidate results"] + O --> V + V --> E["Evidence admission
required for supported claims"] + S --> N["Correction / new report
applicability reassessment pending"] + N --> M +#+end_export + +An external report is evidence of what its author reports, not a local observed +execution. Separate the system-event timeline from the history of what became +known: publication, correction, retrieval and design review. A later explanation +creates a new assessment and can reopen a decision; it does not rewrite the +earlier knowledge cut. Unknown timestamps remain unknown. Do not give a whole +case one =verified= flag: a reported failure, hypothesized cause, tested repair +and unreviewed transfer can coexist. + +Use existing Artifact, Claim, Assumption, Finding, Evidence, Decision and +Obligation projections and POO composition. These conceptual correspondences +are not new implemented constructors or permission to add arbitrary edge names. +POO Flow retains generic temporal/causal and query ownership. No parallel case +graph engine, universal crawler, confidence scorer or MRR integration is +required to qualify the first case. + +** First application and mechanism reuse + +[[file:../adrs/0008-stable-operation-key-after-lost-ack.org][ADR 0008]] applies this method to logical-operation identity. The AWS idempotency +article supplies design questions, not evidence that Aitia or the fixture meets +the AWS contract. The source/coverage matrix in that ADR distinguishes the +existing replay from untested parameter, retention and crash boundaries. + +Authorization conditions belong in the existing POO Flow Cedar Provider, using +trusted context supplied by the Host; do not create an Aitia policy evaluator. +Cedar decides whether an action is allowed for the supplied principal, resource +and context. Runtime must enforce that decision at use. Provider-side atomic +deduplication and durable effects remain execution obligations: two concurrent +requests can both be allowed without an execution-time uniqueness mechanism. +An =Allow= does not establish that a previous attempt failed or did not commit. + +The current =assurance-host-cedar-request= and its currentness predicate are +preflight projections, not a connected Runtime gate. Preserve their owner and +reuse the existing effect-token machinery; qualify the missing handoff instead +of treating the whole authorization design as absent. An operation identity +survives retries, while a new attempt still requires the applicable current +authorization. Outcome-unknown retries require provider reconciliation and +idempotency guarantees, not an invented success or automatic fresh commit. + +* Reuse existing verification methods and engineering assets + +A Recipe is a composition of existing POO objects and slots describing +applicability, bound inputs, operations, required checks, expected artifacts +and failure handling. It is not a new configuration language or a separate +workflow engine. RFC 0003 governs its maintained and advanced expression paths. + +Reuse existing functions, packages, scripts, models, harnesses and tool +commands at a boundary that preserves their correctness conditions. The reuse +unit includes the checking implementation, necessary dependencies, real +execution path and relevant regression tests. When those pieces are coupled, +first qualify the existing boundary or contribute a focused upstream change. +Add a new implementation only for an identified capability gap. + +For every selected component or asset, record exact source revision/digest, +original attribution and license, applicability assumptions, dependencies and +the check establishing that it works in Aitia. The integration inventory must +distinguish candidates from qualified reuse. Package layout alone does not +establish a stable SDK or independent checker entry point. + +The maintained finite-state route is Quint with a pinned TLC backend; Lean +retains its separate theorem lane. TLC state exploration and Lean theorem +checking make different claims. Existing proof evidence kinds can name +different producers without adding a parallel semantic schema for each tool. + +Specula and its case-study archive are candidate sources of verification tools, +models, harnesses, traces and reproductions. The archive stores experiment +records; each selected record needs a replay and attribution audit. The full +Specula application is optional, not the required workflow owner. Calling an +upstream script or bounded tool command is a legitimate reuse choice. +Specula already provides continuous and incremental verification, so Aitia's +proposed value must be demonstrated through design decisions, composition +obligations and changing justification rather than that feature alone. + +* Runtime ownership and future MRR work + +The production Aitia Python Runtime under =bindings/python= serves all Aitia +capabilities across the SDLC. AnyIO is its asynchronous foundation for task +lifetime, cancellation, bounded concurrency and I/O. POO Flow continues to own +generic Graph/FunFlow execution and temporal/causal algorithms. Scheme/POO owns +Aitia's semantic composition, and the C ABI carries typed projections. +Native session thread affinity must be preserved at asynchronous boundaries. +Declaring the AnyIO dependency does not establish async execution or Host +admission of a tool observation. + +MRR is deferred beyond the current Python implementation stage. A later design +will evaluate relation queries, derivation and lineage over lifecycle +snapshots; its role is broader than storing trace logs. Preserve identities, +revisions, typed relations, modalities, cuts, witnesses and unknown frontiers +now so a later integration can project them without redefining Aitia meaning. +Do not add an MRR dependency, query adapter, storage framework or new Python +inference engine in this phase. Future query results must retain snapshot and +lineage bindings and pass the applicable admission boundary before use. + +* Qualification sequence and observable value + +The primary deliverable is one connected lifecycle experiment. The current +job-executor case is a known-fault seed: per-key deduplication can succeed +while a logical operation commits twice after acknowledgement loss. Its +existing ADR 0008 and design-contract tests supply a starting point, not full +causal or Host closure. + +1. Bind requirements for completion and unique logical effects, assumptions, + source, model when available, and at least two design alternatives. +2. Replay the known failure through existing tools and preserve the actual + experimental trajectory. Record the proposed explanation and check its + relationship to the original per-key guarantee. +3. Compare disabling retries with preserving a stable operation identity; + completion and provider retention assumptions remain explicit. +4. Record the chosen revision and derive its obligations. Run the appropriate + reused tools, establish required source/model correspondence and consume + actual Host admission where a supported conclusion is claimed. +5. Change a retention or retry assumption, or admit a late observation. Show + the new cut, affected guarantees, explanation paths and renewed obligations. +6. Apply an implementation optimization and check both contract preservation + and measured benefit under the same workload and environment scope. + +Qualify one sufficiently complete upstream bug case as a reusable method. +A known-answer replay is regression evidence; novel discovery needs a separate +evaluation with unexposed answers. + +* Pending acceptance gates + +- [ ] One historical case records source attribution, applicability and + counterevidence, the design judgment it changed, and the resulting obligation. + Demonstrate a source correction reopening review without overwriting history. +- [ ] ADR 0008's owner/coverage matrix is qualified, including the existing + Cedar/Runtime path and provider-side parameter, retention and crash boundaries; + documentation of a gap is not its executable closure. +- [ ] Upstream reuse inventory binds selected files, revisions, dependencies, + licenses, required checks and replay evidence. +- [ ] A maintained POO Composition selects the qualified Recipe and an + advanced override preserves mandatory checks or opens new obligations. +- [ ] The connected lifecycle case compares design alternatives against the + same requirements, including safety and progress where applicable. +- [ ] System, model, counterfactual and design trajectories preserve scope; + missing parents and late evidence have explicit negative cases. +- [ ] A changed assumption yields attributable invalidation witnesses and + prevents stale support from being reused. +- [ ] Introduction/change/retirement Impact binds pre/post snapshots, typed + relations and explicit temporal scope; proposal and observed effect remain + separate. Generic analysis stays in POO Flow. +- [ ] The retirement case covers historical data/results, current consumers + and queued work; old-but-active subjects are not lost to past-event filtering. + Removed graph nodes cannot hide their old dependents. Unrelated controls are + not reopened, and incomplete external-consumer inventory remains unknown. +- [ ] Historical validity, current applicability and later correction are + distinct; missing inventory and out-of-horizon effects cannot certify safe + deletion. Migration or compensation requires its own authorized receipts. +- [ ] Frozen Python tool observations reach the actual Scheme Host admission + path; serialized success cannot substitute for that path. +- [ ] The maintained POO contract's assumptions and guarantees have reviewed + Quint/Lean interpretations and falsifying controls, followed by concrete + Scheme/Runtime transition correspondence. Source identity alone is not + semantic refinement. +- [ ] A contract-preserving optimization has conformance and performance + evidence, with no unreviewed weakening of the target requirement. +- [ ] Compare the same upstream tools/assets with and without Aitia's design + and assurance links: check missed obligations, unnecessary reruns and the + ability to explain why a conclusion changed. Record limitations and costs. + +The upstream entry points, connected trajectory projection and Host bridge +remain unqualified by this documentation change. Existing bounded primitives +and their receipts retain their existing scope. + +* References + +- [[file:0001-causal-assurance-core.org][RFC 0001 — Causal Assurance Core]] +- [[file:0002-snapshot-and-invalidation-kernel.org][RFC 0002 — Snapshot and Invalidation Kernel]] +- [[file:0003-composition-value-interface.org][RFC 0003 — Composition Value Interface]] +- [[file:0005-org-native-engineering-workspace.org][RFC 0005 — Org-native Engineering Workspace]] +- [[file:../adrs/0006-production-python-sdlc-runtime.org][ADR 0006 — Aitia Python Runtime]] +- [[file:../adrs/0008-stable-operation-key-after-lost-ack.org][ADR 0008 — Stable logical-operation identity]] +- [[file:../../ROADMAP.org][Roadmap and current implementation evidence]] +- [[file:../plans/engineering-case-source-inventory.org][Engineering case sources and transfer judgments]] +- POO Flow =src/modules/temporal-causality/interface.ss= and architecture + =161-temporal-causality-and-causal-trajectory-architecture.org= own the generic + mechanism; Aitia consumes the pinned public library. +- [[https://github.com/specula-org/Specula#continuous-verification][Specula continuous verification]] +- [[https://github.com/specula-org/specula-case-studies][Specula experiment archive]] + +Upstream references were inspected on 2026-09-24. They identify reuse +candidates, not pinned or qualified dependencies of this project. diff --git a/docs/rfcs/0005-org-native-engineering-workspace.org b/docs/rfcs/0005-org-native-engineering-workspace.org new file mode 100644 index 0000000..7dbfccb --- /dev/null +++ b/docs/rfcs/0005-org-native-engineering-workspace.org @@ -0,0 +1,361 @@ +# SPDX-FileCopyrightText: 2026 tao3k team and Contributors +# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +#+title: RFC 0005 — Org-native engineering workspace +#+startup: overview + +:PROPERTIES: +:ID: lambda-aitia-rfc-0005 +:STATUS: draft +:DATE: 2026-09-24 +:PARENT_RFC: lambda-aitia-rfc-0004 +:IMPLEMENTS: lambda-aitia-rfc-0004 +:END: + +* Decision and ownership + +Native Org is a maintained engineering work surface, not a Markdown document +decorated with execution commands. Requirements, assumptions, alternatives, +review work and receipts remain addressable parts of the same design history. +Orgize owns syntax, lossless structure and semantic queries. POO Flow owns +generic composition, organization, causal/temporal mechanisms and the reusable +Org integration. Aitia owns Software/SDLC interpretations and workspaces. +Existing Cedar and Runtime mechanisms retain effect authority and execution. + +The agreed dependency owner is POO Flow. The target is for its =gerbil.pkg= +to declare a qualified Orgize Scheme package once; the current source graph +cannot support that edge. Rust/Rowan runtime delivery is a long-term runtime +direction, not an interim substitute for this package target and not the +current Python milestone. Aitia, Lambda +and other POO Flow consumers must not each acquire an independent Org parser +or duplicate its package pin. +Package availability does not mean implicit Scheme symbol imports: a public +POO Flow module/composition still controls what consumers receive. + +The C ABI is a transport, not a new semantic owner. Orgize owns Org language +semantics, and its Scheme/Gambit package owns the generic Org C ABI. POO Flow +inherits Orgize once and exposes the POO-native Org facade; +its existing downstream native boundary may forward qualified Org operations +without redefining their semantics. Aitia depends on POO Flow and does not +invent a separate Org wire protocol or pin Orgize independently. Rust can call +the Orgize C ABI when it needs that Scheme runtime boundary, while native Rust +consumers can continue using the Scheme-generated AOT library API directly. +Neither path makes the other an implicit fallback or a second authority. + +The first maintained surface is [[file:../../user-interface/workspaces/job-executor.org][the job-executor workspace]], connected to +[[file:../adrs/0008-stable-operation-key-after-lost-ack.org][ADR 0008]]. Its [[file:../../user-interface/contracts/engineering-workspace.org][authoring contract]] uses Orgize's existing =org-contract= +implementation, not an Aitia parser or evaluator. + +** Scheme source authority and Rowan delivery + +The selected upstream surface is =scheme :org-elements= and +=scheme :org-contract=. Org properties hold identity and scope metadata; +the block markers select bounded Scheme declarations. Org Elements supplies +POO query values and selection semantics. Org Contract supplies POO assertion +values and evaluation semantics. Its hygienic forms admit the declarations +before the upstream Scheme generators emit Rust AOT plans. + +Rust consumes these plans over the Scheme-declared lossless Rowan graph. +The public =parse_org_aot= and generated query/contract packs are the consumer +path. A legacy =orgize lint= or =contract trace= executable is not evidence +that this path works, even if its source checkout is on the correct branch. +Do not build a Python evaluator or translate the old contract syntax into a +second authority. Our contracts use the upstream Scheme forms directly. + +** Implementation horizons + +The current executable milestone is Aitia's production, uv-managed Python +Runtime. It consumes the existing Scheme/C ABI, runs real verification tools +with AnyIO-owned task lifetimes, and must carry frozen source-bound observations +to the actual Scheme Host admission path. For this Org workspace, Python may +coordinate an upstream-qualified Org query/contract result as an observation; +it must not parse Org independently, evaluate =org-contract= independently, +or treat a successful CLI exit as admission. Org-specific Python consumption +remains pending until its upstream result and source identity can be conveyed +through a qualified boundary. + +The current Orgize PR has Scheme declarations and generated Rust/Rowan +consumer APIs, but no =gerbil.pkg=/=build.ss= Scheme package entrypoint, +Scheme-exported =c-define= surface, or installed shared-library contract. +Its Rust =org_aot_edit::apply_org_source_edits= is a typed library function, +not that ABI. Aitia's existing Scheme/Gambit C ABI proves the mechanism is +available locally, but does not make Orgize's semantics available through it. +The initial Scheme ABI must be scoped to semantics that Scheme actually runs +over admitted graph values, such as Element selection and Contract evaluation; +do not label it a source-parser ABI until source-to-graph execution and its +ownership, errors and lifetime are implemented and tested through that path. + +The later Rust/Rowan runtime is the intended durable execution path for +Scheme-generated Org artifacts. Its source, generated packs, Rust consumer, +the Scheme-owned ABI and POO Flow facade require their own qualification. Its direction +does not make a Rust FFI build a prerequisite for the current Python/Host work. +The Scheme package-cycle audit below is a separate dependency-design gate; +neither postponing it nor qualifying Rust alone satisfies the requested +=gerbil.pkg= inheritance. + +* Native syntax and proposed semantic projection + +| Native Org element | Aitia interpretation | Boundary | +|--------------------+----------------------+----------| +| Heading and =ID= property | addressable design or review node | title and nesting do not grant identity authority | +| Property drawer | revision, kind, owner-role and subject references | custom properties below are proposed conventions | +| TODO keyword and checkboxes | authoring/review work state | DONE is neither assurance support nor authorization | +| Links and tables | source references and alternative comparison | a link does not admit evidence | +| LOGBOOK | attributed design-revision narrative | not a substitute for trusted execution receipts | +| Source/export block | typed content owned by its language/backend | parsing or rendering must not execute it | + +=AITIA_KIND=, =OWNER_ROLE= and =SUBJECT_ID= are project conventions in this +draft, not existing Orgize built-ins or registered POO principal identities. +Resolve them through an explicit, validated POO-native projection. Reject +duplicate IDs and unresolved required references instead of guessing a role +from the heading hierarchy. User-facing defaults belong to the maintained +Composition; advanced consumers may extend its POO slots. + +* Agent collaboration without ambient authority + +A role-scoped context contains the selected design node, relevant assumptions, +alternatives, contrary material and evidence bounds. Organization Bundle +identities and delegated visibility must be resolved before an Agent runs. +No heading, TODO transition or =OWNER_ROLE= declaration spawns an Agent. + +An edit proposal binds the source digest, stable node ID, original source range +and expected old content. The owner reviews the proposal before application. +Any digest/content conflict requires re-query and review; never patch a stale +offset. Preserve unrelated text, property drawers and comments through Orgize's +source-owned representation. A new design revision reopens the affected +obligations through existing POO Flow causal mechanisms, not a parallel graph. + +Design status, scoped evidence support, authorization and observed execution +remain independent. Agent review cannot certify its own external guarantees. +Historical cases may inspire a judgment; only qualified checks support a claim. + +#+begin_export mermaid +flowchart TD + A["Org source
authored"] --> Q["Orgize projection
source-bound"] + Q --> R["Role-scoped review
proposed"] + R --> P["Edit proposal
owner review pending"] + P --> C{"Source still matches?"} + C -->|no| Q + C -->|yes, accepted| D["Design revision
obligations reopened"] + D --> V["Verification
scoped evidence pending"] + V --> G["Cedar / Runtime admission
effect authority pending"] + G --> E["Execution
receipt observed"] + E --> A +#+end_export + +The diagram specifies intended interactions. The authoring/query checks below +do not qualify this complete loop. + +* Package boundary audit — blocking direct installation + +At the first 2026-09-24 audit, the selected Orgize branch was +=codex/scheme-rowan-org-cutover= at +=4a94883bd47196a175a88f552410d59102f34d24=. Its Scheme definitions live under +=languages/org/v1/=; =modules/org-elements/interface.ss= exports POO query +objects and functions. The branch has no root =gerbil.pkg= or =build.ss=. +Do not infer an installable Gerbil package from the presence of these sources. + +The fetched Orgize branch now includes consumer-owned Contract import +generation at =5cc02a1c5f882a3b182461ce803b8c76c4772c36= and pins +gerbil-parser to =ca01627c0c5ba1d460c0de83b060bc90d3b961e8=. That +parser revision's Scheme =gerbil.pkg= still depends on POO Flow at +=2ce55a9810f8559cc0f40a6c877a52ab7893c166=. In particular, +=graph-projection-objects.ss= and =line-structure-objects.ss= use POO Flow's +contract admission and object-family declarations. This is an actual source +dependency, not just a stale manifest entry. + +Adding a source-package edge now would produce: + +#+begin_export mermaid +flowchart LR + P["POO Flow"] -. "requested, not installed" .-> O["Orgize Scheme package
entrypoint missing"] + O -->|parser declarations| G["gerbil-parser"] + G -->|contracts and object families| P +#+end_export + +The integration must first establish an acyclic build/runtime package boundary. +Two options require review: separate generator dependencies from the delivered +Org runtime package, or relocate genuinely generic shared foundations below +both packages. Do not copy POO contracts into Orgize, silently delete parser +dependencies, or maintain a second parser. This RFC does not authorize or claim +completion of that cross-repository restructuring. + +This cycle concerns installing the Scheme source-package graph. It does not +describe the existing Cargo runtime graph: committed Scheme-generated AOT +artifacts are already consumed by Rust without a Gerbil runtime dependency. +Do not block qualification of that existing consumer path on a hypothetical +source-package installation, or conflate generator dependencies with delivery. + +After a reviewed boundary is qualified, pin the Orgize Scheme dependency in +POO Flow, expose a public unversioned consumer interface, and verify a clean +downstream install using only POO Flow. Qualification of the later Rust runtime +is independent of this source-package gate. Internal grammar versions are not +consumer namespace versions. + +** Source-package cycle cut and qualification order + +The requested Scheme source-package direction would be lower-level POO +primitives → =gerbil-parser= → Orgize → POO Flow → Aitia. At the selected +Orgize cut, =org-elements/types.ss= imports parser-owned graph projection +objects. The parser has seven Scheme modules importing POO Flow. These imports +are not merely incidental helpers: they use =PooFlowContract.= and +=PooFlowNativeObjectContract.=, evidence-bearing validation and admission, +=defpoo-object-family=, and the POO Flow debug trace. Replacing them with bare +=clan/poo= would change semantics; moving or copying those facilities below +POO Flow is not an approved mechanical refactor. + +| Parser source modules | POO Flow-owned dependency | Required observation to retain | +|-----------------------+---------------------------+--------------------------------| +| =types.ss=, =graph-projection-types.ss=, =line-structure-types.ss= | contract prototypes, classification and evidence admission | typed values and rejected candidates keep their current identities and failure semantics | +| =objects.ss=, =graph-projection-objects.ss=, =line-structure-objects.ss= | object-family declaration and evidence-gated constructors | public POO prototypes, slots and constructor rejection remain unchanged | +| =runtime/observability.ss= | debug trace policy and emission | disabled/enabled phase behavior and receipt shape remain unchanged | + +This is a source-level inventory, not a test receipt proving equivalence after +a refactor. The exact consumer graph and all negative cases must be captured +before moving ownership. + +These are distinct future engineering tracks, neither yet delivered: + +1. *Rust/Rowan runtime delivery (long-term target, not current Python work).* Keep + Orgize's Scheme declarations and generators in their current source/tooling + lane. Package the Scheme-generated Rust AOT consumer as an immutable, + independently consumable runtime. It may consume a qualified Scheme C ABI + where that is the right runtime boundary, or expose its own native Rust + library API; do not require an Orgize-Rust-owned C ABI merely to serve + Aitia Python. Orgize already exposes =parse_org_aot=, Element records, + source receipts, query and contract evaluation as Rust library APIs, but + this is not yet a C ABI, a Gerbil package, or an installed POO Flow + dependency. POO Flow owns the POO-native facade and admission; it must call + upstream semantics, not reimplement Org queries or contracts. + Pin generator and runtime revisions/digests together and qualify a clean + binary/runtime install. This can supply a durable POO Flow-owned runtime + dependency for downstreams, but it does *not* fulfill the separate request + that Orgize Scheme source be inherited through =gerbil.pkg=. +2. *Scheme source-graph refactor (the stated package target).* Keep the desired + source-package inheritance only after resolving parser → POO Flow without + losing its contract/evidence, object-family and observability behavior. + An existing lower library may own genuinely generic mechanisms only if + authority and public observations move together; otherwise the dependency + direction must be redesigned. Do not fork these semantics, create a second + AST, or add compatibility forwarding simply to make installation pass. + +Qualify the stated Scheme package target in this order: + +1. Lock positive and negative observations for the seven parser imports, + including contract admission, object-family behavior and debug traces. +2. Review the actual authority-preserving source-graph cut. Only after it is + accepted, remove parser → POO Flow and replay parser/compiler/runtime tests + from a clean dependency closure. +3. Add Orgize's source-owned Gerbil package/build entrypoint and qualify its + Scheme Elements/Contract declarations and Rowan/AOT generation against + that parser revision. +4. Pin Orgize once in POO Flow's =gerbil.pkg=, expose its POO-native unversioned + Org interface, and replay an Aitia workspace with POO Flow as its sole + direct Org dependency. Prove clean installation without ambient + =GERBIL_LOADPATH= or an old compiled module. + +When the later Rust runtime track begins, qualify exact generated +pack/source/parser identity and stale-pack rejection first; then define an +upstream-owned stable FFI with explicit ownership, errors and memory limits. +A Rust =pub fn= or prebuilt executable alone is not that boundary. That runtime +must not be relabeled as Scheme source-package closure. The existing =4a94883= +Cargo/AOT checks remain independent evidence, not evidence that either +package or FFI gate has closed. RFC 0005 stays draft until those gates pass. + +* Acceptance and evidence scope + +1. Authoring: upstream Rowan/CST tangling reads the native Org metadata and + tagged Scheme declarations. Scheme admission and AOT generation qualify the + six document and five decision-local assertions. No legacy CLI substitution. +2. Query/evaluation: run those generated plans over the actual workspace and + negative fixtures. Missing content, a single alternative, content under + another decision and a receipt link in the wrong section must fail the + intended assertion. This checks structure, not evidence truth or authority. +3. Package: clean, acyclic Orgize Scheme source → POO Flow inheritance; + downstreams declare only POO Flow. The later Rust/Rowan runtime has a + separate delivery gate. Both remain open, neither blocks the present + Python/Host admission work. +4. Projection: POO-native identity/reference/role resolution plus negative + duplicate, missing-reference and unauthorized-visibility tests. Pending. +5. Mutation: Aitia Python rejects moved spans, stale source digests and + overlapping proposals before owner review. Orgize PR #15 now has a + Scheme-AOT-graph-backed candidate source transformation that checks unique + node =:ID:= ownership, rejects wrong-node spans and preserves unrelated + bytes. The transport from Aitia Python to that API and reviewed persistence + are still pending; neither preflight nor a candidate is authorization. +6. Lifecycle: an accepted edit reopens an actual obligation, a real tool runs + through existing admission, and its scoped receipt feeds a design review. + Pending; a TODO transition or successful lint is not this receipt. + +** Correction of the initial receipt — 2026-09-24 + +The earlier authoring receipt used an installed CLI at +=52d315155ebbba917f0694068444d3bbde5f9920=, not the selected Scheme/Rowan +consumer path. Its six passing assertions and task queries do not qualify +this integration. The later cross-decision negative tests on that old path +also failed. Neither result establishes behavior of the new Scheme modules. +The legacy CLI recipe and its Python qualification test have been withdrawn; +the contract source now uses upstream Scheme declarations. + +The earlier fetch/rebase confirmed that =codex/scheme-rowan-org-cutover= and +its remote were both at =4a94883bd47196a175a88f552410d59102f34d24= at +that time. The mistake was the +consumer path, not an unrebased HEAD. New evidence must identify the source +revision, generated artifact and actual consumer entrypoint separately. + +** Correct-path bounded receipt — 2026-09-24 + +At the fetched/rebased =4a94883= revision, the following upstream checks passed +using locked, offline Cargo dependencies and two build workers: + +| Check | Result | Scope | +|-------+--------+-------| +| =integration_test org_parser_aot= | 21 passed | Scheme-generated parser, Rowan graph and existing generated contract execution | +| =integration_test org_element_query= | 3 passed | generated queries, consumer pack, scope and fail-closed checks | +| =--example org_contract_tangle= | 5 passed | Scheme block extraction, metadata and ancestry rejection | +| =--example org_elements_tangle= | 5 passed | Scheme query extraction and existing consumer source/artifact consistency | +| Upstream =org_contract_tangle= on Aitia's registry | 2 definitions / 11 assertions extracted | actual Org CST to Scheme source, not Scheme admission or AOT execution | + +The first build selected Nix's =cc= and failed to link =iconv=. Retrying with +process-local =CC=/usr/bin/cc= and +=CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER=/usr/bin/cc= passed; no global +environment or toolchain configuration was changed. Commands also unset +=SDKROOT= for that process and set =CARGO_BUILD_JOBS= to 2. + +At that earlier cut, the extracted Aitia source was temporary and had not +been compiled/evaluated: the extractor emitted upstream-relative interface +imports. Consumer-owned module location, Scheme admission, AOT generation and +cross-decision negatives were still required. The 34 upstream passes did not +constitute Aitia end-to-end completion. + +** Consumer-owned Scheme source and AOT plan slice — 2026-09-24 + +At the later fetched Orgize =5cc02a1= cut, =org_contract_tangle= accepts an +optional Contract and Element interface pair. The default output remains +byte-for-byte equal to upstream's committed generated Scheme file. Six +generator tests pass, including empty-interface rejection and consumer-owned +imports. Running that generator on Aitia's actual +=engineering-workspace.org= yielded two definitions and eleven assertions. +Gerbil then constructed these declarations and the upstream Scheme AOT +generator emitted a plan with the current Org graph projection digest +=sha256:f26d350a0456114b37a9af347b5b4694a9eff8dd787df988296814f1a60f29a8=. + +This is a bounded source/plan result, not actual Aitia workspace evaluation. +The isolated invocation still emitted a POO Flow observability warning from +mixed local Gerbil objects. A clean package closure and consumer-side Rust +evaluation with cross-decision negatives remain unqualified. The direct +Orgize source-package cycle described above is unchanged; do not promote the +generated plan or this warning-bearing local check to a published interface. + +** Source-owned mutation implementation on Orgize PR #15 — 2026-09-24 + +[[https://github.com/tao3k/orgize/pull/15][Orgize PR #15]] now includes commit =54de4fe=, which adds +=org_aot_edit::apply_org_source_edits=. It consumes the Scheme-AOT Element +graph, validates exact source digest, unique headline =:ID:=, UTF-8 byte +range, expected old text and non-overlap, then reparses the candidate source. +It does not call the old =Org::replace_range= path or write a file. Local +integration tests passed 367/367 with two ignored; focused mutation tests +passed 4/4, and strict Clippy plus formatting passed. This closes the +Orgize-owned candidate transformation, not Aitia's transport or an +owner-approved persistent edit. The PR itself remains draft for the broader +parser cutover and dependency gates. diff --git a/justfile b/justfile index 81c7d0e..c56e4dc 100644 --- a/justfile +++ b/justfile @@ -11,6 +11,7 @@ export GERBIL_PATH := env_var_or_default("GERBIL_PATH", self_root + "/.gerbil") export GERBIL_LOADPATH := env_var_or_default("GERBIL_LOADPATH", self_root + ":" + GERBIL_PATH + "/lib") native_root := GERBIL_PATH + "/native" native_library := native_root + "/libpoo_flow_aitia.dylib" +python_native_dir := self_root + "/bindings/python/src/lambda_aitia/_native" gerbil_env := if os() == "macos" { "env -u SDKROOT" } else { "env" } export UV_CACHE_DIR := env_var_or_default("UV_CACHE_DIR", self_root + "/.cache/uv") darwin_cc := if os() == "macos" { "/usr/bin/cc" } else { "cc" } @@ -62,7 +63,50 @@ check-semantic: # Build the Aitia-owned assurance laws independently of POO Flow's proof package. [group('check')] check-proof: - cd proof/lean && lake build LambdaAitiaModuleAssuranceProof + cd proof/lean && lake build LambdaAitiaModuleAssuranceProof LambdaAitiaModuleSdlcProof + cd proof/lean && output="$(lake env lean AxiomAudit.lean)" && printf '%s\n' "$output" && ! grep -q 'sorryAx' <<< "$output" + +# Quint 0.33.0 and its Apalache 0.62.1 TLC backend are explicit inputs. +# The checker exhausts each bounded model and requires negative-control traces. +[group('check')] +check-quint module='all': + python3 proof/quint/check_contract.py '{{module}}' + +# Pinned Quint MBT traces, native Scheme execution, and explicit fault refusals. +[group('check')] +check-quint-scheme: + python3 proof/quint/check_scheme_replay.py + +# Complete reachable lifecycle states, named actions, and fault refusals. +[group('check')] +check-lifecycle-scheme: build-scheme + python3 proof/quint/test_lifecycle_replay.py + python3 proof/quint/check_lifecycle_replay.py + +# Every gate must run; none substitutes for semantic implementation refinement. +[group('check')] +check-formal: + just test-scheme-atomic formal retirement-binding-test.ss + just test-scheme-atomic formal retirement-execution-test.ss + just check-quint-scheme + just check-lifecycle-scheme + just check-quint + just check-proof + +# Refresh the in-place Python extension without rebuilding POO Flow. +[group('dev')] +dev-python: + test -f '{{ GERBIL_PATH }}/pkg/github.com/tao3k/orgize/bindings/c/include/orgize.h' || { echo 'Orgize C header missing from GERBIL_PATH; install the declared POO Flow Gerbil dependencies first' >&2; exit 1; } + just clean-python-cffi + cd bindings/python && {{ gerbil_env }} uv sync --locked --extra test + cd bindings/python && {{ gerbil_env }} uv run --locked --extra test python src/lambda_aitia/_native/_build.py + cd bindings/python && {{ gerbil_env }} uv run --locked --extra test python -c 'from lambda_aitia._native._aitia_cffi import ffi; ffi.typeof("orgize_element_row"); print("aitia-cffi-current")' + +# Remove only generated CFFI artifacts, not Python sources or the uv venv. +[group('clean')] +clean-python-cffi: + find '{{ python_native_dir }}' -maxdepth 1 -type f -name '_aitia_cffi.*' -delete + if test -d '{{ python_native_dir }}/_build_temp'; then find '{{ python_native_dir }}/_build_temp' -depth -delete; fi # Aitia owns its native ABI and Python Runtime checks. [group('check')] @@ -77,9 +121,16 @@ check-native: '{{ native_root }}/aitia_dynamic_harness' '{{ native_library }}' just test-scheme bindings cd bindings/python && {{ gerbil_env }} uv lock --check + just clean-python-cffi cd bindings/python && {{ gerbil_env }} uv run --locked --extra test python src/lambda_aitia/_native/_build.py cd bindings/python && LAMBDA_AITIA_NATIVE_LIBRARY='{{ native_library }}' {{ gerbil_env }} uv run --locked --extra test pytest just build-python-wheel '{{ native_library }}' + just check-installed-wheel + +# Exercise the delivered wheel from a fresh environment outside the checkout. +[group('check')] +check-installed-wheel: + cd bindings/python && {{ gerbil_env }} uv run --locked --extra test pytest tests/installed_wheel_qualification.py # Build the production wheel from Aitia's locked uv environment. The preceding # sync makes the declared PEP 517 backend available without a second isolated @@ -87,12 +138,12 @@ check-native: [group('build')] build-python-wheel native_library: test -f '{{ native_library }}' - cd bindings/python && LAMBDA_AITIA_NATIVE_LIBRARY='{{ native_library }}' env -u SDKROOT uv sync --locked --extra test - cd bindings/python && LAMBDA_AITIA_NATIVE_LIBRARY='{{ native_library }}' env -u SDKROOT uv build --wheel --offline --no-build-isolation + cd bindings/python && LAMBDA_AITIA_NATIVE_LIBRARY='{{ native_library }}' {{ gerbil_env }} uv sync --locked --extra test + cd bindings/python && LAMBDA_AITIA_NATIVE_LIBRARY='{{ native_library }}' {{ gerbil_env }} uv build --wheel --offline --no-build-isolation # Current repository closure. No recipe grants runtime authority. [group('check')] -check: check-license-contract check-semantic check-proof test-integration check-native +check: check-license-contract check-semantic check-proof check-quint test-integration check-native [group('check')] check-all: check diff --git a/modules/ADR/config.ss b/modules/ADR/config.ss index c4d5244..99225cf 100644 --- a/modules/ADR/config.ss +++ b/modules/ADR/config.ss @@ -3,7 +3,7 @@ ;;; ;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later -(import :poo-flow/src/module-system/declaration/interface +(import :poo-flow/src/user-interface/module-selection :poo-flow/lambda-aitia/modules/ADR/funs) (export ADR-config) diff --git a/modules/ADR/funs.ss b/modules/ADR/funs.ss index 55bbb17..d6f6cd1 100644 --- a/modules/ADR/funs.ss +++ b/modules/ADR/funs.ss @@ -4,7 +4,7 @@ ;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later (import (only-in :clan/poo/object .ref) - (only-in :poo-flow/src/modules/governance/funs + (only-in :poo-flow/modules/governance/funs poo-flow-governance-contribution) :poo-flow/lambda-aitia/modules/ADR/types :poo-flow/lambda-aitia/modules/ADR/objects diff --git a/modules/ADR/objects.ss b/modules/ADR/objects.ss index 8769ec3..7bf9275 100644 --- a/modules/ADR/objects.ss +++ b/modules/ADR/objects.ss @@ -4,8 +4,8 @@ ;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later (import (only-in :clan/poo/object .o) - :poo-flow/src/module-system/contribution/model - (only-in :poo-flow/src/modules/governance/objects + :poo-flow/core/poo-clos/interface + (only-in :poo-flow/modules/governance/objects PooFlowGovernanceProfile. poo-flow-governance-source) :poo-flow/lambda-aitia/modules/ADR/types) @@ -32,24 +32,24 @@ "modules/ADR/expired-references.gql" 'gql)))) (def (ADR-link relation-value target-value) - (poo-flow-check-model + (poo-clos-check-model ADRLink - (.o (:: @ (poo-flow-model-prototype ADRLink)) + (.o (:: @ (poo-clos-model-prototype ADRLink)) relation: relation-value target: target-value))) (def (ADR-option identity-value title-value description-value disposition-value pros-value cons-value) - (poo-flow-check-model + (poo-clos-check-model ADROption - (.o (:: @ (poo-flow-model-prototype ADROption)) + (.o (:: @ (poo-clos-model-prototype ADROption)) identity: identity-value title: title-value description: description-value disposition: disposition-value pros: pros-value cons: cons-value))) (def (ADR-consequence sentiment-value statement-value) - (poo-flow-check-model + (poo-clos-check-model ADRConsequence - (.o (:: @ (poo-flow-model-prototype ADRConsequence)) + (.o (:: @ (poo-clos-model-prototype ADRConsequence)) sentiment: sentiment-value statement: statement-value))) (def (ADR-record identity-value title-value status-value date-value @@ -58,9 +58,9 @@ confirmation-value decision-makers-value consulted-value informed-value links-value) (let ((record - (poo-flow-check-model + (poo-clos-check-model ADRRecord - (.o (:: @ (poo-flow-model-prototype ADRRecord)) + (.o (:: @ (poo-clos-model-prototype ADRRecord)) identity: identity-value title: title-value status: status-value date: date-value revision: revision-value context: context-value decision: decision-value decision-drivers: drivers-value diff --git a/modules/ADR/types.ss b/modules/ADR/types.ss index 7e94194..bf76c4f 100644 --- a/modules/ADR/types.ss +++ b/modules/ADR/types.ss @@ -5,8 +5,8 @@ (import (only-in :clan/poo/object .ref .slot?) :std/list/list - :poo-flow/src/module-system/contribution/model - (only-in :poo-flow/src/modules/governance/types + :poo-flow/core/poo-clos/interface + (only-in :poo-flow/modules/governance/types poo-flow-governance-profile?)) (export ADR-profile? @@ -95,10 +95,10 @@ (predicate-slot 'informed ADR-text-list?) (predicate-slot 'links ADR-link-list?)))) -(def (ADR-link? value) (poo-flow-model? ADRLink value)) -(def (ADR-option? value) (poo-flow-model? ADROption value)) -(def (ADR-consequence? value) (poo-flow-model? ADRConsequence value)) -(def (ADR-record? value) (poo-flow-model? ADRRecord value)) +(def (ADR-link? value) (poo-clos-model? ADRLink value)) +(def (ADR-option? value) (poo-clos-model? ADROption value)) +(def (ADR-consequence? value) (poo-clos-model? ADRConsequence value)) +(def (ADR-record? value) (poo-clos-model? ADRRecord value)) (def (unique-by? projection input-values) (length=? input-values diff --git a/modules/assurance/config.ss b/modules/assurance/config.ss index b9e2d69..4960c6f 100644 --- a/modules/assurance/config.ss +++ b/modules/assurance/config.ss @@ -4,9 +4,12 @@ ;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later (import (only-in :clan/poo/object .o) + (only-in :gerbil/core list-sort) + :std/list/list + :poo-flow/core/poo-clos/interface :poo-flow/lambda-aitia/modules/assurance/types) -(export AssurancePolicy) +(export AssurancePolicy assurance-verification-policy) ;;; Pure declared defaults. This value invokes no verifier and grants no ;;; authority. @@ -18,3 +21,18 @@ conflict-resolution: 'explicit planner-executes?: #f grants-authority?: #f)) + +(def (capabilitystring left) (symbol->string right))) + +;;; Capabilities describe available adapters; this value cannot invoke one. +(def (assurance-verification-policy identity-value revision-value capability-values) + (unless (and (list? capability-values) (every symbol? capability-values)) + (error "verification policy capabilities must be symbols" capability-values)) + (poo-clos-check-model + AssuranceVerificationPolicy + (.o (:: @ (poo-clos-model-prototype AssuranceVerificationPolicy)) + identity: identity-value revision: revision-value + capabilities: + (list-sort capabilitysemantic-root + poo-flow-cedar-decision->authorized-effect-token)) +(export assurance-effect-host assurance-effect-host-epoch + assurance-effect-host-authorize! assurance-effect-host-grant-current? + assurance-effect-host-reserve! assurance-effect-host-deliver! + assurance-effect-host-revoke!) + +(def hosts (make-hash-table-eq weak-keys: #t)) +(def grants (make-hash-table-eq weak-keys: #t)) +(def reservations (make-hash-table-eq weak-keys: #t)) + +(def (entry host) + (let (state (hash-get hosts host)) + (unless (and state (eq? (vector-ref state 0) (current-thread))) + (error "effect Host requires its exact serialized owner")) + (when (vector-ref state 7) (error "effect Host mutation during runtime handoff")) + state)) +(def (instant state) + (let (now ((vector-ref state 3))) + (unless (and (exact-integer? now) (>= now (vector-ref state 8))) + (error "effect Host clock must be monotonic and nonnegative")) + (vector-set! state 8 now) + now)) +(def (handoff state operation) + (dynamic-wind + (lambda () (vector-set! state 7 #t)) + operation + (lambda () (vector-set! state 7 #f)))) + +;;; Runtime authorize: Request -> CedarDecision or #f (deny/indeterminate). +;;; Runtime deliver: Request Token -> delivered / failed / indeterminate. +;;; 'delivered is an attributable acknowledgement, not inferred from start. +(def (assurance-effect-host authorize deliver clock lease) + (unless (and (procedure? authorize) (procedure? deliver) (procedure? clock) + (exact-integer? lease) (> lease 0)) + (error "invalid effect Host runtime configuration")) + (let (host (.o kind: 'aitia.effect-host.v1)) + (hash-put! hosts host (vector (current-thread) authorize deliver clock lease 0 0 #f 0)) + host)) +(def (assurance-effect-host-epoch host) (vector-ref (entry host) 5)) +(def (assurance-effect-host-revoke! host) + (let (state (entry host)) + (vector-set! state 5 (+ 1 (vector-ref state 5))))) + +(def (grant-entry host grant) + (let (issued (hash-get grants grant)) + (and issued (eq? host (vector-ref issued 0)) + (equal? (.ref grant 'expires-at) (vector-ref issued 5)) + (equal? (.ref grant 'epoch) (vector-ref issued 4)) + (equal? (poo-flow-authorized-effect-token-digest (.ref grant 'token)) + (vector-ref issued 6)) issued))) +(def (current? state issued) + (and issued + (= (vector-ref state 5) (vector-ref issued 4)) + (< (instant state) (vector-ref issued 5)) + (assurance-host-cedar-request-current? (vector-ref issued 1) (vector-ref issued 2)) + ;; Source validation may take time. Apply strict expiry to the same + ;; final instant passed to the token validator, whose endpoint is + ;; otherwise inclusive; an earlier clock sample must not authorize use. + (let (now (instant state)) + (and (< now (vector-ref issued 5)) + (.ref (poo-flow-authorized-effect-token-validate + (vector-ref issued 3) + (poo-flow-token-validation-context + (.ref (vector-ref issued 3) 'semantic-root) + (.ref (vector-ref issued 3) 'binding) + now (vector-ref state 5) '())) 'accepted?))))) +(def (assurance-effect-host-grant-current? host grant) + (let (state (entry host)) + (and (current? state (grant-entry host grant)) #t))) + +(def (assurance-effect-host-authorize! host verification-host request) + (let* ((state (entry host)) (now (instant state)) (epoch (vector-ref state 5))) + (and (assurance-host-cedar-request-current? verification-host request) + (let (decision (handoff state (lambda () ((vector-ref state 1) request)))) + (and decision (poo-flow-cedar-decision-permit? decision) + (= epoch (vector-ref state 5)) + (< (instant state) (+ now (vector-ref state 4))) + (assurance-host-cedar-request-current? verification-host request) + (let* ((next (+ 1 (vector-ref state 6))) + (context (.ref request 'context)) + (bundle (.ref context 'aitia_snapshot_semantic_digest)) + (intent (.ref request 'intent)) + (root (poo-flow-cedar-decision->semantic-root decision bundle intent)) + (binding + (poo-flow-effect-binding + bundle epoch (.ref decision 'policy-digest) (.ref decision 'entity-digest) + (.ref decision 'decision-digest) intent next (.ref request 'action) + 'configured-runtime 'local 1 1 'local 0 0 1 intent 'local)) + (until (+ now (vector-ref state 4))) + (token (poo-flow-cedar-decision->authorized-effect-token + decision next next root binding + (poo-flow-token-validity now now until epoch) + 'strict 1 'configured-runtime "local-adapter")) + (grant (let ((token-value token) (epoch-value epoch)) + (.o kind: 'aitia.effect-grant.v1 token: token-value epoch: epoch-value expires-at: until)))) + (vector-set! state 6 next) + (hash-put! grants grant + (vector host verification-host request token epoch until + (poo-flow-authorized-effect-token-digest token) #f)) + grant)))))) + +;;; Reservation consumes the grant once, but executes no external effect. +(def (assurance-effect-host-reserve! host grant) + (let* ((state (entry host)) (issued (grant-entry host grant))) + (and (current? state issued) (not (vector-ref issued 7)) + (let (reservation (.o kind: 'aitia.effect-reservation.v1)) + (vector-set! issued 7 #t) + (hash-put! reservations reservation (vector host grant issued #f)) + reservation)))) + +(def (delivery-receipt outcome-value invoked?) + (.o kind: 'aitia.effect-delivery.v1 outcome: outcome-value handoff-executed?: invoked? + delivered?: (eq? outcome-value 'delivered) durable?: #f distributed-authority?: #f)) +(def (assurance-effect-host-deliver! host reservation) + (let* ((state (entry host)) (issued (hash-get reservations reservation))) + (cond + ((or (not issued) (not (eq? host (vector-ref issued 0))) (vector-ref issued 3)) + (delivery-receipt 'rejected #f)) + (else + ;; Consume even on rejection / exception / unknown acknowledgement. + (vector-set! issued 3 #t) + (let (grant-state (vector-ref issued 2)) + (if (and (grant-entry host (vector-ref issued 1)) (current? state grant-state)) + (let (outcome + (handoff state + (lambda () ((vector-ref state 2) (vector-ref grant-state 2) + (vector-ref grant-state 3))))) + (unless (memq outcome '(delivered failed indeterminate)) + (error "runtime delivery returned an invalid acknowledgement" outcome)) + (delivery-receipt outcome #t)) + (delivery-receipt 'rejected #f))))))) diff --git a/modules/assurance/evidence-admission.ss b/modules/assurance/evidence-admission.ss new file mode 100644 index 0000000..4c47446 --- /dev/null +++ b/modules/assurance/evidence-admission.ss @@ -0,0 +1,291 @@ +;;; -*- Gerbil -*- +;;; SPDX-FileCopyrightText: 2026 tao3k team and Contributors +;;; +;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +;;; Pure eligibility and sealed support checks. The Host still owns adapter +;;; identity, execution attestation and the transition to admitted evidence. +(import (only-in :clan/poo/object .o .ref) + (only-in :gerbil/core list-sort) + (only-in :std/list/list find every) + :poo-flow/core/poo-clos/interface + (only-in :poo-flow/core/contribution/verification + poo-flow-verification-valid?) + (only-in :poo-flow/lambda-aitia/modules/assurance/funs + assurance-canonical-digest assurance-node-canonical + assurance-relation-canonical) + :poo-flow/lambda-aitia/modules/assurance/types) + +(export assurance-verifier-input assurance-verifier-outcome + assurance-evaluate-evidence-admission + assurance-evidence-outcome-bound? + assurance-verification-subject + assurance-snapshot-semantic-digest + assurance-verification-subject-snapshot + assurance-candidate-support-structural? + assurance-support-sealed-under-adapter?) + +(def (assurance-verifier-input artifact) + (unless (assurance-artifact? artifact) + (error "verifier input requires an artifact")) + (poo-clos-check-model + AssuranceVerifierInput + (.o (:: @ (poo-clos-model-prototype AssuranceVerifierInput)) + identity: (.ref artifact 'identity) + revision: (.ref artifact 'revision) + digest: (.ref artifact 'content-digest)))) + +(def (assurance-verifier-outcome identity-value status-value + producer: producer-value tool: tool-value + tool-version: tool-version-value + obligation: obligation-value + subject: subject-value scope: scope-value + snapshot-revision: snapshot-revision-value + snapshot-context-digest: context-value + inputs: input-values + output-digest: (output-value #f) + accountable-authority: (authority-value #f) + review-scope: (review-scope-value #f)) + (poo-clos-check-model + AssuranceVerifierOutcome + (.o (:: @ (poo-clos-model-prototype AssuranceVerifierOutcome)) + identity: identity-value status: status-value + producer: producer-value tool: tool-value + tool-version: tool-version-value + obligation: obligation-value subject: subject-value scope: scope-value + snapshot-revision: snapshot-revision-value + snapshot-context-digest: context-value inputs: input-values + output-digest: output-value accountable-authority: authority-value + review-scope: review-scope-value))) + +(def (snapshot-node snapshot identity) + (find (lambda (node) (string=? (.ref node 'identity) identity)) + (.ref snapshot 'nodes))) +(def (current-input? snapshot input) + (let ((artifact (snapshot-node snapshot (.ref input 'identity)))) + (and artifact (assurance-artifact? artifact) + (string=? (.ref input 'revision) (.ref artifact 'revision)) + (string=? (.ref input 'digest) (.ref artifact 'content-digest))))) +(def (duplicate-inputs? inputs) + (let loop ((rest inputs) (seen '())) + (and (pair? rest) + (let ((identity (.ref (car rest) 'identity))) + (or (member identity seen) + (loop (cdr rest) (cons identity seen))))))) + +(def (outcome-canonical reported-result) + (list (.ref reported-result 'identity) (.ref reported-result 'status) + (.ref reported-result 'producer) (.ref reported-result 'tool) + (.ref reported-result 'tool-version) + (.ref reported-result 'obligation) (.ref reported-result 'subject) + (.ref reported-result 'scope) + (.ref reported-result 'snapshot-revision) + (.ref reported-result 'snapshot-context-digest) + (map (lambda (input) + (list (.ref input 'identity) (.ref input 'revision) + (.ref input 'digest))) + (.ref reported-result 'inputs)) + (.ref reported-result 'output-digest) + (.ref reported-result 'accountable-authority) + (.ref reported-result 'review-scope))) + +(def (assurance-verification-subject snapshot-value obligation-value + evidence-value outcome-value) + (poo-clos-check-model + AssuranceVerificationSubject + (.o (:: @ (poo-clos-model-prototype AssuranceVerificationSubject)) + snapshot: snapshot-value obligation: obligation-value + evidence: evidence-value outcome: outcome-value))) + +(def (snapshot-semantic-fields snapshot-value) + (list (.ref snapshot-value 'identity) + (.ref snapshot-value 'revision) + (.ref snapshot-value 'graph-identity) + (.ref snapshot-value 'source-revisions) + (.ref snapshot-value 'claim-revisions) + (.ref snapshot-value 'fact-cut) + (.ref snapshot-value 'policy-identity) + (.ref snapshot-value 'policy-revision) + (.ref snapshot-value 'evidence-identities) + (.ref snapshot-value 'state) + (.ref snapshot-value 'context-digest) + (.ref snapshot-value 'digest) + (map assurance-node-canonical (.ref snapshot-value 'nodes)) + (map assurance-relation-canonical + (.ref snapshot-value 'relations)) + (.ref snapshot-value 'unresolved) + (.ref snapshot-value 'conflicts))) + +;;; Use the entire semantic snapshot, not its editable displayed digest slot, +;;; when the Host detects a change and advances its invalidation epoch. +(def (assurance-snapshot-semantic-digest snapshot-value) + (unless (assurance-snapshot? snapshot-value) + (error "invalid assurance snapshot")) + (assurance-canonical-digest + (cons 'lambda-aitia.snapshot-semantic + (snapshot-semantic-fields snapshot-value)))) + +;;; POO Flow's adapter freezes this string before and after its host-trusted +;;; operation. Recompute from every semantic field: mutating only a displayed +;;; snapshot digest must not preserve an issued seal. +(def (assurance-verification-subject-snapshot subject-value) + (unless (assurance-verification-subject? subject-value) + (error "invalid assurance verification subject")) + (assurance-canonical-digest + (append + (cons 'lambda-aitia.verification-subject + (snapshot-semantic-fields (.ref subject-value 'snapshot))) + (list (assurance-node-canonical (.ref subject-value 'obligation)) + (assurance-node-canonical (.ref subject-value 'evidence)) + (outcome-canonical (.ref subject-value 'outcome)))))) + +(def (snapshot-relation snapshot-value identity-value) + (find (lambda (relation) + (string=? (.ref relation 'identity) identity-value)) + (.ref snapshot-value 'relations))) +(def (same-text-inventory? left right) + (and (= (length left) (length right)) + (equal? (list-sort stringutf8) :std/list/list (only-in :std/encoding/hex hex-encode) - :poo-flow/src/module-system/contribution/model + (only-in :poo-flow/src/graph/algorithms + poo-flow-graph-cycle-path + poo-flow-graph-loop-analysis-receipt + poo-flow-graph-reachable-ids + poo-flow-graph-topological-order) + :poo-flow/core/poo-clos/interface :poo-flow/lambda-aitia/modules/assurance/types (only-in :poo-flow/lambda-aitia/modules/assurance/invalidation-projection - assurance-invalidation-analysis assurance-invalidation-graph)) + assurance-invalidation-analysis assurance-invalidation-graph) + (only-in :poo-flow/lambda-aitia/modules/assurance/verification-projection + assurance-verification-dependency-graph)) (export assurance-node-canonical assurance-relation-canonical assurance-canonical-digest assurance-snapshot - assurance-support-admissible? assurance-invalidation-graph - assurance-invalidate) + assurance-snapshot-canonical? + assurance-bind-obligation-to-snapshot + assurance-invalidation-graph + assurance-invalidate assurance-derive-replacement-requirements + assurance-declare-replacement-obligation + assurance-plan-verification assurance-explain-verifier-choices + assurance-derive-composition-requirements) (def (assurance-node-semantic-slots kind) (case kind @@ -28,7 +40,8 @@ ((event) '(subject event-kind observation payload-digest modality commitment-state causal-parents)) ((action) '(subject action-kind requested-by scope)) - ((obligation) '(subject claim snapshot evidence-kind capability scope)) + ((obligation) '(subject claim snapshot snapshot-revision + snapshot-context-digest evidence-kind capability scope)) ((evidence) '(producer tool tool-version input-artifacts obligation subject scope valid-from valid-until admission-state)) ((counterexample) '(subject challenges evidence details-digest)) @@ -43,6 +56,16 @@ (.ref node 'state) (.ref node 'content-digest) (map (lambda (name) (list name (.ref node name))) (assurance-node-semantic-slots (.ref node 'kind))))) +;;; The context projection excludes only the obligation's own binding slots. +;;; Every other semantic change, including another obligation or relation, +;;; changes the context digest without creating a self-referential hash. +(def (assurance-node-context-canonical node) + (if (assurance-obligation? node) + (list (.ref node 'identity) (.ref node 'kind) (.ref node 'revision) + (.ref node 'state) (.ref node 'content-digest) + (map (lambda (name) (list name (.ref node name))) + '(subject claim snapshot evidence-kind capability scope))) + (assurance-node-canonical node))) (def (assurance-relation-canonical relation) (unless (assurance-relation? relation) (error "invalid assurance relation" relation)) @@ -221,48 +244,85 @@ (memq 'hypothesized node-states) (memq 'counterfactual node-states)) 'unknown) (else 'supported))) + (context-canonical + (list 'lambda-aitia.assurance-snapshot-context + identity-value revision-value graph-identity-value + canonical-source-revisions canonical-claim-revisions + fact-cut-value policy-identity-value policy-revision-value + canonical-evidence-identities snapshot-state + (canonical-objects ordered-nodes + assurance-node-context-canonical) + (canonical-objects ordered-relations + assurance-relation-canonical) + missing all-conflicts)) + (snapshot-context-digest + (assurance-canonical-digest context-canonical)) (canonical (list 'lambda-aitia.assurance-snapshot identity-value revision-value graph-identity-value canonical-source-revisions canonical-claim-revisions fact-cut-value policy-identity-value policy-revision-value canonical-evidence-identities - snapshot-state + snapshot-state snapshot-context-digest (canonical-objects ordered-nodes assurance-node-canonical) (canonical-objects ordered-relations assurance-relation-canonical) missing all-conflicts)) (snapshot-digest (assurance-canonical-digest canonical))) - (poo-flow-check-model + (poo-clos-check-model AssuranceSnapshot - (.o (:: @ (poo-flow-model-prototype AssuranceSnapshot)) + (.o (:: @ (poo-clos-model-prototype AssuranceSnapshot)) identity: identity-value revision: revision-value graph-identity: graph-identity-value source-revisions: canonical-source-revisions claim-revisions: canonical-claim-revisions fact-cut: fact-cut-value policy-identity: policy-identity-value policy-revision: policy-revision-value evidence-identities: canonical-evidence-identities - state: snapshot-state digest: snapshot-digest + state: snapshot-state context-digest: snapshot-context-digest + digest: snapshot-digest nodes: ordered-nodes relations: ordered-relations unresolved: missing conflicts: all-conflicts))))))))) -;;; Shape and a green-looking relation never grant support. The exact -;;; evidence and obligation identities must match, and alternate modalities -;;; cannot discharge an observed-fact obligation. -(def (assurance-support-admissible? relation evidence obligation) - (and (assurance-relation? relation) - (assurance-evidence? evidence) - (assurance-obligation? obligation) - (eq? (.ref relation 'plane) 'assurance) - (memq (.ref relation 'relation) '(supports discharges)) - (equal? (.ref relation 'source) (.ref evidence 'identity)) - (equal? (.ref relation 'target) (.ref obligation 'identity)) - (equal? (.ref evidence 'obligation) (.ref obligation 'identity)) - (equal? (.ref evidence 'subject) (.ref obligation 'subject)) - (equal? (.ref evidence 'scope) (.ref obligation 'scope)) - (equal? (.ref evidence 'revision) (.ref obligation 'revision)) - (eq? (.ref evidence 'state) 'supported) - (eq? (.ref evidence 'admission-state) 'admitted) - (not (memq (.ref relation 'modality) '(hypothesized counterfactual))))) +;;; A Host source must supply a self-consistent snapshot, not an editable POO +;;; presentation whose displayed digest/context no longer match its inventory. +(def (assurance-snapshot-canonical? snapshot) + (and (assurance-snapshot? snapshot) + (let (rebuilt + (assurance-snapshot + (.ref snapshot 'identity) (.ref snapshot 'revision) + (.ref snapshot 'graph-identity) + (.ref snapshot 'source-revisions) + (.ref snapshot 'claim-revisions) + (.ref snapshot 'fact-cut) + (.ref snapshot 'policy-identity) + (.ref snapshot 'policy-revision) + (.ref snapshot 'nodes) (.ref snapshot 'relations) + evidence-identities: (.ref snapshot 'evidence-identities) + unresolved: (.ref snapshot 'unresolved))) + (and (equal? (.ref snapshot 'digest) (.ref rebuilt 'digest)) + (equal? (.ref snapshot 'context-digest) + (.ref rebuilt 'context-digest)) + (eq? (.ref snapshot 'state) (.ref rebuilt 'state)) + (equal? (.ref snapshot 'conflicts) (.ref rebuilt 'conflicts)) + (equal? (.ref snapshot 'unresolved) + (.ref rebuilt 'unresolved)))))) + +;;; Binding is explicit: snapshot construction never silently upgrades an old +;;; obligation. The caller must reconstruct the final snapshot with this value; +;;; planning then compares both revision and non-circular context digest. +(def (assurance-bind-obligation-to-snapshot obligation snapshot) + (unless (and (assurance-obligation? obligation) + (assurance-snapshot? snapshot) + (string=? (.ref obligation 'snapshot) (.ref snapshot 'identity)) + (not (.ref obligation 'snapshot-revision)) + (not (.ref obligation 'snapshot-context-digest))) + (error "obligation cannot bind to this snapshot")) + (let ((bound-revision (.ref snapshot 'revision)) + (bound-context (.ref snapshot 'context-digest))) + (poo-clos-check-model + AssuranceObligation + (.o (:: @ obligation) + snapshot-revision: bound-revision + snapshot-context-digest: bound-context)))) (def (identity-member? identity values) (if (member identity values) #t #f)) @@ -311,3 +371,669 @@ (impacted-kind-identities nodes impacted 'effect) witnesses unresolved strong-components cyclic-components #f #f #f))))) + +;;; This derives the need for fresh declarations from the old snapshot's +;;; invalidation closure. It deliberately does not make a new obligation: a +;;; revised claim or policy may require a different question or evidence kind, +;;; and inserting an obligation would change the target snapshot context. +(def (replacement-requirement-blocker source-obligation source target + change-unresolved) + (let ((source-claim + (node-by-identity (.ref source 'nodes) + (.ref source-obligation 'claim))) + (target-claim + (node-by-identity (.ref target 'nodes) + (.ref source-obligation 'claim)))) + (cond + ((pair? (.ref source 'conflicts)) 'source-conflicted) + ((pair? (.ref source 'unresolved)) 'source-unresolved) + ((pair? change-unresolved) 'change-unresolved) + ((or (not (equal? (.ref source-obligation 'snapshot) + (.ref source 'identity))) + (not (equal? (.ref source-obligation 'snapshot-revision) + (.ref source 'revision))) + (not (equal? (.ref source-obligation 'snapshot-context-digest) + (.ref source 'context-digest)))) + 'stale-source) + ((equal? (.ref source 'digest) (.ref target 'digest)) 'target-unchanged) + ((pair? (.ref target 'conflicts)) 'target-conflicted) + ((pair? (.ref target 'unresolved)) 'target-unresolved) + ((not (equal? (.ref source 'identity) (.ref target 'identity))) + 'target-identity-changed) + ((not (equal? (.ref source 'graph-identity) + (.ref target 'graph-identity))) + 'graph-changed) + ((or (not (equal? (.ref source 'policy-identity) + (.ref target 'policy-identity))) + (not (equal? (.ref source 'policy-revision) + (.ref target 'policy-revision)))) + 'policy-changed) + ((not (assurance-claim? target-claim)) 'claim-missing) + ((or (not (assurance-claim? source-claim)) + (not (equal? (assurance-node-canonical source-claim) + (assurance-node-canonical target-claim)))) + 'claim-changed) + (else #f)))) + +(def (assurance-derive-replacement-requirements derivation-identity source target + changed-identities) + (unless (and (assurance-text? derivation-identity) + (assurance-snapshot? source) + (assurance-snapshot? target)) + (error "invalid replacement derivation input")) + (let* ((invalidation + (assurance-invalidate + (string-append derivation-identity "/invalidation") + source changed-identities)) + (changed-values + (assurance-invalidation-receipt-ref invalidation 'changed)) + (change-witnesses + (assurance-invalidation-receipt-ref invalidation 'witnesses)) + (change-unresolved + (assurance-invalidation-receipt-ref invalidation 'unresolved)) + (obligation-ids + (assurance-invalidation-receipt-ref + invalidation 'required-obligations)) + (derived-requirements + (map + (lambda (obligation-id) + (let ((obligation + (node-by-identity (.ref source 'nodes) obligation-id))) + (poo-clos-check-model + AssuranceReplacementRequirement + (.o (:: @ (poo-clos-model-prototype + AssuranceReplacementRequirement)) + source-obligation: obligation-id + source-revision: (.ref obligation 'revision) + claim: (.ref obligation 'claim) + subject: (.ref obligation 'subject) + evidence-kind: (.ref obligation 'evidence-kind) + capability: (.ref obligation 'capability) + scope: (.ref obligation 'scope) + blocker: + (replacement-requirement-blocker + obligation source target change-unresolved))))) + obligation-ids)) + (derivation-digest + (assurance-canonical-digest + (list "lambda-aitia.replacement-requirements" + derivation-identity + (.ref source 'digest) (.ref target 'digest) + changed-values change-witnesses change-unresolved + (map (lambda (requirement) + (list (.ref requirement 'source-obligation) + (.ref requirement 'source-revision) + (.ref requirement 'claim) + (.ref requirement 'subject) + (.ref requirement 'evidence-kind) + (.ref requirement 'capability) + (.ref requirement 'scope) + (.ref requirement 'blocker))) + derived-requirements))))) + (poo-clos-check-model + AssuranceReplacementDerivation + (.o (:: @ (poo-clos-model-prototype AssuranceReplacementDerivation)) + schema: "lambda-aitia.replacement-requirements" + identity: derivation-identity + source-snapshot-digest: (.ref source 'digest) + target-snapshot-digest: (.ref target 'digest) + changed: changed-values + witnesses: change-witnesses + unresolved: change-unresolved + requirements: derived-requirements digest: derivation-digest + verifier-executed?: #f release-authorized?: #f)))) + +;;; The new obligation, revised claim and supports edge must already be present +;;; in the caller's draft. Only its snapshot binding is filled here. The +;;; derivation is replayed against exact source/draft digests so a forged or +;;; unrelated receipt cannot be used to promote a candidate. +(def (snapshot-canonical? snapshot) + (let ((rebuilt + (assurance-snapshot + (.ref snapshot 'identity) (.ref snapshot 'revision) + (.ref snapshot 'graph-identity) + (.ref snapshot 'source-revisions) (.ref snapshot 'claim-revisions) + (.ref snapshot 'fact-cut) (.ref snapshot 'policy-identity) + (.ref snapshot 'policy-revision) + (.ref snapshot 'nodes) (.ref snapshot 'relations) + evidence-identities: (.ref snapshot 'evidence-identities) + unresolved: (.ref snapshot 'unresolved)))) + (and (equal? (.ref rebuilt 'digest) (.ref snapshot 'digest)) + (equal? (.ref rebuilt 'context-digest) + (.ref snapshot 'context-digest))))) + +(def (replacement-declaration-blocker derivation source draft + source-id candidate-id) + (let* ((expected + (assurance-derive-replacement-requirements + (.ref derivation 'identity) source draft (.ref derivation 'changed))) + (requirement + (find (lambda (item) + (equal? (.ref item 'source-obligation) source-id)) + (.ref derivation 'requirements))) + (candidate (node-by-identity (.ref draft 'nodes) candidate-id)) + (claim (and (assurance-obligation? candidate) + (node-by-identity (.ref draft 'nodes) + (.ref candidate 'claim)))) + (support-links + (filter (lambda (relation) + (and (eq? (.ref relation 'relation) 'supports) + (equal? (.ref relation 'source) candidate-id) + (assurance-obligation? candidate) + (equal? (.ref relation 'target) + (.ref candidate 'claim)))) + (.ref draft 'relations)))) + (cond + ((or (not (snapshot-canonical? source)) + (not (snapshot-canonical? draft)) + (not (equal? (.ref derivation 'source-snapshot-digest) + (.ref source 'digest))) + (not (equal? (.ref derivation 'target-snapshot-digest) + (.ref draft 'digest))) + (not (equal? (.ref derivation 'digest) (.ref expected 'digest)))) + 'derivation-mismatch) + ((not requirement) 'source-requirement-missing) + ((not (memq (.ref requirement 'blocker) '(#f claim-changed))) + 'source-requirement-blocked) + ((or (pair? (.ref draft 'unresolved)) (pair? (.ref draft 'conflicts))) + 'target-frontier) + ((not (assurance-obligation? candidate)) 'candidate-missing) + ((node-by-identity (.ref source 'nodes) candidate-id) + 'candidate-not-fresh) + ((or (not (equal? (.ref candidate 'snapshot) (.ref draft 'identity))) + (.ref candidate 'snapshot-revision) + (.ref candidate 'snapshot-context-digest)) + 'candidate-not-unbound) + ((not (eq? (.ref candidate 'state) 'unknown)) 'candidate-not-unknown) + ((not (equal? (.ref candidate 'claim) (.ref requirement 'claim))) + 'candidate-claim-mismatch) + ((not (assurance-claim? claim)) 'candidate-claim-mismatch) + ((not (equal? (.ref candidate 'subject) (.ref claim 'subject))) + 'candidate-subject-mismatch) + ((not (equal? (.ref candidate 'scope) (.ref claim 'scope))) + 'candidate-scope-mismatch) + ((not (member candidate-id (.ref claim 'support-requirements))) + 'claim-not-declared) + ((null? support-links) 'support-link-missing) + ((not (find (lambda (relation) + (memq (.ref relation 'modality) + '(observed declared derived))) + support-links)) + 'support-link-hypothetical) + (else #f)))) + +(def (assurance-declare-replacement-obligation declaration-identity source draft derivation + source-id candidate-id) + (unless (and (assurance-text? declaration-identity) + (assurance-text? source-id) (assurance-text? candidate-id) + (assurance-snapshot? source) (assurance-snapshot? draft) + (assurance-replacement-derivation? derivation)) + (error "invalid replacement declaration input")) + (let* ((declaration-blocker + (replacement-declaration-blocker + derivation source draft source-id candidate-id)) + (final-snapshot + (and (not declaration-blocker) + (let* ((candidate + (node-by-identity (.ref draft 'nodes) candidate-id)) + (bound + (assurance-bind-obligation-to-snapshot candidate draft))) + (assurance-snapshot + (.ref draft 'identity) (.ref draft 'revision) + (.ref draft 'graph-identity) + (.ref draft 'source-revisions) (.ref draft 'claim-revisions) + (.ref draft 'fact-cut) (.ref draft 'policy-identity) + (.ref draft 'policy-revision) + (map (lambda (node) + (if (equal? (.ref node 'identity) candidate-id) + bound node)) + (.ref draft 'nodes)) + (.ref draft 'relations) + evidence-identities: (.ref draft 'evidence-identities) + unresolved: (.ref draft 'unresolved))))) + (final-digest (and final-snapshot (.ref final-snapshot 'digest))) + (receipt-digest + (assurance-canonical-digest + (list "lambda-aitia.replacement-declaration" declaration-identity + (.ref derivation 'digest) (.ref source 'digest) + (.ref draft 'digest) final-digest + source-id candidate-id declaration-blocker))) + (receipt + (poo-clos-check-model + AssuranceReplacementDeclarationReceipt + (.o (:: @ (poo-clos-model-prototype + AssuranceReplacementDeclarationReceipt)) + schema: "lambda-aitia.replacement-declaration" + identity: declaration-identity + derivation-digest: (.ref derivation 'digest) + source-snapshot-digest: (.ref source 'digest) + draft-snapshot-digest: (.ref draft 'digest) + final-snapshot-digest: final-digest + source-obligation: source-id target-obligation: candidate-id + blocker: declaration-blocker digest: receipt-digest + verifier-executed?: #f release-authorized?: #f)))) + (when (and final-snapshot + (not (equal? (.ref final-snapshot 'context-digest) + (.ref draft 'context-digest)))) + (error "replacement binding changed snapshot context")) + (values final-snapshot receipt))) + +;;; A structural 'composes edge declares a composite claim's components. +;;; Only an independent, explicitly linked and current obligation can answer +;;; its composition question; this receipt never discharges that obligation. +(def (composition-component-ids snapshot composition-id) + (ordered-unique-text + (map (lambda (relation) (.ref relation 'target)) + (filter (lambda (relation) + (and (eq? (.ref relation 'relation) 'composes) + (equal? (.ref relation 'source) composition-id))) + (.ref snapshot 'relations))))) + +(def (composition-direct-obligations snapshot composition-id claim-node) + (if (not (assurance-claim? claim-node)) + '() + (ordered-unique-text + (map + (lambda (obligation) (.ref obligation 'identity)) + (filter + (lambda (obligation) + (and (assurance-obligation? obligation) + (equal? (.ref obligation 'claim) composition-id) + (equal? (.ref obligation 'subject) + (.ref claim-node 'subject)) + (equal? (.ref obligation 'scope) + (.ref claim-node 'scope)) + (member (.ref obligation 'identity) + (.ref claim-node 'support-requirements)) + (find + (lambda (relation) + (and (eq? (.ref relation 'relation) 'supports) + (eq? (.ref relation 'plane) 'assurance) + (memq (.ref relation 'modality) + '(declared observed derived)) + (equal? (.ref relation 'source) + (.ref obligation 'identity)) + (equal? (.ref relation 'target) composition-id))) + (.ref snapshot 'relations)))) + (.ref snapshot 'nodes)))))) + +(def (composition-current-obligation? snapshot obligation-id) + (let ((obligation (node-by-identity (.ref snapshot 'nodes) obligation-id))) + (and (assurance-obligation? obligation) + (equal? (.ref obligation 'snapshot) (.ref snapshot 'identity)) + (equal? (.ref obligation 'snapshot-revision) + (.ref snapshot 'revision)) + (equal? (.ref obligation 'snapshot-context-digest) + (.ref snapshot 'context-digest))))) + +(def (composition-requirement-blocker snapshot composition-claim + component-ids direct-ids current-ids) + (cond + ((pair? (.ref snapshot 'conflicts)) 'conflicted-snapshot) + ((pair? (.ref snapshot 'unresolved)) 'unresolved-frontier) + ((not (assurance-claim? composition-claim)) + 'invalid-composition-claim) + ((< (length component-ids) 2) 'insufficient-components) + ((find (lambda (component-id) + (not (assurance-claim? + (node-by-identity (.ref snapshot 'nodes) component-id)))) + component-ids) + 'invalid-component-claim) + ((pair? current-ids) #f) + ((pair? direct-ids) 'stale-direct-obligation) + (else 'missing-direct-obligation))) + +(def (assurance-derive-composition-requirements derivation-identity snapshot) + (unless (and (assurance-text? derivation-identity) + (assurance-snapshot? snapshot)) + (error "invalid composition derivation input")) + (let* ((composition-ids + (ordered-unique-text + (map (lambda (relation) (.ref relation 'source)) + (filter + (lambda (relation) + (eq? (.ref relation 'relation) 'composes)) + (.ref snapshot 'relations))))) + (derived-requirements + (map + (lambda (composition-id) + (let* ((composition-claim + (node-by-identity (.ref snapshot 'nodes) + composition-id)) + (component-ids + (composition-component-ids snapshot composition-id)) + (direct-ids + (composition-direct-obligations + snapshot composition-id composition-claim)) + (current-ids + (filter + (lambda (obligation-id) + (composition-current-obligation? + snapshot obligation-id)) + direct-ids)) + (blocker-value + (composition-requirement-blocker + snapshot composition-claim component-ids + direct-ids current-ids))) + (poo-clos-check-model + AssuranceCompositionRequirement + (.o (:: @ (poo-clos-model-prototype + AssuranceCompositionRequirement)) + composition-claim: composition-id + component-claims: component-ids + direct-obligations: direct-ids + current-obligations: current-ids + blocker: blocker-value)))) + composition-ids)) + (derivation-digest + (assurance-canonical-digest + (list "lambda-aitia.composition-requirements" + derivation-identity (.ref snapshot 'digest) + (map (lambda (requirement) + (list (.ref requirement 'composition-claim) + (.ref requirement 'component-claims) + (.ref requirement 'direct-obligations) + (.ref requirement 'current-obligations) + (.ref requirement 'blocker))) + derived-requirements))))) + (poo-clos-check-model + AssuranceCompositionDerivation + (.o (:: @ (poo-clos-model-prototype AssuranceCompositionDerivation)) + schema: "lambda-aitia.composition-requirements" + identity: derivation-identity + snapshot-digest: (.ref snapshot 'digest) + requirements: derived-requirements + digest: derivation-digest + verifier-executed?: #f release-authorized?: #f)))) + +;;; Phase 3 selection boundary. This plan names only obligations already bound +;;; to the supplied snapshot. Deriving replacement revisions and invoking +;;; verifiers belong to later phases; neither can be inferred from a change. +(def (verification-base-blocker obligation snapshot capabilities + unresolved conflicts cyclic-ids + prerequisites planned-ids) + (cond + ((pair? conflicts) 'conflicted-snapshot) + ((pair? unresolved) 'unresolved-frontier) + ((member (.ref obligation 'identity) cyclic-ids) 'dependency-cycle) + ((or (not (string=? (.ref obligation 'snapshot) + (.ref snapshot 'identity))) + (not (equal? (.ref obligation 'snapshot-revision) + (.ref snapshot 'revision))) + (not (equal? (.ref obligation 'snapshot-context-digest) + (.ref snapshot 'context-digest)))) + 'stale-obligation) + ((not (memq (.ref obligation 'capability) capabilities)) + 'capability-unavailable) + ((find (lambda (id) (not (member id planned-ids))) prerequisites) + 'dependency-unplanned) + (else #f))) + +(def (assurance-plan-verification identity snapshot changed-identities policy) + (unless (and (assurance-text? identity) + (assurance-snapshot? snapshot) + (assurance-verification-policy? policy)) + (error "invalid assurance verification planning input")) + (unless (and (string=? (.ref policy 'identity) + (.ref snapshot 'policy-identity)) + (string=? (.ref policy 'revision) + (.ref snapshot 'policy-revision))) + (error "verification policy does not match the snapshot")) + (let* ((invalidation + (assurance-invalidate + (string-append identity "/invalidation") + snapshot changed-identities)) + (obligation-ids + (assurance-invalidation-receipt-ref + invalidation 'required-obligations)) + (changed + (assurance-invalidation-receipt-ref invalidation 'changed)) + (impacted + (assurance-invalidation-receipt-ref invalidation 'impacted)) + (blocked-effects + (assurance-invalidation-receipt-ref invalidation 'blocked-effects)) + (witnesses + (assurance-invalidation-receipt-ref invalidation 'witnesses)) + (unresolved + (assurance-invalidation-receipt-ref invalidation 'unresolved)) + (conflicts (.ref snapshot 'conflicts)) + (capabilities (.ref policy 'capabilities))) + (let-values (((dependency-graph prerequisites) + (assurance-verification-dependency-graph + snapshot obligation-ids))) + (let* ((cycle-path + (or (poo-flow-graph-cycle-path dependency-graph) '())) + (cyclic-components + (.ref (poo-flow-graph-loop-analysis-receipt dependency-graph) + 'cyclic-components)) + (cyclic-ids (concatenate cyclic-components)) + (cycle-blocked + (poo-flow-graph-reachable-ids dependency-graph cyclic-ids)) + (base-blockers + (map + (lambda (obligation-id) + (cons + obligation-id + (verification-base-blocker + (node-by-identity (.ref snapshot 'nodes) obligation-id) + snapshot capabilities unresolved conflicts cyclic-ids + (cdr (assoc obligation-id prerequisites)) obligation-ids))) + obligation-ids)) + (blocked-closure + (poo-flow-graph-reachable-ids + dependency-graph + (append cycle-blocked + (map car (filter (lambda (entry) (cdr entry)) + base-blockers))))) + (available-ids + (filter (lambda (id) (not (member id blocked-closure))) + obligation-ids)) + (available-order + (let-values (((available-graph _) + (assurance-verification-dependency-graph + snapshot available-ids))) + (poo-flow-graph-topological-order available-graph))) + (blocked-order + (filter (lambda (id) (member id blocked-closure)) + (if (null? cycle-path) + (poo-flow-graph-topological-order dependency-graph) + obligation-ids))) + (ordered-ids (append available-order blocked-order)) + (requests + (map + (lambda (obligation-id) + (let* ((obligation + (node-by-identity (.ref snapshot 'nodes) + obligation-id)) + (base-reason (cdr (assoc obligation-id base-blockers))) + (request-reason + (or base-reason + (and (member obligation-id blocked-closure) + 'dependency-blocked))) + (request-id obligation-id) + (request-dependencies + (cdr (assoc obligation-id prerequisites)))) + (poo-clos-check-model + AssuranceVerificationRequest + (.o (:: @ (poo-clos-model-prototype + AssuranceVerificationRequest)) + obligation-identity: request-id + subject: (.ref obligation 'subject) + claim: (.ref obligation 'claim) + snapshot-digest: (.ref snapshot 'digest) + evidence-kind: (.ref obligation 'evidence-kind) + capability: (.ref obligation 'capability) + dependencies: request-dependencies + selected?: (not request-reason) + blocker: request-reason)))) + ordered-ids)) + (blocked + (map (lambda (request) (.ref request 'obligation-identity)) + (filter (lambda (request) (not (.ref request 'selected?))) + requests))) + (selected + (map (lambda (request) (.ref request 'obligation-identity)) + (filter (lambda (request) (.ref request 'selected?)) + requests))) + (request-projection + (map (lambda (request) + (list (.ref request 'obligation-identity) + (.ref request 'subject) (.ref request 'claim) + (.ref request 'evidence-kind) + (.ref request 'capability) + (.ref request 'dependencies) + (.ref request 'selected?) (.ref request 'blocker))) + requests)) + (digest + (assurance-canonical-digest + (list "lambda-aitia.planning-result" identity + (.ref snapshot 'digest) + (.ref policy 'identity) (.ref policy 'revision) + (.ref policy 'capabilities) + changed impacted blocked-effects witnesses + cycle-path cyclic-components + selected blocked request-projection + unresolved conflicts)))) + (let ((plan-identity identity) + (plan-digest digest) + (plan-selected selected) + (plan-blocked blocked) + (plan-changed changed) + (plan-impacted impacted) + (plan-blocked-effects blocked-effects) + (plan-cycle-path cycle-path) + (plan-cyclic-components cyclic-components) + (plan-witnesses witnesses) + (plan-requests requests) + (plan-unresolved unresolved) + (plan-conflicts conflicts)) + (poo-clos-check-model + AssuranceVerificationPlan + (.o (:: @ (poo-clos-model-prototype AssuranceVerificationPlan)) + schema: "lambda-aitia.planning-result" + identity: plan-identity snapshot-digest: (.ref snapshot 'digest) + policy-identity: (.ref policy 'identity) + policy-revision: (.ref policy 'revision) + digest: plan-digest + selected-obligations: plan-selected + blocked-obligations: plan-blocked + changed: plan-changed impacted: plan-impacted + blocked-effects: plan-blocked-effects + cycle-path: plan-cycle-path + cyclic-components: plan-cyclic-components + witnesses: plan-witnesses + requests: plan-requests + unresolved: plan-unresolved conflicts: plan-conflicts + verifier-executed?: #f release-authorized?: #f + runtime-executed?: #f))))))) + +;;; Candidate selection is a pure explanation over explicit declarations. +;;; It does not establish that an adapter is installed, runnable or trusted. +(def (verifier-candidate-matches? candidate request) + (and (memq (.ref request 'evidence-kind) + (.ref candidate 'evidence-kinds)) + (eq? (.ref request 'capability) (.ref candidate 'capability)))) + +(def (verifier-choice-reason candidate request selected-candidate) + (cond + ((not (.ref request 'selected?)) 'request-blocked) + ((not (memq (.ref request 'evidence-kind) + (.ref candidate 'evidence-kinds))) + 'evidence-kind-unsupported) + ((not (eq? (.ref request 'capability) + (.ref candidate 'capability))) + 'capability-mismatch) + ((and selected-candidate + (equal? (.ref candidate 'identity) + (.ref selected-candidate 'identity))) + 'chosen) + (else 'lower-priority))) + +(def (assurance-explain-verifier-choices explanation-identity plan candidates) + (unless (and (assurance-text? explanation-identity) + (assurance-verification-plan? plan) + (list? candidates) + (every assurance-verifier-candidate? candidates)) + (error "invalid verifier choice input")) + (let* ((ordered-candidates + (list-sort + (lambda (left right) + (or (< (.ref left 'priority) (.ref right 'priority)) + (and (= (.ref left 'priority) (.ref right 'priority)) + (string= now 0)) + (error "assurance Host clock must return a nonnegative instant")) + (when (< now (vector-ref entry 4)) + (error "assurance Host clock moved backwards")) + (vector-set! entry 4 now) + now)) + +(def (invalidate-host-cut! entry) + ;; A weak receipt index avoids retaining every old seal for the Host's life. + ;; The epoch blocks all prior seals at this Host even if the old cut returns. + (vector-set! entry 8 (+ (vector-ref entry 8) 1))) + +(def (host-current-snapshot entry) + (let (snapshot ((vector-ref entry 5))) + (unless (and (assurance-snapshot? snapshot) + (assurance-snapshot-canonical? snapshot)) + (invalidate-host-cut! entry) + (error "assurance Host snapshot source returned a noncanonical snapshot")) + (let ((current-digest (assurance-snapshot-semantic-digest snapshot)) + (previous-digest (vector-ref entry 7))) + (when (and previous-digest + (not (equal? previous-digest current-digest))) + (invalidate-host-cut! entry)) + (vector-set! entry 7 (string-copy current-digest))) + snapshot)) + +(def (host-snapshot-current? entry snapshot obligation evidence outcome) + (equal? + (assurance-verification-subject-snapshot + (assurance-verification-subject snapshot obligation evidence outcome)) + (assurance-verification-subject-snapshot + (assurance-verification-subject + (host-current-snapshot entry) obligation evidence outcome)))) + +(def (assurance-verification-host + identity-value operation clock lease snapshot-source + source-reader: (source-reader #f)) + (unless (and (string? identity-value) + (> (string-length identity-value) 0) + (procedure? operation) (procedure? clock) + (exact-integer? lease) (> lease 0) + (procedure? snapshot-source) + (or (not source-reader) (procedure? source-reader))) + (error "invalid assurance verification Host configuration")) + (let* ((host (.o identity: (string-copy identity-value))) + (adapter + (poo-flow-verification-adapter + identity-value + (lambda (subject now until) + (if source-reader + (let (inputs + (host-materialized-inputs + host (.ref subject 'snapshot) (.ref subject 'outcome))) + (and inputs + (let (expected + (map (lambda (input) + (vector (string-copy (.ref input 'identity)) + (string-copy (.ref input 'revision)) + (string-copy (.ref input 'digest)))) + inputs)) + (and (eq? (operation subject now until inputs) #t) + (host-handed-inputs-current? inputs expected))))) + (operation subject now until))) + assurance-verification-subject-snapshot))) + (hash-put! issued-hosts host + (vector (string-copy identity-value) adapter clock lease -1 + snapshot-source 0 #f 0 source-reader)) + host)) + +;;; An eligible report alone is never an issued seal. The configured operation +;;; must independently return #t over the complete frozen semantic subject. +(def (assurance-host-verify host snapshot obligation evidence outcome) + (let* ((entry (required-host-entry host)) + (eligibility + (assurance-evaluate-evidence-admission snapshot obligation outcome))) + (and (.ref eligibility 'eligible?) + (assurance-evidence-outcome-bound? snapshot evidence outcome) + (host-snapshot-current? entry snapshot obligation evidence outcome) + (host-outcome-inputs-current? host snapshot outcome) + (let (now (host-instant entry)) + (let (receipt + (poo-flow-verify + (vector-ref entry 1) + (assurance-verification-subject + snapshot obligation evidence outcome) + now (+ now (vector-ref entry 3)))) + (and receipt + (if (host-outcome-inputs-current? host snapshot outcome) + (begin + (hash-put! issued-host-receipts receipt + (vector host (vector-ref entry 8))) + receipt) + (begin + (poo-flow-revoke-verification! (vector-ref entry 1) + receipt) + #f)))))))) + +;;; This checks a Host-issued seal; it does not promote evidence or grant an effect. +(def (assurance-host-sealed-support? + host relation evidence obligation snapshot outcome receipt) + (let (entry (required-host-entry host)) + (let (issued (hash-get issued-host-receipts receipt)) + (and issued (eq? host (vector-ref issued 0)) + (host-snapshot-current? entry snapshot obligation evidence outcome) + (host-outcome-inputs-current? host snapshot outcome) + (= (vector-ref issued 1) (vector-ref entry 8)) + (assurance-support-sealed-under-adapter? + relation evidence obligation snapshot outcome + (vector-ref entry 1) receipt (host-instant entry)))))) + +(def (assurance-host-revoke! host receipt) + (let* ((entry (required-host-entry host)) + (issued (hash-get issued-host-receipts receipt))) + (unless (and issued (eq? host (vector-ref issued 0))) + (error "receipt was not issued by this assurance Host")) + (hash-remove! issued-host-receipts receipt) + (poo-flow-revoke-verification! (vector-ref entry 1) receipt))) + +;;; Admission is a separate Host-issued capability over the candidate cut. +;;; It does not mutate AssuranceEvidence or the snapshot that the outcome names. +(def (assurance-host-admit host obligation evidence outcome) + (let* ((entry (required-host-entry host)) + (snapshot (host-current-snapshot entry))) + (and (assurance-evidence-outcome-bound? snapshot evidence outcome) + (eq? (.ref evidence 'admission-state) 'candidate) + (let (receipt (assurance-host-verify host snapshot obligation evidence outcome)) + (and receipt + (assurance-host-admit-verified host obligation evidence outcome receipt)))))) + +;;; Verify and Admit can be separate lifecycle stages. Admission consumes an +;;; exact current Host-issued seal; it must not execute a verifier again or +;;; refresh an expired seal from a submitted success report. +(def (assurance-host-verification-current? host receipt snapshot obligation evidence outcome) + (let* ((entry (required-host-entry host)) + (issued (hash-get issued-host-receipts receipt))) + (and issued (eq? host (vector-ref issued 0)) + (host-snapshot-current? entry snapshot obligation evidence outcome) + (= (vector-ref issued 1) (vector-ref entry 8)) + (host-outcome-inputs-current? host snapshot outcome) + (assurance-evidence-outcome-bound? snapshot evidence outcome) + (poo-flow-verification-valid? + (vector-ref entry 1) receipt + (assurance-verification-subject snapshot obligation evidence outcome) + (host-instant entry)) + (= (vector-ref issued 1) (vector-ref entry 8))))) + +(def (assurance-host-admit-verified host obligation evidence outcome receipt) + (let* ((entry (required-host-entry host)) + (snapshot (host-current-snapshot entry))) + (and (assurance-evidence-outcome-bound? snapshot evidence outcome) + (eq? (.ref evidence 'admission-state) 'candidate) + (assurance-host-verification-current? + host receipt snapshot obligation evidence outcome) + (if (host-snapshot-current? entry snapshot obligation evidence outcome) + (let* ((next (+ (vector-ref entry 6) 1)) + (identity-value + (string-append (vector-ref entry 0) "/admission/" (number->string next))) + (subject-digest-value + (assurance-verification-subject-snapshot + (assurance-verification-subject snapshot obligation evidence outcome))) + (issued-at-value (.ref receipt 'issued-at)) + (expires-at-value (.ref receipt 'expires-at)) + (admission + (.o identity: identity-value issuer: (vector-ref entry 0) + issued-at: issued-at-value expires-at: expires-at-value + subject-digest: subject-digest-value))) + (vector-set! entry 6 next) + (hash-put! issued-admissions admission + (vector host receipt identity-value (vector-ref entry 0) + issued-at-value expires-at-value subject-digest-value + obligation evidence outcome (vector-ref entry 8))) + admission) + (begin (assurance-host-revoke! host receipt) #f))))) + +(def (admission-entry host admission) + (let (entry (hash-get issued-admissions admission)) + (and entry (eq? host (vector-ref entry 0)) + (object? admission) + (.slot? admission 'identity) (.slot? admission 'issuer) + (.slot? admission 'issued-at) (.slot? admission 'expires-at) + (.slot? admission 'subject-digest) + (equal? (.ref admission 'identity) (vector-ref entry 2)) + (equal? (.ref admission 'issuer) (vector-ref entry 3)) + (equal? (.ref admission 'issued-at) (vector-ref entry 4)) + (equal? (.ref admission 'expires-at) (vector-ref entry 5)) + (equal? (.ref admission 'subject-digest) (vector-ref entry 6)) + entry))) + +(def (current-admission-snapshot host-state issued) + (and issued + (let* ((now (host-instant host-state)) + (snapshot (host-current-snapshot host-state)) + (obligation (vector-ref issued 7)) + (evidence (vector-ref issued 8)) + (outcome (vector-ref issued 9)) + (eligibility + (assurance-evaluate-evidence-admission + snapshot obligation outcome)) + (subject + (assurance-verification-subject + snapshot obligation evidence outcome))) + (and (= (vector-ref issued 10) (vector-ref host-state 8)) + (<= (vector-ref issued 4) now) + (< now (vector-ref issued 5)) + (.ref eligibility 'eligible?) + (assurance-evidence-outcome-bound? + snapshot evidence outcome) + (host-outcome-inputs-current? + (vector-ref issued 0) snapshot outcome) + (eq? (.ref evidence 'admission-state) 'candidate) + (equal? (assurance-verification-subject-snapshot subject) + (vector-ref issued 6)) + (poo-flow-verification-valid? + (vector-ref host-state 1) (vector-ref issued 1) + subject now) + snapshot)))) + +(def (assurance-host-admission-current? host admission) + (let (host-state (required-host-entry host)) + (and (current-admission-snapshot + host-state (admission-entry host admission)) #t))) + +;;; Consume an exact Host admission as one current assurance-edge witness. +;;; This does not close the claim's full support set or authorize a Decision. +(def (assurance-host-admitted-support? host relation admission) + (let* ((host-state (required-host-entry host)) + (issued (admission-entry host admission))) + (and issued + (let (snapshot (current-admission-snapshot host-state issued)) + (and snapshot + (assurance-candidate-support-structural? + relation (vector-ref issued 8) (vector-ref issued 7) + snapshot)))))) + +(def (snapshot-node snapshot identity) + (find (lambda (node) (equal? (.ref node 'identity) identity)) + (.ref snapshot 'nodes))) + +;;; Source bytes come only from the Host's configured reader. This is a +;;; conditional application trust boundary, not a claim about Git provenance +;;; or the verifier's actual process inputs. Re-read and recheck the cut after +;;; materialization so a moving snapshot cannot qualify an old source. +(def (host-source-payload host snapshot artifact) + (let* ((entry (required-host-entry host)) + (reader (vector-ref entry 9))) + (and reader + (assurance-snapshot? snapshot) + (assurance-snapshot-canonical? snapshot) + (assurance-artifact? artifact) + (let* ((current (host-current-snapshot entry)) + (expected-cut (assurance-snapshot-semantic-digest snapshot)) + (current-artifact + (snapshot-node current (.ref artifact 'identity))) + (revision (assoc (.ref artifact 'identity) + (.ref current 'source-revisions)))) + (and (equal? expected-cut + (assurance-snapshot-semantic-digest current)) + current-artifact + (assurance-artifact? current-artifact) + (equal? (assurance-node-canonical current-artifact) + (assurance-node-canonical artifact)) + revision + (equal? (cdr revision) (.ref artifact 'revision)) + (eq? (.ref artifact 'state) 'supported) + (let (payload (reader (.ref artifact 'identity))) + (let (matches? + (and (or (string? payload) (u8vector? payload)) + (equal? (source-lock-payload-digest payload) + (.ref artifact 'content-digest)))) + (unless matches? (invalidate-host-cut! entry)) + (and matches? + (equal? expected-cut + (assurance-snapshot-semantic-digest + (host-current-snapshot entry))) + payload)))))))) + +(def (assurance-host-source-current? host snapshot artifact) + (and (host-source-payload host snapshot artifact) #t)) + +;;; The configured operation receives these POO values as its fourth argument +;;; when source-reader: is present. The Host, not the operation, selects and +;;; validates the bytes. It rehashes the handed values after the operation. +(def (host-materialized-inputs host snapshot outcome) + (let loop ((remaining (.ref outcome 'inputs)) (collected '())) + (if (null? remaining) + (reverse collected) + (let* ((input (car remaining)) + (artifact (snapshot-node snapshot (.ref input 'identity))) + (payload-value (and artifact + (host-source-payload host snapshot artifact))) + (handed-payload + (and payload-value + (if (string? payload-value) + (string-copy payload-value) + (u8vector-copy payload-value))))) + (and payload-value + (loop (cdr remaining) + (cons (.o identity: (.ref input 'identity) + revision: (.ref input 'revision) + digest: (.ref input 'digest) + payload: handed-payload) + collected))))))) + +(def (host-handed-inputs-current? inputs expected) + (let loop ((remaining inputs) (original expected)) + (cond + ((null? remaining) (null? original)) + ((null? original) #f) + (else + (let ((input (car remaining)) (fields (car original))) + (and (equal? (.ref input 'identity) (vector-ref fields 0)) + (equal? (.ref input 'revision) (vector-ref fields 1)) + (equal? (.ref input 'digest) (vector-ref fields 2)) + (let (payload-value (.ref input 'payload)) + (and (or (string? payload-value) (u8vector? payload-value)) + (equal? (source-lock-payload-digest payload-value) + (vector-ref fields 2)))) + (loop (cdr remaining) (cdr original)))))))) + +;;; An optional Host source reader strengthens issuance and currentness for +;;; every declared verifier input. With no configured reader the old Host +;;; remains a verification Host, but it cannot claim source-byte checking. +(def (host-outcome-inputs-current? host snapshot outcome) + (let (entry (required-host-entry host)) + (or (not (vector-ref entry 9)) + (every (lambda (input) + (let (artifact (snapshot-node snapshot + (.ref input 'identity))) + (and artifact + (assurance-host-source-current? + host snapshot artifact)))) + (.ref outcome 'inputs))))) + +(def (required-support-link? snapshot obligation claim) + (find (lambda (relation) + (and (eq? (.ref relation 'plane) 'assurance) + (eq? (.ref relation 'relation) 'supports) + (equal? (.ref relation 'source) (.ref obligation 'identity)) + (equal? (.ref relation 'target) (.ref claim 'identity)) + (not (memq (.ref relation 'modality) + '(hypothesized counterfactual))))) + (.ref snapshot 'relations))) + +(def (admitted-requirement-blocker host host-state snapshot obligation admissions) + (let* ((matching + (filter (lambda (admission) + (let (issued (admission-entry host admission)) + (and issued + (equal? (.ref (vector-ref issued 7) 'identity) + (.ref obligation 'identity))))) + admissions))) + (if (null? matching) + 'admission-missing + (let loop ((remaining matching) (saw-current? #f)) + (if (null? remaining) + (if saw-current? 'evidence-link-missing 'admission-invalid) + (let* ((issued (admission-entry host (car remaining))) + (current (current-admission-snapshot host-state issued))) + (if (and current + (equal? (assurance-snapshot-semantic-digest current) + (assurance-snapshot-semantic-digest snapshot))) + (if (find (lambda (relation) + (assurance-candidate-support-structural? + relation (vector-ref issued 8) obligation snapshot)) + (.ref snapshot 'relations)) + #f + (loop (cdr remaining) #t)) + (loop (cdr remaining) saw-current?)))))))) + +(def (required-support-row host host-state snapshot claim-value identity admissions) + (let* ((obligation (snapshot-node snapshot identity)) + (reason + (cond + ((not (and obligation (assurance-obligation? obligation))) + 'obligation-missing) + ((or (not (equal? (.ref obligation 'claim) (.ref claim-value 'identity))) + (not (equal? (.ref obligation 'subject) (.ref claim-value 'subject))) + (not (equal? (.ref obligation 'scope) (.ref claim-value 'scope))) + (not (equal? (.ref obligation 'snapshot) + (.ref snapshot 'identity))) + (not (equal? (.ref obligation 'snapshot-revision) + (.ref snapshot 'revision))) + (not (equal? (.ref obligation 'snapshot-context-digest) + (.ref snapshot 'context-digest))) + (memq (.ref obligation 'state) + '(violated stale conflicted hypothesized counterfactual + not-applicable))) + 'obligation-stale) + ((not (required-support-link? snapshot obligation claim-value)) + 'support-link-missing) + (else + (admitted-requirement-blocker + host host-state snapshot obligation admissions))))) + (poo-clos-check-model + AssessmentRequiredSupport + (.o (:: @ (poo-clos-model-prototype AssessmentRequiredSupport)) + obligation: identity supported?: (not reason) blocker: reason)))) + +;;; Inert, explainable input for a later Decision evaluator. Completion means +;;; only that every declared support requirement has a current Host witness; +;;; it does not resolve policy, validity intervals or Runtime effect authority. +(def (assurance-host-required-support host claim-value admissions) + (unless (and (assurance-claim? claim-value) (list? admissions)) + (error "invalid required-support inputs")) + (let* ((host-state (required-host-entry host)) + (snapshot (host-current-snapshot host-state)) + (initial-epoch (vector-ref host-state 8)) + (current-claim (snapshot-node snapshot (.ref claim-value 'identity))) + (required-identities (.ref claim-value 'support-requirements)) + (rows + (map (lambda (identity) + (required-support-row + host host-state snapshot claim-value identity admissions)) + required-identities)) + (global-blockers + (append + (if (pair? (.ref snapshot 'conflicts)) '(snapshot-conflicted) '()) + (if (pair? (.ref snapshot 'unresolved)) '(snapshot-unresolved) '()) + (if (and current-claim (assurance-claim? current-claim) + (equal? (assurance-node-canonical claim-value) + (assurance-node-canonical current-claim)) + (member (cons (.ref claim-value 'identity) + (.ref claim-value 'revision)) + (.ref snapshot 'claim-revisions))) + '() '(claim-not-current)) + (if (null? required-identities) '(no-requirements) '()) + (if (= (length required-identities) + (length (delete-duplicates/hash required-identities))) + '() '(duplicate-requirements)) + (if (pair? (.ref claim-value 'assumptions)) + '(assumptions-unresolved) '()) + (if (pair? (.ref claim-value 'defeaters)) + '(defeaters-unresolved) '()) + (if (memq (.ref claim-value 'state) + '(violated stale conflicted hypothesized counterfactual + not-applicable)) + '(claim-state-blocked) '()) + (if (find (lambda (node) + (and (memq (.ref node 'kind) + '(counterexample finding)) + (equal? (.ref node 'challenges) + (.ref claim-value 'identity)))) + (.ref snapshot 'nodes)) + '(challenge-unresolved) '()))) + (final-snapshot (host-current-snapshot host-state)) + (final-blockers + (append global-blockers + (if (and (= initial-epoch (vector-ref host-state 8)) + (equal? + (assurance-snapshot-semantic-digest snapshot) + (assurance-snapshot-semantic-digest final-snapshot))) + '() '(source-changed))))) + (poo-clos-check-model + AssuranceRequiredSupport + (.o (:: @ (poo-clos-model-prototype AssuranceRequiredSupport)) + schema: "lambda-aitia.required-support" + claim: (.ref claim-value 'identity) + snapshot-digest: (.ref snapshot 'digest) + requirements: rows blockers: final-blockers + complete?: (and (null? final-blockers) + (pair? rows) + (every (lambda (row) (.ref row 'supported?)) rows)) + release-authorized?: #f)))) + +(def (decision-depends-on-claim? snapshot decision-value claim-value) + (find (lambda (relation) + (and (eq? (.ref relation 'plane) 'structural) + (eq? (.ref relation 'relation) 'depends-on) + (equal? (.ref relation 'source) + (.ref decision-value 'identity)) + (equal? (.ref relation 'target) + (.ref claim-value 'identity)) + (not (memq (.ref relation 'modality) + '(hypothesized counterfactual))))) + (.ref snapshot 'relations))) + +(def (decision-preflight-bound? snapshot decision-value claim-value) + (and (assurance-decision? decision-value) + (assurance-claim? claim-value) + (let ((current-decision + (snapshot-node snapshot (.ref decision-value 'identity))) + (current-claim + (snapshot-node snapshot (.ref claim-value 'identity)))) + (and current-decision (assurance-decision? current-decision) + current-claim (assurance-claim? current-claim) + (equal? (assurance-node-canonical current-decision) + (assurance-node-canonical decision-value)) + (equal? (assurance-node-canonical current-claim) + (assurance-node-canonical claim-value)))) + (eq? (.ref decision-value 'outcome) 'unknown) + (eq? (.ref decision-value 'state) 'unknown) + (equal? (.ref decision-value 'subject) + (.ref claim-value 'subject)) + (equal? (.ref decision-value 'snapshot) + (.ref snapshot 'identity)) + (equal? (.ref decision-value 'policy) + (.ref snapshot 'policy-identity)) + (decision-depends-on-claim? snapshot decision-value claim-value))) + +(def (same-host-cut? host-state snapshot-value epoch-value) + (and (= epoch-value (vector-ref host-state 8)) + (equal? (assurance-snapshot-semantic-digest snapshot-value) + (assurance-snapshot-semantic-digest + (host-current-snapshot host-state))) + (= epoch-value (vector-ref host-state 8)))) + +;;; This capability attests a currently bound *undecided* Decision candidate. +;;; It cannot be projected as Cedar permit or Runtime effect authorization. +(def (assurance-host-preflight-decision + host decision-value claim-value admissions) + (unless (list? admissions) + (error "Decision preflight requires an admission list")) + (let* ((host-state (required-host-entry host)) + (snapshot-value (host-current-snapshot host-state)) + (epoch-value (vector-ref host-state 8))) + (and (decision-preflight-bound? + snapshot-value decision-value claim-value) + (.ref (assurance-host-required-support + host claim-value admissions) + 'complete?) + (same-host-cut? host-state snapshot-value epoch-value) + (let* ((next (+ (vector-ref host-state 6) 1)) + (identity-value + (string-append (vector-ref host-state 0) "/decision-preflight/" + (number->string next))) + (issuer-value (vector-ref host-state 0)) + (decision-identity-value (.ref decision-value 'identity)) + (claim-identity-value (.ref claim-value 'identity)) + (snapshot-digest-value (.ref snapshot-value 'digest)) + (semantic-digest-value + (assurance-snapshot-semantic-digest snapshot-value)) + (preflight + (poo-clos-check-model + AssuranceDecisionPreflight + (.o (:: @ (poo-clos-model-prototype + AssuranceDecisionPreflight)) + schema: "lambda-aitia.decision-preflight" + identity: identity-value issuer: issuer-value + decision: decision-identity-value + claim: claim-identity-value + snapshot-digest: snapshot-digest-value + release-authorized?: #f)))) + (vector-set! host-state 6 next) + (hash-put! issued-decision-preflights preflight + (vector host identity-value issuer-value + decision-identity-value claim-identity-value + snapshot-digest-value semantic-digest-value + epoch-value decision-value claim-value + admissions)) + preflight)))) + +(def (decision-preflight-entry host preflight) + (let (issued (hash-get issued-decision-preflights preflight)) + (and issued (eq? host (vector-ref issued 0)) + (assurance-decision-preflight? preflight) + (equal? (.ref preflight 'identity) (vector-ref issued 1)) + (equal? (.ref preflight 'issuer) (vector-ref issued 2)) + (equal? (.ref preflight 'decision) (vector-ref issued 3)) + (equal? (.ref preflight 'claim) (vector-ref issued 4)) + (equal? (.ref preflight 'snapshot-digest) (vector-ref issued 5)) + issued))) + +(def (assurance-host-decision-preflight-current? host preflight) + (let* ((host-state (required-host-entry host)) + (issued (decision-preflight-entry host preflight))) + (and issued + (= (vector-ref issued 7) (vector-ref host-state 8)) + (let (snapshot-value (host-current-snapshot host-state)) + (and (= (vector-ref issued 7) (vector-ref host-state 8)) + (equal? (.ref snapshot-value 'digest) + (vector-ref issued 5)) + (equal? (assurance-snapshot-semantic-digest snapshot-value) + (vector-ref issued 6)) + (decision-preflight-bound? + snapshot-value (vector-ref issued 8) (vector-ref issued 9)) + (.ref (assurance-host-required-support + host (vector-ref issued 9) (vector-ref issued 10)) + 'complete?) + (same-host-cut? + host-state snapshot-value (vector-ref issued 7))))))) + +;;; Project a current preflight into POO Flow's inert Cedar request family. +;;; The request is editable input, not a Host seal, Cedar outcome or effect grant. +;;; The Runtime Host must independently validate the current source cut at use. +(def (cedar-request-fingerprint request) + ;; Freeze a value digest, not aliases to caller-visible mutable strings. + (assurance-canonical-digest + (list (.ref request 'principal) (.ref request 'action) + (.ref request 'resource) (.alist (.ref request 'context)) + (.ref request 'intent) (.alist (.ref request 'handoff))))) + +(def (assurance-host-cedar-request host preflight action-value intent-value + handoff-value) + (unless (poo-flow-cedar-runtime-handoff? handoff-value) + (error "Decision Cedar projection requires a POO Flow runtime handoff")) + (let* ((host-state (required-host-entry host)) + (issued (decision-preflight-entry host preflight))) + (and issued + (assurance-host-decision-preflight-current? host preflight) + (let* ((snapshot-value (host-current-snapshot host-state)) + (epoch-value (vector-ref host-state 8)) + (decision-value (vector-ref issued 8)) + (claim-value (vector-ref issued 9)) + (context-value + (.o aitia_schema: "lambda-aitia.cedar-preflight.v1" + poo_flow_preflight_only: #t + aitia_preflight: (vector-ref issued 1) + aitia_decision: (vector-ref issued 3) + aitia_claim: (vector-ref issued 4) + aitia_snapshot_digest: (vector-ref issued 5) + aitia_snapshot_semantic_digest: (vector-ref issued 6) + aitia_policy: (.ref decision-value 'policy) + aitia_policy_revision: (.ref snapshot-value 'policy-revision) + aitia_preflight_only: #t)) + (request + (poo-flow-cedar-authorization-request + (.ref decision-value 'authority) action-value + (.ref claim-value 'subject) context-value intent-value + handoff-value))) + (and (= epoch-value (vector-ref issued 7)) + (same-host-cut? host-state snapshot-value epoch-value) + (assurance-host-decision-preflight-current? host preflight) + (begin + (hash-put! issued-cedar-requests request + (vector host preflight epoch-value + (cedar-request-fingerprint request))) + request)))))) + +;;; Source-owned use-time check for the exact Host-issued request. This does +;;; not evaluate Cedar or permit an effect. A future Runtime integration must +;;; call it at use and retain authorization and effect-handoff ownership. +(def (assurance-host-cedar-request-current? host request) + (let* ((host-state (required-host-entry host)) + (issued (hash-get issued-cedar-requests request))) + (and issued + (eq? host (vector-ref issued 0)) + (poo-flow-cedar-authorization-request? request) + (object? (.ref request 'context)) + (poo-flow-cedar-runtime-handoff? (.ref request 'handoff)) + (= (vector-ref issued 2) (vector-ref host-state 8)) + (equal? (cedar-request-fingerprint request) (vector-ref issued 3)) + (assurance-host-decision-preflight-current? + host (vector-ref issued 1)) + (= (vector-ref issued 2) (vector-ref host-state 8))))) + +(def (assurance-host-revoke-decision-preflight! host preflight) + (required-host-entry host) + (unless (decision-preflight-entry host preflight) + (error "unissued or modified assurance Decision preflight")) + (hash-remove! issued-decision-preflights preflight)) + +(def (assurance-host-revoke-admission! host admission) + (required-host-entry host) + (let (issued (admission-entry host admission)) + (unless issued (error "unissued or modified assurance admission")) + (assurance-host-revoke! host (vector-ref issued 1)) + (hash-remove! issued-admissions admission))) diff --git a/modules/assurance/interface.ss b/modules/assurance/interface.ss index cfe8620..286ed2d 100644 --- a/modules/assurance/interface.ss +++ b/modules/assurance/interface.ss @@ -9,5 +9,11 @@ (export (import: :poo-flow/lambda-aitia/modules/assurance/objects)) (import :poo-flow/lambda-aitia/modules/assurance/funs) (export (import: :poo-flow/lambda-aitia/modules/assurance/funs)) +(import :poo-flow/lambda-aitia/modules/assurance/evidence-admission) +(export (import: :poo-flow/lambda-aitia/modules/assurance/evidence-admission)) +(import :poo-flow/lambda-aitia/modules/assurance/host) +(export (import: :poo-flow/lambda-aitia/modules/assurance/host)) +(import :poo-flow/lambda-aitia/modules/assurance/effect-host) +(export (import: :poo-flow/lambda-aitia/modules/assurance/effect-host)) (import :poo-flow/lambda-aitia/modules/assurance/config) (export (import: :poo-flow/lambda-aitia/modules/assurance/config)) diff --git a/modules/assurance/invalidation-projection.ss b/modules/assurance/invalidation-projection.ss index 8792612..b9f8091 100644 --- a/modules/assurance/invalidation-projection.ss +++ b/modules/assurance/invalidation-projection.ss @@ -12,7 +12,7 @@ poo-flow-graph-loop-analysis-receipt) (only-in :poo-flow/src/graph/types poo-flow-graph poo-flow-graph-edge poo-flow-graph-node) - (only-in :poo-flow/src/modules/temporal-causality/funs + (only-in :poo-flow/modules/temporal-causality/funs poo-flow-structural-impact-analyze) (only-in :gerbil/core list-sort) :std/list/list @@ -21,7 +21,7 @@ (export assurance-invalidation-graph assurance-invalidation-analysis) (def +assurance-invalidation-relations+ - '(depends-on implements tests refines supports discharges + '(depends-on composes implements tests refines supports discharges invalidates defeats authorizes denies requires-review)) (def (invalidation-graph-id identity) @@ -31,7 +31,7 @@ (let ((plane (.ref relation 'plane)) (kind (.ref relation 'relation))) (cond ((and (eq? plane 'structural) - (memq kind '(depends-on implements tests refines))) + (memq kind '(depends-on composes implements tests refines))) (cons (.ref relation 'target) (.ref relation 'source))) ((and (eq? plane 'assurance) (memq kind '(supports discharges))) (cons (.ref relation 'target) (.ref relation 'source))) diff --git a/modules/assurance/objects.ss b/modules/assurance/objects.ss index a3c57f8..a570743 100644 --- a/modules/assurance/objects.ss +++ b/modules/assurance/objects.ss @@ -4,20 +4,22 @@ ;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later (import (only-in :clan/poo/object .o) - :poo-flow/src/module-system/contribution/model + (only-in :gerbil/core list-sort) + :std/list/list + :poo-flow/core/poo-clos/interface :poo-flow/lambda-aitia/modules/assurance/types) (export assurance-artifact assurance-claim assurance-assumption assurance-observation assurance-event assurance-action assurance-obligation assurance-evidence assurance-counterexample assurance-finding assurance-decision assurance-effect - assurance-relation) + assurance-relation assurance-verifier-candidate) (def (assurance-artifact identity-value revision-value state-value digest-value owner: owner-value media-kind: media-kind-value provenance: (provenance-values '())) - (poo-flow-check-model AssuranceArtifact - (.o (:: @ (poo-flow-model-prototype AssuranceArtifact)) + (poo-clos-check-model AssuranceArtifact + (.o (:: @ (poo-clos-model-prototype AssuranceArtifact)) identity: identity-value kind: 'artifact revision: revision-value state: state-value content-digest: digest-value owner: owner-value media-kind: media-kind-value provenance: provenance-values))) @@ -28,8 +30,8 @@ support-requirements: (support-values '()) scope: scope-value valid-from: valid-from-value valid-until: (valid-until-value #f)) - (poo-flow-check-model AssuranceClaim - (.o (:: @ (poo-flow-model-prototype AssuranceClaim)) + (poo-clos-check-model AssuranceClaim + (.o (:: @ (poo-clos-model-prototype AssuranceClaim)) identity: identity-value kind: 'claim revision: revision-value state: state-value content-digest: digest-value subject: subject-value predicate: predicate-value assumptions: assumption-values @@ -40,8 +42,8 @@ owner: owner-value scope: scope-value review-policy: review-policy-value expires-at: (expires-at-value #f)) - (poo-flow-check-model AssuranceAssumption - (.o (:: @ (poo-flow-model-prototype AssuranceAssumption)) + (poo-clos-check-model AssuranceAssumption + (.o (:: @ (poo-clos-model-prototype AssuranceAssumption)) identity: identity-value kind: 'assumption revision: revision-value state: state-value content-digest: digest-value owner: owner-value scope: scope-value review-policy: review-policy-value @@ -50,8 +52,8 @@ subject: subject-value source: source-value clock-role: clock-role-value logical-position: logical-position-value) - (poo-flow-check-model AssuranceObservation - (.o (:: @ (poo-flow-model-prototype AssuranceObservation)) + (poo-clos-check-model AssuranceObservation + (.o (:: @ (poo-clos-model-prototype AssuranceObservation)) identity: identity-value kind: 'observation revision: revision-value state: state-value content-digest: digest-value subject: subject-value source: source-value clock-role: clock-role-value @@ -61,8 +63,8 @@ observation: observation-value payload-digest: payload-digest-value modality: modality-value commitment-state: commitment-value causal-parents: (causal-parent-values '())) - (poo-flow-check-model AssuranceEvent - (.o (:: @ (poo-flow-model-prototype AssuranceEvent)) + (poo-clos-check-model AssuranceEvent + (.o (:: @ (poo-clos-model-prototype AssuranceEvent)) identity: identity-value kind: 'event revision: revision-value state: state-value content-digest: digest-value subject: subject-value event-kind: event-kind-value observation: observation-value @@ -71,8 +73,8 @@ (def (assurance-action identity-value revision-value state-value digest-value subject: subject-value action-kind: action-kind-value requested-by: requested-by-value scope: scope-value) - (poo-flow-check-model AssuranceAction - (.o (:: @ (poo-flow-model-prototype AssuranceAction)) + (poo-clos-check-model AssuranceAction + (.o (:: @ (poo-clos-model-prototype AssuranceAction)) identity: identity-value kind: 'action revision: revision-value state: state-value content-digest: digest-value subject: subject-value action-kind: action-kind-value requested-by: requested-by-value @@ -80,12 +82,16 @@ (def (assurance-obligation identity-value revision-value state-value digest-value subject: subject-value claim: claim-value snapshot: snapshot-value evidence-kind: evidence-kind-value - capability: capability-value scope: scope-value) - (poo-flow-check-model AssuranceObligation - (.o (:: @ (poo-flow-model-prototype AssuranceObligation)) + capability: capability-value scope: scope-value + snapshot-revision: (snapshot-revision-value #f) + snapshot-context-digest: (snapshot-context-value #f)) + (poo-clos-check-model AssuranceObligation + (.o (:: @ (poo-clos-model-prototype AssuranceObligation)) identity: identity-value kind: 'obligation revision: revision-value state: state-value content-digest: digest-value subject: subject-value claim: claim-value snapshot: snapshot-value evidence-kind: evidence-kind-value + snapshot-revision: snapshot-revision-value + snapshot-context-digest: snapshot-context-value capability: capability-value scope: scope-value))) (def (assurance-evidence identity-value revision-value state-value digest-value producer: producer-value tool: tool-value @@ -95,8 +101,8 @@ scope: scope-value valid-from: valid-from-value valid-until: (valid-until-value #f) admission-state: admission-state-value) - (poo-flow-check-model AssuranceEvidence - (.o (:: @ (poo-flow-model-prototype AssuranceEvidence)) + (poo-clos-check-model AssuranceEvidence + (.o (:: @ (poo-clos-model-prototype AssuranceEvidence)) identity: identity-value kind: 'evidence revision: revision-value state: state-value content-digest: digest-value producer: producer-value tool: tool-value tool-version: tool-version-value @@ -106,8 +112,8 @@ (def (assurance-counterexample identity-value revision-value state-value digest-value subject: subject-value challenges: challenges-value evidence: evidence-value details-digest: details-digest-value) - (poo-flow-check-model AssuranceCounterexample - (.o (:: @ (poo-flow-model-prototype AssuranceCounterexample)) + (poo-clos-check-model AssuranceCounterexample + (.o (:: @ (poo-clos-model-prototype AssuranceCounterexample)) identity: identity-value kind: 'counterexample revision: revision-value state: state-value content-digest: digest-value subject: subject-value challenges: challenges-value evidence: evidence-value @@ -115,8 +121,8 @@ (def (assurance-finding identity-value revision-value state-value digest-value subject: subject-value challenges: challenges-value evidence: evidence-value finding-kind: finding-kind-value) - (poo-flow-check-model AssuranceFinding - (.o (:: @ (poo-flow-model-prototype AssuranceFinding)) + (poo-clos-check-model AssuranceFinding + (.o (:: @ (poo-clos-model-prototype AssuranceFinding)) identity: identity-value kind: 'finding revision: revision-value state: state-value content-digest: digest-value subject: subject-value challenges: challenges-value evidence: evidence-value @@ -125,8 +131,8 @@ subject: subject-value snapshot: snapshot-value policy: policy-value authority: authority-value outcome: outcome-value) - (poo-flow-check-model AssuranceDecision - (.o (:: @ (poo-flow-model-prototype AssuranceDecision)) + (poo-clos-check-model AssuranceDecision + (.o (:: @ (poo-clos-model-prototype AssuranceDecision)) identity: identity-value kind: 'decision revision: revision-value state: state-value content-digest: digest-value subject: subject-value snapshot: snapshot-value policy: policy-value authority: authority-value @@ -134,8 +140,8 @@ (def (assurance-effect identity-value revision-value state-value digest-value subject: subject-value decision: decision-value effect-kind: effect-kind-value effect-state: effect-state-value) - (poo-flow-check-model AssuranceEffect - (.o (:: @ (poo-flow-model-prototype AssuranceEffect)) + (poo-clos-check-model AssuranceEffect + (.o (:: @ (poo-clos-model-prototype AssuranceEffect)) identity: identity-value kind: 'effect revision: revision-value state: state-value content-digest: digest-value subject: subject-value decision: decision-value effect-kind: effect-kind-value @@ -149,8 +155,27 @@ (unless (eq? plane-value owned-plane) (error "assurance relation crosses its owned plane" relation-value plane-value owned-plane)) - (poo-flow-check-model + (poo-clos-check-model AssuranceRelation - (.o (:: @ (poo-flow-model-prototype AssuranceRelation)) + (.o (:: @ (poo-clos-model-prototype AssuranceRelation)) identity: identity-value plane: plane-value relation: relation-value source: source-value target: target-value modality: modality-value)))) + +(def (assurance-verifier-candidate identity-value revision-value + priority-value evidence-kind-values + capability-value) + (unless (and (list? evidence-kind-values) + (pair? evidence-kind-values) + (every assurance-evidence-kind? evidence-kind-values)) + (error "invalid verifier evidence kinds" evidence-kind-values)) + (poo-clos-check-model + AssuranceVerifierCandidate + (.o (:: @ (poo-clos-model-prototype AssuranceVerifierCandidate)) + identity: identity-value revision: revision-value + priority: priority-value + evidence-kinds: + (list-sort + (lambda (left right) + (stringstring left) (symbol->string right))) + (delete-duplicates/hash evidence-kind-values)) + capability: capability-value))) diff --git a/modules/assurance/types.ss b/modules/assurance/types.ss index 800c9bc..e5c9346 100644 --- a/modules/assurance/types.ss +++ b/modules/assurance/types.ss @@ -4,27 +4,51 @@ ;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later (import (only-in :clan/poo/object .o .ref .slot? object?) - :poo-flow/src/module-system/contribution/model + :poo-flow/core/poo-clos/interface :std/list/list) (export assurance-text? assurance-digest? assurance-node-kind? assurance-state? assurance-relation-plane? assurance-relation-kind? assurance-relation-kind-plane - assurance-modality? + assurance-modality? assurance-evidence-kind? +assurance-node-kinds+ +assurance-states+ +assurance-relation-planes+ +assurance-modalities+ + +assurance-evidence-kinds+ AssuranceNode AssuranceArtifact AssuranceClaim AssuranceAssumption AssuranceObservation AssuranceEvent AssuranceAction AssuranceObligation AssuranceEvidence AssuranceCounterexample AssuranceFinding AssuranceDecision AssuranceEffect AssuranceRelation AssuranceSnapshot + AssuranceReplacementRequirement AssuranceReplacementDerivation + AssuranceReplacementDeclarationReceipt + AssuranceVerificationPolicy AssuranceVerificationRequest + AssuranceVerificationPlan AssuranceVerifierCandidate + AssuranceVerifierChoice AssuranceVerifierChoiceReceipt + AssuranceVerifierInput AssuranceVerifierOutcome + AssuranceEvidenceAdmissionReceipt AssuranceVerificationSubject + AssuranceRequiredSupport AssessmentRequiredSupport + AssuranceDecisionPreflight + AssuranceCompositionRequirement AssuranceCompositionDerivation make-assurance-invalidation-receipt-record assurance-node? assurance-artifact? assurance-claim? assurance-assumption? assurance-observation? assurance-event? assurance-action? assurance-obligation? assurance-evidence? assurance-counterexample? assurance-finding? assurance-decision? assurance-effect? assurance-relation? assurance-snapshot? - assurance-invalidation-receipt? assurance-invalidation-receipt-ref) + assurance-replacement-requirement? + assurance-replacement-derivation? + assurance-replacement-declaration-receipt? + assurance-invalidation-receipt? assurance-invalidation-receipt-ref + assurance-verification-policy? assurance-verification-request? + assurance-verification-plan? assurance-verifier-candidate? + assurance-verifier-choice? assurance-verifier-choice-receipt? + assurance-verifier-input? assurance-verifier-outcome? + assurance-evidence-admission-receipt? + assurance-required-support? assessment-required-support? + assurance-decision-preflight? + assurance-verification-subject? + assurance-composition-requirement? + assurance-composition-derivation?) (def +assurance-node-kinds+ '(artifact claim assumption observation event action obligation evidence @@ -36,6 +60,9 @@ '(provenance structural causal assurance authority)) (def +assurance-modalities+ '(observed declared derived hypothesized counterfactual)) +(def +assurance-evidence-kinds+ + '(proof model-check replay unit-test integration-test native-test + static-analysis authorization-differential human-review)) (def +assurance-relation-kind-planes+ '((derived-from . provenance) @@ -43,6 +70,7 @@ (cites . provenance) (used . provenance) (depends-on . structural) + (composes . structural) (implements . structural) (tests . structural) (refines . structural) @@ -73,6 +101,8 @@ (def (assurance-relation-plane? value) (if (memq value +assurance-relation-planes+) #t #f)) (def (assurance-modality? value) (if (memq value +assurance-modalities+) #t #f)) +(def (assurance-evidence-kind? value) + (if (memq value +assurance-evidence-kinds+) #t #f)) (def (assurance-relation-kind-plane value) (let ((entry (assq value +assurance-relation-kind-planes+))) (and entry (cdr entry)))) @@ -84,8 +114,45 @@ (def (enum-slot name predicate) (poo-clos-direct-slot-definition name type-predicate: predicate)) (def (maybe-text? value) (or (not value) (assurance-text? value))) +(def (maybe-digest? value) (or (not value) (assurance-digest? value))) (def (text-list? values) (and (list? values) (every assurance-text? values))) +(def (symbol-list? values) + (and (list? values) (every symbol? values))) +(def (evidence-kind-list? values) + (and (list? values) (pair? values) + (every assurance-evidence-kind? values))) +(def (verifier-choice-reason? value) + (memq value '(chosen request-blocked evidence-kind-unsupported + capability-mismatch lower-priority))) +(def (composition-blocker? value) + (or (not value) + (memq value '(conflicted-snapshot unresolved-frontier + invalid-composition-claim insufficient-components + invalid-component-claim missing-direct-obligation + stale-direct-obligation)))) +(def (verification-blocker? value) + (or (not value) + (memq value '(conflicted-snapshot unresolved-frontier + capability-unavailable stale-obligation + dependency-unplanned dependency-blocked + dependency-cycle)))) +(def (replacement-blocker? value) + (or (not value) + (memq value '(source-conflicted source-unresolved change-unresolved + stale-source + target-unchanged target-conflicted target-unresolved + target-identity-changed graph-changed + policy-changed claim-missing claim-changed)))) +(def (replacement-declaration-blocker? value) + (or (not value) + (memq value '(derivation-mismatch source-requirement-missing + source-requirement-blocked target-frontier + candidate-missing candidate-not-fresh candidate-not-unbound + candidate-not-unknown candidate-claim-mismatch + candidate-subject-mismatch candidate-scope-mismatch + claim-not-declared support-link-missing + support-link-hypothetical)))) (def (revision-bindings? values) (and (list? values) (every (lambda (binding) @@ -95,6 +162,15 @@ values))) (def (witness-list? values) (and (list? values) (every text-list? values))) +(def (invalidation-trajectories? values) + (and (list? values) + (every (lambda (entry) + (and (list? entry) + (= (length entry) 3) + (assurance-text? (car entry)) + (text-list? (cadr entry)) + (text-list? (caddr entry)))) + values))) (def (nonnegative-integer? value) (and (integer? value) (>= value 0))) (def (one-of values) @@ -151,7 +227,10 @@ (semantic-node-class 'aitia/obligation (list (text-slot 'subject) (text-slot 'claim) (text-slot 'snapshot) - (enum-slot 'evidence-kind symbol?) (enum-slot 'capability symbol?) + (enum-slot 'snapshot-revision maybe-text?) + (enum-slot 'snapshot-context-digest maybe-digest?) + (enum-slot 'evidence-kind assurance-evidence-kind?) + (enum-slot 'capability symbol?) (text-slot 'scope)))) (def AssuranceEvidence (semantic-node-class @@ -213,12 +292,288 @@ (text-slot 'policy-revision) (enum-slot 'evidence-identities text-list?) (enum-slot 'state assurance-state?) + (enum-slot 'context-digest assurance-digest?) (enum-slot 'digest assurance-digest?) (enum-slot 'nodes node-list?) (enum-slot 'relations relation-list?) (enum-slot 'unresolved text-list?) (enum-slot 'conflicts text-list?)))) +;;; A derivation names questions requiring a fresh declaration; it does not +;;; construct, bind, discharge or authorize a replacement obligation. +(def AssuranceReplacementRequirement + (poo-clos-class 'aitia/replacement-requirement + direct-slots: + (list (text-slot 'source-obligation) + (text-slot 'source-revision) + (text-slot 'claim) + (text-slot 'subject) + (enum-slot 'evidence-kind assurance-evidence-kind?) + (enum-slot 'capability symbol?) + (text-slot 'scope) + (enum-slot 'blocker replacement-blocker?)))) +(def (replacement-requirement-list? values) + (and (list? values) (every assurance-replacement-requirement? values))) +(def AssuranceReplacementDerivation + (poo-clos-class 'aitia/replacement-derivation + direct-slots: + (list (enum-slot 'schema + (lambda (value) + (equal? value "lambda-aitia.replacement-requirements"))) + (text-slot 'identity) + (enum-slot 'source-snapshot-digest assurance-digest?) + (enum-slot 'target-snapshot-digest assurance-digest?) + (enum-slot 'changed text-list?) + (enum-slot 'witnesses invalidation-trajectories?) + (enum-slot 'unresolved text-list?) + (enum-slot 'requirements replacement-requirement-list?) + (enum-slot 'digest assurance-digest?) + (enum-slot 'verifier-executed? (one-of '(#f))) + (enum-slot 'release-authorized? (one-of '(#f)))))) + +;;; A caller-authored target obligation is checked and bound, never invented. +;;; This receipt is inert even when its blocker is false. +(def AssuranceReplacementDeclarationReceipt + (poo-clos-class 'aitia/replacement-declaration-receipt + direct-slots: + (list (enum-slot 'schema + (lambda (value) + (equal? value "lambda-aitia.replacement-declaration"))) + (text-slot 'identity) + (enum-slot 'derivation-digest assurance-digest?) + (enum-slot 'source-snapshot-digest assurance-digest?) + (enum-slot 'draft-snapshot-digest assurance-digest?) + (enum-slot 'final-snapshot-digest maybe-digest?) + (text-slot 'source-obligation) + (text-slot 'target-obligation) + (enum-slot 'blocker replacement-declaration-blocker?) + (enum-slot 'digest assurance-digest?) + (enum-slot 'verifier-executed? (one-of '(#f))) + (enum-slot 'release-authorized? (one-of '(#f)))))) + +;;; Public planning values remain POO-native and contain no executable hook. +(def AssuranceVerificationPolicy + (poo-clos-class 'aitia/verification-policy + direct-slots: + (list (text-slot 'identity) + (text-slot 'revision) + (enum-slot 'capabilities symbol-list?) + (enum-slot 'planner-executes? (one-of '(#f))) + (enum-slot 'grants-authority? (one-of '(#f)))))) +(def AssuranceVerificationRequest + (poo-clos-class 'aitia/verification-request + direct-slots: + (list (text-slot 'obligation-identity) + (text-slot 'subject) + (text-slot 'claim) + (enum-slot 'snapshot-digest assurance-digest?) + (enum-slot 'evidence-kind assurance-evidence-kind?) + (enum-slot 'capability symbol?) + (enum-slot 'dependencies text-list?) + (enum-slot 'selected? boolean?) + (enum-slot 'blocker verification-blocker?)))) +(def (verification-request-list? values) + (and (list? values) + (every (lambda (value) + (assurance-verification-request? value)) + values))) +(def AssuranceVerificationPlan + (poo-clos-class 'aitia/verification-plan + direct-slots: + (list (enum-slot 'schema + (lambda (value) + (equal? value "lambda-aitia.planning-result"))) + (text-slot 'identity) + (enum-slot 'snapshot-digest assurance-digest?) + (text-slot 'policy-identity) + (text-slot 'policy-revision) + (enum-slot 'digest assurance-digest?) + (enum-slot 'selected-obligations text-list?) + (enum-slot 'blocked-obligations text-list?) + (enum-slot 'changed text-list?) + (enum-slot 'impacted text-list?) + (enum-slot 'blocked-effects text-list?) + (enum-slot 'cycle-path text-list?) + (enum-slot 'cyclic-components witness-list?) + (enum-slot 'witnesses invalidation-trajectories?) + (enum-slot 'requests verification-request-list?) + (enum-slot 'unresolved text-list?) + (enum-slot 'conflicts text-list?) + (enum-slot 'verifier-executed? (one-of '(#f))) + (enum-slot 'release-authorized? (one-of '(#f))) + (enum-slot 'runtime-executed? (one-of '(#f)))))) + +;;; These are declared candidates and inert explanation values, never adapter +;;; handles or execution receipts. +(def AssuranceVerifierCandidate + (poo-clos-class 'aitia/verifier-candidate + direct-slots: + (list (text-slot 'identity) + (text-slot 'revision) + (enum-slot 'priority nonnegative-integer?) + (enum-slot 'evidence-kinds evidence-kind-list?) + (enum-slot 'capability symbol?)))) +(def (verifier-candidate-list? values) + (and (list? values) (every assurance-verifier-candidate? values))) +(def AssuranceVerifierChoice + (poo-clos-class 'aitia/verifier-choice + direct-slots: + (list (text-slot 'obligation-identity) + (text-slot 'candidate-identity) + (enum-slot 'selected? boolean?) + (enum-slot 'reason verifier-choice-reason?)))) +(def (verifier-choice-list? values) + (and (list? values) (every assurance-verifier-choice? values))) +(def AssuranceVerifierChoiceReceipt + (poo-clos-class 'aitia/verifier-choice-receipt + direct-slots: + (list (enum-slot 'schema + (lambda (value) + (equal? value "lambda-aitia.verifier-choices"))) + (text-slot 'identity) + (enum-slot 'plan-digest assurance-digest?) + (enum-slot 'candidates verifier-candidate-list?) + (enum-slot 'choices verifier-choice-list?) + (enum-slot 'unassigned-obligations text-list?) + (enum-slot 'digest assurance-digest?) + (enum-slot 'verifier-executed? (one-of '(#f))) + (enum-slot 'release-authorized? (one-of '(#f)))))) + +;;; A reported tool result is not an admission. Input bindings carry both the +;;; source revision and digest so a green exit cannot float across snapshots. +(def AssuranceVerifierInput + (poo-clos-class 'aitia/verifier-input + direct-slots: + (list (text-slot 'identity) (text-slot 'revision) + (enum-slot 'digest assurance-digest?)))) +(def (verifier-input-list? values) + (and (list? values) (every assurance-verifier-input? values))) +(def AssuranceVerifierOutcome + (poo-clos-class 'aitia/verifier-outcome + direct-slots: + (list (text-slot 'identity) + (enum-slot 'status + (one-of '(succeeded failed timed-out unavailable indeterminate))) + (text-slot 'producer) (text-slot 'tool) (text-slot 'tool-version) + (text-slot 'obligation) (text-slot 'subject) (text-slot 'scope) + (text-slot 'snapshot-revision) + (enum-slot 'snapshot-context-digest assurance-digest?) + (enum-slot 'inputs verifier-input-list?) + (enum-slot 'output-digest maybe-digest?) + (enum-slot 'accountable-authority maybe-text?) + (enum-slot 'review-scope maybe-text?)))) +(def AssuranceEvidenceAdmissionReceipt + (poo-clos-class 'aitia/evidence-admission-receipt + direct-slots: + (list (enum-slot 'schema + (lambda (value) + (equal? value "lambda-aitia.evidence-admission"))) + (text-slot 'outcome) (text-slot 'obligation) + (enum-slot 'outcome-digest assurance-digest?) + (enum-slot 'snapshot-digest assurance-digest?) + (enum-slot 'digest assurance-digest?) + (enum-slot 'eligible? boolean?) + (enum-slot 'blocker + (lambda (value) + (or (not value) + (memq value + '(outcome-not-successful snapshot-conflicted + stale-obligation + outcome-binding-mismatch missing-output + missing-inputs duplicate-inputs + input-binding-mismatch + human-authority-missing))))) + (enum-slot 'verifier-executed? (one-of '(#f))) + (enum-slot 'release-authorized? (one-of '(#f)))))) + +;;; Exact subject for a Host-owned POO Flow verification adapter. No +;;; caller-authored presentation slot is an execution attestation by itself. +(def AssuranceVerificationSubject + (poo-clos-class 'aitia/verification-subject + direct-slots: + (list (enum-slot 'snapshot (lambda (value) (assurance-snapshot? value))) + (enum-slot 'obligation (lambda (value) (assurance-obligation? value))) + (enum-slot 'evidence (lambda (value) (assurance-evidence? value))) + (enum-slot 'outcome + (lambda (value) (assurance-verifier-outcome? value)))))) + +;;; Host-checked support inventory is an input to Decision evaluation, not a +;;; Decision or an authority receipt. A POO presentation cannot grant release. +(def (required-support-blocker? value) + (or (not value) + (memq value '(obligation-missing obligation-stale support-link-missing + admission-missing admission-invalid + evidence-link-missing)))) +(def AssessmentRequiredSupport + (poo-clos-class 'aitia/required-support-assessment + direct-slots: + (list (text-slot 'obligation) + (enum-slot 'supported? boolean?) + (enum-slot 'blocker required-support-blocker?)))) +(def (required-support-list? values) + (and (list? values) (every assessment-required-support? values))) +(def (required-support-global-blocker? value) + (memq value '(snapshot-conflicted snapshot-unresolved claim-not-current + no-requirements duplicate-requirements + assumptions-unresolved defeaters-unresolved + claim-state-blocked challenge-unresolved source-changed))) +(def (required-support-global-blockers? values) + (and (list? values) + (every required-support-global-blocker? values))) +(def AssuranceRequiredSupport + (poo-clos-class 'aitia/required-support + direct-slots: + (list (enum-slot 'schema + (lambda (value) + (equal? value "lambda-aitia.required-support"))) + (text-slot 'claim) + (enum-slot 'snapshot-digest assurance-digest?) + (enum-slot 'requirements required-support-list?) + (enum-slot 'blockers required-support-global-blockers?) + (enum-slot 'complete? boolean?) + (enum-slot 'release-authorized? (one-of '(#f)))))) + +;;; A Host-issued preflight proves only that an undecided Decision and its +;;; required support were current at issuance. Cedar/Runtime own authority. +(def AssuranceDecisionPreflight + (poo-clos-class 'aitia/decision-preflight + direct-slots: + (list (enum-slot 'schema + (lambda (value) + (equal? value "lambda-aitia.decision-preflight"))) + (text-slot 'identity) + (text-slot 'issuer) + (text-slot 'decision) + (text-slot 'claim) + (enum-slot 'snapshot-digest assurance-digest?) + (enum-slot 'release-authorized? (one-of '(#f)))))) + +;;; A composition needs its own snapshot-bound obligation. Component support +;;; remains separate and cannot discharge this structural requirement. +(def AssuranceCompositionRequirement + (poo-clos-class 'aitia/composition-requirement + direct-slots: + (list (text-slot 'composition-claim) + (enum-slot 'component-claims text-list?) + (enum-slot 'direct-obligations text-list?) + (enum-slot 'current-obligations text-list?) + (enum-slot 'blocker composition-blocker?)))) +(def (composition-requirement-list? values) + (and (list? values) + (every assurance-composition-requirement? values))) +(def AssuranceCompositionDerivation + (poo-clos-class 'aitia/composition-derivation + direct-slots: + (list (enum-slot 'schema + (lambda (value) + (equal? value "lambda-aitia.composition-requirements"))) + (text-slot 'identity) + (enum-slot 'snapshot-digest assurance-digest?) + (enum-slot 'requirements composition-requirement-list?) + (enum-slot 'digest assurance-digest?) + (enum-slot 'verifier-executed? (one-of '(#f))) + (enum-slot 'release-authorized? (one-of '(#f)))))) + (defstruct assurance-invalidation-receipt-record (identity snapshot-digest changed impacted invalidated-evidence required-obligations invalidated-decisions blocked-effects witnesses unresolved @@ -227,22 +582,22 @@ transparent: #t) (def (node-of-kind? class kind value) - (and (poo-flow-model? class value) (eq? (.ref value 'kind) kind))) + (and (poo-clos-model? class value) (eq? (.ref value 'kind) kind))) (def (assurance-node? value) (and (object? value) (.slot? value 'kind) (case (.ref value 'kind) - ((artifact) (poo-flow-model? AssuranceArtifact value)) - ((claim) (poo-flow-model? AssuranceClaim value)) - ((assumption) (poo-flow-model? AssuranceAssumption value)) - ((observation) (poo-flow-model? AssuranceObservation value)) - ((event) (poo-flow-model? AssuranceEvent value)) - ((action) (poo-flow-model? AssuranceAction value)) - ((obligation) (poo-flow-model? AssuranceObligation value)) - ((evidence) (poo-flow-model? AssuranceEvidence value)) - ((counterexample) (poo-flow-model? AssuranceCounterexample value)) - ((finding) (poo-flow-model? AssuranceFinding value)) - ((decision) (poo-flow-model? AssuranceDecision value)) - ((effect) (poo-flow-model? AssuranceEffect value)) + ((artifact) (poo-clos-model? AssuranceArtifact value)) + ((claim) (poo-clos-model? AssuranceClaim value)) + ((assumption) (poo-clos-model? AssuranceAssumption value)) + ((observation) (poo-clos-model? AssuranceObservation value)) + ((event) (poo-clos-model? AssuranceEvent value)) + ((action) (poo-clos-model? AssuranceAction value)) + ((obligation) (poo-clos-model? AssuranceObligation value)) + ((evidence) (poo-clos-model? AssuranceEvidence value)) + ((counterexample) (poo-clos-model? AssuranceCounterexample value)) + ((finding) (poo-clos-model? AssuranceFinding value)) + ((decision) (poo-clos-model? AssuranceDecision value)) + ((effect) (poo-clos-model? AssuranceEffect value)) (else #f)))) (def (assurance-artifact? value) (node-of-kind? AssuranceArtifact 'artifact value)) (def (assurance-claim? value) (node-of-kind? AssuranceClaim 'claim value)) @@ -264,10 +619,69 @@ (node-of-kind? AssuranceDecision 'decision value)) (def (assurance-effect? value) (node-of-kind? AssuranceEffect 'effect value)) (def (assurance-relation? value) - (and (poo-flow-model? AssuranceRelation value) + (and (poo-clos-model? AssuranceRelation value) (eq? (.ref value 'plane) (assurance-relation-kind-plane (.ref value 'relation))))) -(def (assurance-snapshot? value) (poo-flow-model? AssuranceSnapshot value)) +(def (assurance-snapshot? value) (poo-clos-model? AssuranceSnapshot value)) +(def (assurance-replacement-requirement? value) + (poo-clos-model? AssuranceReplacementRequirement value)) +(def (assurance-replacement-derivation? value) + (poo-clos-model? AssuranceReplacementDerivation value)) +(def (assurance-replacement-declaration-receipt? value) + (and (poo-clos-model? AssuranceReplacementDeclarationReceipt value) + (eq? (not (.ref value 'blocker)) + (if (.ref value 'final-snapshot-digest) #t #f)))) +(def (assurance-verification-policy? value) + (poo-clos-model? AssuranceVerificationPolicy value)) +(def (assurance-verification-request? value) + (and (poo-clos-model? AssuranceVerificationRequest value) + (eq? (.ref value 'selected?) + (not (.ref value 'blocker))))) +(def (assurance-verification-plan? value) + (and (poo-clos-model? AssuranceVerificationPlan value) + (equal? + (.ref value 'selected-obligations) + (map (lambda (request) (.ref request 'obligation-identity)) + (filter (lambda (request) (.ref request 'selected?)) + (.ref value 'requests)))) + (equal? + (.ref value 'blocked-obligations) + (map (lambda (request) (.ref request 'obligation-identity)) + (filter (lambda (request) (not (.ref request 'selected?))) + (.ref value 'requests)))))) +(def (assurance-verifier-candidate? value) + (poo-clos-model? AssuranceVerifierCandidate value)) +(def (assurance-verifier-choice? value) + (and (poo-clos-model? AssuranceVerifierChoice value) + (eq? (.ref value 'selected?) + (eq? (.ref value 'reason) 'chosen)))) +(def (assurance-verifier-choice-receipt? value) + (poo-clos-model? AssuranceVerifierChoiceReceipt value)) +(def (assurance-verifier-input? value) + (poo-clos-model? AssuranceVerifierInput value)) +(def (assurance-verifier-outcome? value) + (poo-clos-model? AssuranceVerifierOutcome value)) +(def (assurance-evidence-admission-receipt? value) + (and (poo-clos-model? AssuranceEvidenceAdmissionReceipt value) + (eq? (.ref value 'eligible?) (not (.ref value 'blocker))))) +(def (assessment-required-support? value) + (and (poo-clos-model? AssessmentRequiredSupport value) + (eq? (.ref value 'supported?) (not (.ref value 'blocker))))) +(def (assurance-required-support? value) + (and (poo-clos-model? AssuranceRequiredSupport value) + (eq? (.ref value 'complete?) + (and (null? (.ref value 'blockers)) + (pair? (.ref value 'requirements)) + (every (lambda (item) (.ref item 'supported?)) + (.ref value 'requirements)))))) +(def (assurance-decision-preflight? value) + (poo-clos-model? AssuranceDecisionPreflight value)) +(def (assurance-verification-subject? value) + (poo-clos-model? AssuranceVerificationSubject value)) +(def (assurance-composition-requirement? value) + (poo-clos-model? AssuranceCompositionRequirement value)) +(def (assurance-composition-derivation? value) + (poo-clos-model? AssuranceCompositionDerivation value)) (def (assurance-invalidation-receipt? value) (assurance-invalidation-receipt-record? value)) (def (assurance-invalidation-receipt-ref receipt name) diff --git a/modules/assurance/verification-projection.ss b/modules/assurance/verification-projection.ss new file mode 100644 index 0000000..58237ba --- /dev/null +++ b/modules/assurance/verification-projection.ss @@ -0,0 +1,73 @@ +;;; -*- Gerbil -*- +;;; SPDX-FileCopyrightText: 2026 tao3k team and Contributors +;;; +;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +;;; Aitia owns the meaning of obligation dependencies; POO Flow owns Graph +;;; ordering, reachability and cycle algorithms. No verifier runs here. + +(import (only-in :clan/poo/object .ref) + (only-in :gerbil/core list-sort) + :std/list/list + (only-in :poo-flow/src/graph/types + poo-flow-graph poo-flow-graph-edge poo-flow-graph-node) + :poo-flow/lambda-aitia/modules/assurance/types) + +(export assurance-verification-dependency-graph) + +(def (canonical-ids ids) + (list-sort string.+ {repository: {type: NonEmptyString} diff --git a/modules/sdlc/README.org b/modules/sdlc/README.org index 0dfd17f..67bce21 100644 --- a/modules/sdlc/README.org +++ b/modules/sdlc/README.org @@ -13,10 +13,81 @@ Projects may select multiple standards. No standard is automatically activated. The activity vocabulary does not impose a waterfall sequence; projects own the ordering, iteration and release process. +* Design contracts and replacement review + +=design.ss= adds POO-native component design declarations under the SDLC owner. +A contract names its ADR decision, responsibilities, owned state, allowed +effects, assumptions, guarantees and variation points. Each clause has an +identity, revision and statement; a variation declares which guarantees its +change may affect. These are engineering declarations, not executable logical +predicates or admitted evidence. Accepting the named ADR does not establish a +guarantee. + +=sdlc-design-replacement-review= compares two revisions of the same contract. +Its intentionally narrow declaration-level rule permits only exact clause +identity/revision/statement matches: a candidate may require no new assumption +and may discard no old guarantee. Added effects, changed state ownership, +responsibility, ADR decision or variation declarations remain visible blockers. +The receipt lists changed identities and guarantees requiring fresh checking; +it never admits evidence or grants release authority. An unchanged declaration +at a new revision still requires re-verification. Textual similarity is not +treated as logical implication, and a =declared-substitutable?= candidate is +not a proof of implementation refinement. + +Consumers may feed =changed-identities= into the existing +=sdlc-change-impact= projection, which delegates trajectory analysis to POO +Flow Graph. The job-executor regression does this for a retry-policy change: +only the explicitly affected unique-effect guarantee and its verification +are selected by the declared graph. If a variation omits a necessary +dependency, this slice cannot discover it; model-to-implementation conformance +and independent acceptance remain further qualification work. +An alternative local-deduplication candidate changes state ownership, +responsibility and the ADR decision. It is therefore blocked for design review +even though its guarantee statements have the same text as the original. + +=implementation.ss= adds an inert design-to-source link. A link binds one +guarantee revision to an Assurance artifact identity/revision/digest and the +canonical Assurance snapshot context in which that source appears. The review +requires every guarantee to have a current link and rejects changed contracts, +guarantees, snapshots, source digests and unresolved/conflicted snapshot cuts. +It reuses the Assurance snapshot owner rather than defining another source +ledger. A current mapping means only that the declaration is consistent with +the supplied snapshot. It does not validate the bytes behind an artifact, +prove the implementation satisfies the guarantee, admit evidence or authorize +release. Independent verifier/Host acceptance remains a separate gate. + +=verification.ss= projects a design guarantee through the existing Assurance +Host. It requires a current implementation mapping, an exact current Claim +whose predicate is the guarantee statement and whose assumptions match the +design contract, a declared Claim-to-source edge, +and a Host admission whose verifier input names that same source +identity/revision/digest. The Host must also report the Claim's full required +support set current. Missing source dependencies, unrelated inputs, copied or +revoked admissions fail closed. The projection issues no admission and grants +no release authority. Its =source-bound-support-current?= result is conditional +on the application-configured Host; it does not establish verifier +independence, semantic implementation conformance or trusted deployment. +An application may configure the Assurance Host's =source-reader:= capability. +With that reader, Host issuance and currentness check every declared verifier +input against its Artifact SHA-256 using POO Flow's Source Lock algorithm. +The configured verification operation then receives Host-materialized POO input +values as its fourth argument, each with =identity=, =revision=, =digest= and a +copy of the validated =payload=. The Host rechecks those values after the +operation; replacing both payload and digest cannot retain a seal. Without a +reader, the operation retains its three-argument contract. +A byte mismatch or moving snapshot blocks issuance; observed byte drift +invalidates previously issued receipts, which cannot revive merely because +the bytes later return. =source-bytes-checked-support-current?= additionally +requires the mapped design source to pass that check. Without the Host reader, +the stronger result fails closed. SDLC review callers cannot supply replacement +payloads. The application still owns the reader's repository/provenance trust +and the verifier's actual process inputs; matching bytes alone proves neither +Host independence nor semantic implementation conformance. + SDLC does not own CI providers or deployment environments. It contributes the standard, project, obligation, evidence, traceability, tailoring and lifecycle -models. Applications combine this module with POO Flow-owned workflow modules -through =use-composition=. +models. Applications combine these POO-native Profiles through POO Flow's +=use-module= and =compose profiles=. #+begin_export mermaid flowchart LR @@ -29,25 +100,20 @@ flowchart LR Consumer -->|"host-owned execution"| Runtime[GitHub Actions or another CI host] #+end_export -The user-owned POO Flow =user-interface/init.ss= selects SDLC using the existing -custom module syntax (paths are relative to that user directory): +The maintained software-engineering Project selects base SDLC and NASA's +standard-family contribution separately: #+begin_src scheme -(poo-flow! - :custom - (sdlc +nasa-7150-2d)) +(poo-flow-user-module-selection 'software-engineering 'sdlc '()) +(poo-flow-user-module-selection 'nasa 'sdlc '(+npr-7150.2)) #+end_src -Here =sdlc= resolves through POO Flow's trusted contribution registry. A user -can instead load a complete private collection with =(@ "./modules")=. - -Omit =+nasa-7150-2d= to select SDLC without NASA. =config.ss= owns the pure -=sdlc-config= projection from a native module selection to a contribution. -Unknown and duplicate flags fail explicitly. POO Flow's user-owned -=user-interface/sdlc.ss= consumes the root init selections and exports -=poo-flow-user-sdlc-contributions=. Consumers import that projection when needed; -importing root init alone still only declares module selections. There is no -automatic runtime activation. No example project is part of the SDLC build. +Omit the NASA selection for standard-free SDLC. =config.ss= owns the pure +=sdlc-config= projection; it rejects unsupported feature flags. The public +profile identifies the standard family as =nasa/sdlc/npr-7150.2=. The selected +edition =D= belongs to the Standard descriptor and source lock, so later +editions need not change the family-level composition name. Composition does +not activate a runtime. =standards/nasa-7150-2d-profile.ss= owns the standard descriptor used by =config.ss= and scenario Profiles. It contains the immutable source/catalog diff --git a/modules/sdlc/config.ss b/modules/sdlc/config.ss index 3d694ae..46d5b04 100644 --- a/modules/sdlc/config.ss +++ b/modules/sdlc/config.ss @@ -2,8 +2,9 @@ ;;; ;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later -;;; User selection projection. Importing this module enables no standard. -(import :poo-flow/src/module-system/declaration/interface +;;; Legacy module-selection projection is standard-free. The NASA Standard is +;;; selected through its own typed nasa/sdlc Module Profile, not an SDLC flag. +(import :poo-flow/src/user-interface/module-selection :poo-flow/lambda-aitia/modules/sdlc/types :poo-flow/lambda-aitia/modules/sdlc/objects :poo-flow/lambda-aitia/modules/sdlc/funs @@ -21,9 +22,6 @@ (equal? (poo-flow-user-module-selection-key selection) '(custom . sdlc))) (error "expected a custom/sdlc selection")) (let ((flags (poo-flow-user-module-selection-flags selection))) - (unless (every (lambda (flag) (eq? flag '+nasa-7150-2d)) flags) - (error "unsupported SDLC feature" flags)) - (unless (<= (length flags) 1) - (error "duplicate SDLC feature" flags)) - (sdlc-contribution - (if (null? flags) SdlcProfile sdlc-nasa-7150-profile)))) + (unless (null? flags) + (error "SDLC has no feature flags; select standards as Modules" flags)) + (sdlc-contribution SdlcProfile))) diff --git a/modules/sdlc/design.ss b/modules/sdlc/design.ss new file mode 100644 index 0000000..d318783 --- /dev/null +++ b/modules/sdlc/design.ss @@ -0,0 +1,239 @@ +;;; SPDX-FileCopyrightText: 2026 tao3k team and Contributors +;;; +;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +;;; Aitia owns software-design meaning; POO Flow owns graph traversal and +;;; runtime authority. This is an inert declaration-level replacement check. +(import (only-in :clan/poo/object .o .ref) + (only-in :gerbil/core list-sort) + :std/list/list + :poo-flow/core/poo-clos/interface + (only-in :poo-flow/lambda-aitia/modules/assurance/funs + assurance-canonical-digest) + :poo-flow/lambda-aitia/modules/sdlc/types) + +(export SdlcDesignClause SdlcDesignVariation SdlcDesignContract + sdlc-design-clause? sdlc-design-variation? sdlc-design-contract? + sdlc-design-assumption sdlc-design-guarantee + sdlc-design-variation sdlc-design-contract + sdlc-design-contract-digest + sdlc-design-replacement-review) + +(def (text-list? values) + (and (list? values) (every sdlc-text? values) + (= (length values) (length (delete-duplicates/hash values))))) +(def (nonempty-text-list? values) + (and (text-list? values) (pair? values))) +(def (identified-list? values predicate) + (and (list? values) (every predicate values) + (let (ids (map (lambda (value) (.ref value 'identity)) values)) + (= (length ids) (length (delete-duplicates/hash ids)))))) +(def (clause-list? kind) + (lambda (values) + (and (identified-list? values sdlc-design-clause?) + (every (lambda (value) (eq? (.ref value 'kind) kind)) values)))) +(def (variation-list? values) + (identified-list? values sdlc-design-variation?)) +(def (slot name predicate) + (poo-clos-direct-slot-definition name type-predicate: predicate)) + +(def SdlcDesignClause + (poo-clos-class 'sdlc/design-clause + direct-slots: + (list (slot 'identity sdlc-text?) (slot 'revision sdlc-text?) + (slot 'kind (lambda (value) (if (memq value '(assumption guarantee)) #t #f))) + (slot 'statement sdlc-text?)))) +(def SdlcDesignVariation + (poo-clos-class 'sdlc/design-variation + direct-slots: + (list (slot 'identity sdlc-text?) (slot 'revision sdlc-text?) + (slot 'affects-guarantees nonempty-text-list?)))) +(def SdlcDesignContract + (poo-clos-class 'sdlc/design-contract + direct-slots: + (list (slot 'identity sdlc-text?) (slot 'revision sdlc-text?) + (slot 'decision sdlc-text?) + (slot 'responsibilities text-list?) + (slot 'owned-state text-list?) + (slot 'allowed-effects text-list?) + (slot 'assumptions (clause-list? 'assumption)) + (slot 'guarantees (clause-list? 'guarantee)) + (slot 'variations variation-list?)))) + +(def (sdlc-design-clause? value) + (poo-clos-model? SdlcDesignClause value)) +(def (sdlc-design-variation? value) + (poo-clos-model? SdlcDesignVariation value)) +(def (sdlc-design-contract? value) + (and (poo-clos-model? SdlcDesignContract value) + (pair? (.ref value 'responsibilities)) + (pair? (.ref value 'guarantees)) + (let (guarantees (map (lambda (clause) (.ref clause 'identity)) + (.ref value 'guarantees))) + (every (lambda (variation) + (every (lambda (id) (member id guarantees)) + (.ref variation 'affects-guarantees))) + (.ref value 'variations))))) + +(def (design-clause kind-value id revision-value statement-value) + (poo-clos-check-model + SdlcDesignClause + (.o (:: @ (poo-clos-model-prototype SdlcDesignClause)) + identity: id revision: revision-value kind: kind-value + statement: statement-value))) +(def (sdlc-design-assumption id revision-value statement-value) + (design-clause 'assumption id revision-value statement-value)) +(def (sdlc-design-guarantee id revision-value statement-value) + (design-clause 'guarantee id revision-value statement-value)) +(def (sdlc-design-variation id revision-value affected-guarantees) + (poo-clos-check-model + SdlcDesignVariation + (.o (:: @ (poo-clos-model-prototype SdlcDesignVariation)) + identity: id revision: revision-value + affects-guarantees: affected-guarantees))) +(def (sdlc-design-contract id revision-value decision-value responsibility-values + owned-state-values effect-values assumption-values + guarantee-values variation-values) + (let (value + (poo-clos-check-model + SdlcDesignContract + (.o (:: @ (poo-clos-model-prototype SdlcDesignContract)) + identity: id revision: revision-value decision: decision-value + responsibilities: responsibility-values + owned-state: owned-state-values + allowed-effects: effect-values assumptions: assumption-values + guarantees: guarantee-values variations: variation-values))) + (unless (sdlc-design-contract? value) + (error "design contract lacks a responsibility or guarantee, or a variation references an undeclared guarantee" id)) + value)) + +(def (ordered-ids values) + (list-sort string intro clause present <;> exact present + +theorem refinesTrans {Clause : Type} + (first second third : Contract Clause) + (firstToSecond : Refines first second) + (secondToThird : Refines second third) : + Refines first third := by + constructor + · intro clause required + exact firstToSecond.1 clause (secondToThird.1 clause required) + · intro clause promised + exact secondToThird.2 clause (firstToSecond.2 clause promised) + +theorem strongerAssumptionBlocks {Clause : Type} + (old candidate : Contract Clause) (clause : Clause) + (required : candidate.assumptions clause) + (notPreviouslyRequired : ¬ old.assumptions clause) : + ¬ Refines old candidate := by + intro replacement + exact notPreviouslyRequired (replacement.1 clause required) + +theorem droppedGuaranteeBlocks {Clause : Type} + (old candidate : Contract Clause) (clause : Clause) + (promised : old.guarantees clause) + (notPromised : ¬ candidate.guarantees clause) : + ¬ Refines old candidate := by + intro replacement + exact notPromised (replacement.2 clause promised) + +end LambdaAitiaProof.Modules.Sdlc.Design diff --git a/proof/lean/LambdaAitiaProof/Modules/Sdlc/Retirement.lean b/proof/lean/LambdaAitiaProof/Modules/Sdlc/Retirement.lean new file mode 100644 index 0000000..e01cee2 --- /dev/null +++ b/proof/lean/LambdaAitiaProof/Modules/Sdlc/Retirement.lean @@ -0,0 +1,81 @@ +-- SPDX-FileCopyrightText: 2026 tao3k team and Contributors +-- SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +/-! +Design-level retirement safety. `Step.observe` overapproximates every +pre-retirement observation, including late work and inventory loss; safety +cannot rely on an old review. Terminal states admit no further observations. +This is not a proof of Quint/Scheme correspondence, inventory truth, liveness, +post-retirement ingress exclusion, persistence, or effect authorization. +-/ + +namespace LambdaAitiaProof.Modules.Sdlc.Retirement + +structure State where + active : Bool + queued : Bool + known : Bool + reviewed : Bool + cancelRequested : Bool + retired : Bool + +def Safe (s : State) : Prop := + s.retired = true → s.active = false ∧ s.queued = false ∧ s.known = true + +def Initial : State := ⟨true, true, false, false, false, false⟩ + +inductive Step : State → State → Prop where + | observe (s t : State) (before : s.retired = false) + (after : t.retired = false) : Step s t + | finalize (s : State) (before : s.retired = false) + (review : s.reviewed = true) (idle : s.active = false) + (drained : s.queued = false) (inventory : s.known = true) : + Step s { s with retired := true } + | terminal (s : State) (done : s.retired = true) : Step s s + +theorem initialSafe : Safe Initial := by + intro done + cases done + +theorem stepPreservesSafety {s t : State} (safe : Safe s) + (step : Step s t) : Safe t := by + cases step with + | observe t before after => + intro done + have impossible : false = true := after.symm.trans done + cases impossible + | finalize before review idle drained inventory => + intro _ + exact ⟨idle, drained, inventory⟩ + | terminal done => exact safe + +inductive Reachable : State → Prop where + | initial : Reachable Initial + | next {s t : State} : Reachable s → Step s t → Reachable t + +theorem reachableSafe {s : State} (reachable : Reachable s) : Safe s := by + induction reachable with + | initial => exact initialSafe + | next previous step inductionHypothesis => + exact stepPreservesSafety inductionHypothesis step + +def CancellationOnly : State := ⟨true, true, true, false, true, true⟩ +def StaleReview : State := ⟨true, false, true, true, false, true⟩ +def UnknownInventory : State := ⟨false, false, false, true, false, true⟩ + +theorem cancellationDoesNotJustifyRetirement : ¬ Safe CancellationOnly := by + intro safe + have impossible := (safe rfl).1 + cases impossible + +theorem oldReviewDoesNotJustifyRetirement : ¬ Safe StaleReview := by + intro safe + have impossible := (safe rfl).1 + cases impossible + +theorem unknownInventoryBlocksRetirement : ¬ Safe UnknownInventory := by + intro safe + have impossible := (safe rfl).2.2 + cases impossible + +end LambdaAitiaProof.Modules.Sdlc.Retirement diff --git a/proof/lean/LambdaAitiaProof/Modules/Sdlc/RetirementIngress.lean b/proof/lean/LambdaAitiaProof/Modules/Sdlc/RetirementIngress.lean new file mode 100644 index 0000000..98a76fe --- /dev/null +++ b/proof/lean/LambdaAitiaProof/Modules/Sdlc/RetirementIngress.lean @@ -0,0 +1,92 @@ +-- SPDX-FileCopyrightText: 2026 tao3k team and Contributors +-- SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +import LambdaAitiaProof.Modules.Sdlc.Retirement + +/-! +An abstract extension of retirement review with an ingress fence. Requests +and rejections may stutter after retirement; an effect with a current epoch +cannot start there. Finalization requires a closed issuance boundary and an +invalidated cached epoch. No proof of deployed revocation, Quint correspondence, +grant issuance, fairness or persistent state is claimed. +-/ + +namespace LambdaAitiaProof.Modules.Sdlc.RetirementIngress + +structure State where + review : Retirement.State + ingressOpen : Bool + epoch : Nat + grantEpoch : Nat + +def Safe (s : State) : Prop := + Retirement.Safe s.review ∧ + (s.review.retired = true → s.ingressOpen = false ∧ s.grantEpoch ≠ s.epoch) + +def Initial : State := ⟨Retirement.Initial, true, 0, 0⟩ + +theorem initialSafe : Safe Initial := by + constructor + · exact Retirement.initialSafe + · intro retired + cases retired + +theorem currentGrantExcludesRetirement {s : State} (safe : Safe s) + (current : s.grantEpoch = s.epoch) : s.review.retired ≠ true := by + intro retired + exact (safe.2 retired).2 current + +inductive Step : State → State → Prop where + | review (s : State) (t : Retirement.State) + (step : Retirement.Step s.review t) (beforeFinalization : t.retired = false) : + Step s {s with review := t} + | close (s : State) (notRetired : s.review.retired = false) + (wasOpen : s.ingressOpen = true) : + Step s {s with ingressOpen := false, epoch := s.epoch + 1} + | finalize (s : State) + (step : Retirement.Step s.review {s.review with retired := true}) + (closed : s.ingressOpen = false) (revoked : s.grantEpoch ≠ s.epoch) : + Step s {s with review := {s.review with retired := true}} + | start (s : State) (current : s.grantEpoch = s.epoch) : + Step s {s with review := {s.review with active := true}} + | requestOrReject (s : State) : Step s s + +theorem stepPreservesSafety {s t : State} (safe : Safe s) + (step : Step s t) : Safe t := by + cases step with + | review t step beforeFinalization => + constructor + · exact Retirement.stepPreservesSafety safe.1 step + · intro retired + have impossible : false = true := beforeFinalization.symm.trans retired + cases impossible + | close notRetired wasOpen => + constructor + · exact safe.1 + · intro retired + have impossible : false = true := notRetired.symm.trans retired + cases impossible + | finalize step closed revoked => + constructor + · exact Retirement.stepPreservesSafety safe.1 step + · intro _ + exact ⟨closed, revoked⟩ + | start current => + constructor + · intro retired + exact False.elim (currentGrantExcludesRetirement safe current retired) + · intro retired + exact False.elim (currentGrantExcludesRetirement safe current retired) + | requestOrReject => exact safe + +inductive Reachable : State → Prop where + | initial : Reachable Initial + | next {s t : State} : Reachable s → Step s t → Reachable t + +theorem reachableSafe {s : State} (reachable : Reachable s) : Safe s := by + induction reachable with + | initial => exact initialSafe + | next previous step inductionHypothesis => + exact stepPreservesSafety inductionHypothesis step + +end LambdaAitiaProof.Modules.Sdlc.RetirementIngress diff --git a/proof/lean/README.org b/proof/lean/README.org index be6556f..5e0305c 100644 --- a/proof/lean/README.org +++ b/proof/lean/README.org @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2026 tao3k team and Contributors # SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later -#+title: Lambda Aitia Assurance Proofs +#+title: Lambda Aitia Assurance and SDLC Proofs This is Lambda Aitia's source-owned Lean package. It states laws over Aitia assurance vocabulary and does not import or copy POO Flow's generic proof @@ -22,3 +22,23 @@ that impact is monotone when the changed set expands over one fixed reachability relation, that a non-empty unresolved frontier prevents closure, and that an inert invalidation receipt grants no release authority or runtime execution. + +=LambdaAitiaProof.Modules.Assurance.Composition= proves a separate admission +law: current evidence belonging only to component obligations cannot support +the composite claim. Any admitted composite has a current discharged direct +obligation distinct from each component obligation. These theorems assume the +explicit =Contract= fields; the Scheme acceptance tests check that the +runtime projection observes the same boundary, rather than treating a Lean +theorem as runtime authority. + +The independently selectable =LambdaAitiaModuleSdlcProof= library mirrors +=modules/sdlc/design.ss=. Its =Design= module proves reflexivity and +transitivity of the declaration-level assumption/guarantee replacement +direction and shows that an added assumption or dropped guarantee blocks that +relation. It does not certify natural-language clause meanings, the Scheme +comparison implementation or any Runtime effect. + +=AxiomAudit.lean= prints the transitive axioms of every exported law. The +=just check-proof= and CI gates reject =sorryAx= in that output; a successful +compile alone is not accepted as evidence that theorems contain no admitted +holes. diff --git a/proof/lean/lakefile.lean b/proof/lean/lakefile.lean index e320cef..e9cc7bb 100644 --- a/proof/lean/lakefile.lean +++ b/proof/lean/lakefile.lean @@ -19,3 +19,6 @@ explicit repository-wide integration aggregate. @[default_target] lean_lib LambdaAitiaModuleAssuranceProof where roots := #[`LambdaAitiaProof.Modules.Assurance] + +lean_lib LambdaAitiaModuleSdlcProof where + roots := #[`LambdaAitiaProof.Modules.Sdlc] diff --git a/proof/quint/AssuranceLifecycle.qnt b/proof/quint/AssuranceLifecycle.qnt new file mode 100644 index 0000000..a70a685 --- /dev/null +++ b/proof/quint/AssuranceLifecycle.qnt @@ -0,0 +1,130 @@ +// SPDX-FileCopyrightText: 2026 tao3k team and Contributors +// SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later +module AssuranceLifecycle { + import EffectAdmission.* from "./EffectAdmission" + + // One obligation, two source revisions, four clock values, three epochs. + type Lifecycle = {revision: int, planned: int, verified: int, admitted: int, + decided: int, granted: int, grantEpoch: int, epoch: int, clock: int, + sealUntil: int, grantUntil: int, adapterInstalled: bool, + reportedSuccess: bool, effectStarted: bool, startSafe: bool, + reservationRevision: int, reservationEpoch: int, reservationUntil: int, + deliveryStatus: int, deliveryCalls: int, deliverySafe: bool} + var q: Lifecycle + pure val Initial = {revision: 1, planned: 0, verified: 0, admitted: 0, + decided: 0, granted: 0, grantEpoch: 0, epoch: 0, clock: 0, + sealUntil: 0, grantUntil: 0, adapterInstalled: false, + reportedSuccess: false, effectStarted: false, startSafe: true, + reservationRevision: 0, reservationEpoch: 0, reservationUntil: 0, + deliveryStatus: 0, deliveryCalls: 0, deliverySafe: true} + action init = q' = Initial + action InstallAdapter = all {not(q.adapterInstalled), q' = {...q, adapterInstalled: true}} + action ReportSuccess = all {not(q.reportedSuccess), q' = {...q, reportedSuccess: true}} + action Plan = all {q.planned != q.revision, q' = {...q, planned: q.revision}} + action Verify = all {q.adapterInstalled, q.planned == q.revision, q.clock < 3, + q' = {...q, verified: q.revision, sealUntil: q.clock + 1}} + action Admit = all {q.verified == q.revision, q.clock < q.sealUntil, + q.admitted != q.revision, q' = {...q, admitted: q.revision}} + action Decide = all {q.admitted == q.revision, q.decided != q.revision, + q' = {...q, decided: q.revision}} + action Authorize = all {q.decided == q.revision, q.admitted == q.revision, + q.granted != q.revision, q.clock < 3, + q' = {...q, granted: q.revision, grantEpoch: q.epoch, grantUntil: q.clock + 1}} + val Current = currentGrant(q.granted, q.revision, q.admitted, q.decided, + q.grantEpoch, q.epoch, q.clock, q.grantUntil) + action StartEffect = all {not(q.effectStarted), Current, + q' = {...q, effectStarted: true, + reservationRevision: q.revision, reservationEpoch: q.grantEpoch, + reservationUntil: q.grantUntil, + startSafe: currentGrant(q.granted, q.revision, q.admitted, q.decided, + q.grantEpoch, q.epoch, q.clock, q.grantUntil)}} + // Start is a one-shot reservation, not delivery or an acknowledgement. + val DeliveryCurrent = Current and q.reservationRevision == q.revision and + q.reservationEpoch == q.epoch and q.clock < q.reservationUntil + // 0 = pending/not-started, 1 = acknowledged, 2 = rejected, 3 = failed, + // 4 = unknown acknowledgement. Unknown never authorizes a blind retry. + action DeliverEffect = all {q.effectStarted, q.deliveryStatus == 0, DeliveryCurrent, + q' = {...q, deliveryStatus: 1, deliveryCalls: q.deliveryCalls + 1, deliverySafe: true}} + action RejectDelivery = all {q.effectStarted, q.deliveryStatus == 0, not(DeliveryCurrent), + q' = {...q, deliveryStatus: 2}} + action FailDelivery = all {q.effectStarted, q.deliveryStatus == 0, DeliveryCurrent, + q' = {...q, deliveryStatus: 3, deliveryCalls: q.deliveryCalls + 1}} + action UnknownDelivery = all {q.effectStarted, q.deliveryStatus == 0, DeliveryCurrent, + q' = {...q, deliveryStatus: 4, deliveryCalls: q.deliveryCalls + 1}} + action ChangeSource = all {q.revision == 1, + q' = {...q, revision: 2, planned: 0, verified: 0, admitted: 0, + decided: 0, granted: 0, sealUntil: 0, grantUntil: 0}} + action Tick = all {q.clock < 3, + q' = {...q, clock: q.clock + 1, + admitted: if (q.clock + 1 < q.sealUntil) q.admitted else 0, + decided: if (q.clock + 1 < q.sealUntil) q.decided else 0, + granted: if (q.clock + 1 < q.sealUntil and q.clock + 1 < q.grantUntil) + q.granted else 0}} + action Revoke = all {q.epoch < 2, + q' = {...q, epoch: q.epoch + 1, verified: 0, admitted: 0, + decided: 0, granted: 0, sealUntil: 0}} + action stableStep = any {InstallAdapter, ReportSuccess, Plan, Verify, Admit, + Decide, Authorize, StartEffect, DeliverEffect} + action Terminal = all {q.clock == 3, q.epoch == 2, q.revision == 2, + q.adapterInstalled, q.reportedSuccess, q.planned == q.revision, + (not(q.effectStarted) or q.deliveryStatus != 0), q' = q} + action step = any {stableStep, ChangeSource, Tick, Revoke, RejectDelivery, + FailDelivery, UnknownDelivery, Terminal} + + // Each mutation is isolated by selecting its own step operator. + action UnsafeStart = all {q.reportedSuccess, not(q.effectStarted), + q' = {...q, effectStarted: true, startSafe: Current}} + action UnsafeLateAdmit = all {q.revision == 2, q.reportedSuccess, + q.admitted == 0, q' = {...q, admitted: 1}} + action UnsafeEarlyGrant = all {q.revision == 2, q.reportedSuccess, + q.admitted == 0, q.granted == 0, q.clock < 3, + q' = {...q, granted: q.revision, grantEpoch: q.epoch, + grantUntil: q.clock + 1}} + action UnsafeAfterRevoke = all {q.epoch > q.grantEpoch, + q.grantUntil > q.clock, q.granted == 0, not(q.effectStarted), + q' = {...q, effectStarted: true, startSafe: Current}} + action unsafeStep = any {step, UnsafeStart} + action lateAdmitStep = any {step, UnsafeLateAdmit} + action earlyGrantStep = any {step, UnsafeEarlyGrant} + action afterRevokeStep = any {step, UnsafeAfterRevoke} + action UnsafeDelivery = all {q.effectStarted, q.deliveryStatus == 0, not(DeliveryCurrent), + q' = {...q, deliveryStatus: 1, deliveryCalls: q.deliveryCalls + 1, deliverySafe: false}} + action UnsafeBeforeStart = all {q.reportedSuccess, not(q.effectStarted), + q' = {...q, deliveryStatus: 1, deliveryCalls: q.deliveryCalls + 1, deliverySafe: false}} + action UnsafeReplay = all {q.deliveryStatus == 1, + q' = {...q, deliveryCalls: q.deliveryCalls + 1}} + action unsafeDeliveryStep = any {step, UnsafeDelivery} + action beforeStartStep = any {step, UnsafeBeforeStart} + action replayStep = any {step, UnsafeReplay} + + val TypeOK = q.revision >= 1 and q.revision <= 2 and + q.planned >= 0 and q.planned <= 2 and q.verified >= 0 and q.verified <= 2 and + q.admitted >= 0 and q.admitted <= 2 and q.decided >= 0 and q.decided <= 2 and + q.granted >= 0 and q.granted <= 2 and q.grantEpoch >= 0 and q.grantEpoch <= 2 and + q.epoch >= 0 and q.epoch <= 2 and q.clock >= 0 and q.clock <= 3 and + q.sealUntil >= 0 and q.sealUntil <= 4 and q.grantUntil >= 0 and q.grantUntil <= 4 and + q.reservationRevision >= 0 and q.reservationRevision <= 2 and + q.reservationEpoch >= 0 and q.reservationEpoch <= 2 and + q.reservationUntil >= 0 and q.reservationUntil <= 4 and + q.deliveryStatus >= 0 and q.deliveryStatus <= 4 and q.deliveryCalls >= 0 and q.deliveryCalls <= 1 + val AdmissionIsCurrent = q.admitted == 0 or + (q.admitted == q.revision and q.verified == q.revision and q.clock < q.sealUntil) + val DecisionRequiresAdmission = q.decided == 0 or + (q.decided == q.revision and q.admitted == q.revision) + val GrantRequiresCurrentDecision = q.granted == 0 or + (q.granted == q.revision and q.decided == q.revision and + q.admitted == q.revision and q.grantEpoch == q.epoch and q.clock < q.grantUntil) + val NoUnsafeEffectStart = q.startSafe + val NoEffectStarted = not(q.effectStarted) + val NoUnsafeDelivery = q.deliverySafe + val DeliveryRequiresStart = q.deliveryCalls == 0 or q.effectStarted + val AtMostOneDelivery = q.deliveryCalls <= 1 + + temporal stableFairness = InstallAdapter.weakFair(q) and Plan.weakFair(q) and + Verify.weakFair(q) and Admit.weakFair(q) and Decide.weakFair(q) and + Authorize.weakFair(q) and StartEffect.weakFair(q) and DeliverEffect.weakFair(q) + temporal stableSpec = q == Initial and stableStep.orKeep(q).always() and stableFairness + temporal stableSpecNoFair = q == Initial and stableStep.orKeep(q).always() + temporal EventuallyEffect = q.effectStarted.eventually() + temporal EventuallyDelivery = (q.deliveryStatus == 1).eventually() +} diff --git a/proof/quint/EffectAdmission.qnt b/proof/quint/EffectAdmission.qnt new file mode 100644 index 0000000..515d640 --- /dev/null +++ b/proof/quint/EffectAdmission.qnt @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2026 tao3k team and Contributors +// SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later +module EffectAdmission { + pure def currentGrant(granted: int, revision: int, admitted: int, decided: int, + grantEpoch: int, epoch: int, clock: int, grantUntil: int): bool = + granted == revision and admitted == revision and decided == revision and + grantEpoch == epoch and clock < grantUntil +} diff --git a/proof/quint/README.org b/proof/quint/README.org new file mode 100644 index 0000000..df04332 --- /dev/null +++ b/proof/quint/README.org @@ -0,0 +1,178 @@ +# SPDX-FileCopyrightText: 2026 tao3k team and Contributors +# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +#+title: Quint design-model qualification + +:PROPERTIES: +:STATUS: design-validation-partial +:DATE: 2026-10-11 +:END: + +* Model boundaries + +The maintained model source is Quint 0.33.0. =WorkCore.qnt= shares the +cancellation/completion distinction; =EffectAdmission.qnt= shares the current +grant predicate. Four finite models now check 25 contracts (including two +temporal cases), extending the former TLA+ lane's 22 cases: + +| Lane | Safe check | Required negative/reachability controls | +|------+------------+-----------------------------------------| +| work | cancellation does not drain active or queued work | cancellation and quiescence reachable | +| retirement | final cut requires quiescence and known inventory | cancellation-as-completion, stale review, unknown inventory, retirement reachable | +| ingress | retirement fences epoch and rejects later stale authority | unfenced retirement, cached grant, rejection and valid start reachable | +| lifecycle | current admission, decision, grant, one-shot reservation and delivery | untrusted report, late admission, early grant, after-revoke start, stale delivery, delivery before start, replay, valid start reachable, no-fair delivery counterexample | + +The =check_contract.py= runner checks all reachable finite states through +Quint's TLC backend, requires a completed queue for safe cases, and requires +the expected invariant failure and actual trace for each negative control. +The lifecycle's weak-fairness case is checked by =check_progress.py=: it compiles +the Quint source to a temporary TLA+ artifact and asks TLC to check +=EventuallyDelivery= under the Quint-defined =stableSpec=. The same property +fails under =stableSpecNoFair=. Generated TLA+ is a checker intermediate, not +a second maintained model. Quint's ordinary =verify --temporal= path cannot +select that fairness-bearing specification as TLC's =SPECIFICATION=. + +#+begin_src sh +npm ci --prefix proof/quint +AITIA_QUINT_BIN="$PWD/proof/quint/node_modules/.bin/quint" just check-quint +AITIA_QUINT_BIN="$PWD/proof/quint/node_modules/.bin/quint" just check-quint ingress +#+end_src + +The lockfile pins Quint 0.33.0 and its npm transitive graph. The checker +requires Apalache 0.62.1's TLC backend, Java and a local loopback endpoint. +Each check prints its name and result; a tool error or a stall is not a +successful counterexample. No implicit dependency on the POO Flow ASCENT pin +is introduced here: ASCENT belongs to the parent POO Flow dependency graph. + +* Claim boundary + +** Executable correspondence cut + +=just check-quint-scheme= runs Quint 0.33.0's TypeScript evaluator with native +=--mbt= action metadata and ITF traces. A fixed seed (34), 512 samples and an +18-step bound currently cover 177 of the TLC lane's 248 reachable ingress +states and all 13 safe actions. This is reproducible sampled correspondence, +not exhaustive Scheme refinement. The runner fails if sampled state coverage +falls below 177, or any safe action or the post-retirement rejection witness +is missing. Every projected field is checked +after every action against the actual serialized Scheme Host; effect starts +also check a real callback counter. Model schema changes fail closed. + +Two separately generated invariant-failure traces exercise =RetireWithoutFence= +and =TrustCachedGrant=. Their safe prefixes must match Scheme, while the final +unsafe transition must be rejected with no effect callback. A tool failure is +not a negative control. ITF is translated to closed data, never evaluated as +Scheme code. The tests retain Gerbil's native testing lifecycle and heap/time +bounds. =check-formal= includes source binding, Host controls, MBT replay, +exhaustive Quint checks and Lean checks; CI runs these distinct evidence lanes. + +=just check-lifecycle-scheme= exhausts the lifecycle TLC graph, requires its +complete 19,042-state queue and 21-field schema, then replays 3,084 covering +paths through real Scheme APIs. A DAG path cover plus real init prefixes covers every reachable +state, plus all 16 named safe actions. Every field is compared after every +step; model output is never used to manufacture Scheme receipts. This is +complete bounded state coverage, not replay of all 62,345 graph edges or a +general refinement proof. Renewal and terminal stutters remain explicit +API witnesses. The cover executes 64,206 state steps; each native batch is +bounded by 160 actual steps rather than a fixed number of traces. A single +oversized trace fails instead of being split or given more time. Four native +processes retain the existing per-test 1G heap / 60-second bound; failed batches, +missing final =OK=, incomplete +graphs, unknown actions and missing fault traces fail closed. + +Seven additional traces start at reachable states selected from that graph. +Quint executes the original unsafe operator and must produce an actual +invariant-failure ITF trace. Scheme must match the safe prefix, refuse the +final unsafe operation, preserve the projected state and execute no delivery +callback. This covers report-as-authority, stale admission, early grant, +after-revoke start, stale delivery, pre-start delivery and delivery replay. +The gate prints SHA-256 hashes of its declared model / Scheme source cut and +rejects source changes during the run. +Failed native inputs and partial diagnostics are preserved under +=.cache/lifecycle-failures/=; the shared checker watchdog terminates the owned +process group, not just its immediate launcher. Completion counters are +emitted only after processing a real trace and do not replace final =OK= / exit status. + +| Lifecycle stage | Actual Scheme operation | Authority / effect observation | +|-----------------+-------------------------+--------------------------------| +| Plan | =assurance-plan-verification= | selects the real obligation, no authority | +| Verify | =assurance-host-verify= | configured verifier runs and privately issues a seal | +| Admit | =assurance-host-admit-verified= | exact current seal, no verifier rerun or expiry renewal | +| Decide | =assurance-host-preflight-decision= | current admission becomes a preflight, not permission | +| Authorize | =assurance-host-cedar-request= / =assurance-effect-host-authorize!= | exact current request plus configured permit issues a private grant | +| StartEffect | =assurance-effect-host-reserve!= | consumes grant once; callback count stays zero | +| Delivery | =assurance-effect-host-deliver!= | rechecks source / epoch / strict expiry; acknowledged, rejected, failed or unknown | + +=lifecycle-boundary-test.ss= also checks deny / indeterminate authorization +(refusal stutters, never a grant), copied or foreign seals / requests / grants +/ reservations, intent substitution, mutation of an actually issued token, +in-place request intent / action / handoff payload mutation, +observed source ABA, expiry exactly at the boundary, source change during +authorization, throwing authorization / delivery, reentrancy and blind retry +after failure or unknown acknowledgement. Unknown acknowledgement may coexist +with a service-side effect, but is never reported as delivered or retry authority. +Renewing verification or issuing a fresh grant cannot extend an existing +reservation's source / epoch / deadline binding. + +| Quint cut | Scheme evidence | Remaining boundary | +|-----------+-----------------+--------------------| +| WorkCore / WorkLifecycle | cancellation, completion and queue observations in ingress replay; isolated queued-work refusal | fair completion is an environment assumption | +| RetirementReview | review, late activity, inventory loss/recovery and retirement in replay; missing-review / unknown-inventory controls | inventory truth is supplied by the local caller | +| RetirementIngress / EffectAdmission | 13 action projections, sampled state correspondence, unsafe-model refusals and real callbacks | one owner, cached grant, request and attempt only | +| AssuranceLifecycle | all 19,042 projected states; 16 safe actions; seven actual unsafe-model refusals; additional authorization / delivery unit controls | one obligation / attempt, serialized local adapter assumptions; no native Cedar evaluator or deployed delivery qualification | + +The Host exception regression additionally verifies that a throwing callback +propagates its failure and clears the transient handoff fence, without marking +work complete or permitting a second start. Exceptions, continuation capture +and process crashes are not transitions of the current Quint ingress model. + +** Qualification limits + +These are bounded design experiments, not generated projections of the POO +Contract, proof of Scheme/Runtime refinement, or deployed authority. A separate +local Scheme Host in =modules/sdlc/retirement-host.ss= executes the bounded +ingress fence/start cut, including one real effect callback, and +=t/formal/retirement-execution-test.ss= checks selected positive and negative +behaviors. This is an executable correspondence check, not a general refinement +proof or a deployed authority integration. The lifecycle bounds are two source revisions, four clock values +and three epochs. Ingress isolates one cached grant, one request and one +attempt; it does not model new grant issuance, multiple clients, persistence, +crash recovery or inventory truth. Fair progress assumes a stable source and +clock, no revocation, and weak fairness of each Host stage. It says nothing +about progress under a changing environment. + +The lifecycle effect Host is an explicit local composition adapter, not a new +Cedar policy engine or native Runtime Host. It composes existing POO Flow +semantic-root / token validation primitives with private exact-identity +issuance and one-shot reservation indexes. The configured authorization +operation must evaluate policy for the frozen request; the configured delivery +operation owns the actual effect and acknowledgement. The model's successful +handoff assumes trusted, synchronous callbacks under one serialized owner; +it does not prove atomic source / authority revalidation at a native service's +physical effect linearization point. Deferred work or continuation reuse is +outside this adapter contract. The lifecycle fixture supplies declared source +snapshot metadata and a controlled verifier, not a proof of verifier truth or +filesystem bytes. The existing evidence-admission tests remain a separate +source-reader qualification lane. The model's successful +authorization and fair delivery assume permit and eventual acknowledgement; +refusal, failed and unknown outcomes do not satisfy that progress premise. +The grant's synthetic local binding is not a native buffer / session capability. +Pure Cedar decisions, token-shaped objects and editable delivery receipts have +no issuing authority. A delivery receipt's =durable?= and +=distributed-authority?= are explicitly false. Strict expiry requires +=now < expires-at= even though the underlying pure token validator admits its +inclusive endpoint. +Strict expiry is rechecked at the final token-validation instant after source +validation; a clock crossing the deadline within reserve or deliver rejects +without a delivery callback. A native boundary control covers both operations +with an advancing effect clock. This is additional within-operation coverage, +not an asynchronous or physical-effect linearization proof. +Native dual-engine Cedar evaluation, distributed +deduplication, continuations, persistence and crash recovery are not qualified +by this local test cut. + +The retirement binding test hashes the declared Lean, Quint and Scheme source +cut. A current mapping alone is not semantic correspondence; the execution +test supplies bounded behavioral evidence for this particular Host. Multi-owner +concurrency, crash recovery, new grant issuance, and complete implementation +conformance remain open. diff --git a/proof/quint/RetirementIngress.qnt b/proof/quint/RetirementIngress.qnt new file mode 100644 index 0000000..12b1cec --- /dev/null +++ b/proof/quint/RetirementIngress.qnt @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2026 tao3k team and Contributors +// SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later +module RetirementIngress { + import WorkCore.* from "./WorkCore" + import EffectAdmission.* from "./EffectAdmission" + + type Ingress = {work: Work, known: bool, reviewed: bool, retired: bool, + lateFound: bool, gapFound: bool, ingressOpen: bool, epoch: int, + grantEpoch: int, pending: bool, attempted: bool, + rejectedAfterRetirement: bool, startedAfterRetirement: bool} + var q: Ingress + action init = q' = {work: {active: true, queued: true, cancelRequested: false}, + known: false, reviewed: false, retired: false, lateFound: false, gapFound: false, + ingressOpen: true, epoch: 0, grantEpoch: 0, pending: false, attempted: false, + rejectedAfterRetirement: false, startedAfterRetirement: false} + + action RequestCancellation = all {not(q.retired), not(q.work.cancelRequested), + q' = {...q, work: cancel(q.work)}} + action ObserveCompletion = all {not(q.retired), q.work.active, + q' = {...q, work: complete(q.work)}} + action ObserveQueueDrained = all {not(q.retired), q.work.queued, + q' = {...q, work: drain(q.work)}} + action CompleteInventory = all {not(q.retired), not(q.known), q' = {...q, known: true}} + action ReviewCut = all {not(q.retired), not(q.reviewed), quiescent(q.work), q.known, + q' = {...q, reviewed: true}} + action ObserveLateActivity = all {not(q.retired), q.reviewed, not(q.lateFound), + q' = {...q, work: {...q.work, active: true}, lateFound: true}} + action ObserveInventoryGap = all {not(q.retired), q.reviewed, not(q.gapFound), + q' = {...q, known: false, gapFound: true}} + action Observe = any {RequestCancellation, ObserveCompletion, ObserveQueueDrained, + CompleteInventory, ReviewCut, ObserveLateActivity, ObserveInventoryGap} + action CloseIngress = all {q.ingressOpen, q' = {...q, ingressOpen: false, epoch: q.epoch + 1}} + action Request = all {not(q.pending), not(q.attempted), q' = {...q, pending: true}} + val Current = currentGrant(1, 1, 1, 1, q.grantEpoch, q.epoch, 0, 1) + action Start = all {q.pending, Current, + q' = {...q, work: {...q.work, active: true}, pending: false, attempted: true, + startedAfterRetirement: q.retired}} + action TrustCachedGrant = all {q.pending, + q' = {...q, work: {...q.work, active: true}, pending: false, attempted: true, + startedAfterRetirement: q.retired}} + action Reject = all {q.pending, not(Current), + q' = {...q, pending: false, attempted: true, rejectedAfterRetirement: q.retired}} + action Retire = all {not(q.ingressOpen), not(q.retired), q.reviewed, + quiescent(q.work), q.known, q' = {...q, retired: true}} + action RetireWithoutFence = all {not(q.retired), q.reviewed, + quiescent(q.work), q.known, q' = {...q, retired: true}} + action Terminal = all {q.retired, q.attempted, q' = q} + action Environment = any {Observe, CloseIngress, Request, Reject, Terminal} + action step = any {Environment, Start, Retire} + action unfencedStep = any {Environment, Start, RetireWithoutFence} + action cachedGrantStep = any {Environment, TrustCachedGrant, Retire} + + val TypeOK = q.epoch >= 0 and q.epoch <= 1 and q.grantEpoch == 0 + val RetiredIsFenced = q.retired implies (not(q.ingressOpen) and q.grantEpoch != q.epoch) + val NoPostRetirementStart = not(q.startedAfterRetirement) + val NoOutstandingWork = q.retired implies quiescent(q.work) + val InventoryKnown = q.retired implies q.known + val NeverRejectAfterRetirement = not(q.rejectedAfterRetirement) + val NeverStarted = not(q.attempted and not(q.rejectedAfterRetirement) and q.epoch == 0) +} diff --git a/proof/quint/RetirementReview.qnt b/proof/quint/RetirementReview.qnt new file mode 100644 index 0000000..8b1a7ff --- /dev/null +++ b/proof/quint/RetirementReview.qnt @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2026 tao3k team and Contributors +// SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later +module RetirementReview { + import WorkCore.* from "./WorkCore" + + type Review = {work: Work, known: bool, reviewed: bool, retired: bool, + lateFound: bool, gapFound: bool} + var q: Review + action init = q' = {work: {active: true, queued: true, cancelRequested: false}, + known: false, reviewed: false, retired: false, lateFound: false, gapFound: false} + + action RequestCancellation = all {not(q.retired), not(q.work.cancelRequested), + q' = {...q, work: cancel(q.work)}} + action ObserveCompletion = all {not(q.retired), q.work.active, + q' = {...q, work: complete(q.work)}} + action ObserveQueueDrained = all {not(q.retired), q.work.queued, + q' = {...q, work: drain(q.work)}} + action CompleteInventory = all {not(q.retired), not(q.known), q' = {...q, known: true}} + action ReviewCut = all {not(q.retired), not(q.reviewed), quiescent(q.work), q.known, + q' = {...q, reviewed: true}} + action ObserveLateActivity = all {not(q.retired), q.reviewed, not(q.lateFound), + q' = {...q, work: {...q.work, active: true}, lateFound: true}} + action ObserveInventoryGap = all {not(q.retired), q.reviewed, not(q.gapFound), + q' = {...q, known: false, gapFound: true}} + action Observe = any {RequestCancellation, ObserveCompletion, ObserveQueueDrained, + CompleteInventory, ReviewCut, ObserveLateActivity, ObserveInventoryGap} + action Retire = all {not(q.retired), q.reviewed, quiescent(q.work), q.known, + q' = {...q, retired: true}} + action TrustCancellation = all {not(q.retired), q.work.cancelRequested, q.known, + q' = {...q, retired: true}} + action TrustOldReview = all {not(q.retired), q.reviewed, q.known, + q' = {...q, retired: true}} + action IgnoreInventoryGap = all {not(q.retired), q.reviewed, quiescent(q.work), + q' = {...q, retired: true}} + action Terminal = all {q.retired, q' = q} + action step = any {Observe, Retire, Terminal} + action cancellationStep = any {Observe, TrustCancellation, Terminal} + action staleStep = any {Observe, TrustOldReview, Terminal} + action unknownStep = any {Observe, IgnoreInventoryGap, Terminal} + + val TypeOK = true + val NoOutstandingWork = q.retired implies quiescent(q.work) + val InventoryKnown = q.retired implies q.known + val NeverRetired = not(q.retired) +} diff --git a/proof/quint/WorkCore.qnt b/proof/quint/WorkCore.qnt new file mode 100644 index 0000000..00e058e --- /dev/null +++ b/proof/quint/WorkCore.qnt @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2026 tao3k team and Contributors +// SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later +module WorkCore { + type Work = {active: bool, queued: bool, cancelRequested: bool} + pure def cancel(w: Work): Work = {...w, cancelRequested: true} + pure def complete(w: Work): Work = {...w, active: false} + pure def drain(w: Work): Work = {...w, queued: false} + pure def quiescent(w: Work): bool = not(w.active) and not(w.queued) +} diff --git a/proof/quint/WorkLifecycle.qnt b/proof/quint/WorkLifecycle.qnt new file mode 100644 index 0000000..7c243e4 --- /dev/null +++ b/proof/quint/WorkLifecycle.qnt @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2026 tao3k team and Contributors +// SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later +module WorkLifecycle { + import WorkCore.* from "./WorkCore" + // Cancellation is a request, never an observation of completion. + var q: Work + action init = q' = {active: true, queued: true, cancelRequested: false} + action RequestCancellation = all {not(q.cancelRequested), q' = cancel(q)} + action ObserveCompletion = all {q.active, q' = complete(q)} + action ObserveQueueDrained = all {q.queued, q' = drain(q)} + action Terminal = all {quiescent(q), q.cancelRequested, q' = q} + action step = any {RequestCancellation, ObserveCompletion, ObserveQueueDrained, Terminal} + action cancellationStep = any {RequestCancellation, all {q.cancelRequested, q' = q}} + val TypeOK = true + val WorkRemains = q.active and q.queued + val NeverCancelled = not(q.cancelRequested) + val NeverQuiescent = not(quiescent(q)) +} diff --git a/proof/quint/check_contract.py b/proof/quint/check_contract.py new file mode 100644 index 0000000..50c9c29 --- /dev/null +++ b/proof/quint/check_contract.py @@ -0,0 +1,98 @@ +# SPDX-FileCopyrightText: 2026 tao3k team and Contributors +# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +"""Exhaust Quint design models and require typed negative-control traces.""" + +from __future__ import annotations + +import argparse +import os +from pathlib import Path +import re +import shutil +import subprocess + +from process_runner import run + + +ROOT = Path(__file__).resolve().parent +QUINT = os.environ.get("AITIA_QUINT_BIN") or shutil.which("quint") +COUNTS = re.compile(r"(\d+) states generated, (\d+) distinct states found, (\d+) states left on queue") +SAFE_DISTINCT = {"work-safe": 8, "work-cancellation": 2, + "retirement-safe": 42, "ingress-safe": 248, + "lifecycle-safe": 19042} +# lane, name, model, transition, invariant(s), expected counterexample +CASES = ( + ("work", "work-safe", "WorkLifecycle", "step", "TypeOK", False), + ("work", "work-cancellation", "WorkLifecycle", "cancellationStep", "WorkRemains", False), + ("work", "work-cancel-reachable", "WorkLifecycle", "cancellationStep", "NeverCancelled", True), + ("work", "work-quiescent-reachable", "WorkLifecycle", "step", "NeverQuiescent", True), + ("retirement", "retirement-safe", "RetirementReview", "step", "TypeOK,NoOutstandingWork,InventoryKnown", False), + ("retirement", "retirement-trust-cancel", "RetirementReview", "cancellationStep", "NoOutstandingWork", True), + ("retirement", "retirement-stale-review", "RetirementReview", "staleStep", "NoOutstandingWork", True), + ("retirement", "retirement-unknown-inventory", "RetirementReview", "unknownStep", "InventoryKnown", True), + ("retirement", "retirement-reachable", "RetirementReview", "step", "NeverRetired", True), + ("ingress", "ingress-safe", "RetirementIngress", "step", "TypeOK,RetiredIsFenced,NoPostRetirementStart,NoOutstandingWork,InventoryKnown", False), + ("ingress", "ingress-unfenced", "RetirementIngress", "unfencedStep", "NoPostRetirementStart", True), + ("ingress", "ingress-cached-grant", "RetirementIngress", "cachedGrantStep", "NoPostRetirementStart", True), + ("ingress", "ingress-reject-reachable", "RetirementIngress", "step", "NeverRejectAfterRetirement", True), + ("ingress", "ingress-start-reachable", "RetirementIngress", "step", "NeverStarted", True), + ("lifecycle", "lifecycle-safe", "AssuranceLifecycle", "step", "TypeOK,AdmissionIsCurrent,DecisionRequiresAdmission,GrantRequiresCurrentDecision,NoUnsafeEffectStart,NoUnsafeDelivery,DeliveryRequiresStart,AtMostOneDelivery", False), + ("lifecycle", "lifecycle-start-reachable", "AssuranceLifecycle", "step", "NoEffectStarted", True), + ("lifecycle", "lifecycle-untrusted-report", "AssuranceLifecycle", "unsafeStep", "NoUnsafeEffectStart", True), + ("lifecycle", "lifecycle-late-admission", "AssuranceLifecycle", "lateAdmitStep", "AdmissionIsCurrent", True), + ("lifecycle", "lifecycle-early-grant", "AssuranceLifecycle", "earlyGrantStep", "GrantRequiresCurrentDecision", True), + ("lifecycle", "lifecycle-after-revoke", "AssuranceLifecycle", "afterRevokeStep", "NoUnsafeEffectStart", True), + ("lifecycle", "lifecycle-stale-delivery", "AssuranceLifecycle", "unsafeDeliveryStep", "NoUnsafeDelivery", True), + ("lifecycle", "lifecycle-delivery-before-start", "AssuranceLifecycle", "beforeStartStep", "DeliveryRequiresStart", True), + ("lifecycle", "lifecycle-replay-delivery", "AssuranceLifecycle", "replayStep", "AtMostOneDelivery", True), +) + + +def check_case(case: tuple) -> None: + lane, name, model, transition, invariants, negative = case + print(f"QUINT-CHECK: {name}", flush=True) + command = [QUINT, "verify", str(ROOT / f"{model}.qnt"), "--backend", "tlc", + "--step", transition, "--tlc-config", str(ROOT / "tlc-config.json"), + "--verbosity", "3"] + if "," in invariants: + command += ["--invariants", *invariants.split(",")] + else: + command += ["--invariant", invariants] + status, output = run(command, ROOT, name) + counts = COUNTS.search(output) + expected_code = 1 if negative else 0 + marker = "Error: Invariant q_inv is violated." if negative else "[ok] No violation found" + if (status != expected_code or marker not in output or not counts + or int(counts[2]) == 0 or (not negative and int(counts[3]) != 0) + or (name in SAFE_DISTINCT and int(counts[2]) != SAFE_DISTINCT[name]) + or (negative and ("error: found a counterexample" not in output + or "behavior up to this point" not in output))): + raise SystemExit(f"QUINT-CHECK-FAILED: {name} exit={status}\n{output[-6000:]}") + print(f"QUINT-CHECK-OK: {name} distinct={counts[2]} exit={status}", flush=True) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("lane", nargs="?", default="all", + choices=("all", "work", "retirement", "ingress", "lifecycle")) + selected = parser.parse_args().lane + if not QUINT: + parser.error("Quint executable is required") + version = subprocess.check_output((QUINT, "--version"), text=True, timeout=5).strip() + if version != "0.33.0": + parser.error(f"expected Quint 0.33.0, found {version!r}") + for case in CASES: + if selected in ("all", case[0]): + check_case(case) + if selected in ("all", "lifecycle"): + result = subprocess.run(("python3", str(ROOT / "check_progress.py")), + cwd=ROOT, timeout=120, check=False) + if result.returncode: + raise SystemExit(result.returncode) + print("QUINT-CHECK-OK: selected model contracts complete", flush=True) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/proof/quint/check_lifecycle_replay.py b/proof/quint/check_lifecycle_replay.py new file mode 100644 index 0000000..94f89a4 --- /dev/null +++ b/proof/quint/check_lifecycle_replay.py @@ -0,0 +1,322 @@ +# SPDX-FileCopyrightText: 2026 tao3k team and Contributors +# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later +"""Exhaust the Quint lifecycle graph and replay every reachable state in Scheme.""" + +from collections import deque +from concurrent.futures import ThreadPoolExecutor, as_completed +import hashlib +import json +import os +from pathlib import Path +import re +import subprocess +import tempfile + +from process_runner import run + +ROOT = Path(__file__).resolve().parent +REPO = ROOT.parent.parent +ACTIONS = ("InstallAdapter", "ReportSuccess", "Plan", "Verify", "Admit", "Decide", + "Authorize", "StartEffect", "DeliverEffect", "ChangeSource", "Tick", "Revoke", + "RejectDelivery", "FailDelivery", "UnknownDelivery", "Terminal") +FIELDS = ("revision", "planned", "verified", "admitted", "decided", "granted", + "grantEpoch", "epoch", "clock", "sealUntil", "grantUntil", "adapterInstalled", + "reportedSuccess", "effectStarted", "startSafe", "reservationRevision", + "reservationEpoch", "reservationUntil", "deliveryStatus", "deliveryCalls", "deliverySafe") +INVARIANTS = ("TypeOK", "AdmissionIsCurrent", "DecisionRequiresAdmission", + "GrantRequiresCurrentDecision", "NoUnsafeEffectStart", "NoUnsafeDelivery", + "DeliveryRequiresStart", "AtMostOneDelivery") +FAULTS = (("unsafeStep", "NoUnsafeEffectStart", "UnsafeStart"), + ("lateAdmitStep", "AdmissionIsCurrent", "UnsafeLateAdmit"), + ("earlyGrantStep", "GrantRequiresCurrentDecision", "UnsafeEarlyGrant"), + ("afterRevokeStep", "NoUnsafeEffectStart", "UnsafeAfterRevoke"), + ("unsafeDeliveryStep", "NoUnsafeDelivery", "UnsafeDelivery"), + ("beforeStartStep", "DeliveryRequiresStart", "UnsafeBeforeStart"), + ("replayStep", "AtMostOneDelivery", "UnsafeReplay")) +SOURCE_CUT = ("proof/quint/AssuranceLifecycle.qnt", "proof/quint/EffectAdmission.qnt", + "proof/quint/package-lock.json", "proof/quint/check_lifecycle_replay.py", + "proof/quint/test_lifecycle_replay.py", + "proof/quint/process_runner.py", + "modules/assurance/host.ss", "modules/assurance/effect-host.ss", + "modules/assurance/interface.ss", "t/formal/lifecycle-fixture.ss", + "t/formal/lifecycle-replay-test.ss", "t/assurance/unit/lifecycle-boundary-test.ss") + + +def source_hashes(): + return {path: hashlib.sha256((REPO / path).read_bytes()).hexdigest() for path in SOURCE_CUT} + + +def export_graph(quint, work): + status, output = run([quint, "compile", str(ROOT / "AssuranceLifecycle.qnt"), + "--target", "tlaplus", "--step", "step"], ROOT, "lifecycle-compile") + start = output.find("-------------------------- MODULE AssuranceLifecycle") + if status or start < 0 or not output.rstrip().endswith("===="): + raise RuntimeError(f"Quint compilation failed: {output[-2000:]}") + # Only a checker intermediate: all transition bodies come from Quint. + # Expanding the compiled step into its named leaf operators lets TLC label + # edges without adding a history variable / changing the state quotient. + model = (ROOT / "AssuranceLifecycle.qnt").read_text() + unions = dict(re.findall(r"action\s+(\w+)\s*=\s*any\s*\{([^}]+)\}", model)) + def leaf_actions(name): + if name not in unions: + return {name} + return set().union(*(leaf_actions(part.strip()) for part in unions[name].split(","))) + if leaf_actions("step") != set(ACTIONS): + raise RuntimeError("Quint action surface changed") + source = re.sub(r"=+\s*$", "", output[start:]) + source += "\nReplayNext == " + " \\/ ".join(ACTIONS) + "\n====\n" + (work / "AssuranceLifecycle.tla").write_text(source) + (work / "safe.cfg").write_text("INIT init\nNEXT ReplayNext\nINVARIANTS " + " ".join(INVARIANTS) + "\n") + jar = Path(os.environ.get("QUINT_HOME", str(Path.home() / ".quint"))) / "apalache-dist-0.62.1/apalache/lib/apalache.jar" + if not jar.is_file(): + raise RuntimeError("pinned Quint TLC backend jar is required") + status, output = run(["java", "-Xmx512m", "-cp", str(jar), "tlc2.TLC", "-workers", "1", + "-metadir", str(work / "meta"), "-config", str(work / "safe.cfg"), + "-dump", "dot,actionlabels", str(work / "graph"), + str(work / "AssuranceLifecycle.tla")], work, "lifecycle-graph") + counts = re.search(r"(\d+) states generated, (\d+) distinct states found, (\d+) states left on queue", output) + if status or "Model checking completed. No error has been found." not in output or not counts or int(counts[3]): + raise RuntimeError(f"incomplete lifecycle graph: exit={status}\n{output[-3000:]}") + return int(counts[2]), work / "graph.dot" + + +def read_graph(path, expected): + nodes, edges, initial = {}, [], [] + for line in path.read_text().splitlines(): + node = re.match(r'\s*"?(-?\d+)"?\s+\[label="([^"]*)"', line) + if node: + values = {key: (value == "TRUE" if value in ("TRUE", "FALSE") else int(value)) + for key, value in re.findall(r"(\w+)\s*\|->\s*(TRUE|FALSE|-?\d+)", node[2])} + if set(values) != set(FIELDS): + raise ValueError(f"model state schema changed: {set(values) ^ set(FIELDS)}") + nodes[node[1]] = values + if "style = filled" in line or "style=filled" in line: + initial.append(node[1]) + edge = re.match(r'\s*"?(-?\d+)"?\s*->\s*"?(-?\d+)"?.*label="([^"]+)"', line) + if edge: + label = edge[3].split("\\n")[0] + if label not in ACTIONS: + raise ValueError(f"unknown TLC action label {label!r}") + edges.append((edge[1], edge[2], label)) + if len(nodes) != expected or len(initial) != 1 or not edges: + raise ValueError(f"incomplete graph: nodes={len(nodes)}/{expected} initial={initial} edges={len(edges)}") + if any(origin not in nodes or target not in nodes for origin, target, _ in edges): + raise ValueError("edge references a missing state") + return nodes, edges, initial[0] + + +def covering_traces(nodes, edges, initial): + adjacency = {} + for origin, target, action in edges: + adjacency.setdefault(origin, []).append((target, action)) + paths = {initial: [("init", nodes[initial])]} + pending = deque([initial]) + while pending: + origin = pending.popleft() + for target, action in adjacency.get(origin, []): + if target not in paths: + paths[target] = paths[origin] + [(action, nodes[target])] + pending.append(target) + if set(paths) != set(nodes): + raise ValueError("TLC dump contains unreachable or missing nodes") + # Ignoring self loops, this bounded lifecycle is a DAG. Its minimum vertex + # path cover is a bipartite maximum matching. Each path gets a real safe + # prefix from init, then continues through original TLC edges. No Host is + # cloned or reset mid-path, and no model state is synthesized in Scheme. + distinct = {origin: list(dict.fromkeys(target for target, _ in targets if target != origin)) + for origin, targets in adjacency.items()} + indegree = dict.fromkeys(nodes, 0) + for targets in distinct.values(): + for target in targets: + indegree[target] += 1 + queue = deque(node for node in nodes if not indegree[node]) + ordered = [] + while queue: + origin = queue.popleft() + ordered.append(origin) + for target in distinct.get(origin, []): + indegree[target] -= 1 + if not indegree[target]: + queue.append(target) + if len(ordered) != len(nodes): + raise ValueError("non-stuttering lifecycle graph is not a DAG") + left, right = {}, {} + def augment(origin, seen): + for target in distinct.get(origin, []): + if target in seen: + continue + seen.add(target) + if target not in right or augment(right[target], seen): + left[origin], right[target] = target, origin + return True + return False + for origin in nodes: + augment(origin, set()) + labels = {(origin, target): action for origin, target, action in edges} + traces = [] + for origin in nodes: + if origin in right: + continue + trace = list(paths[origin]) + while origin in left: + target = left[origin] + trace.append((labels[origin, target], nodes[target])) + origin = target + traces.append(trace) + covered = {action for trace in traces for action, _ in trace} + stutters = {action for origin, target, action in edges if origin == target} + for action in ACTIONS: + if action not in covered and action not in stutters: + origin, target, _ = next(edge for edge in edges if edge[2] == action) + traces.append(paths[origin] + [(action, nodes[target])]) + # Preserve renewal and terminal stutter witnesses as actual API calls. + for action in ACTIONS: + if action not in stutters: + continue + origin, target, _ = min((edge for edge in edges if edge[0] == edge[1] and edge[2] == action), + key=lambda edge: len(paths[edge[0]])) + traces.append(paths[origin] + [(action, nodes[target])]) + if {json.dumps(state, sort_keys=True) for trace in traces for _, state in trace} != { + json.dumps(state, sort_keys=True) for state in nodes.values()}: + raise ValueError("covering paths do not cover the complete state space") + return traces + + +def batches(traces, step_budget=160): + batch, steps = [], 0 + for trace in traces: + if len(trace) > step_budget: + raise ValueError("one trace exceeds the native step budget") + if batch and steps + len(trace) > step_budget: + yield batch + batch, steps = [], 0 + batch.append(trace) + steps += len(trace) + if batch: + yield batch + + +def sexpr_trace(trace): + return "(" + " ".join( + f"({action} (" + " ".join(f"({key} {'#t' if value is True else '#f' if value is False else value})" + for key, value in state.items()) + "))" + for action, state in trace) + ")" + + +def negative_traces(quint, work, safe): + current = lambda q: (q["granted"] == q["revision"] == q["admitted"] == q["decided"] + and q["grantEpoch"] == q["epoch"] and q["clock"] < q["grantUntil"]) + selectors = { + "UnsafeStart": lambda q: q["reportedSuccess"] and not q["effectStarted"] and not current(q), + "UnsafeLateAdmit": lambda q: q["revision"] == 2 and q["reportedSuccess"] and q["admitted"] == 0, + "UnsafeEarlyGrant": lambda q: q["revision"] == 2 and q["reportedSuccess"] and q["admitted"] == 0 and q["granted"] == 0 and q["clock"] < 3, + "UnsafeAfterRevoke": lambda q: q["epoch"] > q["grantEpoch"] and q["grantUntil"] > q["clock"] and q["granted"] == 0 and not q["effectStarted"], + "UnsafeDelivery": lambda q: q["effectStarted"] and q["deliveryStatus"] == 0 and not current(q), + "UnsafeBeforeStart": lambda q: q["reportedSuccess"] and not q["effectStarted"], + "UnsafeReplay": lambda q: q["deliveryStatus"] == 1, + } + traces = [] + for step, invariant, fault in FAULTS: + prefixes = [trace[:index + 1] for trace in safe for index, (_, state) in enumerate(trace) + if selectors[fault](state)] + if not prefixes: + raise RuntimeError(f"no reachable fault input for {fault}") + prefix = min(prefixes, key=len) + state = prefix[-1][1] + record = "{" + ", ".join(f"{key}: {str(value).lower() if isinstance(value, bool) else value}" + for key, value in state.items()) + "}" + # Select an actually reachable input, then execute the original Quint + # fault operator. No guessed output state or random-hit requirement. + source = (ROOT / "AssuranceLifecycle.qnt").read_text() + generated = work / f"{fault}.qnt" + generated.write_text(source.rstrip()[:-1] + f"\n action FaultInit = q' = {record}\n}}\n") + # Import resolution remains pinned to the same source cut. + (work / "EffectAdmission.qnt").write_text((ROOT / "EffectAdmission.qnt").read_text()) + status, output = run([quint, "run", str(generated), "--main", "AssuranceLifecycle", + "--backend", "typescript", "--seed", "34", "--max-samples", "1", + "--n-traces", "1", "--max-steps", "1", "--mbt", "--init", "FaultInit", "--step", fault, + "--invariant", invariant, "--verbosity", "1", + "--out-itf", str(work / f"{fault}_{{seq}}.json")], REPO, fault) + if status != 1 or "Invariant violated" not in output: + raise RuntimeError(f"missing typed counterexample for {fault}: {output[-2000:]}") + candidates = [json.loads(path.read_text())["states"] for path in work.glob(f"{fault}_*.json")] + candidates = [trace for trace in candidates if trace[-1]["mbt::actionTaken"] == fault] + if not candidates: + raise RuntimeError(f"no failing ITF trace for {fault}") + trace = list(prefix) + for row in candidates[0][1:]: + action, state = row["mbt::actionTaken"], row["q"] + if action not in set(ACTIONS) | {"init", fault} or set(state) != set(FIELDS): + raise ValueError("negative model action/state schema changed") + converted = {} + for key, value in state.items(): + if isinstance(value, bool): + converted[key] = value + elif isinstance(value, dict) and set(value) == {"#bigint"}: + converted[key] = int(value["#bigint"]) + else: + raise ValueError("unexpected ITF scalar") + trace.append((action, converted)) + traces.append(trace) + return traces + + +def replay(work, chunk, traces): + fixture = work / f"traces-{chunk}.scm" + fixture.write_text("(" + "\n".join(map(sexpr_trace, traces)) + ")\n") + try: + status, output = run(["env", f"AITIA_LIFECYCLE_REPLAY={fixture}", "just", + "test-scheme-atomic", "formal", "lifecycle-replay-test.ss"], + REPO, f"native-lifecycle-{chunk}", timeout=65) + if status or not re.search(r"^OK$", output, re.MULTILINE): + raise RuntimeError(f"Scheme replay failed: chunk={chunk} exit={status}\n{output[-8000:]}") + except (RuntimeError, TimeoutError) as error: + # Keep the exact failed input after the temporary graph is removed. + evidence = REPO / ".cache/lifecycle-failures" + evidence.mkdir(parents=True, exist_ok=True) + saved = evidence / f"traces-{chunk}.scm" + saved.write_bytes(fixture.read_bytes()) + (evidence / f"traces-{chunk}.log").write_text(str(error)) + raise RuntimeError(f"{error}\nFAILED-LIFECYCLE-INPUT: {saved}") from error + return len(traces) + + +def main(): + hashes = source_hashes() + quint = os.environ.get("AITIA_QUINT_BIN", str(ROOT / "node_modules/.bin/quint")) + if subprocess.check_output([quint, "--version"], text=True, timeout=5).strip() != "0.33.0": + raise RuntimeError("Quint 0.33.0 is required") + with tempfile.TemporaryDirectory(prefix="aitia-lifecycle-") as temporary: + work = Path(temporary) + count, graph = export_graph(quint, work) + if count != 19042: + raise RuntimeError(f"lifecycle state quotient changed: {count} != 19042") + nodes, edges, initial = read_graph(graph, count) + traces = covering_traces(nodes, edges, initial) + covered = {edge[2] for edge in edges} + if covered != set(ACTIONS): + raise RuntimeError(f"incomplete action coverage: {covered ^ set(ACTIONS)}") + print(f"LIFECYCLE-GRAPH-OK: states={count} graph-edges={len(edges)} actions={len(covered)} traces={len(traces)}", flush=True) + faults = negative_traces(quint, work, traces) + replay(work, "faults", faults) + completed = 0 + # Four bounded native processes; each retains the existing 1G/60s gate. + with ThreadPoolExecutor(max_workers=4) as pool: + jobs = [pool.submit(replay, work, chunk, batch) + for chunk, batch in enumerate(batches(traces))] + try: + for job in as_completed(jobs): + completed += job.result() + print(f"LIFECYCLE-REPLAY: traces={completed}/{len(traces)}", flush=True) + except BaseException: + for job in jobs: + job.cancel() + raise + if hashes != source_hashes(): + raise RuntimeError("declared lifecycle source cut changed during verification") + print("LIFECYCLE-SOURCE-CUT: " + json.dumps(hashes, sort_keys=True), flush=True) + print(f"LIFECYCLE-SCHEME-OK: states={count}/{count} actions={len(covered)}/16 traces={completed}/{len(traces)} faults={len(faults)}/7", flush=True) + + +if __name__ == "__main__": + main() diff --git a/proof/quint/check_progress.py b/proof/quint/check_progress.py new file mode 100644 index 0000000..8517394 --- /dev/null +++ b/proof/quint/check_progress.py @@ -0,0 +1,77 @@ +# SPDX-FileCopyrightText: 2026 tao3k team and Contributors +# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +"""Check Quint's fair and unfair lifecycle specs with TLC's native temporal lane. + +Quint's ``verify --temporal`` places its default transition spec beside the +selected property; TLC rejects an implication that embeds weak-fairness actions. +Compiling the Quint source and selecting its named spec in a TLC config keeps +the source of truth in Quint while exercising the exact fairness formula. +""" + +from __future__ import annotations + +import os +from pathlib import Path +import re +import shutil +import subprocess +import tempfile + +from process_runner import run + + +ROOT = Path(__file__).resolve().parent +QUINT = os.environ.get("AITIA_QUINT_BIN") or shutil.which("quint") +APALACHE = (Path(os.environ["QUINT_HOME"]) if "QUINT_HOME" in os.environ + else Path.home() / ".quint") / "apalache-dist-0.62.1/apalache/lib/apalache.jar" + + +def main() -> int: + if not QUINT or not APALACHE.is_file(): + raise SystemExit("Quint 0.33.0 and its Apalache 0.62.1 TLC jar are required") + version = subprocess.check_output((QUINT, "--version"), text=True, timeout=5).strip() + if version != "0.33.0": + raise SystemExit(f"expected Quint 0.33.0, found {version}") + print("QUINT-PROGRESS: compiling lifecycle to temporary TLA+", flush=True) + compiled_status, compiled_output = run( + (QUINT, "compile", str(ROOT / "AssuranceLifecycle.qnt"), + "--target", "tlaplus", "--step", "stableStep", + "--temporal", "EventuallyDelivery"), + ROOT, "quint-compile", + ) + module_start = compiled_output.find("-------------------------- MODULE AssuranceLifecycle") + source = compiled_output[module_start:] if module_start >= 0 else "" + if compiled_status or module_start < 0 or not source.rstrip().endswith("===="): + raise SystemExit(f"Quint compilation failed: {compiled_output[-3000:]}") + with tempfile.TemporaryDirectory(prefix="aitia-quint-progress-") as directory: + work = Path(directory) + (work / "AssuranceLifecycle.tla").write_text(source) + for name, spec, expected_code, marker in ( + ("fair", "stableSpec", 0, "Model checking completed. No error has been found."), + ("no-fair", "stableSpecNoFair", 13, "Error: Temporal property EventuallyDelivery was violated."), + ): + print(f"QUINT-PROGRESS: checking {name}", flush=True) + config = work / f"{name}.cfg" + config.write_text(f"SPECIFICATION {spec}\nPROPERTY EventuallyDelivery\n") + meta = work / f"meta-{name}" + meta.mkdir() + status, output = run( + ("java", "-XX:+UseParallelGC", "-Xmx512m", "-cp", str(APALACHE), + "tlc2.TLC", "-workers", "1", "-metadir", str(meta), + "-config", str(config), str(work / "AssuranceLifecycle.tla")), + work, f"quint-progress-{name}", + ) + counts = re.search(r"(\d+) states generated, (\d+) distinct states found, (\d+) states left on queue", output) + if status != expected_code or marker not in output or not counts: + raise SystemExit(f"QUINT-PROGRESS-FAILED: {name} exit={status}\n{output[-5000:]}") + if int(counts[2]) != 20 or int(counts[3]) != 0: + raise SystemExit(f"QUINT-PROGRESS-FAILED: unexpected {name} state space") + if expected_code and "counter-example" not in output: + raise SystemExit(f"QUINT-PROGRESS-FAILED: no counterexample for {name}") + print(f"QUINT-PROGRESS-OK: {name} distinct={counts[2]}", flush=True) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/proof/quint/check_scheme_replay.py b/proof/quint/check_scheme_replay.py new file mode 100644 index 0000000..00c5d24 --- /dev/null +++ b/proof/quint/check_scheme_replay.py @@ -0,0 +1,128 @@ +# SPDX-FileCopyrightText: 2026 tao3k team and Contributors +# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +"""Replay pinned Quint MBT traces through the actual serialized Scheme Host. + +Sampling is not exhaustive refinement. Require all safe actions and both real +unsafe-model counterexamples; compare every projected state, not just outcomes. +""" + +from __future__ import annotations + +import json +import os +from pathlib import Path +import shutil +import subprocess +import tempfile + +from process_runner import run + +ROOT = Path(__file__).resolve().parent +REPO = ROOT.parent.parent +ACTIONS = {"RequestCancellation", "ObserveCompletion", "ObserveQueueDrained", + "CompleteInventory", "ReviewCut", "ObserveLateActivity", + "ObserveInventoryGap", "CloseIngress", "Request", "Start", "Reject", + "Retire", "Terminal"} +FIELDS = (("active?", "work", "active"), ("queued?", "work", "queued"), + ("cancellation-requested?", "work", "cancelRequested"), + ("inventory-known?", "known"), ("reviewed?", "reviewed"), + ("late-activity-found?", "lateFound"), ("inventory-gap-found?", "gapFound"), + ("retired?", "retired"), ("ingress-open?", "ingressOpen"), + ("authority-epoch", "epoch"), ("cached-grant-epoch", "grantEpoch"), + ("pending?", "pending"), ("attempted?", "attempted")) + + +def scalar(value): + if isinstance(value, bool): + return "#t" if value else "#f" + if isinstance(value, dict) and set(value) == {"#bigint"}: + return str(int(value["#bigint"])) + raise ValueError(f"unexpected ITF scalar: {value!r}") + + +def state_row(state): + q = state["q"] + expected_keys = {path[1] for path in FIELDS} | {"rejectedAfterRetirement", "startedAfterRetirement"} + if set(q) != expected_keys or set(q["work"]) != {"active", "queued", "cancelRequested"}: + raise ValueError("model state schema changed; update the explicit correspondence") + fields = [] + for slot, *path in FIELDS: + value = q + for key in path: + value = value[key] + fields.append(f"({slot} {scalar(value)})") + # MBT action names are a closed set, never arbitrary executable Scheme. + action = state["mbt::actionTaken"] + if action not in ACTIONS | {"init", "TrustCachedGrant", "RetireWithoutFence"}: + raise ValueError(f"unknown Quint action: {action!r}") + return f"({action} ({' '.join(fields)}))" + + +def generate(quint, directory, name, step, invariant, samples): + print(f"QUINT-SCHEME: generate {name}", flush=True) + negative = step != "step" + status, output = run( + [quint, "run", str(ROOT / "RetirementIngress.qnt"), "--backend", "typescript", + "--seed", "34", "--max-samples", str(samples), + "--n-traces", "1" if negative else str(samples), + "--max-steps", "18", "--mbt", "--step", step, "--invariant", invariant, + "--verbosity", "1", "--out-itf", str(directory / f"{name}_{{seq}}.json")], + REPO, name) + marker = "Invariant violated" if negative else "[ok] No violation found" + if status != int(negative) or marker not in output: + raise RuntimeError(f"{name}: unexpected Quint result exit={status}\n{output[-2000:]}") + traces = [json.loads(path.read_text())["states"] + for path in sorted(directory.glob(f"{name}_*.json"))] + if not traces or (not negative and len(traces) != samples): + raise RuntimeError(f"{name}: incomplete trace output ({len(traces)})") + if negative: + # Quint may retain passing samples too. Only actual failing traces + # belong to the negative lane; their final unsafe action is refused. + fault = "RetireWithoutFence" if step == "unfencedStep" else "TrustCachedGrant" + traces = [trace for trace in traces if trace[-1]["mbt::actionTaken"] == fault + and (trace[-1]["q"]["retired"] if fault == "RetireWithoutFence" + else trace[-1]["q"]["startedAfterRetirement"])] + if not traces: + raise RuntimeError(f"{name}: missing typed unsafe counterexample") + return traces + + +def main(): + quint = os.environ.get("AITIA_QUINT_BIN") or shutil.which("quint") + if not quint: + quint = str(ROOT / "node_modules/.bin/quint") + version = subprocess.check_output([quint, "--version"], text=True, timeout=5).strip() + if version != "0.33.0": + raise RuntimeError(f"expected Quint 0.33.0, found {version!r}") + with tempfile.TemporaryDirectory(prefix="aitia-quint-replay-") as temporary: + directory = Path(temporary) + safe = generate(quint, directory, "safe", "step", + "TypeOK and RetiredIsFenced and NoPostRetirementStart and NoOutstandingWork and InventoryKnown", 512) + covered = {state["mbt::actionTaken"] for trace in safe for state in trace[1:]} + if covered != ACTIONS: + raise RuntimeError(f"safe action coverage mismatch: {covered ^ ACTIONS}") + witnesses = [state["q"] for trace in safe for state in trace] + distinct = len({json.dumps(q, sort_keys=True) for q in witnesses}) + if distinct < 177: + raise RuntimeError(f"safe state coverage regressed: {distinct} < 177") + if not any(q["rejectedAfterRetirement"] for q in witnesses): + raise RuntimeError("missing post-retirement rejection witness") + unfenced = generate(quint, directory, "unfenced", "unfencedStep", "RetiredIsFenced", 512) + cached = generate(quint, directory, "cached", "cachedGrantStep", "NoPostRetirementStart", 512) + lanes = (("safe", safe), ("unfenced", unfenced), ("cached", cached)) + fixture = directory / "traces.scm" + fixture.write_text("(" + "\n".join( + f"({name} ({' '.join('(' + ' '.join(map(state_row, trace)) + ')' for trace in traces)}))" + for name, traces in lanes) + ")\n") + environment = dict(os.environ, AITIA_QUINT_REPLAY=str(fixture)) + result = subprocess.run(["just", "test-scheme-atomic", "formal", "retirement-replay-test.ss"], + cwd=REPO, env=environment, timeout=65, check=False) + if result.returncode: + raise SystemExit(result.returncode) + print(f"QUINT-SCHEME-OK: safe-traces={len(safe)} states={distinct}/248 " + f"actions={len(covered)} negative-traces={len(unfenced) + len(cached)}", flush=True) + + +if __name__ == "__main__": + main() diff --git a/proof/quint/package-lock.json b/proof/quint/package-lock.json new file mode 100644 index 0000000..3abe5d9 --- /dev/null +++ b/proof/quint/package-lock.json @@ -0,0 +1,799 @@ +{ + "name": "lambda-aitia-quint-checks", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "lambda-aitia-quint-checks", + "dependencies": { + "@informalsystems/quint": "0.33.0" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.5", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.5.tgz", + "integrity": "sha512-7VZM+SVdEcUUqSQeNI3zM8Qs/BhQKZndPo2h5VkYkAM8Iz0wJIa8mKV5ekQGqG8UUsnkQ0NMxIxwkIHYvj0qOw==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", + "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@informalsystems/quint": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@informalsystems/quint/-/quint-0.33.0.tgz", + "integrity": "sha512-1hr+l+3wyKimjNXNl9lovpwmH5C9Ck898FL4yGbuEmFPiz7YRqvFvsB9hlHsaYukXqdUY5nuFKn/qiCon0O88g==", + "license": "Apache 2.0", + "dependencies": { + "@grpc/grpc-js": "^1.11.1", + "@grpc/proto-loader": "^0.7.7", + "@octokit/request": "^8.1.1", + "@sweet-monads/either": "~3.2.0", + "@sweet-monads/maybe": "~3.2.0", + "@types/line-column": "^1.0.0", + "@types/lodash.clonedeep": "4.5.0", + "@types/seedrandom": "^3.0.4", + "adm-zip": "^0.5.16", + "antlr4ts": "^0.5.0-alpha.4", + "chalk": "^4.1.2", + "cli-progress": "^3.12.0", + "cross-spawn": "^7.0.6", + "eol": "^0.9.1", + "immutable": "^4.3.0", + "json-bigint": "^1.0.0", + "line-column": "^1.0.2", + "lodash": "^4.17.21", + "lodash.clonedeep": "4.5.0", + "seedrandom": "^3.0.5", + "tar": "^7.5.13", + "yargs": "^17.7.2" + }, + "bin": { + "quint": "dist/src/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@octokit/endpoint": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", + "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" + }, + "node_modules/@octokit/request": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", + "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^9.0.6", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", + "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause" + }, + "node_modules/@sweet-monads/either": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@sweet-monads/either/-/either-3.2.0.tgz", + "integrity": "sha512-n+nR0b60GRTKb+D76qhTf4NEBXU9zfpigYYEtKtSYbV+5+i5gxr9jFd64pYkY2O7hVsb/G7nspbAeFni/i1ltA==", + "license": "MIT", + "dependencies": { + "@sweet-monads/interfaces": "^3.2.0" + } + }, + "node_modules/@sweet-monads/interfaces": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@sweet-monads/interfaces/-/interfaces-3.3.0.tgz", + "integrity": "sha512-66akGvjPD4lizQy+w4JSltJilc2w/QPdw8lPAniLJGHwyjmrw9xMJLx76Q/GDnbCU59Werses4aZJLWOlJrL5A==", + "license": "MIT" + }, + "node_modules/@sweet-monads/maybe": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@sweet-monads/maybe/-/maybe-3.2.0.tgz", + "integrity": "sha512-/t+K0D/kBfkYOkZaePEsrK868at0M9UIEVgehcM0xscrCSZhKWGteE41vl2XJQqh8WyiFo/mZ5y7eAPSYzS+pg==", + "license": "MIT", + "dependencies": { + "@sweet-monads/interfaces": "^3.2.0" + } + }, + "node_modules/@types/line-column": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/line-column/-/line-column-1.0.2.tgz", + "integrity": "sha512-099oFQmp/Tlf20xW5XI5R4F69N6lF/zQ09XDzc3R5BOLFlqIotgKoNIyj0HD4fQLWcGDreDJv8k/BkLJscrDrw==", + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.25.tgz", + "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==", + "license": "MIT" + }, + "node_modules/@types/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-IHijjFVPJTvzvrNPz+6nQy5lZQb7uh807RfTIEaQBrZXrIGjZy0L2dEb3hju34J0eqbXLCY6Hub/g81Jl4pGCA==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/node": { + "version": "26.6.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.4.tgz", + "integrity": "sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@types/seedrandom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-3.0.8.tgz", + "integrity": "sha512-TY1eezMU2zH2ozQoAFAQFOPpvP15g+ZgSfTZt31AUUH/Rxtnz3H+A/Sv1Snw2/amp//omibc+AEkTaA8KUeOLQ==", + "license": "MIT" + }, + "node_modules/adm-zip": { + "version": "0.5.18", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.18.tgz", + "integrity": "sha512-ufJnssQGbxzLNS1Ho9bCtX4rQKCCvoVuDLHoJyc3F9dOGDB4BkWs2Ci0kv53lqocAEQ/Cbi+I2XCsNYGqVYqng==", + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/antlr4ts": { + "version": "0.5.0-alpha.4", + "resolved": "https://registry.npmjs.org/antlr4ts/-/antlr4ts-0.5.0-alpha.4.tgz", + "integrity": "sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==", + "license": "BSD-3-Clause" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cli-progress": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", + "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", + "license": "MIT", + "dependencies": { + "string-width": "^4.2.3" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/eol": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/eol/-/eol-0.9.1.tgz", + "integrity": "sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg==", + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/immutable": { + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.9.tgz", + "integrity": "sha512-ObHy4YN7ycwZOUCLI1/6svfyAFu7vL8RhAvVu/bh/RZW9EPlOyDaQ9jDQWCtdqzaXUjgXZCW1migtHE7YI7UGQ==", + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "license": "MIT", + "dependencies": { + "isarray": "1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/line-column": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/line-column/-/line-column-1.0.2.tgz", + "integrity": "sha512-Ktrjk5noGYlHsVnYWh62FLVs4hTb8A3e+vucNZMgPeAOITdshMSgv4cCZQeRDjm7+goqmo6+liZwTXo+U3sVww==", + "license": "MIT", + "dependencies": { + "isarray": "^1.0.0", + "isobject": "^2.0.0" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/protobufjs": { + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", + "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/seedrandom": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "license": "MIT" + }, + "node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "license": "ISC" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/proof/quint/package.json b/proof/quint/package.json new file mode 100644 index 0000000..262330b --- /dev/null +++ b/proof/quint/package.json @@ -0,0 +1,9 @@ +{ + "name": "lambda-aitia-quint-checks", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0 AND LGPL-2.1-or-later", + "dependencies": { + "@informalsystems/quint": "0.33.0" + } +} diff --git a/proof/quint/process_runner.py b/proof/quint/process_runner.py new file mode 100644 index 0000000..f9fda54 --- /dev/null +++ b/proof/quint/process_runner.py @@ -0,0 +1,68 @@ +# SPDX-FileCopyrightText: 2026 tao3k team and Contributors +# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +"""Run a model checker with real output and a bounded silence watchdog.""" + +from __future__ import annotations + +import os +import selectors +import signal +import subprocess +import time +from pathlib import Path +from typing import Sequence + + +PROGRESS = ( + "Compiling to TLA+", "Starting checker server", "SanyParser", + "Running TLC", "Computing initial states", "states generated", + "Model checking completed", "No violation found", "Invariant q_inv is violated", + "found a counterexample", "Checking temporal properties", "Temporal property", +) + + +def run(command: Sequence[str], cwd: Path, label: str, *, timeout: float = 60, + silence: float = 12) -> tuple[int, str]: + """Return status and exact output; never count a silent checker as progress.""" + process = subprocess.Popen(command, cwd=cwd, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, start_new_session=True) + assert process.stdout is not None + selector = selectors.DefaultSelector() + selector.register(process.stdout, selectors.EVENT_READ) + chunks: list[bytes] = [] + pending = b"" + began = last_output = time.monotonic() + try: + while selector.get_map(): + now = time.monotonic() + if now - began > timeout or now - last_output > silence: + reason = "total timeout" if now - began > timeout else "silent checker" + os.killpg(process.pid, signal.SIGTERM) + try: + process.wait(timeout=3) + except subprocess.TimeoutExpired: + os.killpg(process.pid, signal.SIGKILL) + process.wait() + partial = b"".join(chunks).decode(errors="replace") + raise TimeoutError(f"{label}: {reason} after {now - last_output:.1f}s without output\n{partial[-4000:]}") + events = selector.select(timeout=min(1.0, silence)) + for key, _ in events: + data = os.read(key.fd, 65536) + if not data: + selector.unregister(key.fileobj) + continue + last_output = time.monotonic() + chunks.append(data) + pending += data + while b"\n" in pending: + line, pending = pending.split(b"\n", 1) + decoded = line.decode(errors="replace") + if any(marker in decoded for marker in PROGRESS): + print(f"{label}: {decoded.strip()}", flush=True) + if pending and any(marker in pending.decode(errors="replace") for marker in PROGRESS): + print(f"{label}: {pending.decode(errors='replace').strip()}", flush=True) + return process.wait(timeout=3), b"".join(chunks).decode(errors="replace") + finally: + selector.close() + process.stdout.close() diff --git a/proof/quint/test_lifecycle_replay.py b/proof/quint/test_lifecycle_replay.py new file mode 100644 index 0000000..49e2ccd --- /dev/null +++ b/proof/quint/test_lifecycle_replay.py @@ -0,0 +1,79 @@ +# SPDX-FileCopyrightText: 2026 tao3k team and Contributors +# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later +"""Fail-closed controls for the finite lifecycle graph admission gate.""" +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +import check_lifecycle_replay as checker + + +class LifecycleReplayTests(unittest.TestCase): + def graph(self, suffix='', omitted=None): + values = {field: False if field.endswith('Safe') or field in + ('adapterInstalled', 'reportedSuccess', 'effectStarted') else 0 + for field in checker.FIELDS if field != omitted} + label = ', '.join(f'{key} |-> {str(value).upper() if isinstance(value, bool) else value}' + for key, value in values.items()) + return f'"1" [label="q = [{label}]", style = filled];\n"1" -> "1" [label="Terminal"];\n' + suffix + + def read(self, source, expected=1): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / 'graph.dot' + path.write_text(source) + return checker.read_graph(path, expected) + + def test_complete_schema(self): + nodes, edges, initial = self.read(self.graph()) + self.assertEqual(set(nodes[initial]), set(checker.FIELDS)) + self.assertEqual(edges, [('1', '1', 'Terminal')]) + + def test_missing_field_is_not_partial_correspondence(self): + with self.assertRaisesRegex(ValueError, 'schema'): + self.read(self.graph(omitted='deliveryCalls')) + + def test_missing_node_fails(self): + with self.assertRaisesRegex(ValueError, 'missing state'): + self.read(self.graph('"1" -> "2" [label="Verify"];')) + + def test_unknown_action_is_not_scheme_code(self): + with self.assertRaisesRegex(ValueError, 'unknown TLC action'): + self.read(self.graph('"1" -> "1" [label="Unexpected"];')) + + def test_incomplete_tlc_count_fails(self): + with self.assertRaisesRegex(ValueError, 'incomplete graph'): + self.read(self.graph(), expected=2) + + def test_unreachable_state_fails(self): + with patch.object(checker, 'ACTIONS', ('Terminal',)): + with self.assertRaisesRegex(ValueError, 'unreachable'): + checker.covering_traces({'1': {'clock': 0}, '2': {'clock': 1}}, + [('1', '1', 'Terminal')], '1') + + def test_covering_paths_cover_all_states_and_self_loop_action(self): + with patch.object(checker, 'ACTIONS', ('Verify', 'Terminal')): + traces = checker.covering_traces({'1': {'clock': 0}, '2': {'clock': 1}}, + [('1', '2', 'Verify'), ('2', '2', 'Terminal')], '1') + self.assertEqual({state['clock'] for trace in traces for _, state in trace}, {0, 1}) + self.assertEqual({action for trace in traces for action, _ in trace}, {'init', 'Verify', 'Terminal'}) + + def test_non_stuttering_cycle_fails_closed(self): + with self.assertRaisesRegex(ValueError, 'not a DAG'): + checker.covering_traces({'1': {'clock': 0}, '2': {'clock': 1}}, + [('1', '2', 'Verify'), ('2', '1', 'Revoke')], '1') + + def test_native_batches_preserve_every_trace_and_bound_actual_steps(self): + traces = [[('init', {})] * size for size in (80, 80, 21, 140, 20)] + packed = list(checker.batches(traces)) + self.assertEqual([trace for batch in packed for trace in batch], traces) + self.assertTrue(all(sum(map(len, batch)) <= 160 for batch in packed)) + self.assertEqual([sum(map(len, batch)) for batch in packed], [160, 21, 160]) + + def test_single_oversized_trace_is_not_split_or_given_a_larger_timeout(self): + with self.assertRaisesRegex(ValueError, 'one trace exceeds'): + list(checker.batches([[('init', {})] * 161])) + + +if __name__ == '__main__': + unittest.main() diff --git a/proof/quint/tlc-config.json b/proof/quint/tlc-config.json new file mode 100644 index 0000000..e55923a --- /dev/null +++ b/proof/quint/tlc-config.json @@ -0,0 +1 @@ +{"workers":"1","maxHeap":"-Xmx512m"} diff --git a/schemas/vocabulary-manifest.json b/schemas/vocabulary-manifest.json index 0a244ad..b0bc9dd 100644 --- a/schemas/vocabulary-manifest.json +++ b/schemas/vocabulary-manifest.json @@ -1,6 +1,6 @@ { "manifestSchema": "lambda-aitia.vocabulary-manifest", - "revision": 2, + "revision": 5, "semanticOwner": "lambda-aitia", "nodeKinds": [ "artifact", @@ -33,7 +33,7 @@ "observation": ["subject", "source", "clock-role", "logical-position"], "event": ["subject", "event-kind", "observation", "payload-digest", "modality", "commitment-state", "causal-parents"], "action": ["subject", "action-kind", "requested-by", "scope"], - "obligation": ["subject", "claim", "snapshot", "evidence-kind", "capability", "scope"], + "obligation": ["subject", "claim", "snapshot", "snapshot-revision", "snapshot-context-digest", "evidence-kind", "capability", "scope"], "evidence": ["producer", "tool", "tool-version", "input-artifacts", "obligation", "subject", "scope", "valid-from", "valid-until", "admission-state"], "counterexample": ["subject", "challenges", "evidence", "details-digest"], "finding": ["subject", "challenges", "evidence", "finding-kind"], @@ -56,7 +56,7 @@ }, "relationPlanes": { "provenance": ["derived-from", "generated-by", "cites", "used"], - "structural": ["depends-on", "implements", "tests", "refines"], + "structural": ["depends-on", "composes", "implements", "tests", "refines"], "causal": ["precedes", "causal-parent", "enables", "prevents"], "assurance": ["supports", "discharges", "defeats", "invalidates"], "authority": ["authorizes", "denies", "requires-review"] @@ -66,6 +66,11 @@ "lambda-aitia.assurance-snapshot", "lambda-aitia.invalidation-receipt", "lambda-aitia.planning-result", + "lambda-aitia.replacement-requirements", + "lambda-aitia.replacement-declaration", + "lambda-aitia.verifier-choices", + "lambda-aitia.evidence-admission", + "lambda-aitia.composition-requirements", "lambda-aitia.native-descriptor", "lambda-aitia.native-error" ] diff --git a/scripts/tests/test_build_native_library.py b/scripts/tests/test_build_native_library.py index f3b0fe0..7a067d0 100644 --- a/scripts/tests/test_build_native_library.py +++ b/scripts/tests/test_build_native_library.py @@ -28,6 +28,25 @@ def build_module(): class NativeLinkPolicyTest(unittest.TestCase): + def test_orgize_header_search_path_names_the_include_directory(self) -> None: + module = build_module() + with tempfile.TemporaryDirectory() as directory: + header_root = ( + Path(directory) + / "pkg/github.com/tao3k/orgize/bindings/c/include" + ) + header_root.mkdir(parents=True) + (header_root / "orgize.h").touch() + with patch.dict( + os.environ, + {"GERBIL_PATH": directory, "C_INCLUDE_PATH": "/caller/include"}, + ): + module.configure_orgize_header() + self.assertEqual( + os.environ["C_INCLUDE_PATH"], + f"{header_root.resolve()}:/caller/include", + ) + def test_gambit_compiler_uses_the_gerbil_release_not_path(self) -> None: module = build_module() with tempfile.TemporaryDirectory() as directory: diff --git a/scripts/tests/test_quint_replay.py b/scripts/tests/test_quint_replay.py new file mode 100644 index 0000000..bf13b03 --- /dev/null +++ b/scripts/tests/test_quint_replay.py @@ -0,0 +1,74 @@ +# SPDX-FileCopyrightText: 2026 tao3k team and Contributors +# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later +"""The correspondence runner must fail closed on schema and checker failures.""" + +import importlib.util +from pathlib import Path +import sys +import tempfile +import unittest +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[2] / "proof/quint" +sys.path.insert(0, str(ROOT)) +SPEC = importlib.util.spec_from_file_location("quint_replay", ROOT / "check_scheme_replay.py") +replay = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(replay) + + +def initial(): + return {"mbt::actionTaken": "init", "q": { + "work": {"active": True, "queued": True, "cancelRequested": False}, + "known": False, "reviewed": False, "retired": False, "lateFound": False, + "gapFound": False, "ingressOpen": True, "epoch": {"#bigint": "0"}, + "grantEpoch": {"#bigint": "0"}, "pending": False, "attempted": False, + "rejectedAfterRetirement": False, "startedAfterRetirement": False}} + + +class QuintReplayTest(unittest.TestCase): + def test_all_projected_slots_and_itf_integer_are_data(self): + row = replay.state_row(initial()) + self.assertIn("(authority-epoch 0)", row) + self.assertIn("(cancellation-requested? #f)", row) + self.assertEqual(row.count("?") + row.count("-epoch"), len(replay.FIELDS)) + + def test_added_model_field_cannot_be_silently_ignored(self): + state = initial() + state["q"]["newAuthority"] = True + with self.assertRaisesRegex(ValueError, "schema changed"): + replay.state_row(state) + + def test_missing_work_field_cannot_be_silently_ignored(self): + state = initial() + del state["q"]["work"]["queued"] + with self.assertRaisesRegex(ValueError, "schema changed"): + replay.state_row(state) + + def test_action_text_cannot_be_executed(self): + state = initial() + state["mbt::actionTaken"] = "init) (error injected) (" + with self.assertRaisesRegex(ValueError, "unknown Quint action"): + replay.state_row(state) + + def test_non_scalar_itf_value_is_rejected(self): + with self.assertRaises(ValueError): + replay.scalar({"#set": []}) + + def test_tool_failure_is_not_a_negative_control(self): + with tempfile.TemporaryDirectory() as directory, patch.object(replay, "run", return_value=(2, "Invariant violated")): + with self.assertRaisesRegex(RuntimeError, "unexpected Quint result"): + replay.generate("quint", Path(directory), "fault", "unfencedStep", "RetiredIsFenced", 1) + + def test_success_without_completed_output_is_rejected(self): + with tempfile.TemporaryDirectory() as directory, patch.object(replay, "run", return_value=(0, "[ok] No violation found")): + with self.assertRaisesRegex(RuntimeError, "incomplete trace output"): + replay.generate("quint", Path(directory), "safe", "step", "TypeOK", 1) + + def test_success_exit_without_success_marker_is_rejected(self): + with tempfile.TemporaryDirectory() as directory, patch.object(replay, "run", return_value=(0, "tool error")): + with self.assertRaisesRegex(RuntimeError, "unexpected Quint result"): + replay.generate("quint", Path(directory), "safe", "step", "TypeOK", 1) + + +if __name__ == "__main__": + unittest.main() diff --git a/t/ADR/unit/ADR-module-test.ss b/t/ADR/unit/ADR-module-test.ss index 272da63..f2effce 100644 --- a/t/ADR/unit/ADR-module-test.ss +++ b/t/ADR/unit/ADR-module-test.ss @@ -6,7 +6,7 @@ (import :std/test (only-in :std/error Error?) (only-in :clan/poo/object .ref) - :poo-flow/src/module-system/declaration/interface + :poo-flow/src/user-interface/module-selection (only-in :poo-flow/lambda-aitia/modules/assurance/interface assurance-decision?) :poo-flow/lambda-aitia/modules/ADR/interface) diff --git a/t/assurance/unit/assurance-kernel-test.ss b/t/assurance/unit/assurance-kernel-test.ss index 38f2ea4..7adeeb8 100644 --- a/t/assurance/unit/assurance-kernel-test.ss +++ b/t/assurance/unit/assurance-kernel-test.ss @@ -5,7 +5,10 @@ (import :std/test (only-in :std/error Error?) + (only-in :std/list/list find) (only-in :clan/poo/object .o .ref) + (only-in :poo-flow/core/contribution/verification + poo-flow-verification-adapter poo-flow-verify) :poo-flow/lambda-aitia/modules/assurance/interface) (export assurance-kernel-test) @@ -64,17 +67,102 @@ (assurance-relation "relation/reverse-dependency" 'structural 'implements "artifact/source" "claim/release" 'declared)) -(def (software-snapshot nodes relations (unresolved '())) - (assurance-snapshot - "snapshot/software" "r1" "graph/software" - '(("artifact/source" . "r1")) '(("claim/release" . "r1")) - "event-cut/1" "policy/release" "r1" nodes relations - evidence-identities: '("evidence/test") unresolved: unresolved)) +(def (verification-obligation id capability) + (assurance-obligation + id "r1" 'unknown digest-a + subject: "software/release" claim: "claim/release" + snapshot: "snapshot/software" evidence-kind: 'native-test + capability: capability scope: "repository")) +(def (verification-support id) + (assurance-relation + (string-append "relation/support/" id) 'assurance 'supports + id "claim/release" 'declared)) +(def (verification-dependency source target) + (assurance-relation + (string-append "relation/depends/" source "/" target) + 'structural 'depends-on source target 'declared)) +(def (verification-snapshot obligations relations) + (software-snapshot + (append (list artifact claim evidence) obligations) + (append (list dependency) relations))) + +(def (software-snapshot nodes relations (unresolved '()) (revision "r1")) + (def (construct values) + (assurance-snapshot + "snapshot/software" revision "graph/software" + '(("artifact/source" . "r1")) '(("claim/release" . "r1")) + "event-cut/1" "policy/release" "r1" values relations + evidence-identities: '("evidence/test") unresolved: unresolved)) + (let* ((candidate (construct nodes)) + (bound-nodes + (map (lambda (node) + (if (and (assurance-obligation? node) + (string=? (.ref node 'snapshot) + (.ref candidate 'identity)) + (not (.ref node 'snapshot-revision)) + (not (.ref node 'snapshot-context-digest))) + (assurance-bind-obligation-to-snapshot node candidate) + node)) + nodes))) + (construct bound-nodes))) (def full-software-snapshot (software-snapshot (list artifact claim obligation evidence decision effect) (list dependency discharge obligation-support decision-dependency authorization))) +(def current-obligation + (find (lambda (node) + (and (assurance-obligation? node) + (equal? (.ref node 'identity) "obligation/verify"))) + (.ref full-software-snapshot 'nodes))) +(def support-outcome + (assurance-verifier-outcome + "outcome/kernel" 'succeeded + producer: "gxtest" tool: "gerbil-test" tool-version: "v19" + obligation: "obligation/verify" subject: "software/release" + scope: "repository" + snapshot-revision: (.ref full-software-snapshot 'revision) + snapshot-context-digest: (.ref full-software-snapshot 'context-digest) + inputs: (list (assurance-verifier-input artifact)) + output-digest: digest-b)) +(def support-adapter + (poo-flow-verification-adapter + "test/kernel-support" (lambda (subject-value now-value until-value) #t) + assurance-verification-subject-snapshot)) +(def support-receipt + (poo-flow-verify + support-adapter + (assurance-verification-subject + full-software-snapshot current-obligation evidence support-outcome) + 10 20)) +(def (kernel-support? relation-value evidence-value) + (assurance-support-sealed-under-adapter? + relation-value evidence-value current-obligation full-software-snapshot + support-outcome support-adapter support-receipt 11)) + +(def replacement-claim + (assurance-claim + "claim/release" "r2" 'unknown digest-b + subject: "software/release" predicate: "release-ready-v2" + assumptions: '("assumption/toolchain") + support-requirements: '("obligation/verify-next") + scope: "repository" valid-from: "2026-09-17")) +(def replacement-candidate + (assurance-obligation + "obligation/verify-next" "r2" 'unknown digest-a + subject: "software/release" claim: "claim/release" + snapshot: "snapshot/software" evidence-kind: 'native-test + capability: 'gerbil-test scope: "repository")) +(def replacement-support + (assurance-relation + "relation/replacement-support" 'assurance 'supports + "obligation/verify-next" "claim/release" 'declared)) +(def (replacement-draft nodes relations) + (assurance-snapshot + "snapshot/software" "r2" "graph/software" + '(("artifact/source" . "r1")) '(("claim/release" . "r2")) + "event-cut/1" "policy/release" "r1" + nodes relations)) (def assurance-kernel-test (test-suite "Aitia POO-native assurance kernel" @@ -102,6 +190,12 @@ (test-case "relation kinds cannot cross their owned plane" (check-equal? (assurance-relation? dependency) #t) + (check-equal? + (assurance-relation? + (assurance-relation + "relation/composes" 'structural 'composes + "claim/composite" "claim/component-a" 'declared)) + #t) (check-exception (assurance-relation "bad" 'causal 'depends-on "claim/release" "artifact/source" 'declared) @@ -235,35 +329,35 @@ (test-case "alternate evidence and temporal order grant no support" (check-equal? - (assurance-support-admissible? discharge evidence obligation) #t) + (kernel-support? discharge evidence) #t) (check-equal? - (assurance-support-admissible? - discharge (.o (:: @ evidence) state: 'hypothesized) obligation) + (kernel-support? + discharge (.o (:: @ evidence) state: 'hypothesized)) #f) (check-equal? - (assurance-support-admissible? + (kernel-support? (.o (:: @ discharge) modality: 'counterfactual) - evidence obligation) + evidence) #f) (check-equal? - (assurance-support-admissible? + (kernel-support? (assurance-relation "relation/precedes" 'causal 'precedes "evidence/test" "obligation/verify" 'observed) - evidence obligation) + evidence) #f)) (test-case "candidate or mismatched evidence cannot discharge an obligation" (check-equal? - (assurance-support-admissible? - discharge (.o (:: @ evidence) admission-state: 'candidate) obligation) + (kernel-support? + discharge (.o (:: @ evidence) admission-state: 'candidate)) #f) (check-equal? - (assurance-support-admissible? - discharge (.o (:: @ evidence) obligation: "obligation/other") obligation) + (kernel-support? + discharge (.o (:: @ evidence) obligation: "obligation/other")) #f) (check-equal? - (assurance-support-admissible? - discharge (.o (:: @ evidence) revision: "r2") obligation) + (kernel-support? + discharge (.o (:: @ evidence) revision: "r2")) #f)) (test-case "change invalidation is deterministic and never grants authority" @@ -359,6 +453,811 @@ (cons "artifact/source" (assurance-invalidation-receipt-ref claim-change 'impacted))))) + (test-case "verification selection is pure and permutation-stable" + (let* ((policy + (assurance-verification-policy + "policy/release" "r1" '(lean gerbil-test gerbil-test))) + (permuted-policy + (assurance-verification-policy + "policy/release" "r1" '(gerbil-test lean))) + (permuted + (software-snapshot + (list effect decision evidence obligation claim artifact) + (list authorization decision-dependency obligation-support + discharge dependency))) + (left + (assurance-plan-verification + "plan/release" full-software-snapshot + '("artifact/source") policy)) + (right + (assurance-plan-verification + "plan/release" permuted + '("artifact/source") permuted-policy))) + (check-equal? (assurance-verification-policy? policy) #t) + (check-equal? (.ref policy 'capabilities) '(gerbil-test lean)) + (check-equal? (assurance-verification-plan? left) #t) + (check-equal? (.ref left 'schema) "lambda-aitia.planning-result") + (check-equal? + (.ref obligation 'snapshot-context-digest) #f) + (check-equal? + (.ref (car (filter assurance-obligation? + (.ref full-software-snapshot 'nodes))) + 'snapshot-context-digest) + (.ref full-software-snapshot 'context-digest)) + (check-equal? (.ref left 'selected-obligations) + '("obligation/verify")) + (check-equal? (.ref left 'blocked-obligations) '()) + (check-equal? + (assurance-verification-request? (car (.ref left 'requests))) #t) + (check-equal? (.ref (car (.ref left 'requests)) 'capability) + 'gerbil-test) + (check-equal? (.ref (car (.ref left 'requests)) 'selected?) #t) + (check-equal? (.ref left 'digest) (.ref right 'digest)) + (check-equal? (.ref left 'verifier-executed?) #f) + (check-equal? (.ref left 'release-authorized?) #f) + (check-equal? (.ref left 'runtime-executed?) #f) + (check-equal? (.ref left 'blocked-effects) '("effect/deploy")) + (check-equal? (car (.ref left 'witnesses)) + '("artifact/source" ("artifact/source") ())) + (check-equal? + (assurance-verification-request? + (.o (:: @ (car (.ref left 'requests))) + blocker: 'capability-unavailable)) + #f) + (check-equal? + (assurance-verification-plan? + (.o (:: @ left) release-authorized?: #t)) + #f))) + + (test-case "replacement requirements never silently rebind an old obligation" + (let* ((target + (software-snapshot + (list artifact claim evidence) (list dependency) '() "r2")) + (permuted-source + (software-snapshot + (list effect decision evidence obligation claim artifact) + (list authorization decision-dependency obligation-support + discharge dependency))) + (left + (assurance-derive-replacement-requirements + "replacement/source" full-software-snapshot target + '("artifact/source"))) + (right + (assurance-derive-replacement-requirements + "replacement/source" permuted-source target + '("artifact/source"))) + (requirement (car (.ref left 'requirements))) + (unchanged + (assurance-derive-replacement-requirements + "replacement/unchanged" full-software-snapshot + full-software-snapshot '("artifact/source"))) + (unknown-change + (assurance-derive-replacement-requirements + "replacement/unknown-change" full-software-snapshot + target '("artifact/source" "artifact/unknown"))) + (unknown-only + (assurance-derive-replacement-requirements + "replacement/unknown-only" full-software-snapshot + target '("artifact/unknown"))) + (stale-source + (software-snapshot + (list artifact claim evidence decision effect + (assurance-obligation + "obligation/verify" "r1" 'unknown digest-a + subject: "software/release" claim: "claim/release" + snapshot: "snapshot/software" evidence-kind: 'native-test + capability: 'gerbil-test scope: "repository" + snapshot-revision: "r0" + snapshot-context-digest: digest-a)) + (list dependency discharge obligation-support + decision-dependency authorization))) + (stale + (assurance-derive-replacement-requirements + "replacement/stale-source" stale-source target + '("artifact/source")))) + (check-equal? (assurance-replacement-derivation? left) #t) + (check-equal? (.ref left 'digest) (.ref right 'digest)) + (check-equal? (length (.ref left 'requirements)) 1) + (check-equal? (assurance-replacement-requirement? requirement) #t) + (check-equal? (.ref requirement 'source-obligation) + "obligation/verify") + (check-equal? (.ref requirement 'evidence-kind) 'native-test) + (check-equal? (.ref requirement 'blocker) #f) + (check-equal? (car (.ref left 'witnesses)) + '("artifact/source" ("artifact/source") ())) + (check-equal? (.ref (car (.ref unchanged 'requirements)) 'blocker) + 'target-unchanged) + (check-equal? + (.ref (car (.ref unknown-change 'requirements)) 'blocker) + 'change-unresolved) + (check-equal? (.ref unknown-only 'requirements) '()) + (check-equal? (.ref unknown-only 'unresolved) + '("artifact/unknown")) + (check-equal? (.ref (car (.ref stale 'requirements)) 'blocker) + 'stale-source) + (check-equal? (.ref left 'verifier-executed?) #f) + (check-equal? (.ref left 'release-authorized?) #f) + (check-equal? + (filter assurance-obligation? (.ref target 'nodes)) '()))) + + (test-case "revised claims and unknown targets block automatic replacement" + (let* ((revised-claim + (assurance-claim + "claim/release" "r2" 'unknown digest-a + subject: "software/release" predicate: "release-ready-v2" + assumptions: '("assumption/toolchain") + support-requirements: '("obligation/verify") + scope: "repository" valid-from: "2026-09-17")) + (changed-claim-target + (assurance-snapshot + "snapshot/software" "r2" "graph/software" + '(("artifact/source" . "r1")) '(("claim/release" . "r2")) + "event-cut/1" "policy/release" "r1" + (list artifact revised-claim) + (list dependency))) + (unknown-target + (software-snapshot + (list artifact claim evidence) (list dependency) + '("artifact/unresolved") "r2")) + (foreign-target + (assurance-snapshot + "snapshot/other" "r2" "graph/software" + '(("artifact/source" . "r1")) '(("claim/release" . "r1")) + "event-cut/1" "policy/release" "r1" + (list artifact claim) (list dependency))) + (changed-claim + (assurance-derive-replacement-requirements + "replacement/changed-claim" full-software-snapshot + changed-claim-target '("artifact/source"))) + (unknown + (assurance-derive-replacement-requirements + "replacement/unknown" full-software-snapshot + unknown-target '("artifact/source"))) + (foreign + (assurance-derive-replacement-requirements + "replacement/foreign" full-software-snapshot + foreign-target '("artifact/source")))) + (check-equal? + (.ref (car (.ref changed-claim 'requirements)) 'blocker) + 'claim-changed) + (check-equal? + (.ref (car (.ref unknown 'requirements)) 'blocker) + 'target-unresolved) + (check-equal? + (.ref (car (.ref foreign 'requirements)) 'blocker) + 'target-identity-changed))) + + (test-case "an explicit fresh replacement binds without promoting old evidence" + (let* ((draft + (replacement-draft + (list artifact replacement-claim replacement-candidate) + (list dependency replacement-support))) + (permuted-draft + (replacement-draft + (list replacement-candidate artifact replacement-claim) + (list replacement-support dependency))) + (derivation + (assurance-derive-replacement-requirements + "replacement/explicit" full-software-snapshot draft + '("artifact/source"))) + (permuted-derivation + (assurance-derive-replacement-requirements + "replacement/explicit" full-software-snapshot permuted-draft + '("artifact/source")))) + (check-equal? (.ref (car (.ref derivation 'requirements)) 'blocker) + 'claim-changed) + (let-values (((final receipt) + (assurance-declare-replacement-obligation + "replacement/declaration" full-software-snapshot + draft derivation "obligation/verify" + "obligation/verify-next")) + ((permuted-final permuted-receipt) + (assurance-declare-replacement-obligation + "replacement/declaration" full-software-snapshot + permuted-draft permuted-derivation + "obligation/verify" "obligation/verify-next"))) + (let ((bound + (find (lambda (node) + (equal? (.ref node 'identity) + "obligation/verify-next")) + (.ref final 'nodes)))) + (check-equal? (assurance-replacement-declaration-receipt? receipt) + #t) + (check-equal? (.ref receipt 'blocker) #f) + (check-equal? (.ref bound 'snapshot-revision) "r2") + (check-equal? (.ref bound 'snapshot-context-digest) + (.ref final 'context-digest)) + (check-equal? (.ref replacement-candidate 'snapshot-revision) #f) + (check-equal? (.ref receipt 'final-snapshot-digest) + (.ref final 'digest)) + (check-equal? (.ref receipt 'verifier-executed?) #f) + (check-equal? (.ref receipt 'release-authorized?) #f) + (check-equal? (.ref final 'digest) (.ref permuted-final 'digest)) + (check-equal? (.ref receipt 'digest) + (.ref permuted-receipt 'digest)) + (check-equal? + (assurance-replacement-declaration-receipt? + (.o (:: @ receipt) release-authorized?: #t)) + #f))))) + + (test-case "replacement declaration rejects missing and hypothetical links" + (let* ((draft + (replacement-draft + (list artifact replacement-claim replacement-candidate) + (list dependency replacement-support))) + (derivation + (assurance-derive-replacement-requirements + "replacement/negative" full-software-snapshot draft + '("artifact/source"))) + (missing-link + (replacement-draft + (list artifact replacement-claim replacement-candidate) + (list dependency))) + (hypothetical-link + (replacement-draft + (list artifact replacement-claim replacement-candidate) + (list dependency + (.o (:: @ replacement-support) + modality: 'hypothesized)))) + (foreign-claim + (replacement-draft + (list artifact replacement-claim + (.o (:: @ replacement-candidate) + claim: "claim/foreign")) + (list dependency replacement-support))) + (forged-draft (.o (:: @ draft) digest: digest-a))) + (let-values (((missing-final missing-receipt) + (assurance-declare-replacement-obligation + "replacement/missing" full-software-snapshot + missing-link + (assurance-derive-replacement-requirements + "replacement/missing-derivation" + full-software-snapshot missing-link + '("artifact/source")) + "obligation/verify" "obligation/verify-next")) + ((hypothetical-final hypothetical-receipt) + (assurance-declare-replacement-obligation + "replacement/hypothetical" full-software-snapshot + hypothetical-link + (assurance-derive-replacement-requirements + "replacement/hypothetical-derivation" + full-software-snapshot hypothetical-link + '("artifact/source")) + "obligation/verify" "obligation/verify-next")) + ((stale-final stale-receipt) + (assurance-declare-replacement-obligation + "replacement/stale" full-software-snapshot + hypothetical-link derivation + "obligation/verify" "obligation/verify-next")) + ((foreign-final foreign-receipt) + (assurance-declare-replacement-obligation + "replacement/foreign-claim" full-software-snapshot + foreign-claim + (assurance-derive-replacement-requirements + "replacement/foreign-claim-derivation" + full-software-snapshot foreign-claim + '("artifact/source")) + "obligation/verify" "obligation/verify-next")) + ((forged-final forged-receipt) + (assurance-declare-replacement-obligation + "replacement/forged" full-software-snapshot + forged-draft + (assurance-derive-replacement-requirements + "replacement/forged-derivation" + full-software-snapshot forged-draft + '("artifact/source")) + "obligation/verify" "obligation/verify-next"))) + (check-equal? missing-final #f) + (check-equal? (.ref missing-receipt 'blocker) 'support-link-missing) + (check-equal? hypothetical-final #f) + (check-equal? (.ref hypothetical-receipt 'blocker) + 'support-link-hypothetical) + (check-equal? stale-final #f) + (check-equal? (.ref stale-receipt 'blocker) + 'derivation-mismatch) + (check-equal? foreign-final #f) + (check-equal? (.ref foreign-receipt 'blocker) + 'candidate-claim-mismatch) + (check-equal? forged-final #f) + (check-equal? (.ref forged-receipt 'blocker) + 'derivation-mismatch)))) + + (test-case "verification obligations have a closed evidence-kind vocabulary" + (check-equal? + +assurance-evidence-kinds+ + '(proof model-check replay unit-test integration-test native-test + static-analysis authorization-differential human-review)) + (for-each + (lambda (kind) + (check-equal? + (assurance-obligation? + (assurance-obligation + "obligation/kind" "r1" 'unknown digest-a + subject: "software/release" claim: "claim/release" + snapshot: "snapshot/software" evidence-kind: kind + capability: 'verifier scope: "repository")) + #t)) + +assurance-evidence-kinds+) + (check-exception + (assurance-obligation + "obligation/invalid-kind" "r1" 'unknown digest-a + subject: "software/release" claim: "claim/release" + snapshot: "snapshot/software" evidence-kind: 'arbitrary-pass + capability: 'verifier scope: "repository") + Error?)) + + (test-case "verifier choices explain matching and rejected declarations" + (let* ((policy + (assurance-verification-policy + "policy/release" "r1" '(gerbil-test))) + (plan + (assurance-plan-verification + "plan/verifier-choice" full-software-snapshot + '("artifact/source") policy)) + (kind-mismatch + (assurance-verifier-candidate + "candidate/a" "r1" 0 '(proof) 'gerbil-test)) + (capability-mismatch + (assurance-verifier-candidate + "candidate/b" "r1" 1 '(native-test) 'lean)) + (chosen + (assurance-verifier-candidate + "candidate/z" "r1" 2 '(native-test unit-test) 'gerbil-test)) + (alternative + (assurance-verifier-candidate + "candidate/c" "r1" 3 '(native-test) 'gerbil-test)) + (catalog + (list alternative kind-mismatch chosen capability-mismatch)) + (left + (assurance-explain-verifier-choices + "choices/release" plan catalog)) + (right + (assurance-explain-verifier-choices + "choices/release" plan (reverse catalog))) + (revised + (assurance-explain-verifier-choices + "choices/release" plan + (list alternative kind-mismatch capability-mismatch + (assurance-verifier-candidate + "candidate/z" "r2" 2 '(native-test unit-test) + 'gerbil-test)))) + (missing + (assurance-explain-verifier-choices + "choices/missing" plan (list kind-mismatch))) + (blocked-plan + (assurance-plan-verification + "plan/no-verifier" full-software-snapshot + '("artifact/source") + (assurance-verification-policy + "policy/release" "r1" '()))) + (blocked + (assurance-explain-verifier-choices + "choices/blocked" blocked-plan (list chosen)))) + (check-equal? (assurance-verifier-choice-receipt? left) #t) + (check-equal? (map (lambda (candidate) (.ref candidate 'identity)) + (.ref left 'candidates)) + '("candidate/a" "candidate/b" "candidate/z" + "candidate/c")) + (check-equal? (.ref left 'digest) (.ref right 'digest)) + (check-equal? (equal? (.ref left 'digest) (.ref revised 'digest)) + #f) + (check-equal? (map (lambda (choice) (.ref choice 'reason)) + (.ref left 'choices)) + '(evidence-kind-unsupported capability-mismatch + chosen lower-priority)) + (check-equal? (map (lambda (choice) (.ref choice 'selected?)) + (.ref left 'choices)) + '(#f #f #t #f)) + (check-equal? (.ref left 'unassigned-obligations) '()) + (check-equal? (.ref missing 'unassigned-obligations) + '("obligation/verify")) + (check-equal? (.ref (car (.ref blocked 'choices)) 'reason) + 'request-blocked) + (check-equal? (.ref blocked 'unassigned-obligations) '()) + (check-equal? (.ref left 'verifier-executed?) #f) + (check-equal? (.ref left 'release-authorized?) #f) + (check-exception + (assurance-explain-verifier-choices + "choices/duplicate" plan (list chosen chosen)) + Error?) + (check-exception + (assurance-verifier-candidate + "candidate/unknown" "r1" 0 '(arbitrary-pass) 'gerbil-test) + Error?))) + + (test-case "component support never supplies a composition obligation" + (let* ((component-a + (assurance-claim + "claim/component-a" "r1" 'supported digest-a + subject: "software/component-a" predicate: "ready" + scope: "repository" valid-from: "2026-09-17")) + (component-b + (assurance-claim + "claim/component-b" "r1" 'supported digest-a + subject: "software/component-b" predicate: "ready" + scope: "repository" valid-from: "2026-09-17")) + (composite + (assurance-claim + "claim/composite" "r1" 'unknown digest-b + subject: "software/composite" predicate: "composition-safe" + support-requirements: '("obligation/composition") + scope: "repository" valid-from: "2026-09-17")) + (candidate-obligation + (assurance-obligation + "obligation/composition" "r1" 'unknown digest-a + subject: "software/composite" claim: "claim/composite" + snapshot: "snapshot/composition" + evidence-kind: 'integration-test + capability: 'composition-test scope: "repository")) + (components + (list (assurance-relation + "relation/composes/a" 'structural 'composes + "claim/composite" "claim/component-a" 'declared) + (assurance-relation + "relation/composes/b" 'structural 'composes + "claim/composite" "claim/component-b" 'declared))) + (direct-support + (assurance-relation + "relation/support/composition" 'assurance 'supports + "obligation/composition" "claim/composite" 'declared))) + (def (construct nodes relations) + (assurance-snapshot + "snapshot/composition" "r1" "graph/composition" + '() '(("claim/component-a" . "r1") + ("claim/component-b" . "r1") + ("claim/composite" . "r1")) + "event-cut/1" "policy/composition" "r1" + nodes relations)) + (let* ((missing-snapshot + (construct (list component-a component-b composite) + components)) + (missing + (assurance-derive-composition-requirements + "composition/missing" missing-snapshot)) + (permuted + (assurance-derive-composition-requirements + "composition/missing" + (construct (list composite component-b component-a) + (reverse components)))) + (candidate + (construct + (list component-a component-b composite candidate-obligation) + (cons direct-support components))) + (stale-obligation + (assurance-obligation + "obligation/composition" "r1" 'unknown digest-a + subject: "software/composite" claim: "claim/composite" + snapshot: "snapshot/composition" + evidence-kind: 'integration-test + capability: 'composition-test scope: "repository" + snapshot-revision: "r0" + snapshot-context-digest: digest-a)) + (stale + (assurance-derive-composition-requirements + "composition/stale" + (construct + (list component-a component-b composite stale-obligation) + (cons direct-support components)))) + (bound-obligation + (assurance-bind-obligation-to-snapshot + candidate-obligation candidate)) + (bound-snapshot + (construct + (list component-a component-b composite bound-obligation) + (cons direct-support components))) + (declared + (assurance-derive-composition-requirements + "composition/declared" bound-snapshot)) + (changed + (assurance-invalidate + "composition/changed" bound-snapshot + '("claim/component-a")))) + (check-equal? (assurance-composition-derivation? missing) #t) + (check-equal? (.ref missing 'digest) (.ref permuted 'digest)) + (check-equal? + (.ref (car (.ref missing 'requirements)) 'component-claims) + '("claim/component-a" "claim/component-b")) + (check-equal? + (.ref (car (.ref missing 'requirements)) 'blocker) + 'missing-direct-obligation) + (check-equal? + (.ref (car (.ref stale 'requirements)) 'direct-obligations) + '("obligation/composition")) + (check-equal? + (.ref (car (.ref stale 'requirements)) 'current-obligations) '()) + (check-equal? + (.ref (car (.ref stale 'requirements)) 'blocker) + 'stale-direct-obligation) + (check-equal? + (.ref (car (.ref declared 'requirements)) 'direct-obligations) + '("obligation/composition")) + (check-equal? + (.ref (car (.ref declared 'requirements)) 'current-obligations) + '("obligation/composition")) + (check-equal? + (.ref (car (.ref declared 'requirements)) 'blocker) #f) + (check-equal? (.ref declared 'verifier-executed?) #f) + (check-equal? (.ref declared 'release-authorized?) #f) + (check-equal? + (assurance-invalidation-receipt-ref changed 'required-obligations) + '("obligation/composition"))))) + + (test-case "POO Graph orders affected prerequisites before dependents" + (let* ((a (verification-obligation "obligation/a" 'gerbil-test)) + (b (verification-obligation "obligation/b" 'gerbil-test)) + (c (verification-obligation "obligation/c" 'gerbil-test)) + (relations + (list (verification-support "obligation/a") + (verification-support "obligation/b") + (verification-support "obligation/c") + (verification-dependency "obligation/a" "obligation/b") + (verification-dependency "obligation/b" "obligation/c"))) + (policy + (assurance-verification-policy + "policy/release" "r1" '(gerbil-test))) + (left + (assurance-plan-verification + "plan/ordered" + (verification-snapshot (list a b c) relations) + '("artifact/source") policy)) + (right + (assurance-plan-verification + "plan/ordered" + (verification-snapshot (list c a b) (reverse relations)) + '("artifact/source") policy))) + (check-equal? (.ref left 'selected-obligations) + '("obligation/c" "obligation/b" "obligation/a")) + (check-equal? (.ref left 'blocked-obligations) '()) + (check-equal? (.ref left 'cycle-path) '()) + (check-equal? (.ref left 'cyclic-components) '()) + (check-equal? (.ref (car (.ref left 'requests)) 'dependencies) '()) + (check-equal? (.ref (cadr (.ref left 'requests)) 'dependencies) + '("obligation/c")) + (check-equal? (.ref left 'digest) (.ref right 'digest)) + (check-equal? (.ref left 'verifier-executed?) #f))) + + (test-case "an unplanned prerequisite blocks its dependent" + (let* ((a (verification-obligation "obligation/a" 'gerbil-test)) + (b (verification-obligation "obligation/b" 'gerbil-test)) + (snapshot + (verification-snapshot + (list a b) + (list (verification-support "obligation/a") + (verification-dependency "obligation/a" "obligation/b")))) + (policy + (assurance-verification-policy + "policy/release" "r1" '(gerbil-test))) + (plan + (assurance-plan-verification + "plan/unplanned" snapshot '("artifact/source") policy))) + (check-equal? (.ref plan 'selected-obligations) '()) + (check-equal? (.ref plan 'blocked-obligations) '("obligation/a")) + (check-equal? (.ref (car (.ref plan 'requests)) 'dependencies) + '("obligation/b")) + (check-equal? (.ref (car (.ref plan 'requests)) 'blocker) + 'dependency-unplanned))) + + (test-case "a blocked prerequisite propagates along POO Graph reachability" + (let* ((a (verification-obligation "obligation/a" 'gerbil-test)) + (b (verification-obligation "obligation/b" 'lean)) + (snapshot + (verification-snapshot + (list a b) + (list (verification-support "obligation/a") + (verification-support "obligation/b") + (verification-dependency "obligation/a" "obligation/b")))) + (policy + (assurance-verification-policy + "policy/release" "r1" '(gerbil-test))) + (plan + (assurance-plan-verification + "plan/blocked-prerequisite" snapshot + '("artifact/source") policy))) + (check-equal? (.ref plan 'selected-obligations) '()) + (check-equal? (.ref plan 'blocked-obligations) + '("obligation/b" "obligation/a")) + (check-equal? (.ref (car (.ref plan 'requests)) 'blocker) + 'capability-unavailable) + (check-equal? (.ref (cadr (.ref plan 'requests)) 'blocker) + 'dependency-blocked))) + + (test-case "a dependency cycle blocks its SCC but preserves an independent branch" + (let* ((a (verification-obligation "obligation/a" 'gerbil-test)) + (b (verification-obligation "obligation/b" 'gerbil-test)) + (c (verification-obligation "obligation/c" 'gerbil-test)) + (d (verification-obligation "obligation/d" 'gerbil-test)) + (snapshot + (verification-snapshot + (list a b c d) + (list (verification-support "obligation/a") + (verification-support "obligation/b") + (verification-support "obligation/c") + (verification-support "obligation/d") + (verification-dependency "obligation/a" "obligation/b") + (verification-dependency "obligation/b" "obligation/a") + (verification-dependency "obligation/d" "obligation/a")))) + (policy + (assurance-verification-policy + "policy/release" "r1" '(gerbil-test))) + (plan + (assurance-plan-verification + "plan/cycle" snapshot '("artifact/source") policy))) + (check-equal? (.ref plan 'selected-obligations) '("obligation/c")) + (check-equal? (.ref plan 'blocked-obligations) + '("obligation/a" "obligation/b" "obligation/d")) + (check-equal? (.ref plan 'cycle-path) + '("obligation/a" "obligation/b" "obligation/a")) + (check-equal? (.ref plan 'cyclic-components) + '(("obligation/a" "obligation/b"))) + (check-equal? (map (lambda (request) (.ref request 'blocker)) + (.ref plan 'requests)) + '(#f dependency-cycle dependency-cycle + dependency-blocked)))) + + (test-case "every independent dependency cycle is explained" + (let* ((ids '("obligation/a" "obligation/b" "obligation/c" + "obligation/d" "obligation/e" "obligation/f" + "obligation/g")) + (nodes (map (lambda (id) + (verification-obligation id 'gerbil-test)) + ids)) + (relations + (append + (map verification-support ids) + (list (verification-dependency "obligation/a" "obligation/b") + (verification-dependency "obligation/b" "obligation/a") + (verification-dependency "obligation/d" "obligation/a") + (verification-dependency "obligation/e" "obligation/f") + (verification-dependency "obligation/f" "obligation/e") + (verification-dependency "obligation/g" "obligation/e")))) + (policy + (assurance-verification-policy + "policy/release" "r1" '(gerbil-test))) + (left + (assurance-plan-verification + "plan/two-cycles" + (verification-snapshot nodes relations) + '("artifact/source") policy)) + (right + (assurance-plan-verification + "plan/two-cycles" + (verification-snapshot (reverse nodes) (reverse relations)) + '("artifact/source") policy))) + (check-equal? (.ref left 'selected-obligations) '("obligation/c")) + (check-equal? (.ref left 'blocked-obligations) + '("obligation/a" "obligation/b" "obligation/d" + "obligation/e" "obligation/f" "obligation/g")) + (check-equal? (.ref left 'cyclic-components) + '(("obligation/a" "obligation/b") + ("obligation/e" "obligation/f"))) + (check-equal? (.ref left 'digest) (.ref right 'digest)) + (check-equal? (.ref left 'cyclic-components) + (.ref right 'cyclic-components)))) + + (test-case "missing capability remains an explained blocked obligation" + (let* ((policy + (assurance-verification-policy "policy/release" "r1" '())) + (plan + (assurance-plan-verification + "plan/no-capability" full-software-snapshot + '("artifact/source") policy))) + (check-equal? (.ref plan 'selected-obligations) '()) + (check-equal? (.ref plan 'blocked-obligations) + '("obligation/verify")) + (check-equal? (.ref (car (.ref plan 'requests)) 'blocker) + 'capability-unavailable) + (check-equal? (.ref plan 'release-authorized?) #f))) + + (test-case "unresolved inputs and old obligation snapshots fail closed" + (let* ((policy + (assurance-verification-policy + "policy/release" "r1" '(gerbil-test))) + (unresolved-snapshot + (software-snapshot + (list artifact claim obligation evidence decision effect) + (list dependency discharge obligation-support + decision-dependency authorization) + '("artifact/missing"))) + (unresolved-plan + (assurance-plan-verification + "plan/unresolved" unresolved-snapshot + '("artifact/source") policy)) + (old-obligation + (.o (:: @ obligation) snapshot: "snapshot/old")) + (old-snapshot + (software-snapshot + (list artifact claim old-obligation evidence decision effect) + (list dependency discharge obligation-support + decision-dependency authorization))) + (old-plan + (assurance-plan-verification + "plan/old" old-snapshot '("artifact/source") policy))) + (check-equal? (.ref unresolved-plan 'selected-obligations) '()) + (check-equal? (.ref unresolved-plan 'unresolved) + '("artifact/missing")) + (check-equal? + (.ref (car (.ref unresolved-plan 'requests)) 'blocker) + 'unresolved-frontier) + (check-equal? (.ref old-plan 'selected-obligations) '()) + (check-equal? (.ref (car (.ref old-plan 'requests)) 'blocker) + 'stale-obligation))) + + (test-case "same-identity snapshot revisions and contents cannot reuse old obligations" + (let* ((policy + (assurance-verification-policy + "policy/release" "r1" '(gerbil-test))) + (nodes (.ref full-software-snapshot 'nodes)) + (relations (.ref full-software-snapshot 'relations)) + (new-revision + (software-snapshot nodes relations '() "r2")) + (changed-content + (software-snapshot + (map (lambda (node) + (if (equal? (.ref node 'identity) "claim/release") + (.o (:: @ node) predicate: "release-ready-v2") + node)) + nodes) + relations)) + (revision-plan + (assurance-plan-verification + "plan/new-revision" new-revision + '("artifact/source") policy)) + (content-plan + (assurance-plan-verification + "plan/changed-content" changed-content + '("artifact/source") policy))) + (check-equal? (.ref new-revision 'identity) + (.ref full-software-snapshot 'identity)) + (check-equal? (.ref changed-content 'revision) + (.ref full-software-snapshot 'revision)) + (check-equal? (.ref revision-plan 'selected-obligations) '()) + (check-equal? (.ref content-plan 'selected-obligations) '()) + (check-equal? (.ref (car (.ref revision-plan 'requests)) 'blocker) + 'stale-obligation) + (check-equal? (.ref (car (.ref content-plan 'requests)) 'blocker) + 'stale-obligation) + (check-equal? (.ref revision-plan 'verifier-executed?) #f) + (check-equal? (.ref content-plan 'release-authorized?) #f) + (check-exception + (assurance-bind-obligation-to-snapshot + (car (filter assurance-obligation? nodes)) new-revision) + Error?))) + + (test-case "conflicted snapshot remains distinct from unknown frontier" + (let* ((snapshot + (assurance-snapshot + "snapshot/software" "r1" "graph/software" + '(("artifact/source" . "r2")) + '(("claim/release" . "r1")) + "event-cut/1" "policy/release" "r1" + (list artifact claim obligation evidence decision effect) + (list dependency discharge obligation-support + decision-dependency authorization) + evidence-identities: '("evidence/test"))) + (policy + (assurance-verification-policy + "policy/release" "r1" '(gerbil-test))) + (plan + (assurance-plan-verification + "plan/conflict" snapshot '("artifact/source") policy))) + (check-equal? (.ref plan 'conflicts) '("artifact/source")) + (check-equal? (.ref plan 'unresolved) '()) + (check-equal? (.ref plan 'selected-obligations) '()) + (check-equal? (.ref (car (.ref plan 'requests)) 'blocker) + 'conflicted-snapshot))) + + (test-case "verification policy cannot silently change snapshot authority" + (check-exception + (assurance-verification-policy "policy/release" "r1" '("gerbil-test")) + Error?) + (check-exception + (assurance-plan-verification + "plan/wrong-policy" full-software-snapshot + '("artifact/source") + (assurance-verification-policy + "policy/other" "r1" '(gerbil-test))) + Error?)) + (test-case "structural cycles have deterministic SCCs and witnesses" (let* ((snapshot (software-snapshot diff --git a/t/assurance/unit/decision-preflight-test.ss b/t/assurance/unit/decision-preflight-test.ss new file mode 100644 index 0000000..fe02e8c --- /dev/null +++ b/t/assurance/unit/decision-preflight-test.ss @@ -0,0 +1,228 @@ +;;; -*- Gerbil -*- +;;; SPDX-FileCopyrightText: 2026 tao3k team and Contributors +;;; +;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +(import :std/test :std/error + (only-in :clan/poo/object .o .ref) + (only-in :poo-flow/modules/authorization/providers/cedar/interface + poo-flow-cedar-runtime-handoff + poo-flow-cedar-authorization-request? + poo-flow-cedar-authorization-request->runtime) + :poo-flow/lambda-aitia/modules/assurance/interface) + +(export decision-preflight-test) + +(def digest-a (string-append "sha256:" (make-string 64 #\a))) +(def digest-b (string-append "sha256:" (make-string 64 #\b))) +(def artifact + (assurance-artifact "artifact/source" "r1" 'supported digest-a + owner: "maintainer" media-kind: 'scheme-source)) +(def claim + (assurance-claim "claim/release" "r1" 'unknown digest-b + subject: "software/release" predicate: "ready" + support-requirements: '("obligation/test") + scope: "repository" valid-from: "2026-09-23")) +(def obligation + (assurance-obligation + "obligation/test" "r1" 'unknown digest-a + subject: "software/release" claim: "claim/release" + snapshot: "snapshot/software" evidence-kind: 'native-test + capability: 'gerbil-test scope: "repository")) +(def evidence + (assurance-evidence + "evidence/test" "r1" 'supported digest-b + producer: "gxtest" tool: "gerbil-test" tool-version: "v19" + input-artifacts: '("artifact/source") obligation: "obligation/test" + subject: "software/release" scope: "repository" + valid-from: "2026-09-23" admission-state: 'candidate)) +(def decision + (assurance-decision + "decision/release" "r1" 'unknown digest-a + subject: "software/release" snapshot: "snapshot/software" + policy: "policy/release" authority: "maintainer" outcome: 'unknown)) +(def evidence-link + (assurance-relation "relation/discharge" 'assurance 'discharges + "evidence/test" "obligation/test" 'observed)) +(def requirement-link + (assurance-relation "relation/requirement" 'assurance 'supports + "obligation/test" "claim/release" 'declared)) +(def decision-link + (assurance-relation "relation/decision-dependency" 'structural 'depends-on + "decision/release" "claim/release" 'declared)) +(def relations (list evidence-link requirement-link decision-link)) + +(def (construct obligation-value relation-values fact-cut-value) + (assurance-snapshot + "snapshot/software" "r1" "graph/software" + '(("artifact/source" . "r1")) '(("claim/release" . "r1")) + fact-cut-value "policy/release" "r1" + (list artifact claim obligation-value evidence decision) relation-values)) +(def (bound-cut relation-values) + (let (draft (construct obligation relation-values "event-cut/1")) + (construct (assurance-bind-obligation-to-snapshot obligation draft) + relation-values "event-cut/1"))) +(def (bound-obligation snapshot-value) + (let loop ((nodes (.ref snapshot-value 'nodes))) + (and (pair? nodes) + (if (equal? (.ref (car nodes) 'identity) "obligation/test") + (car nodes) + (loop (cdr nodes)))))) +(def (reported-outcome snapshot-value) + (assurance-verifier-outcome + "outcome/test" 'succeeded + producer: "gxtest" tool: "gerbil-test" tool-version: "v19" + obligation: "obligation/test" subject: "software/release" + scope: "repository" snapshot-revision: "r1" + snapshot-context-digest: (.ref snapshot-value 'context-digest) + inputs: (list (assurance-verifier-input artifact)) + output-digest: digest-b)) +(def (make-host source) + (assurance-verification-host + "host/decision-preflight" + (lambda (subject-value issued-at expires-at) #t) + (lambda () 11) 9 source)) +(def handoff + (poo-flow-cedar-runtime-handoff + 1 #u8(1 2 3) digest-a digest-a digest-b digest-b)) + +(def decision-preflight-test + (test-suite "Host Decision preflight is not authorization" + (test-case "exact undecided Decision and support issue one Host preflight" + (let* ((snapshot-value (bound-cut relations)) + (host (make-host (lambda () snapshot-value))) + (admission + (assurance-host-admit + host (bound-obligation snapshot-value) evidence + (reported-outcome snapshot-value))) + (preflight + (assurance-host-preflight-decision + host decision claim (list admission))) + (other-host (make-host (lambda () snapshot-value)))) + (check-equal? (assurance-decision-preflight? preflight) #t) + (check-equal? (.ref preflight 'release-authorized?) #f) + (check-equal? + (assurance-decision-preflight? + (.o (:: @ preflight) release-authorized?: #t)) #f) + (check-equal? + (assurance-host-decision-preflight-current? host preflight) #t) + (check-equal? + (assurance-host-decision-preflight-current? + host (.o (:: @ preflight))) #f) + (check-equal? + (assurance-host-decision-preflight-current? + other-host preflight) #f) + (check-equal? + (assurance-host-preflight-decision + host (.o (:: @ decision) outcome: 'allow) + claim (list admission)) #f) + (check-equal? + (assurance-host-preflight-decision host decision claim '()) #f) + (assurance-host-revoke-admission! host admission) + (check-equal? + (assurance-host-decision-preflight-current? host preflight) #f))) + + (test-case "missing Decision dependency cannot preflight" + (let* ((snapshot-value + (bound-cut (list evidence-link requirement-link))) + (host (make-host (lambda () snapshot-value))) + (admission + (assurance-host-admit + host (bound-obligation snapshot-value) evidence + (reported-outcome snapshot-value)))) + (check-equal? + (assurance-host-preflight-decision + host decision claim (list admission)) #f))) + + (test-case "only a current Host preflight projects an inert Cedar request" + (let* ((snapshot-value (bound-cut relations)) + (host (make-host (lambda () snapshot-value))) + (admission + (assurance-host-admit + host (bound-obligation snapshot-value) evidence + (reported-outcome snapshot-value))) + (preflight + (assurance-host-preflight-decision + host decision claim (list admission))) + (request + (assurance-host-cedar-request + host preflight "Release" digest-a handoff)) + (other-host (make-host (lambda () snapshot-value))) + (runtime-value + (poo-flow-cedar-authorization-request->runtime request)) + (context-value (hash-get runtime-value "context"))) + (check-equal? (poo-flow-cedar-authorization-request? request) #t) + (check-equal? (assurance-host-cedar-request-current? host request) #t) + (check-equal? + (assurance-host-cedar-request-current? + host (.o (:: @ request))) #f) + (check-equal? + (assurance-host-cedar-request-current? + host (.o (:: @ request) action: "Deploy")) #f) + (check-equal? + (assurance-host-cedar-request-current? other-host request) #f) + (check-equal? (hash-get runtime-value "principal") "maintainer") + (check-equal? (hash-get runtime-value "resource") "software/release") + (check-equal? (hash-get context-value "aitia_preflight") + (.ref preflight 'identity)) + (check-equal? (hash-get context-value "aitia_policy") "policy/release") + (check-equal? (hash-get context-value "aitia_preflight_only") #t) + (check-equal? (hash-get context-value "poo_flow_preflight_only") #t) + (check-equal? + (assurance-host-cedar-request + host (.o (:: @ preflight)) "Release" digest-a handoff) #f) + (check-exception + (assurance-host-cedar-request + host preflight "Release" digest-a (.o)) Error?) + (assurance-host-revoke-decision-preflight! host preflight) + (check-equal? + (assurance-host-cedar-request-current? host request) #f) + (check-equal? + (assurance-host-cedar-request + host preflight "Release" digest-a handoff) #f))) + + (test-case "source change and rollback cannot revive a preflight" + (let* ((current (bound-cut relations)) + (host (make-host (lambda () current))) + (admission + (assurance-host-admit + host (bound-obligation current) evidence + (reported-outcome current))) + (preflight + (assurance-host-preflight-decision + host decision claim (list admission))) + (request + (assurance-host-cedar-request + host preflight "Release" digest-a handoff)) + (old-cut current)) + (check-equal? + (assurance-host-decision-preflight-current? host preflight) #t) + (check-equal? + (assurance-host-cedar-request-current? host request) #t) + (set! current + (construct (bound-obligation old-cut) relations "event-cut/2")) + (check-equal? + (assurance-host-decision-preflight-current? host preflight) #f) + (check-equal? + (assurance-host-cedar-request-current? host request) #f) + (set! current old-cut) + (check-equal? + (assurance-host-decision-preflight-current? host preflight) #f) + (check-equal? + (assurance-host-cedar-request-current? host request) #f))) + + (test-case "explicit preflight revocation is separate from evidence" + (let* ((snapshot-value (bound-cut relations)) + (host (make-host (lambda () snapshot-value))) + (admission + (assurance-host-admit + host (bound-obligation snapshot-value) evidence + (reported-outcome snapshot-value))) + (preflight + (assurance-host-preflight-decision + host decision claim (list admission)))) + (assurance-host-revoke-decision-preflight! host preflight) + (check-equal? + (assurance-host-decision-preflight-current? host preflight) #f) + (check-equal? + (assurance-host-admission-current? host admission) #t))))) diff --git a/t/assurance/unit/evidence-admission-test.ss b/t/assurance/unit/evidence-admission-test.ss new file mode 100644 index 0000000..de8a929 --- /dev/null +++ b/t/assurance/unit/evidence-admission-test.ss @@ -0,0 +1,593 @@ +;;; -*- Gerbil -*- +;;; SPDX-FileCopyrightText: 2026 tao3k team and Contributors +;;; +;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +(import :std/test + (only-in :std/error Error?) + (only-in :clan/poo/object .o .ref) + (only-in :poo-flow/core/contribution/verification + poo-flow-verification-adapter poo-flow-verify + poo-flow-revoke-verification!) + :poo-flow/lambda-aitia/modules/assurance/interface) + +(export evidence-admission-test) + +(def digest-a (string-append "sha256:" (make-string 64 #\a))) +(def digest-b (string-append "sha256:" (make-string 64 #\b))) +(def artifact + (assurance-artifact "artifact/source" "r1" 'supported digest-a + owner: "maintainer" media-kind: 'scheme-source)) +(def claim + (assurance-claim "claim/release" "r1" 'unknown digest-b + subject: "software/release" predicate: "release-ready" + scope: "repository" valid-from: "2026-09-23")) +(def obligation + (assurance-obligation + "obligation/test" "r1" 'unknown digest-a + subject: "software/release" claim: "claim/release" + snapshot: "snapshot/software" evidence-kind: 'native-test + capability: 'gerbil-test scope: "repository")) +(def (make-snapshot artifact-value obligation-value (revision "r1")) + (assurance-snapshot + "snapshot/software" revision "graph/software" + (list (cons "artifact/source" (.ref artifact-value 'revision))) + '(("claim/release" . "r1")) + "event-cut/1" "policy/release" "r1" + (list artifact-value claim obligation-value) '())) +(def draft (make-snapshot artifact obligation)) +(def bound-obligation (assurance-bind-obligation-to-snapshot obligation draft)) +(def snapshot (make-snapshot artifact bound-obligation)) +(def outcome + (assurance-verifier-outcome + "outcome/test" 'succeeded + producer: "gxtest" tool: "gerbil-test" tool-version: "v19" + obligation: "obligation/test" subject: "software/release" + scope: "repository" snapshot-revision: "r1" + snapshot-context-digest: (.ref snapshot 'context-digest) + inputs: (list (assurance-verifier-input artifact)) + output-digest: digest-b)) +(def trusted-evidence + (assurance-evidence + "evidence/test" "r1" 'supported digest-b + producer: "gxtest" tool: "gerbil-test" tool-version: "v19" + input-artifacts: '("artifact/source") obligation: "obligation/test" + subject: "software/release" scope: "repository" + valid-from: "2026-09-23" admission-state: 'admitted)) +(def trusted-discharge + (assurance-relation "relation/discharge" 'assurance 'discharges + "evidence/test" "obligation/test" 'observed)) +(def (make-trusted-snapshot obligation-value) + (assurance-snapshot + "snapshot/software" "r1" "graph/software" + '(("artifact/source" . "r1")) '(("claim/release" . "r1")) + "event-cut/1" "policy/release" "r1" + (list artifact claim obligation-value trusted-evidence) + (list trusted-discharge) + evidence-identities: '("evidence/test"))) +(def trusted-draft (make-trusted-snapshot obligation)) +(def trusted-obligation + (assurance-bind-obligation-to-snapshot obligation trusted-draft)) +(def trusted-snapshot (make-trusted-snapshot trusted-obligation)) +(def trusted-outcome + (.o (:: @ outcome) + snapshot-context-digest: (.ref trusted-snapshot 'context-digest))) +(def candidate-evidence + (.o (:: @ trusted-evidence) admission-state: 'candidate)) +(def (make-candidate-snapshot source-value obligation-value revision-value) + (assurance-snapshot + "snapshot/software" revision-value "graph/software" + (list (cons "artifact/source" (.ref source-value 'revision))) + '(("claim/release" . "r1")) + "event-cut/1" "policy/release" "r1" + (list source-value claim obligation-value candidate-evidence) + (list trusted-discharge))) +(def candidate-draft + (make-candidate-snapshot artifact obligation "r1")) +(def candidate-obligation + (assurance-bind-obligation-to-snapshot obligation candidate-draft)) +(def candidate-snapshot + (make-candidate-snapshot artifact candidate-obligation "r1")) +(def candidate-outcome + (.o (:: @ outcome) + snapshot-context-digest: (.ref candidate-snapshot 'context-digest))) +(def trusted-adapter + (poo-flow-verification-adapter + "host/native-test" + (lambda (subject-value now until) + (and (eq? (.ref (.ref subject-value 'outcome) 'status) 'succeeded) + (equal? (.ref (.ref subject-value 'evidence) 'content-digest) + digest-b))) + assurance-verification-subject-snapshot)) +(def trusted-subject + (assurance-verification-subject + trusted-snapshot trusted-obligation trusted-evidence trusted-outcome)) +(def trusted-receipt + (poo-flow-verify trusted-adapter trusted-subject 10 20)) +(def untrusted-adapter + (poo-flow-verification-adapter + "other/native-test" + (lambda (subject-value now until) #t) + assurance-verification-subject-snapshot)) +(def (verified-support relation-value evidence-value obligation-value + snapshot-value outcome-value receipt-value now-value) + (assurance-support-sealed-under-adapter? + relation-value evidence-value obligation-value snapshot-value outcome-value + trusted-adapter receipt-value now-value)) +(def (blocker snapshot-value obligation-value outcome-value) + (.ref (assurance-evaluate-evidence-admission + snapshot-value obligation-value outcome-value) + 'blocker)) + +(def evidence-admission-test + (test-suite "evidence admission eligibility" + (test-case "bound success is eligible but does not admit or authorize" + (let (receipt + (assurance-evaluate-evidence-admission + snapshot bound-obligation outcome)) + (check-equal? (assurance-evidence-admission-receipt? receipt) #t) + (check-equal? (.ref receipt 'eligible?) #t) + (check-equal? (.ref receipt 'verifier-executed?) #f) + (check-equal? (.ref receipt 'release-authorized?) #f) + (check-equal? (.ref receipt 'snapshot-digest) (.ref snapshot 'digest)) + (check-equal? + (.ref receipt 'digest) + (.ref (assurance-evaluate-evidence-admission + snapshot bound-obligation outcome) + 'digest)) + (check-equal? + (equal? (.ref receipt 'digest) + (.ref (assurance-evaluate-evidence-admission + snapshot bound-obligation + (.o (:: @ outcome) output-digest: digest-a)) + 'digest)) + #f))) + + (test-case "non-success statuses are typed, not green" + (for-each + (lambda (reported-status) + (check-equal? + (blocker snapshot bound-obligation + (.o (:: @ outcome) status: reported-status)) + 'outcome-not-successful)) + '(failed timed-out unavailable indeterminate))) + + (test-case "green exit without output and bound inputs is rejected" + (check-equal? + (blocker snapshot bound-obligation + (.o (:: @ outcome) output-digest: #f)) + 'missing-output) + (check-equal? + (blocker snapshot bound-obligation + (.o (:: @ outcome) inputs: '())) + 'missing-inputs) + (check-equal? + (blocker snapshot bound-obligation + (.o (:: @ outcome) + inputs: (list (assurance-verifier-input artifact) + (assurance-verifier-input artifact)))) + 'duplicate-inputs)) + + (test-case "changed source revision and digest invalidate old result" + (let* ((new-artifact + (.o (:: @ artifact) revision: "r2" content-digest: digest-b)) + (new-draft (make-snapshot new-artifact obligation "r2")) + (new-obligation + (assurance-bind-obligation-to-snapshot obligation new-draft)) + (new-snapshot (make-snapshot new-artifact new-obligation "r2"))) + (check-equal? + (blocker new-snapshot bound-obligation outcome) + 'stale-obligation) + (check-equal? + (blocker new-snapshot new-obligation + (.o (:: @ outcome) + snapshot-revision: "r2" + snapshot-context-digest: + (.ref new-snapshot 'context-digest))) + 'input-binding-mismatch))) + + (test-case "same revision with changed artifact digest is not reusable" + (let* ((new-artifact (.o (:: @ artifact) content-digest: digest-b)) + (new-draft (make-snapshot new-artifact obligation)) + (new-obligation + (assurance-bind-obligation-to-snapshot obligation new-draft)) + (new-snapshot (make-snapshot new-artifact new-obligation))) + (check-equal? + (blocker new-snapshot new-obligation outcome) + 'outcome-binding-mismatch) + (check-equal? + (blocker new-snapshot new-obligation + (.o (:: @ outcome) + snapshot-context-digest: + (.ref new-snapshot 'context-digest))) + 'input-binding-mismatch))) + + (test-case "same revision with forged obligation semantics is stale" + (check-equal? + (blocker snapshot + (.o (:: @ bound-obligation) subject: "software/other") + outcome) + 'stale-obligation)) + + (test-case "conflicting snapshot never accepts a reported success" + (let (conflicted + (assurance-snapshot + "snapshot/software" "r1" "graph/software" + '(("artifact/source" . "r1")) '(("claim/release" . "r1")) + "event-cut/1" "policy/release" "r1" + (list artifact + (.o (:: @ artifact) content-digest: digest-b) + claim bound-obligation) + '())) + (check-equal? + (blocker conflicted bound-obligation outcome) + 'snapshot-conflicted))) + + (test-case "outcome cannot retarget subject, scope or snapshot" + (check-equal? + (blocker snapshot bound-obligation + (.o (:: @ outcome) subject: "software/other")) + 'outcome-binding-mismatch) + (check-equal? + (blocker snapshot bound-obligation + (.o (:: @ outcome) snapshot-revision: "r0")) + 'outcome-binding-mismatch)) + + (test-case "human review names authority and exact scope" + (let* ((human-obligation + (.o (:: @ obligation) evidence-kind: 'human-review)) + (human-draft (make-snapshot artifact human-obligation)) + (human-bound + (assurance-bind-obligation-to-snapshot human-obligation human-draft)) + (human-snapshot (make-snapshot artifact human-bound)) + (human-outcome + (.o (:: @ outcome) + snapshot-context-digest: + (.ref human-snapshot 'context-digest)))) + (check-equal? + (blocker human-snapshot human-bound human-outcome) + 'human-authority-missing) + (check-equal? + (blocker human-snapshot human-bound + (.o (:: @ human-outcome) + accountable-authority: "maintainer" + review-scope: "repository")) + #f))) + + (test-case "support requires a sealed execution receipt" + (check-equal? + (verified-support + trusted-discharge trusted-evidence trusted-obligation + trusted-snapshot trusted-outcome #f 11) + #f) + (check-equal? + (verified-support + trusted-discharge trusted-evidence trusted-obligation + trusted-snapshot trusted-outcome trusted-receipt 11) + #t)) + + (test-case "copied seal and presentation mutation cannot forge support" + (check-equal? + (assurance-support-sealed-under-adapter? + trusted-discharge trusted-evidence trusted-obligation + trusted-snapshot trusted-outcome + (.o identity: "host/native-test" + admits?: (lambda (receipt-value subject-value now-value) #t)) + trusted-receipt 11) + #f) + (check-equal? + (assurance-support-sealed-under-adapter? + trusted-discharge trusted-evidence trusted-obligation + trusted-snapshot trusted-outcome + (.o (:: @ trusted-adapter)) trusted-receipt 11) + #f) + (check-equal? + (verified-support + trusted-discharge trusted-evidence trusted-obligation + trusted-snapshot trusted-outcome + (.o (:: @ trusted-receipt)) 11) + #f) + (check-equal? + (verified-support + trusted-discharge trusted-evidence trusted-obligation + (.o (:: @ trusted-snapshot) state: 'supported) + trusted-outcome trusted-receipt 11) + #f) + (check-equal? + (verified-support + trusted-discharge + (.o (:: @ trusted-evidence) tool-version: "v20") + trusted-obligation trusted-snapshot trusted-outcome + trusted-receipt 11) + #f) + (check-equal? + (verified-support + trusted-discharge trusted-evidence trusted-obligation + trusted-snapshot + (.o (:: @ trusted-outcome) tool-version: "v20") + trusted-receipt 11) + #f) + (check-equal? + (assurance-support-sealed-under-adapter? + trusted-discharge trusted-evidence trusted-obligation + trusted-snapshot trusted-outcome untrusted-adapter + trusted-receipt 11) + #f)) + + (test-case "configured Host owns adapter, clock and revocation" + (let* ((now 11) + (host + (assurance-verification-host + "host/assurance-test" + (lambda (subject-value issued-at expires-at) + (and (eq? (.ref (.ref subject-value 'outcome) 'status) + 'succeeded) + (equal? (.ref (.ref subject-value 'evidence) + 'content-digest) + digest-b))) + (lambda () now) 9 (lambda () trusted-snapshot))) + (receipt + (assurance-host-verify + host trusted-snapshot trusted-obligation trusted-evidence + trusted-outcome))) + (check-equal? (if receipt #t #f) #t) + (check-equal? + (assurance-host-verify + host trusted-snapshot trusted-obligation + (.o (:: @ trusted-evidence) tool-version: "v20") + trusted-outcome) + #f) + (check-equal? + (assurance-host-sealed-support? + host trusted-discharge trusted-evidence trusted-obligation + trusted-snapshot trusted-outcome receipt) + #t) + (check-exception + (assurance-host-sealed-support? + (.o (:: @ host)) trusted-discharge trusted-evidence + trusted-obligation trusted-snapshot trusted-outcome receipt) + Error?) + (check-exception + (assurance-host-sealed-support? + (.o identity: "host/assurance-test") + trusted-discharge trusted-evidence trusted-obligation + trusted-snapshot trusted-outcome receipt) + Error?) + (check-equal? + (assurance-host-verify + host trusted-snapshot trusted-obligation trusted-evidence + (.o (:: @ trusted-outcome) status: 'failed)) + #f) + (set! now 20) + (check-equal? + (assurance-host-sealed-support? + host trusted-discharge trusted-evidence trusted-obligation + trusted-snapshot trusted-outcome receipt) + #f) + (set! now 11) + (check-exception + (assurance-host-sealed-support? + host trusted-discharge trusted-evidence trusted-obligation + trusted-snapshot trusted-outcome receipt) + Error?)) + (let* ((host + (assurance-verification-host + "host/revocation-test" + (lambda (subject-value issued-at expires-at) #t) + (lambda () 11) 9 (lambda () trusted-snapshot))) + (receipt + (assurance-host-verify + host trusted-snapshot trusted-obligation trusted-evidence + trusted-outcome))) + (check-equal? + (assurance-host-sealed-support? + host trusted-discharge trusted-evidence trusted-obligation + trusted-snapshot trusted-outcome receipt) + #t) + (assurance-host-revoke! host receipt) + (check-equal? + (assurance-host-sealed-support? + host trusted-discharge trusted-evidence trusted-obligation + trusted-snapshot trusted-outcome receipt) + #f))) + + (test-case "configured verifier refusal never issues a seal" + (let (host + (assurance-verification-host + "host/refusal-test" + (lambda (subject-value issued-at expires-at) #f) + (lambda () 11) 9 (lambda () trusted-snapshot))) + (check-equal? + (assurance-host-verify + host trusted-snapshot trusted-obligation trusted-evidence + trusted-outcome) + #f))) + + (test-case "old Host seal stays invalid after a source cut returns" + (let* ((current trusted-snapshot) + (host + (assurance-verification-host + "host/seal-cut-test" + (lambda (subject-value issued-at expires-at) #t) + (lambda () 11) 9 (lambda () current))) + (receipt + (assurance-host-verify + host trusted-snapshot trusted-obligation trusted-evidence + trusted-outcome))) + (check-equal? + (assurance-host-sealed-support? + host trusted-discharge trusted-evidence trusted-obligation + trusted-snapshot trusted-outcome receipt) + #t) + (set! current (.o (:: @ trusted-snapshot) fact-cut: "event-cut/2")) + (check-exception + (assurance-host-sealed-support? + host trusted-discharge trusted-evidence trusted-obligation + trusted-snapshot trusted-outcome receipt) + Error?) + (set! current trusted-snapshot) + (check-equal? + (assurance-host-sealed-support? + host trusted-discharge trusted-evidence trusted-obligation + trusted-snapshot trusted-outcome receipt) + #f))) + + (test-case "Host admission binds candidate evidence to an immutable cut" + (let* ((current candidate-snapshot) + (host + (assurance-verification-host + "host/admission-test" + (lambda (subject-value issued-at expires-at) #t) + (lambda () 11) 9 (lambda () current))) + (admission + (assurance-host-admit + host candidate-obligation candidate-evidence candidate-outcome))) + (check-equal? (if admission #t #f) #t) + (check-equal? (.ref candidate-evidence 'admission-state) 'candidate) + (check-equal? (assurance-host-admission-current? host admission) #t) + (check-equal? + (assurance-host-admission-current? host (.o (:: @ admission))) #f) + (check-equal? + (assurance-host-admission-current? + host (.o identity: (.ref admission 'identity))) #f) + (check-equal? + (assurance-host-admit + host candidate-obligation + (.o (:: @ candidate-evidence) tool-version: "v20") + candidate-outcome) + #f) + (let* ((new-source + (.o (:: @ artifact) revision: "r2" content-digest: digest-b)) + (new-snapshot + (make-candidate-snapshot new-source obligation "r2"))) + (set! current new-snapshot) + (check-equal? + (assurance-host-admission-current? host admission) #f) + (set! current candidate-snapshot) + (check-equal? + (assurance-host-admission-current? host admission) #f)))) + + (test-case "only the exact current assurance edge consumes a Host admission" + (let* ((now 11) + (current candidate-snapshot) + (host + (assurance-verification-host + "host/admitted-support-test" + (lambda (subject-value issued-at expires-at) #t) + (lambda () now) 9 (lambda () current))) + (other-host + (assurance-verification-host + "host/admitted-support-other" + (lambda (subject-value issued-at expires-at) #t) + (lambda () now) 9 (lambda () current))) + (admission + (assurance-host-admit + host candidate-obligation candidate-evidence candidate-outcome))) + (check-equal? + (assurance-host-admitted-support? + host trusted-discharge admission) #t) + (check-equal? + (assurance-host-admitted-support? + other-host trusted-discharge admission) #f) + (check-equal? + (assurance-host-admitted-support? + host trusted-discharge (.o (:: @ admission))) #f) + (check-equal? + (assurance-host-admitted-support? + host (.o (:: @ trusted-discharge) modality: 'hypothesized) + admission) #f) + (check-equal? + (assurance-host-admitted-support? + host (.o (:: @ trusted-discharge) target: "obligation/other") + admission) #f) + (set! now 20) + (check-equal? + (assurance-host-admitted-support? + host trusted-discharge admission) #f))) + + (test-case "semantic snapshot mutation with unchanged display digest revokes" + (let* ((current candidate-snapshot) + (host + (assurance-verification-host + "host/semantic-cut-test" + (lambda (subject-value issued-at expires-at) #t) + (lambda () 11) 9 (lambda () current))) + (admission + (assurance-host-admit + host candidate-obligation candidate-evidence candidate-outcome))) + (check-equal? (assurance-host-admission-current? host admission) #t) + (set! current + (.o (:: @ candidate-snapshot) fact-cut: "event-cut/2")) + (check-equal? (.ref current 'digest) + (.ref candidate-snapshot 'digest)) + (check-exception + (assurance-host-admission-current? host admission) Error?) + (set! current candidate-snapshot) + (check-equal? (assurance-host-admission-current? host admission) #f))) + + (test-case "invalid Host snapshot source cannot revive an old admission" + (let* ((current candidate-snapshot) + (host + (assurance-verification-host + "host/invalid-source-test" + (lambda (subject-value issued-at expires-at) #t) + (lambda () 11) 9 (lambda () current))) + (admission + (assurance-host-admit + host candidate-obligation candidate-evidence candidate-outcome))) + (check-equal? (assurance-host-admission-current? host admission) #t) + (set! current #f) + (check-exception + (assurance-host-admission-current? host admission) Error?) + (set! current candidate-snapshot) + (check-equal? (assurance-host-admission-current? host admission) #f))) + + (test-case "Host admission expires, revokes and refuses failed verification" + (let* ((now 11) + (host + (assurance-verification-host + "host/admission-expiry" + (lambda (subject-value issued-at expires-at) #t) + (lambda () now) 9 (lambda () candidate-snapshot))) + (admission + (assurance-host-admit + host candidate-obligation candidate-evidence candidate-outcome))) + (check-equal? (assurance-host-admission-current? host admission) #t) + (set! now 20) + (check-equal? (assurance-host-admission-current? host admission) #f) + (set! now 11) + (check-exception + (assurance-host-admission-current? host admission) Error?)) + (let* ((host + (assurance-verification-host + "host/admission-revocation" + (lambda (subject-value issued-at expires-at) #t) + (lambda () 11) 9 (lambda () candidate-snapshot))) + (admission + (assurance-host-admit + host candidate-obligation candidate-evidence candidate-outcome))) + (check-equal? (assurance-host-admission-current? host admission) #t) + (assurance-host-revoke-admission! host admission) + (check-equal? (assurance-host-admission-current? host admission) #f)) + (let (host + (assurance-verification-host + "host/admission-refusal" + (lambda (subject-value issued-at expires-at) #f) + (lambda () 11) 9 (lambda () candidate-snapshot))) + (check-equal? + (assurance-host-admit + host candidate-obligation candidate-evidence candidate-outcome) + #f))) + + (test-case "expiry and revocation remove verified support" + (check-equal? + (verified-support + trusted-discharge trusted-evidence trusted-obligation + trusted-snapshot trusted-outcome trusted-receipt 20) + #f) + (poo-flow-revoke-verification! trusted-adapter trusted-receipt) + (check-equal? + (verified-support + trusted-discharge trusted-evidence trusted-obligation + trusted-snapshot trusted-outcome trusted-receipt 11) + #f)))) diff --git a/t/assurance/unit/lifecycle-boundary-test.ss b/t/assurance/unit/lifecycle-boundary-test.ss new file mode 100644 index 0000000..3f99e2d --- /dev/null +++ b/t/assurance/unit/lifecycle-boundary-test.ss @@ -0,0 +1,212 @@ +;;; -*- Gerbil -*- +;;; SPDX-FileCopyrightText: 2026 tao3k team and Contributors +;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later +(import :std/test :std/error (only-in :clan/poo/object .o .ref) + :poo-flow/lambda-aitia/modules/assurance/interface + :poo-flow/lambda-aitia/t/formal/lifecycle-fixture) +(export lifecycle-boundary-test) +(def (drive fixture actions) + (for-each (lambda (action) (check (lifecycle-action! fixture action) => #t)) actions) + fixture) +(def (ready) + (drive (make-lifecycle-fixture) '(InstallAdapter Plan Verify Admit Decide Authorize))) +(def lifecycle-boundary-test + (test-suite "Lifecycle authorization, reservation and delivery boundaries" + (test-case "Admit consumes the prior seal without executing or renewing verification" + (let (f (drive (make-lifecycle-fixture) '(InstallAdapter Plan Verify))) + (check (lifecycle-get f 'verifier-calls) => 1) + (check (lifecycle-action! f 'Admit) => #t) + (check (lifecycle-get f 'verifier-calls) => 1) + (check (.ref (lifecycle-get f 'admission) 'expires-at) => 1)) + (let (f (drive (make-lifecycle-fixture) '(InstallAdapter Plan Verify Tick))) + (check (lifecycle-action! f 'Admit) => #f) + (check (lifecycle-get f 'verifier-calls) => 1))) + (test-case "untrusted report cannot admit authorize reserve or deliver" + (let (f (drive (make-lifecycle-fixture) '(ReportSuccess))) + (check (lifecycle-action! f 'UnsafeLateAdmit) => #f) + (check (lifecycle-action! f 'UnsafeEarlyGrant) => #f) + (check (lifecycle-action! f 'UnsafeStart) => #f) + (check (.ref (lifecycle-action! f 'UnsafeBeforeStart) 'outcome) => 'rejected) + (check (lifecycle-get f 'authorization-calls) => 0) + (check (lifecycle-get f 'delivery-calls) => 0))) + (test-case "copied seals and foreign Host seals cannot admit" + (let* ((f (drive (make-lifecycle-fixture) '(InstallAdapter Plan Verify))) + (other (drive (make-lifecycle-fixture) '(InstallAdapter Plan Verify))) + (seal (lifecycle-get f 'seal))) + (lifecycle-set! f 'seal (.o (:: @ seal))) + (check (lifecycle-action! f 'Admit) => #f) + (lifecycle-set! f 'seal (lifecycle-get other 'seal)) + (check (lifecycle-action! f 'Admit) => #f))) + (test-case "deny and indeterminate never receive a grant" + (for-each + (lambda (mode) + (let (f (drive (make-lifecycle-fixture) '(InstallAdapter Plan Verify Admit Decide))) + (lifecycle-set! f 'policy-mode mode) + (check (lifecycle-action! f 'Authorize) => #f) + (check (lifecycle-get f 'authorization-calls) => 1) + (check (lifecycle-action! f 'StartEffect) => #f) + (check (lifecycle-get f 'delivery-calls) => 0))) '(forbid indeterminate))) + (test-case "reserve is not delivery; current reservation hands off exactly once" + (let (f (ready)) + (check (lifecycle-action! f 'StartEffect) => #t) + (check (lifecycle-get f 'service-effects) => 0) + (check (lifecycle-action! f 'StartEffect) => #f) + (check (.ref (lifecycle-action! f 'DeliverEffect) 'delivered?) => #t) + (check (.ref (lifecycle-action! f 'UnsafeReplay) 'outcome) => 'rejected) + (check (lifecycle-get f 'delivery-calls) => 1) + (check (lifecycle-get f 'service-effects) => 1))) + (test-case "renewed verification does not renew an old grant or reservation" + (let (f (ready)) + (check (lifecycle-action! f 'Verify) => #t) + (check (lifecycle-action! f 'StartEffect) => #t) + (check (.ref (lifecycle-action! f 'DeliverEffect) 'delivered?) => #t)) + (let (f (drive (ready) '(StartEffect Tick Verify Admit Decide Authorize))) + (check (.ref (lifecycle-get f 'grant) 'expires-at) => 2) + (check (.ref (lifecycle-action! f 'DeliverEffect) 'outcome) => 'rejected) + (check (lifecycle-get f 'delivery-calls) => 0))) + (test-case "expiry revoke and source change between reserve and delivery fail closed" + (for-each + (lambda (change) + (let (f (drive (ready) '(StartEffect))) + (check (lifecycle-action! f change) => #t) + (check (.ref (lifecycle-action! f 'UnsafeDelivery) 'outcome) => 'rejected) + (check (lifecycle-get f 'delivery-calls) => 0) + (check (lifecycle-get f 'service-effects) => 0))) '(Tick Revoke ChangeSource))) + (test-case "source change during authorization cannot publish a grant" + (let (f (drive (make-lifecycle-fixture) '(InstallAdapter Plan Verify Admit Decide))) + (lifecycle-set! f 'authorization-hook + (lambda () (lifecycle-set! f 'cut (lifecycle-cut 2)))) + (check (lifecycle-action! f 'Authorize) => #f) + (check (lifecycle-get f 'delivery-calls) => 0))) + (test-case "permit cannot publish after policy callback expiry or request mutation" + (for-each + (lambda (change) + (let* ((f (ready)) (request (lifecycle-get f 'request))) + (lifecycle-set! f 'authorization-hook + (lambda () + (case change + ((expiry) (lifecycle-set! f 'clock 1)) + ((request) (string-set! (.ref request 'action) 0 #\X))))) + (check (assurance-effect-host-authorize! (lifecycle-get f 'effect-host) + (lifecycle-get f 'verification-host) request) => #f) + (check (lifecycle-get f 'authorization-calls) => 2) + (check (lifecycle-action! f 'StartEffect) => #f) + (check (lifecycle-get f 'delivery-calls) => 0))) '(expiry request))) + (test-case "copied grant extended expiry and copied reservation are refused" + (let* ((f (ready)) (host (lifecycle-get f 'effect-host)) + (grant (lifecycle-get f 'grant))) + (check (assurance-effect-host-reserve! host (.o (:: @ grant))) => #f) + (check (assurance-effect-host-reserve! host (.o (:: @ grant) expires-at: 99)) => #f) + (let (reservation (assurance-effect-host-reserve! host grant)) + (check (.ref (assurance-effect-host-deliver! host (.o (:: @ reservation))) 'outcome) => 'rejected) + (check (.ref (assurance-effect-host-deliver! host reservation) 'delivered?) => #t)))) + (test-case "foreign Host grant and reservation never reach a runtime callback" + (let* ((f (ready)) (other (ready)) (host (lifecycle-get other 'effect-host)) + (grant (lifecycle-get f 'grant))) + (check (assurance-effect-host-reserve! host grant) => #f) + (check (lifecycle-action! f 'StartEffect) => #t) + (check (.ref (assurance-effect-host-deliver! host (lifecycle-get f 'reservation)) 'outcome) => 'rejected) + (check (lifecycle-get other 'delivery-calls) => 0) + (check (.ref (lifecycle-action! f 'DeliverEffect) 'delivered?) => #t))) + (test-case "issued token binding mutation fails closed, including after reservation" + (for-each + (lambda (reserve-first?) + (let* ((f (ready)) (grant (lifecycle-get f 'grant)) + (binding (.ref (.ref grant 'token) 'binding)) + (digest (.ref binding 'bundle-digest))) + (when reserve-first? (check (lifecycle-action! f 'StartEffect) => #t)) + (string-set! digest 0 (if (char=? (string-ref digest 0) #\a) #\b #\a)) + (if reserve-first? + (check (.ref (lifecycle-action! f 'DeliverEffect) 'outcome) => 'rejected) + (check (lifecycle-action! f 'StartEffect) => #f)) + (check (lifecycle-get f 'delivery-calls) => 0))) '(#f #t))) + (test-case "copied and intent-substituted requests cannot invoke authorization" + (let* ((f (ready)) (host (lifecycle-get f 'effect-host)) + (vh (lifecycle-get f 'verification-host)) (request (lifecycle-get f 'request))) + (check (assurance-effect-host-authorize! host vh (.o (:: @ request))) => #f) + (check (assurance-effect-host-authorize! host vh (.o (:: @ request) intent: "other")) => #f) + (check (lifecycle-get f 'authorization-calls) => 1))) + (test-case "observed source ABA cannot revive an old grant" + (let* ((f (ready)) (cut (lifecycle-get f 'cut))) + (lifecycle-set! f 'cut (lifecycle-cut 2)) + (check (lifecycle-action! f 'StartEffect) => #f) + (lifecycle-set! f 'cut cut) + (check (lifecycle-action! f 'StartEffect) => #f) + (check (lifecycle-get f 'delivery-calls) => 0))) + (test-case "in-place request string and handoff payload changes revoke the frozen request" + (for-each + (lambda (field) + (let* ((f (ready)) (request (lifecycle-get f 'request)) + (text (if (eq? field 'payload-hex) + (.ref (.ref request 'handoff) field) (.ref request field)))) + (string-set! text 0 (if (char=? (string-ref text 0) #\a) #\b #\a)) + (check (assurance-host-cedar-request-current? (lifecycle-get f 'verification-host) request) => #f) + (check (assurance-effect-host-authorize! (lifecycle-get f 'effect-host) + (lifecycle-get f 'verification-host) request) => #f) + (check (lifecycle-action! f 'StartEffect) => #f) + (check (lifecycle-get f 'authorization-calls) => 1) + (check (lifecycle-get f 'delivery-calls) => 0))) '(intent action payload-hex))) + (test-case "authorization exception releases the fence without publishing authority" + (let (f (drive (make-lifecycle-fixture) '(InstallAdapter Plan Verify Admit Decide))) + (lifecycle-set! f 'authorization-hook (lambda () (error "policy runtime failure"))) + (check-exception (lifecycle-action! f 'Authorize) Error?) + (check (lifecycle-get f 'grant) => #f) + (lifecycle-set! f 'authorization-hook #f) + (check (lifecycle-action! f 'Authorize) => #t) + (check (lifecycle-get f 'delivery-calls) => 0))) + (test-case "unknown acknowledgement is not delivered and cannot be retried" + (let (f (drive (ready) '(StartEffect))) + (check (.ref (lifecycle-action! f 'UnknownDelivery) 'delivered?) => #f) + (check (lifecycle-get f 'service-effects) => 1) + (check (.ref (lifecycle-action! f 'UnsafeReplay) 'outcome) => 'rejected) + (check (lifecycle-get f 'service-effects) => 1))) + (test-case "failed delivery is attributable failure, not success or retry authority" + (let (f (drive (ready) '(StartEffect))) + (check (.ref (lifecycle-action! f 'FailDelivery) 'outcome) => 'failed) + (check (lifecycle-get f 'service-effects) => 0) + (check (.ref (lifecycle-action! f 'UnsafeReplay) 'outcome) => 'rejected) + (check (lifecycle-get f 'delivery-calls) => 1))) + (test-case "malformed acknowledgement consumes the attempt without success or retry" + (let (f (drive (ready) '(StartEffect))) + ;; Use the API directly: the model fixture deliberately chooses only + ;; its three typed runtime outcomes. + (lifecycle-set! f 'delivery-mode 'not-an-acknowledgement) + (check-exception (assurance-effect-host-deliver! (lifecycle-get f 'effect-host) + (lifecycle-get f 'reservation)) Error?) + (check (.ref (lifecycle-action! f 'UnsafeReplay) 'outcome) => 'rejected) + (check (lifecycle-get f 'delivery-calls) => 1))) + (test-case "deadline crossed during grant validation cannot reserve or deliver" + (for-each + (lambda (reserve-first?) + (let ((f (ready)) (reads 0)) + (when reserve-first? (check (lifecycle-action! f 'StartEffect) => #t)) + ;; The effect clock advances from 0 to the strict deadline during + ;; this API call. The verification Host remains current at 0. + (lifecycle-set! f 'effect-clock-hook + (lambda () + (let (now (min reads 1)) + (set! reads (+ reads 1)) + now))) + (if reserve-first? + (check (.ref (lifecycle-action! f 'DeliverEffect) 'outcome) => 'rejected) + (check (lifecycle-action! f 'StartEffect) => #f)) + (check reads => 2) + (check (lifecycle-get f 'delivery-calls) => 0) + (check (lifecycle-get f 'service-effects) => 0))) '(#f #t))) + (test-case "clock rollback cannot restore an expired grant" + (let* ((f (ready)) (host (lifecycle-get f 'effect-host))) + (lifecycle-set! f 'clock 1) + (check (assurance-effect-host-grant-current? host (lifecycle-get f 'grant)) => #f) + (lifecycle-set! f 'clock 0) + (check-exception (lifecycle-action! f 'StartEffect) Error?) + (check (lifecycle-get f 'delivery-calls) => 0))) + (test-case "throwing and reentrant delivery stays consumed and releases the transient fence" + (let (f (drive (ready) '(StartEffect))) + (lifecycle-set! f 'delivery-hook + (lambda () + (check-exception (assurance-effect-host-revoke! (lifecycle-get f 'effect-host)) Error?) + (error "runtime failure"))) + (check-exception (lifecycle-action! f 'DeliverEffect) Error?) + (check (.ref (lifecycle-action! f 'UnsafeReplay) 'outcome) => 'rejected) + (assurance-effect-host-revoke! (lifecycle-get f 'effect-host)) + (check (lifecycle-get f 'delivery-calls) => 1))))) diff --git a/t/assurance/unit/required-support-test.ss b/t/assurance/unit/required-support-test.ss new file mode 100644 index 0000000..a721fd7 --- /dev/null +++ b/t/assurance/unit/required-support-test.ss @@ -0,0 +1,183 @@ +;;; -*- Gerbil -*- +;;; SPDX-FileCopyrightText: 2026 tao3k team and Contributors +;;; +;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +(import :std/test + (only-in :std/list/list find) + (only-in :clan/poo/object .o .ref) + (only-in :poo-flow/core/poo-clos/interface + poo-clos-model-prototype) + :poo-flow/lambda-aitia/modules/assurance/interface) + +(export required-support-test) + +(def digest-a (string-append "sha256:" (make-string 64 #\a))) +(def digest-b (string-append "sha256:" (make-string 64 #\b))) +(def artifact + (assurance-artifact "artifact/source" "r1" 'supported digest-a + owner: "maintainer" media-kind: 'scheme-source)) +(def claim + (assurance-claim "claim/release" "r1" 'unknown digest-b + subject: "software/release" predicate: "ready" + support-requirements: '("obligation/test") + scope: "repository" valid-from: "2026-09-23")) +(def obligation + (assurance-obligation + "obligation/test" "r1" 'unknown digest-a + subject: "software/release" claim: "claim/release" + snapshot: "snapshot/software" evidence-kind: 'native-test + capability: 'gerbil-test scope: "repository")) +(def evidence + (assurance-evidence + "evidence/test" "r1" 'supported digest-b + producer: "gxtest" tool: "gerbil-test" tool-version: "v19" + input-artifacts: '("artifact/source") obligation: "obligation/test" + subject: "software/release" scope: "repository" + valid-from: "2026-09-23" admission-state: 'candidate)) +(def evidence-link + (assurance-relation "relation/discharge" 'assurance 'discharges + "evidence/test" "obligation/test" 'observed)) +(def requirement-link + (assurance-relation "relation/requirement" 'assurance 'supports + "obligation/test" "claim/release" 'declared)) +(def relations (list evidence-link requirement-link)) + +(def (construct claim-value obligation-value relation-values) + (assurance-snapshot + "snapshot/software" "r1" "graph/software" + '(("artifact/source" . "r1")) '(("claim/release" . "r1")) + "event-cut/1" "policy/release" "r1" + (list artifact claim-value obligation-value evidence) relation-values)) +(def (bound-cut claim-value relation-values) + (let (draft (construct claim-value obligation relation-values)) + (construct claim-value + (assurance-bind-obligation-to-snapshot obligation draft) + relation-values))) +(def (bound-obligation snapshot) + (find (lambda (node) + (equal? (.ref node 'identity) "obligation/test")) + (.ref snapshot 'nodes))) +(def (reported-outcome snapshot) + (assurance-verifier-outcome + "outcome/test" 'succeeded + producer: "gxtest" tool: "gerbil-test" tool-version: "v19" + obligation: "obligation/test" subject: "software/release" + scope: "repository" snapshot-revision: "r1" + snapshot-context-digest: (.ref snapshot 'context-digest) + inputs: (list (assurance-verifier-input artifact)) + output-digest: digest-b)) +(def (make-host source) + (assurance-verification-host + "host/required-support" + (lambda (subject-value issued-at expires-at) #t) + (lambda () 11) 9 source)) +(def (first-blocker receipt) + (.ref (car (.ref receipt 'requirements)) 'blocker)) + +(def required-support-test + (test-suite "Host required support is an inert Decision input" + (test-case "row model is well formed" + (let (row (.o (:: @ (poo-clos-model-prototype AssessmentRequiredSupport)) + obligation: "obligation/test" supported?: #t blocker: #f)) + (check-equal? (assessment-required-support? row) #t))) + (test-case "exact admission and both relations close declared support only" + (let* ((snapshot (bound-cut claim relations)) + (host (make-host (lambda () snapshot))) + (admission + (assurance-host-admit + host (bound-obligation snapshot) evidence + (reported-outcome snapshot))) + (complete + (assurance-host-required-support host claim (list admission))) + (missing (assurance-host-required-support host claim '())) + (copied + (assurance-host-required-support + host claim (list (.o (:: @ admission)))))) + (check-equal? (assurance-required-support? complete) #t) + (check-equal? (.ref complete 'complete?) #t) + (check-equal? (.ref complete 'release-authorized?) #f) + (check-equal? + (assurance-required-support? + (.o (:: @ complete) release-authorized?: #t)) #f) + (check-equal? (first-blocker missing) 'admission-missing) + (check-equal? (first-blocker copied) 'admission-missing))) + + (test-case "undeclared and disconnected requirements stay blocked" + (let* ((snapshot (bound-cut claim relations)) + (host (make-host (lambda () snapshot))) + (admission + (assurance-host-admit + host (bound-obligation snapshot) evidence + (reported-outcome snapshot))) + (two-required + (.o (:: @ claim) + support-requirements: + '("obligation/test" "obligation/independent"))) + (two-cut (bound-cut two-required relations)) + (two-host (make-host (lambda () two-cut))) + (assessment + (assurance-host-required-support + two-host two-required (list admission))) + (no-link-cut (bound-cut claim (list evidence-link))) + (no-link-host (make-host (lambda () no-link-cut))) + (no-link-admission + (assurance-host-admit + no-link-host (bound-obligation no-link-cut) evidence + (reported-outcome no-link-cut))) + (disconnected + (assurance-host-required-support + no-link-host claim (list no-link-admission)))) + (check-equal? (.ref assessment 'complete?) #f) + (check-equal? + (.ref (cadr (.ref assessment 'requirements)) 'blocker) + 'obligation-missing) + (check-equal? (first-blocker disconnected) 'support-link-missing))) + + (test-case "assumption closure is not inferred from verifier admission" + (let* ((assumed + (.o (:: @ claim) assumptions: '("assumption/toolchain"))) + (snapshot (bound-cut assumed relations)) + (host (make-host (lambda () snapshot))) + (admission + (assurance-host-admit + host (bound-obligation snapshot) evidence + (reported-outcome snapshot))) + (assessment + (assurance-host-required-support host assumed (list admission)))) + (check-equal? (.ref assessment 'complete?) #f) + (check-equal? + (if (memq 'assumptions-unresolved (.ref assessment 'blockers)) #t #f) + #t))) + + (test-case "expiry and revocation reopen required support" + (let* ((now 11) + (snapshot (bound-cut claim relations)) + (host + (assurance-verification-host + "host/required-support-expiry" + (lambda (subject-value issued-at expires-at) #t) + (lambda () now) 9 (lambda () snapshot))) + (admission + (assurance-host-admit + host (bound-obligation snapshot) evidence + (reported-outcome snapshot)))) + (check-equal? + (.ref (assurance-host-required-support host claim (list admission)) + 'complete?) #t) + (set! now 20) + (check-equal? + (first-blocker + (assurance-host-required-support host claim (list admission))) + 'admission-invalid)) + (let* ((snapshot (bound-cut claim relations)) + (host (make-host (lambda () snapshot))) + (admission + (assurance-host-admit + host (bound-obligation snapshot) evidence + (reported-outcome snapshot)))) + (assurance-host-revoke-admission! host admission) + (check-equal? + (first-blocker + (assurance-host-required-support host claim (list admission))) + 'admission-missing))))) diff --git a/t/bindings/unit/aitia-native-test.ss b/t/bindings/unit/aitia-native-test.ss index f209e52..3da237a 100644 --- a/t/bindings/unit/aitia-native-test.ss +++ b/t/bindings/unit/aitia-native-test.ss @@ -20,7 +20,7 @@ (string->json payload +aitia-native-test-json-read-options+)) (def +accepted-change+ - "{\"event\":\"pull-request\",\"repository\":\"tao3k/poo-flow\",\"revision\":\"0123456789abcdef\",\"source-ref\":\"feature/aitia\",\"target-ref\":\"develop\",\"pull-request\":42,\"checks\":[{\"name\":\"commit-policy\",\"conclusion\":\"success\"},{\"name\":\"build\",\"conclusion\":\"success\"},{\"name\":\"unit-test\",\"conclusion\":\"success\"},{\"name\":\"nasa-7150-2d\",\"conclusion\":\"success\"}]}") + "{\"event\":\"pull-request\",\"repository\":\"tao3k/poo-flow\",\"revision\":\"0123456789abcdef\",\"source-ref\":\"feature/aitia\",\"target-ref\":\"develop\",\"pull-request\":42,\"checks\":[{\"name\":\"commit-policy\",\"conclusion\":\"success\"},{\"name\":\"build\",\"conclusion\":\"success\"},{\"name\":\"unit-test\",\"conclusion\":\"success\"},{\"name\":\"nasa/sdlc/npr-7150.2\",\"conclusion\":\"success\",\"standardEdition\":\"D\",\"sourceLockDigest\":\"sha256:cfb963b8cd81fd8e22e9b47251c1dc7927b85fcdf9ec9366057cd38cb94f338c\"}]}") (def aitia-native-test (test-suite "Aitia Scheme-native C ABI" diff --git a/t/fixtures/composition-value/aitia.ss b/t/fixtures/composition-value/aitia.ss deleted file mode 100644 index c110098..0000000 --- a/t/fixtures/composition-value/aitia.ss +++ /dev/null @@ -1,8 +0,0 @@ -;;; SPDX-FileCopyrightText: 2026 tao3k team and Contributors -;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later - -(use-composition aitia - (project - repository: "tao3k/lambda-aitia" - languages: '(scheme python lean tla+)) - (delivery provider: 'github)) diff --git a/t/formal/lifecycle-fixture.ss b/t/formal/lifecycle-fixture.ss new file mode 100644 index 0000000..a577d19 --- /dev/null +++ b/t/formal/lifecycle-fixture.ss @@ -0,0 +1,189 @@ +;;; -*- Gerbil -*- +;;; SPDX-FileCopyrightText: 2026 tao3k team and Contributors +;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +;;; This fixture drives real plan / verification / admission / preflight / +;;; token / reservation APIs. It never imports model expected states. +;;; Configured local runtime callbacks are controls, not a native Cedar proof. +(import :std/test (only-in :clan/poo/object .o .ref) + :poo-flow/lambda-aitia/modules/assurance/interface + (only-in :poo-flow/modules/authorization/providers/cedar/interface + poo-flow-cedar-decision poo-flow-cedar-runtime-handoff)) +(export make-lifecycle-fixture lifecycle-action! lifecycle-state + lifecycle-get lifecycle-set! lifecycle-cut) + +(def digest-a (string-append "sha256:" (make-string 64 #\a))) +(def digest-b (string-append "sha256:" (make-string 64 #\b))) +(def (lifecycle-cut revision) + (let* ((rev (number->string revision)) + (artifact (assurance-artifact "source" rev 'supported + (if (= revision 1) digest-a digest-b) owner: "fixture" media-kind: 'scheme-source)) + (claim (assurance-claim "claim" rev 'unknown digest-b subject: "release" predicate: "ready" + support-requirements: '("test") scope: "repo" valid-from: "2026-10-11")) + (obligation (assurance-obligation "test" rev 'unknown digest-a subject: "release" + claim: "claim" snapshot: "cut" evidence-kind: 'native-test + capability: 'gerbil-test scope: "repo")) + (evidence (assurance-evidence "evidence" rev 'supported digest-b producer: "fixture" + tool: "local-verifier" tool-version: "1" input-artifacts: '("source") + obligation: "test" subject: "release" scope: "repo" + valid-from: "2026-10-11" admission-state: 'candidate)) + (decision (assurance-decision "decision" rev 'unknown digest-a subject: "release" + snapshot: "cut" policy: "policy" authority: "owner" outcome: 'unknown)) + (relations (list + (assurance-relation "input" 'structural 'depends-on "claim" "source" 'declared) + (assurance-relation "support" 'assurance 'supports "test" "claim" 'declared) + (assurance-relation "discharge" 'assurance 'discharges "evidence" "test" 'observed) + (assurance-relation "decision-input" 'structural 'depends-on "decision" "claim" 'declared)))) + (def (snapshot bound) + (assurance-snapshot "cut" rev "graph" (list (cons "source" rev)) + (list (cons "claim" rev)) (string-append "events/" rev) "policy" "1" + (list artifact claim bound evidence decision) relations)) + (let* ((bound (assurance-bind-obligation-to-snapshot obligation (snapshot obligation))) + (cut (snapshot bound)) + (outcome (assurance-verifier-outcome "outcome" 'succeeded producer: "fixture" + tool: "local-verifier" tool-version: "1" obligation: "test" + subject: "release" scope: "repo" snapshot-revision: rev + snapshot-context-digest: (.ref cut 'context-digest) + inputs: (list (assurance-verifier-input artifact)) output-digest: digest-b))) + (let ((revision-value revision) (evidence-value evidence) (outcome-value outcome) + (claim-value claim) (decision-value decision)) + (.o revision: revision-value snapshot: cut obligation: bound evidence: evidence-value + outcome: outcome-value claim: claim-value decision: decision-value))))) +(def cuts (vector (lifecycle-cut 1) (lifecycle-cut 2))) +(def (lifecycle-get fixture key) (hash-ref fixture key)) +(def (lifecycle-set! fixture key value) (hash-put! fixture key value)) +(def (increment! fixture key) (lifecycle-set! fixture key (+ 1 (lifecycle-get fixture key)))) +(def (make-lifecycle-fixture) + (let (fixture (make-hash-table-eq)) + (for-each (lambda (key) (lifecycle-set! fixture key #f)) + '(installed reported plan seal admission admission-seal preflight request grant reservation + started authorization-hook delivery-hook effect-clock-hook)) + (for-each (lambda (key) (lifecycle-set! fixture key 0)) + '(clock verifier-calls authorization-calls delivery-calls service-effects + grant-epoch grant-until reservation-revision reservation-epoch reservation-until delivery-status)) + (lifecycle-set! fixture 'delivery-mode 'delivered) + (lifecycle-set! fixture 'policy-mode 'permit) + (lifecycle-set! fixture 'cut (vector-ref cuts 0)) + (lifecycle-set! fixture 'verification-host + (assurance-verification-host "lifecycle/verification" + (lambda (subject now until) + (increment! fixture 'verifier-calls) + (lifecycle-get fixture 'installed)) + (lambda () (lifecycle-get fixture 'clock)) 1 + (lambda () (.ref (lifecycle-get fixture 'cut) 'snapshot)))) + (lifecycle-set! fixture 'effect-host + (assurance-effect-host + (lambda (request) + (increment! fixture 'authorization-calls) + (when (lifecycle-get fixture 'authorization-hook) + ((lifecycle-get fixture 'authorization-hook))) + (poo-flow-cedar-decision "local-runtime-decision" (lifecycle-get fixture 'policy-mode) + "policy" digest-a digest-b '())) + (lambda (request token) + (increment! fixture 'delivery-calls) + (when (lifecycle-get fixture 'delivery-hook) + ((lifecycle-get fixture 'delivery-hook))) + (unless (eq? (lifecycle-get fixture 'delivery-mode) 'failed) + (increment! fixture 'service-effects)) + (lifecycle-get fixture 'delivery-mode)) + (lambda () + (if (lifecycle-get fixture 'effect-clock-hook) + ((lifecycle-get fixture 'effect-clock-hook)) + (lifecycle-get fixture 'clock))) 1)) + fixture)) + +(def (lifecycle-action! fixture name) + (let* ((cut (lifecycle-get fixture 'cut)) (vh (lifecycle-get fixture 'verification-host)) + (rh (lifecycle-get fixture 'effect-host)) + (snapshot (.ref cut 'snapshot)) (obligation (.ref cut 'obligation)) + (evidence (.ref cut 'evidence)) (outcome (.ref cut 'outcome))) + (case name + ((init Terminal) #t) + ((InstallAdapter) (lifecycle-set! fixture 'installed #t) #t) + ((ReportSuccess) (lifecycle-set! fixture 'reported #t) #t) + ((Plan) + (let (plan (assurance-plan-verification "plan" snapshot '("source") + (assurance-verification-policy "policy" "1" '(gerbil-test)))) + (unless (equal? (.ref plan 'selected-obligations) '("test")) + (error "fixture obligation was not selected")) + (lifecycle-set! fixture 'plan plan) #t)) + ((Verify) + (and (lifecycle-get fixture 'plan) + (let (seal (assurance-host-verify vh snapshot obligation evidence outcome)) + (lifecycle-set! fixture 'seal seal) (if seal #t #f)))) + ((Admit UnsafeLateAdmit) + (let (admission (assurance-host-admit-verified vh obligation evidence outcome + (lifecycle-get fixture 'seal))) + (when admission + (lifecycle-set! fixture 'admission admission) + (lifecycle-set! fixture 'admission-seal (lifecycle-get fixture 'seal))) + (if admission #t #f))) + ((Decide) + (let (preflight (assurance-host-preflight-decision vh (.ref cut 'decision) + (.ref cut 'claim) (if (lifecycle-get fixture 'admission) + (list (lifecycle-get fixture 'admission)) '()))) + (when preflight (lifecycle-set! fixture 'preflight preflight)) + (if preflight #t #f))) + ((Authorize UnsafeEarlyGrant) + (let* ((request (assurance-host-cedar-request vh (lifecycle-get fixture 'preflight) + (string-copy "Release") (string-copy digest-a) (poo-flow-cedar-runtime-handoff + 1 #u8(1) digest-a digest-a digest-b digest-b))) + (grant (and request (assurance-effect-host-authorize! rh vh request)))) + (when grant + (lifecycle-set! fixture 'request request) + (lifecycle-set! fixture 'grant grant) + (lifecycle-set! fixture 'grant-epoch (.ref grant 'epoch)) + (lifecycle-set! fixture 'grant-until (.ref grant 'expires-at))) + (if grant #t #f))) + ((StartEffect UnsafeStart UnsafeAfterRevoke) + (let (reservation (assurance-effect-host-reserve! rh (lifecycle-get fixture 'grant))) + (when reservation + (lifecycle-set! fixture 'reservation reservation) + (lifecycle-set! fixture 'started #t) + (lifecycle-set! fixture 'reservation-revision (.ref cut 'revision)) + (lifecycle-set! fixture 'reservation-epoch (lifecycle-get fixture 'grant-epoch)) + (lifecycle-set! fixture 'reservation-until (lifecycle-get fixture 'grant-until))) + (if reservation #t #f))) + ((DeliverEffect RejectDelivery FailDelivery UnknownDelivery UnsafeDelivery UnsafeBeforeStart UnsafeReplay) + (lifecycle-set! fixture 'delivery-mode + (case name ((FailDelivery) 'failed) ((UnknownDelivery) 'indeterminate) (else 'delivered))) + (let* ((receipt (assurance-effect-host-deliver! rh (lifecycle-get fixture 'reservation))) + (status (case (.ref receipt 'outcome) + ((delivered) 1) ((rejected) 2) ((failed) 3) ((indeterminate) 4)))) + ;; Unsafe probes must not rewrite an already attributable outcome. + (unless (memq name '(UnsafeDelivery UnsafeBeforeStart UnsafeReplay)) + (lifecycle-set! fixture 'delivery-status status)) + receipt)) + ((Tick) (increment! fixture 'clock) #t) + ((ChangeSource) + (lifecycle-set! fixture 'cut (vector-ref cuts 1)) + (for-each (lambda (key) (lifecycle-set! fixture key #f)) '(plan seal admission preflight request)) + (lifecycle-set! fixture 'grant-until 0) #t) + ((Revoke) + (when (lifecycle-get fixture 'admission) + (assurance-host-revoke-admission! vh (lifecycle-get fixture 'admission))) + (when (and (lifecycle-get fixture 'seal) + (not (eq? (lifecycle-get fixture 'seal) (lifecycle-get fixture 'admission-seal)))) + (assurance-host-revoke! vh (lifecycle-get fixture 'seal))) + (for-each (lambda (key) (lifecycle-set! fixture key #f)) '(seal admission preflight request)) + (assurance-effect-host-revoke! rh) #t) + (else (error "unknown lifecycle action" name))))) + +(def (lifecycle-state fixture) + (let* ((cut (lifecycle-get fixture 'cut)) (rev (.ref cut 'revision)) + (vh (lifecycle-get fixture 'verification-host)) (rh (lifecycle-get fixture 'effect-host)) + (seal (lifecycle-get fixture 'seal)) (grant (lifecycle-get fixture 'grant))) + (.o revision: rev planned: (if (lifecycle-get fixture 'plan) rev 0) + verified: (if seal rev 0) sealUntil: (if seal (.ref seal 'expires-at) 0) + admitted: (if (assurance-host-admission-current? vh (lifecycle-get fixture 'admission)) rev 0) + decided: (if (assurance-host-decision-preflight-current? vh (lifecycle-get fixture 'preflight)) rev 0) + granted: (if (assurance-effect-host-grant-current? rh grant) rev 0) + grantEpoch: (lifecycle-get fixture 'grant-epoch) grantUntil: (lifecycle-get fixture 'grant-until) + epoch: (assurance-effect-host-epoch rh) clock: (lifecycle-get fixture 'clock) + adapterInstalled: (lifecycle-get fixture 'installed) reportedSuccess: (lifecycle-get fixture 'reported) + effectStarted: (lifecycle-get fixture 'started) startSafe: #t + reservationRevision: (lifecycle-get fixture 'reservation-revision) + reservationEpoch: (lifecycle-get fixture 'reservation-epoch) + reservationUntil: (lifecycle-get fixture 'reservation-until) + deliveryStatus: (lifecycle-get fixture 'delivery-status) + deliveryCalls: (lifecycle-get fixture 'delivery-calls) deliverySafe: #t))) diff --git a/t/formal/lifecycle-replay-test.ss b/t/formal/lifecycle-replay-test.ss new file mode 100644 index 0000000..840545b --- /dev/null +++ b/t/formal/lifecycle-replay-test.ss @@ -0,0 +1,43 @@ +;;; -*- Gerbil -*- +;;; SPDX-FileCopyrightText: 2026 tao3k team and Contributors +;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later +(import :std/test (only-in :clan/poo/object .ref) + ./lifecycle-fixture) +(export lifecycle-replay-test) +(def lifecycle-replay-test + (test-suite "Full bounded Quint lifecycle / real Scheme API graph replay" + (test-case "every supplied edge has the same projected Scheme state" + (let (file (getenv "AITIA_LIFECYCLE_REPLAY" #f)) + (unless file (error "run through just check-lifecycle-scheme")) + (let (traces (call-with-input-file file read)) + (check (pair? traces) => #t) + (for-each + (lambda (trace) + (let (fixture (make-lifecycle-fixture)) + (for-each + (lambda (row) + (let ((action (car row)) (expected (cadr row))) + (if (memq action '(UnsafeStart UnsafeLateAdmit UnsafeEarlyGrant UnsafeAfterRevoke + UnsafeDelivery UnsafeBeforeStart UnsafeReplay)) + (let* ((before (lifecycle-state fixture)) + (calls (lifecycle-get fixture 'delivery-calls)) + (result (lifecycle-action! fixture action))) + (if (memq action '(UnsafeDelivery UnsafeBeforeStart UnsafeReplay)) + (check (.ref result 'outcome) => 'rejected) + (check result => #f)) + (check (lifecycle-get fixture 'delivery-calls) => calls) + (let (after (lifecycle-state fixture)) + (for-each (lambda (field) + (check (.ref after (car field)) => (.ref before (car field)))) expected))) + (begin + (let (result (lifecycle-action! fixture action)) + (check (if result #t #f) => #t)) + (let (actual (lifecycle-state fixture)) + (for-each (lambda (field) + (check (.ref actual (car field)) => (cadr field))) expected)))) + (when (eq? action 'StartEffect) + (check (lifecycle-get fixture 'delivery-calls) => 0) + (check (lifecycle-get fixture 'service-effects) => 0)))) + trace) + (displayln "LIFECYCLE-TRACE-COMPLETE steps=" (length trace)) + (force-output))) traces)))))) diff --git a/t/formal/retirement-binding-test.ss b/t/formal/retirement-binding-test.ss new file mode 100644 index 0000000..8a6bce1 --- /dev/null +++ b/t/formal/retirement-binding-test.ss @@ -0,0 +1,91 @@ +;;; SPDX-FileCopyrightText: 2026 tao3k team and Contributors +;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +(import :std/test + (only-in :std/misc/ports read-all-as-string) + (only-in :clan/poo/object .ref) + (only-in :poo-flow/src/feature-system/source-lock-feature source-lock-payload-digest) + :poo-flow/lambda-aitia/modules/assurance/interface + :poo-flow/lambda-aitia/modules/sdlc/interface + :poo-flow/lambda-aitia/user-interface/scenarios/job-retirement-contract) +(export retirement-binding-test) + +;;; IO belongs to the qualification fixture, not the public design value. +;;; Hash every member before making one group artifact: changing any model, +;;; config, imported helper or proof source invalidates the declared source cut. +(def (source-group-digest paths) + (assurance-canonical-digest + (map (lambda (path) + (list path (source-lock-payload-digest + (call-with-input-file path read-all-as-string)))) paths))) +(def (source id digest) + (assurance-artifact id "r1" 'supported digest + owner: "lambda-aitia" media-kind: 'formal-source + provenance: '("repository-source-bytes"))) +(def quint-source (source "retirement/quint" (source-group-digest job-retirement-quint-sources))) +(def lean-source (source "retirement/lean" (source-group-digest job-retirement-lean-sources))) +(def scheme-source (source "retirement/scheme" (source-group-digest job-retirement-scheme-sources))) +(def (snapshot quint lean scheme) + (assurance-snapshot + "retirement/formal-cut" "cut-1" "retirement/formal-sources" + '(("retirement/quint" . "r1") ("retirement/lean" . "r1") + ("retirement/scheme" . "r1")) '() + "retirement/design-r1" "formal-review" "r1" (list quint lean scheme) '())) +(def source-cut (snapshot quint-source lean-source scheme-source)) +(def links + (map (lambda (artifact) + (sdlc-design-implementation-link + job-retirement-contract job-retirement-safety artifact source-cut)) + (list quint-source lean-source scheme-source))) + +(def retirement-binding-test + (test-suite "Retirement POO contract to formal source binding" + (test-case "three source cuts bind one declaration without granting conformance" + (let (review (sdlc-design-implementation-review + job-retirement-contract source-cut links)) + (check (.ref review 'mapping-current?) => #t) + (for-each + (lambda (link) + (check (.ref link 'contract) => "job-executor/retirement") + (check (.ref link 'contract-revision) => "r1") + (check (.ref link 'contract-digest) + => (sdlc-design-contract-digest job-retirement-contract))) links) + (check (.ref review 'implementation-conforms?) => #f) + (check (.ref review 'evidence-admitted?) => #f) + (check (.ref review 'release-authorized?) => #f))) + (test-case "same-revision Quint source mutation invalidates the joined mapping" + (let* ((changed (source "retirement/quint" (source-lock-payload-digest "changed model"))) + (review (sdlc-design-implementation-review + job-retirement-contract (snapshot changed lean-source scheme-source) links))) + (check (.ref review 'mapping-current?) => #f) + (check (length (.ref review 'blocked-links)) => 3) + (check (.ref (car (.ref review 'blocked-links)) 'blocker) => 'snapshot-changed))) + (test-case "same-revision Lean source mutation also invalidates the mapping" + (let* ((changed (source "retirement/lean" (source-lock-payload-digest "changed law"))) + (review (sdlc-design-implementation-review + job-retirement-contract (snapshot quint-source changed scheme-source) links))) + (check (.ref review 'mapping-current?) => #f) + (check (length (.ref review 'blocked-links)) => 3) + (check (.ref (car (.ref review 'blocked-links)) 'blocker) => 'snapshot-changed))) + (test-case "same-revision Scheme implementation mutation invalidates the mapping" + (let* ((changed (source "retirement/scheme" (source-lock-payload-digest "changed Host"))) + (review (sdlc-design-implementation-review + job-retirement-contract (snapshot quint-source lean-source changed) links))) + (check (.ref review 'mapping-current?) => #f) + (check (length (.ref review 'blocked-links)) => 3) + (check (.ref (car (.ref review 'blocked-links)) 'blocker) => 'snapshot-changed))) + (test-case "a changed design premise cannot reuse either formal link" + (let* ((changed + (sdlc-design-contract + "job-executor/retirement" "r1" "RFC/0004/retirement-ingress" + (.ref job-retirement-contract 'responsibilities) + (.ref job-retirement-contract 'owned-state) '() + (cons (sdlc-design-assumption "distributed-fence" "a1" + "Closing and epoch advancement may be separate operations.") + (.ref job-retirement-contract 'assumptions)) + (list job-retirement-safety) '())) + (review (sdlc-design-implementation-review changed source-cut links))) + (check (.ref review 'mapping-current?) => #f) + (check (length (.ref review 'blocked-links)) => 3) + (for-each (lambda (link) (check (.ref link 'blocker) => 'contract-changed)) + (.ref review 'blocked-links)))))) diff --git a/t/formal/retirement-execution-test.ss b/t/formal/retirement-execution-test.ss new file mode 100644 index 0000000..72710a5 --- /dev/null +++ b/t/formal/retirement-execution-test.ss @@ -0,0 +1,157 @@ +;;; -*- Gerbil -*- +;;; SPDX-FileCopyrightText: 2026 tao3k team and Contributors +;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +;;; Execute the bounded retirement-ingress controls against the local Scheme +;;; Host. The callback counter is an actual handoff result, not model metadata. +(import :std/test + (only-in :clan/poo/object .ref) + :poo-flow/lambda-aitia/modules/sdlc/retirement-host) +(export retirement-execution-test) + +(def (accepted? result) (.ref result 'accepted?)) +(def (reason result) (.ref result 'reason)) +(def (ready-for-review host) + (aitia-retirement-host-observe-completion! host) + (aitia-retirement-host-observe-queue-drained! host) + (aitia-retirement-host-complete-inventory! host) + host) + +(def retirement-execution-test + (test-suite "Retirement ingress actual Scheme Host execution" + (test-case "cancellation is not completion or queue drain" + (let (host (aitia-retirement-host)) + (check (accepted? (aitia-retirement-host-request-cancellation! host)) => #t) + (aitia-retirement-host-complete-inventory! host) + (aitia-retirement-host-close-ingress! host) + (check (reason (aitia-retirement-host-retire! host)) => 'active-work) + (check (.ref (aitia-retirement-host-snapshot host) 'retired?) => #f))) + (test-case "unfenced review cannot retire" + (let (host (ready-for-review (aitia-retirement-host))) + (check (accepted? (aitia-retirement-host-review-cut! host)) => #t) + (check (reason (aitia-retirement-host-retire! host)) => 'ingress-open) + (check (.ref (aitia-retirement-host-snapshot host) 'authority-epoch) => 0) + (check (accepted? (aitia-retirement-host-close-ingress! host)) => #t) + (check (.ref (aitia-retirement-host-snapshot host) 'authority-epoch) => 1) + (check (accepted? (aitia-retirement-host-retire! host)) => #t))) + (test-case "cancellation does not discharge queued work after completion" + (let (host (aitia-retirement-host)) + (aitia-retirement-host-request-cancellation! host) + (aitia-retirement-host-observe-completion! host) + (aitia-retirement-host-complete-inventory! host) + (aitia-retirement-host-close-ingress! host) + (check (reason (aitia-retirement-host-review-cut! host)) => 'queued-work) + (check (reason (aitia-retirement-host-retire! host)) => 'queued-work) + (check (.ref (aitia-retirement-host-snapshot host) 'queued?) => #t))) + (test-case "quiescence inventory and fence cannot substitute for review" + (let (host (ready-for-review (aitia-retirement-host))) + (aitia-retirement-host-close-ingress! host) + (check (reason (aitia-retirement-host-retire! host)) => 'review-missing) + (check (accepted? (aitia-retirement-host-review-cut! host)) => #t) + (check (accepted? (aitia-retirement-host-retire! host)) => #t))) + (test-case "closure is idempotent and fences an already pending request" + (let* ((host (aitia-retirement-host)) (calls 0) + (handoff (aitia-retirement-effect-handoff + (lambda () (set! calls (+ calls 1)))))) + (aitia-retirement-host-request-start! host) + (aitia-retirement-host-close-ingress! host) + (check (reason (aitia-retirement-host-close-ingress! host)) => 'already-closed) + (check (.ref (aitia-retirement-host-snapshot host) 'authority-epoch) => 1) + (check (reason (aitia-retirement-host-attempt-start! host handoff)) => 'stale-grant) + (check calls => 0) + (check (.ref (aitia-retirement-host-snapshot host) 'attempted?) => #t))) + (test-case "failed handoff releases the transient fence without retry or completion" + (let* ((host (ready-for-review (aitia-retirement-host))) (calls 0) + (handoff (aitia-retirement-effect-handoff + (lambda () (set! calls (+ calls 1)) (error "handoff failed"))))) + (aitia-retirement-host-review-cut! host) + (aitia-retirement-host-request-start! host) + (check (with-catch (lambda (_failure) 'failed) + (lambda () (aitia-retirement-host-attempt-start! host handoff) + 'unexpected-success)) => 'failed) + (check calls => 1) + (check (.ref (aitia-retirement-host-snapshot host) 'handoff-in-progress?) => #f) + (check (.ref (aitia-retirement-host-snapshot host) 'active?) => #t) + (check (accepted? (aitia-retirement-host-request-start! host)) => #f) + (check (reason (aitia-retirement-host-retire! host)) => 'active-work) + (check (accepted? (aitia-retirement-host-observe-completion! host)) => #t) + (aitia-retirement-host-close-ingress! host) + (check (accepted? (aitia-retirement-host-retire! host)) => #t))) + (test-case "late work invalidates the old finalization attempt" + (let (host (ready-for-review (aitia-retirement-host))) + (aitia-retirement-host-review-cut! host) + (aitia-retirement-host-observe-late-activity! host) + (aitia-retirement-host-close-ingress! host) + (check (reason (aitia-retirement-host-retire! host)) => 'active-work) + (check (.ref (aitia-retirement-host-snapshot host) 'retired?) => #f))) + (test-case "unknown inventory prevents retirement after review" + (let (host (ready-for-review (aitia-retirement-host))) + (aitia-retirement-host-review-cut! host) + (aitia-retirement-host-observe-inventory-gap! host) + (aitia-retirement-host-close-ingress! host) + (check (reason (aitia-retirement-host-retire! host)) => 'inventory-unknown) + (check (.ref (aitia-retirement-host-snapshot host) 'retired?) => #f))) + (test-case "accepted current grant invokes one real Scheme handoff" + (let* ((host (aitia-retirement-host)) + (calls 0) + (reentered #f) + (handoff (aitia-retirement-effect-handoff + (lambda () + (set! calls (+ calls 1)) + (set! reentered + (accepted? (aitia-retirement-host-request-start! host))))))) + (check (accepted? (aitia-retirement-host-request-start! host)) => #t) + (let (result (aitia-retirement-host-attempt-start! host handoff)) + (check (accepted? result) => #t) + (check (.ref result 'handoff-executed?) => #t)) + (check calls => 1) + (check reentered => #f) + (check (accepted? (aitia-retirement-host-request-start! host)) => #f) + (check (reason (aitia-retirement-host-retire! host)) => 'active-work))) + (test-case "foreign thread cannot use the serialized Host" + (let* ((host (aitia-retirement-host)) + (result + (thread-join! + (thread-start! + (make-thread + (lambda () + (with-catch (lambda (_failure) 'refused) + (lambda () + (aitia-retirement-host-close-ingress! host) + 'accepted)))))))) + (check result => 'refused) + (check (.ref (aitia-retirement-host-snapshot host) 'ingress-open?) => #t))) + (test-case "a reentrant handoff cannot retire before it returns" + (let* ((host (ready-for-review (aitia-retirement-host))) + (premature 'not-attempted) + (handoff + (aitia-retirement-effect-handoff + (lambda () + (set! premature + (with-catch (lambda (_failure) 'refused) + (lambda () + (aitia-retirement-host-observe-completion! host) + (aitia-retirement-host-close-ingress! host) + (accepted? (aitia-retirement-host-retire! host))))))))) + (aitia-retirement-host-review-cut! host) + (aitia-retirement-host-request-start! host) + (check (accepted? (aitia-retirement-host-attempt-start! host handoff)) => #t) + (check premature => 'refused) + (check (.ref (aitia-retirement-host-snapshot host) 'active?) => #t) + (check (.ref (aitia-retirement-host-snapshot host) 'retired?) => #f))) + (test-case "post-retirement cached grant cannot execute the handoff" + (let* ((host (ready-for-review (aitia-retirement-host))) + (calls 0) + (handoff (aitia-retirement-effect-handoff + (lambda () (set! calls (+ calls 1)))))) + (aitia-retirement-host-review-cut! host) + (aitia-retirement-host-close-ingress! host) + (check (accepted? (aitia-retirement-host-retire! host)) => #t) + (check (accepted? (aitia-retirement-host-request-start! host)) => #t) + (let (result (aitia-retirement-host-attempt-start! host handoff)) + (check (accepted? result) => #f) + (check (reason result) => 'stale-grant) + (check (.ref result 'handoff-executed?) => #f)) + (check calls => 0) + (check (.ref (aitia-retirement-host-snapshot host) 'active?) => #f) + (check (.ref (aitia-retirement-host-snapshot host) 'retired?) => #t))))) diff --git a/t/formal/retirement-replay-test.ss b/t/formal/retirement-replay-test.ss new file mode 100644 index 0000000..11b711e --- /dev/null +++ b/t/formal/retirement-replay-test.ss @@ -0,0 +1,85 @@ +;;; -*- Gerbil -*- +;;; SPDX-FileCopyrightText: 2026 tao3k team and Contributors +;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +;;; ITF is projected to data, never evaluated. The native test lifecycle owns +;;; execution; the Quint runner only supplies model states and action names. +(import :std/test + (only-in :clan/poo/object .ref) + :poo-flow/lambda-aitia/modules/sdlc/retirement-host) +(export retirement-replay-test) + +(def (invoke-action name host handoff) + (case name + ((init Terminal) #f) + ((RequestCancellation) (aitia-retirement-host-request-cancellation! host)) + ((ObserveCompletion) (aitia-retirement-host-observe-completion! host)) + ((ObserveQueueDrained) (aitia-retirement-host-observe-queue-drained! host)) + ((CompleteInventory) (aitia-retirement-host-complete-inventory! host)) + ((ReviewCut) (aitia-retirement-host-review-cut! host)) + ((ObserveLateActivity) (aitia-retirement-host-observe-late-activity! host)) + ((ObserveInventoryGap) (aitia-retirement-host-observe-inventory-gap! host)) + ((CloseIngress) (aitia-retirement-host-close-ingress! host)) + ((Request) (aitia-retirement-host-request-start! host)) + ((Start Reject TrustCachedGrant) (aitia-retirement-host-attempt-start! host handoff)) + ((Retire RetireWithoutFence) (aitia-retirement-host-retire! host)) + (else (error "unknown replay action" name)))) + +(def (check-state host expected) + (let (snapshot (aitia-retirement-host-snapshot host)) + (for-each (lambda (field) + (check (.ref snapshot (car field)) => (cadr field))) expected) + (check (.ref snapshot 'handoff-in-progress?) => #f))) + +(def (replay lane trace) + (let* ((host (aitia-retirement-host)) + (calls 0) + (expected-calls 0) + (handoff (aitia-retirement-effect-handoff + (lambda () (set! calls (+ calls 1)))))) + (let loop ((states trace)) + (unless (null? states) + (let* ((row (car states)) (name (car row)) + (before (aitia-retirement-host-snapshot host)) + (result (invoke-action name host handoff)) + (fault? (and (not (eq? lane 'safe)) (null? (cdr states))))) + (cond + (fault? + (check (memq name '(RetireWithoutFence TrustCachedGrant)) ? pair?) + (check (.ref result 'accepted?) => #f) + (check (.ref result 'handoff-executed?) => #f) + (check (.ref result 'reason) + => (if (eq? name 'RetireWithoutFence) 'ingress-open 'stale-grant)) + ;; The intentionally unsafe model's final state must NOT match + ;; Scheme. Compare the unchanged cut, except consumed start flags. + (for-each + (lambda (slot) + (check (.ref (aitia-retirement-host-snapshot host) slot) + => (.ref before slot))) + '(active? queued? retired? ingress-open? authority-epoch)) + (when (eq? name 'TrustCachedGrant) + (check (.ref (aitia-retirement-host-snapshot host) 'pending?) => #f) + (check (.ref (aitia-retirement-host-snapshot host) 'attempted?) => #t))) + (else + (when result + (check (.ref result 'accepted?) => (not (eq? name 'Reject))) + (check (.ref result 'handoff-executed?) + => (if (memq name '(Start TrustCachedGrant)) #t #f))) + (when (memq name '(Start TrustCachedGrant)) + (set! expected-calls (+ expected-calls 1))) + (check-state host (cadr row)))) + (check calls => expected-calls) + (loop (cdr states))))))) + +(def retirement-replay-test + (test-suite "Quint MBT / real Scheme Host correspondence" + (test-case "safe state traces and typed unsafe-model refusals" + (let (fixture (getenv "AITIA_QUINT_REPLAY" #f)) + (unless fixture (error "run via just check-quint-scheme; model traces are required")) + (let (lanes (call-with-input-file fixture read)) + (check (map car lanes) => '(safe unfenced cached)) + (for-each + (lambda (lane) + (check (pair? (cadr lane)) => #t) + (for-each (lambda (trace) (replay (car lane) trace)) (cadr lane))) + lanes)))))) diff --git a/t/gitops/unit/gitops-protocol-test.ss b/t/gitops/unit/gitops-protocol-test.ss index 83de2e1..4694f80 100644 --- a/t/gitops/unit/gitops-protocol-test.ss +++ b/t/gitops/unit/gitops-protocol-test.ss @@ -5,7 +5,7 @@ (import :std/test (only-in :clan/poo/object .cc .ref) (only-in :clan/poo/mop element?) - :poo-flow/src/module-system/poo-clos/interface + :poo-flow/core/poo-clos/interface :poo-flow/lambda-aitia/modules/gitops/interface) (export gitops-protocol-test) diff --git a/t/integration/github-workflow-test.ss b/t/integration/github-workflow-test.ss index 4c2da9e..f46c69e 100644 --- a/t/integration/github-workflow-test.ss +++ b/t/integration/github-workflow-test.ss @@ -3,31 +3,55 @@ ;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later (import :std/test + (only-in :std/list/list find) (only-in :clan/poo/object .ref) - :poo-flow/src/module-system/profile-composition/interface :poo-flow/lambda-aitia/user-interface/config + :poo-flow/lambda-aitia/user-interface/profiles/nasa/sdlc :poo-flow/lambda-aitia/user-interface/scenarios/github-gitops) (export github-workflow-test) (def repository "tao3k/poo-flow") (def revision "0123456789abcdef") +(def nasa-standard (car (.ref nasa-sdlc-npr7150_2 'standards))) +(def (nasa-check (edition (.ref nasa-standard 'edition)) + (digest (.ref nasa-standard 'source-lock-digest))) + (github-check 'nasa/sdlc/npr-7150.2 repository revision 'success + standard-edition: edition source-lock-digest: digest)) (def (successful-checks names (at-revision revision)) - (map (lambda (name) (github-check name repository at-revision 'success)) names)) -(def dev-checks '(commit-policy build unit-test nasa-7150-2d)) + (map (lambda (name) + (if (eq? name 'nasa/sdlc/npr-7150.2) + (github-check name repository at-revision 'success + standard-edition: (.ref nasa-standard 'edition) + source-lock-digest: + (.ref nasa-standard 'source-lock-digest)) + (github-check name repository at-revision 'success))) + names)) +(def dev-checks '(commit-policy build unit-test nasa/sdlc/npr-7150.2)) (def (run-github-gitops change checks) (github-run-gitops github-gitops-sdlc change checks)) (def github-workflow-test (test-suite "GitHub GitOps SDLC lifecycle" (test-case "Dev Staging Production are GitOps Profiles; NASA is one Standard" - (check-equal? (poo-flow-scenario-case-name github-gitops-sdlc) + (check-equal? (.ref github-gitops-sdlc 'name) 'github-gitops-sdlc) (check-equal? (map (lambda (profile) (.ref profile 'name)) - (poo-flow-scenario-case-profiles github-gitops-sdlc)) - '(actions dev staging production nasa-7150-2d)) + (.ref github-gitops-sdlc 'profiles)) + '(actions dev staging production nasa/sdlc/npr-7150.2)) (check-equal? (map (lambda (standard) (.ref standard 'identity)) - (.ref nasa-7150-2d 'standards)) - '("nasa/npr-7150.2d"))) + (.ref nasa-sdlc-npr7150_2 'standards)) + '("nasa/sdlc/npr-7150.2")) + (check-equal? (map (lambda (standard) (.ref standard 'edition)) + (.ref nasa-sdlc-npr7150_2 'standards)) + '("D")) + (let* ((proof + (find (lambda (value) + (eq? (.ref value 'profile-identity) 'npr-7150.2)) + (.ref github-gitops-sdlc 'selection-proofs))) + (module-id (.ref proof 'module-definition))) + (check-equal? (.ref module-id 'namespace) 'nasa) + (check-equal? (.ref module-id 'name) 'sdlc) + (check-equal? (.ref proof 'profile-identity) 'npr-7150.2))) (test-case "PR into develop advances Dev only with current checks and Standard assessment" (let* ((change (github-change 'pull-request repository revision "feature/gitops" "develop" 42)) @@ -36,7 +60,7 @@ (check-equal? (.ref decision 'environment) 'dev) (check-equal? (.ref decision 'accepted) #t) (check-equal? (.ref decision 'next-profile) 'staging) - (check-equal? (.ref decision 'standards) '(nasa-7150-2d)))) + (check-equal? (.ref decision 'standards) '(nasa/sdlc/npr-7150.2)))) (test-case "missing NASA assessment fails closed" (let (decision (run-github-gitops @@ -44,7 +68,34 @@ "feature/gitops" "develop" 42) (successful-checks '(commit-policy build unit-test)))) (check-equal? (.ref decision 'accepted) #f) - (check-equal? (.ref decision 'missing-checks) '(nasa-7150-2d)))) + (check-equal? (.ref decision 'missing-checks) '(nasa/sdlc/npr-7150.2)))) + (test-case "same-name green check without an edition-bound source lock fails closed" + (let (decision + (run-github-gitops + (github-change 'pull-request repository revision + "feature/gitops" "develop" 42) + (append (successful-checks '(commit-policy build unit-test)) + (list (github-check 'nasa/sdlc/npr-7150.2 + repository revision 'success))))) + (check-equal? (.ref decision 'accepted) #f) + (check-equal? (.ref decision 'stale-checks) '(nasa/sdlc/npr-7150.2)) + (check-equal? (.ref decision 'reasons) + '(standard-assessment-mismatch)))) + (test-case "wrong NASA edition or source lock cannot authorize promotion" + (for-each + (lambda (check) + (let (decision + (run-github-gitops + (github-change 'pull-request repository revision + "feature/gitops" "develop" 42) + (append (successful-checks '(commit-policy build unit-test)) + (list check)))) + (check-equal? (.ref decision 'accepted) #f) + (check-equal? (.ref decision 'reasons) + '(standard-assessment-mismatch)))) + (list (nasa-check "C") (nasa-check + (.ref nasa-standard 'edition) + "sha256:wrong-lock")))) (test-case "checks for another Git revision cannot authorize promotion" (let (decision (run-github-gitops @@ -58,13 +109,13 @@ (run-github-gitops (github-change 'push repository revision "develop" "develop" #f) (successful-checks - '(integration-test security-review nasa-7150-2d)))) + '(integration-test security-review nasa/sdlc/npr-7150.2)))) (production-decision (run-github-gitops (github-change 'workflow-dispatch repository revision "develop" "production" #f) (successful-checks - '(release-authority signed-provenance nasa-7150-2d))))) + '(release-authority signed-provenance nasa/sdlc/npr-7150.2))))) (check-equal? (.ref staging-decision 'profile) 'staging) (check-equal? (.ref staging-decision 'next-profile) 'production) (check-equal? (.ref production-decision 'profile) 'production) diff --git a/t/modules/unit/composition-identity-test.ss b/t/modules/unit/composition-identity-test.ss index 01d81ef..e5ceea2 100644 --- a/t/modules/unit/composition-identity-test.ss +++ b/t/modules/unit/composition-identity-test.ss @@ -5,19 +5,15 @@ (import (only-in :std/test check-equal? test-case test-suite) (only-in :clan/poo/object .ref) - (only-in :poo-flow/src/module-system/profile-composition/interface - poo-flow-composition-catalog-ref - poo-flow-composition-identity-official? - poo-flow-composition-identity-qualified-name - poo-flow-load-composition-value + (only-in :poo-flow/src/scenario/case poo-flow-scenario-case?) - (only-in :poo-flow/src/module-system/declaration/interface + (only-in :poo-flow/src/user-interface/module-selection poo-flow-user-module-selection-flags poo-flow-user-module-selection-key) - (only-in :poo-flow/src/modules/proof/interface - poo-flow-proof-module?) (only-in :poo-flow/src/user-interface/profile-core poo-flow-user-profile-modules) + (only-in :poo-flow/modules/proof/interface + poo-flow-proof-module?) :poo-flow/lambda-aitia/modules/formal-methods/interface :poo-flow/lambda-aitia/user-interface/compositions/aitia) @@ -25,58 +21,36 @@ (def lambda-aitia-composition-identity-test (test-suite - "official Aitia Composition identity" - (test-case "publishes Aitia through the reserved unqualified selector" - (check-equal? - (poo-flow-composition-identity-qualified-name - aitia-composition-identity) - 'aitia) - (check-equal? - (poo-flow-composition-identity-official? - aitia-composition-identity) - #t)) - (test-case "registers the official Aitia value without namespace fallback" - (check-equal? - (eq? (poo-flow-composition-catalog-ref - aitia-composition-catalog 'aitia) - aitia-composition) - #t) - (check-equal? - (.ref aitia-composition-catalog 'module-names) - '(gitops sdlc ADR assurance formal-methods))) - (test-case "default Profile selects the complete software engineering closure" + "official Aitia native Profile composition" + (test-case "Aitia owns one qualified semantic Module" + (check-equal? (.ref (.ref AitiaModule 'identity) 'namespace) + 'software-engineering) + (check-equal? (.ref (.ref AitiaModule 'identity) 'name) 'aitia)) + (test-case "root is one closed POO Flow Case" + (check-equal? (poo-flow-scenario-case? aitia) #t) + (check-equal? (.ref aitia 'name) 'aitia) + (check-equal? (length (.ref aitia 'profiles)) 1) + (check-equal? (length (.ref aitia 'profile-bindings)) 1) + (check-equal? (length (.ref aitia 'selection-proofs)) 1) + (check-equal? (.ref (.ref aitia 'admission) 'accepted?) #t) + (check-equal? (.ref aitia 'runtime-executed?) #f)) + (test-case "default Project selects the complete software engineering closure" (let (modules - (poo-flow-user-profile-modules - (.ref aitia-composition 'default-profile))) + (poo-flow-user-profile-modules (car (.ref aitia 'profiles)))) (check-equal? (map poo-flow-user-module-selection-key modules) '((software-engineering . gitops) (software-engineering . sdlc) + (nasa . sdlc) (software-engineering . ADR) (software-engineering . assurance) (software-engineering . formal-methods))) (check-equal? - (poo-flow-user-module-selection-flags (list-ref modules 4)) - '(+tla+ +lean +cedar)))) + (poo-flow-user-module-selection-flags (list-ref modules 5)) + '(+quint +lean +cedar)))) (test-case "formal-method declarations reuse the POO Flow Proof module" (check-equal? (poo-flow-proof-module? aitia-formal-methods-module) #t) - (check-equal? (.ref aitia-tla-plus-provider 'runtime-executed?) #f) + (check-equal? (.ref aitia-quint-provider 'runtime-executed?) #f) (check-equal? (.ref aitia-lean-provider 'obligation-only?) #t) (check-equal? (.ref aitia-cedar-provider 'role) - 'authorization-policy-validation)) - (test-case "loads the downstream file as one closed Case" - (let (case-value - (poo-flow-load-composition-value - "t/fixtures/composition-value/aitia.ss" - aitia-composition-catalog)) - (check-equal? (poo-flow-scenario-case? case-value) #t) - (check-equal? (.ref case-value 'name) 'aitia) - (check-equal? (length (.ref case-value 'profiles)) 1) - (check-equal? (length (.ref case-value 'stages)) 1) - (check-equal? - (.ref (.ref (car (.ref case-value 'profiles)) 'project) 'repository) - "tao3k/lambda-aitia") - (check-equal? - (.ref (.ref (car (.ref case-value 'profiles)) 'delivery) 'provider) - 'github) - (check-equal? (.ref (.ref case-value 'admission) 'accepted?) #t))))) + 'authorization-policy-validation)))) diff --git a/t/modules/unit/source-collection-test.ss b/t/modules/unit/source-collection-test.ss index 87094b8..96177ae 100644 --- a/t/modules/unit/source-collection-test.ss +++ b/t/modules/unit/source-collection-test.ss @@ -3,10 +3,10 @@ ;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later (import (only-in :std/test check-equal? test-case test-suite) - (only-in :poo-flow/src/module-system/loader/source + (only-in :poo-flow/core/module-system/source/objects poo-flow-module-source-ref-metadata poo-flow-module-source-ref-value) - (only-in :poo-flow/src/module-system/loader/module-source-interface + (only-in :poo-flow/src/authoring/module-source-collection make-poo-flow-contribution-module-source poo-flow-load-modules)) diff --git a/t/sdlc/source-admission-test.ss b/t/sdlc/source-admission-test.ss index c41f7b9..3d503cf 100644 --- a/t/sdlc/source-admission-test.ss +++ b/t/sdlc/source-admission-test.ss @@ -16,7 +16,7 @@ (only-in :asp-gerbil-scheme/testing-source-admission-api testing-interface-prepared-source-admission-suite) (only-in :std/test check) - (only-in :poo-flow/src/module-system/observability/source-admission + (only-in :poo-flow/src/testing/source-admission poo-flow-source-admission-observability-profile-prototype poo-flow-observe-source-admission! poo-flow-source-admission) diff --git a/t/sdlc/unit/design-contract-test.ss b/t/sdlc/unit/design-contract-test.ss new file mode 100644 index 0000000..c4eeefb --- /dev/null +++ b/t/sdlc/unit/design-contract-test.ss @@ -0,0 +1,179 @@ +;;; SPDX-FileCopyrightText: 2026 tao3k team and Contributors +;;; +;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +(import :std/test :std/error + (only-in :clan/poo/object .ref) + :poo-flow/lambda-aitia/modules/sdlc/interface) + +(export design-contract-test) + +(def idempotency + (sdlc-design-assumption + "external-idempotency" "a1" + "The provider deduplicates tenant/job/operation keys")) +(def unique-effect + (sdlc-design-guarantee + "unique-effect" "g1" + "At most one externally committed effect per operation key")) +(def cancellation + (sdlc-design-guarantee + "cancel-before-commit" "g1" + "Cancellation before the commit point prevents a new commit")) +(def retry-v1 + (sdlc-design-variation "retry-policy" "v1" '("unique-effect"))) +(def retry-v2 + (sdlc-design-variation "retry-policy" "v2" '("unique-effect"))) + +(def (contract revision assumptions guarantees variations + effects: (effects '("external-operation")) + owned-state: (owned '("job-ledger")) + decision: (decision "ADR/job-executor")) + (sdlc-design-contract + "job-executor" revision decision '("execute-job") owned effects + assumptions guarantees variations)) + +(def baseline + (contract "r1" (list idempotency) (list unique-effect cancellation) + (list retry-v1))) + +(def design-contract-test + (test-suite "SDLC design contract replacement" + (test-case "retry variation reopens only its declared guarantee" + (let* ((next (contract "r2" (list idempotency) + (list unique-effect cancellation) (list retry-v2))) + (review (sdlc-design-replacement-review baseline next)) + (project (sdlc-project "job-service" "r2" "software" #t)) + (retry-node (sdlc-trace-node "retry-policy" 'design project)) + (guarantee-node (sdlc-trace-node "unique-effect" 'design project)) + (verification-node + (sdlc-trace-node "verify-unique-effect" 'verification project)) + (edges + (list (sdlc-trace-edge "retry-to-guarantee" "design-depends-on" + "retry-policy" "unique-effect" project) + (sdlc-trace-edge "guarantee-to-check" "design-depends-on" + "unique-effect" "verify-unique-effect" project))) + (impact + (sdlc-change-impact project + (list retry-node guarantee-node verification-node) + edges (.ref review 'changed-identities) + '("design-depends-on")))) + (check-equal? (.ref review 'declared-substitutable?) #f) + (check-equal? (.ref review 'blockers) '(variation-changed)) + (check-equal? (.ref review 'changed-identities) '("retry-policy")) + (check-equal? (.ref review 'recheck-guarantees) '("unique-effect")) + (check-equal? (.ref impact 'recheck-verifications) + '("verify-unique-effect")) + (check-equal? (.ref review 'evidence-admitted?) #f) + (check-equal? (.ref review 'release-authorized?) #f))) + (test-case "new provider assumption cannot masquerade as compatible" + (let* ((extra (sdlc-design-assumption + "provider-available" "a1" "Provider is always available")) + (next (contract "r2" (list idempotency extra) + (list unique-effect cancellation) (list retry-v1))) + (review (sdlc-design-replacement-review baseline next))) + (check-equal? (.ref review 'blockers) + '(stronger-or-changed-assumption)) + (check-equal? (.ref review 'recheck-guarantees) + '("cancel-before-commit" "unique-effect")))) + (test-case "a weakened guarantee or added effect is an explicit blocker" + (let* ((weakened-effect + (sdlc-design-guarantee + "unique-effect" "g2" + "At most two externally committed effects per operation key")) + (weakened (contract "r2" (list idempotency) + (list weakened-effect cancellation) + (list retry-v1))) + (more-effects + (contract "r2" (list idempotency) + (list unique-effect cancellation) (list retry-v1) + effects: '("external-operation" "audit-write")))) + (check-equal? + (.ref (sdlc-design-replacement-review baseline weakened) 'blockers) + '(weaker-or-changed-guarantee)) + (check-equal? + (.ref (sdlc-design-replacement-review baseline more-effects) 'blockers) + '(new-effect)))) + (test-case "lost ACK exposes the difference between a key and a logical operation" + (let* ((logical-effect + (sdlc-design-guarantee + "unique-effect" "g2" + "At most one external commit per logical job operation across retries and restarts")) + (revised (contract "r2" (list idempotency) + (list logical-effect cancellation) + (list retry-v2))) + (review (sdlc-design-replacement-review baseline revised)) + (with-adr (contract "r2" (list idempotency) + (list logical-effect cancellation) + (list retry-v2) + decision: "lambda-aitia-adr-0008")) + (decision-review + (sdlc-design-replacement-review baseline with-adr))) + (check-equal? (.ref review 'declared-substitutable?) #f) + (check-equal? (.ref review 'recheck-guarantees) '("unique-effect")) + (check-equal? (.ref decision-review 'recheck-guarantees) + '("cancel-before-commit" "unique-effect")) + (check-equal? (.ref review 'evidence-admitted?) #f) + (check-equal? (.ref review 'release-authorized?) #f))) + (test-case "declaration-level compatibility never admits new evidence" + (let* ((next (contract "r2" '() (list unique-effect cancellation) + (list retry-v1))) + (review (sdlc-design-replacement-review baseline next))) + (check-equal? (.ref review 'declared-substitutable?) #t) + (check-equal? (.ref review 'recheck-guarantees) + '("cancel-before-commit" "unique-effect")) + (check-equal? (.ref (sdlc-design-replacement-review + baseline + (contract "r2" (list idempotency) + (list unique-effect cancellation) + (list retry-v1))) + 'changed-identities) + '("job-executor")) + (check-equal? (.ref review 'release-authorized?) #f))) + (test-case "changing the selected ADR reopens every guarantee" + (let (review + (sdlc-design-replacement-review + baseline + (contract "r2" (list idempotency) + (list unique-effect cancellation) (list retry-v1) + decision: "ADR/job-executor-v2"))) + (check-equal? (.ref review 'blockers) '(decision-changed)) + (check-equal? (.ref review 'changed-identities) '("job-executor")) + (check-equal? (.ref review 'recheck-guarantees) + '("cancel-before-commit" "unique-effect")))) + (test-case "local deduplication is a distinct design, not a proof-preserving override" + (let* ((alternative + (sdlc-design-contract + "job-executor" "r2" "ADR/local-deduplication" + '("execute-job" "reconcile-unknown-effect") + '("job-ledger" "dedup-ledger") + '("external-operation") '() + (list unique-effect cancellation) (list retry-v1))) + (review (sdlc-design-replacement-review baseline alternative))) + (check-equal? (.ref review 'declared-substitutable?) #f) + (check-equal? (.ref review 'blockers) + '(responsibility-changed state-ownership-changed + decision-changed)) + (check-equal? (.ref review 'recheck-guarantees) + '("cancel-before-commit" "unique-effect")))) + (test-case "same revision mutation and orphan dependency fail closed" + (check-exception + (sdlc-design-replacement-review + baseline + (contract "r1" (list idempotency) (list cancellation) + (list retry-v1))) + Error?) + (check-exception + (contract "r2" (list idempotency) (list cancellation) + (list retry-v1)) + Error?) + (check-exception + (contract "r2" (list idempotency idempotency) + (list unique-effect cancellation) (list retry-v1)) + Error?) + (check-exception + (contract "r2" (list idempotency) '() '()) + Error?) + (check-exception + (sdlc-design-variation "retry-policy" "v3" '()) + Error?)))) diff --git a/t/sdlc/unit/design-implementation-test.ss b/t/sdlc/unit/design-implementation-test.ss new file mode 100644 index 0000000..2225a28 --- /dev/null +++ b/t/sdlc/unit/design-implementation-test.ss @@ -0,0 +1,105 @@ +;;; SPDX-FileCopyrightText: 2026 tao3k team and Contributors +;;; +;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +(import :std/test :std/error + (only-in :clan/poo/object .ref) + :poo-flow/lambda-aitia/modules/assurance/interface + :poo-flow/lambda-aitia/modules/sdlc/interface) + +(export design-implementation-test) + +(def digest-a (string-append "sha256:" (make-string 64 #\a))) +(def digest-b (string-append "sha256:" (make-string 64 #\b))) +(def guarantee + (sdlc-design-guarantee "unique-effect" "g1" "At most one external commit")) +(def cancellation + (sdlc-design-guarantee "cancel-before-commit" "g1" "No new commit after cancellation")) +(def (contract revision guarantees) + (sdlc-design-contract "job-executor" revision "ADR/job-executor" + '("execute-job") '("job-ledger") + '("external-operation") '() guarantees '())) +(def baseline (contract "r1" (list guarantee))) +(def source + (assurance-artifact "artifact/job-executor" "s1" 'supported digest-a + owner: "lambda-aitia" media-kind: 'scheme-source + provenance: '("repository"))) +(def (snapshot source-value + source-revisions: (revisions (list (cons "artifact/job-executor" "s1"))) + unresolved: (unresolved '())) + (assurance-snapshot "snapshot/job-executor" "cut-1" "graph/job-executor" + revisions '() "fact-cut-1" "policy/design" "p1" + (list source-value) '() unresolved: unresolved)) +(def current-snapshot (snapshot source)) +(def current-link + (sdlc-design-implementation-link baseline guarantee source current-snapshot)) + +(def design-implementation-test + (test-suite "SDLC design to source mapping" + (test-case "exact assurance snapshot binding is current but not conformance" + (let (review + (sdlc-design-implementation-review baseline current-snapshot + (list current-link))) + (check-equal? (.ref review 'mapping-current?) #t) + (check-equal? (.ref review 'mapped-guarantees) '("unique-effect")) + (check-equal? (.ref review 'unmapped-guarantees) '()) + (check-equal? (.ref review 'implementation-conforms?) #f) + (check-equal? (.ref review 'evidence-admitted?) #f) + (check-equal? (.ref review 'release-authorized?) #f))) + (test-case "new contract revision or unmapped guarantee reopens coverage" + (let* ((next (contract "r2" (list guarantee cancellation))) + (review (sdlc-design-implementation-review + next current-snapshot (list current-link)))) + (check-equal? (.ref review 'mapping-current?) #f) + (check-equal? (.ref review 'unmapped-guarantees) + '("cancel-before-commit" "unique-effect")) + (check-equal? (.ref (car (.ref review 'blocked-links)) 'blocker) + 'contract-changed))) + (test-case "same-revision contract mutation cannot reuse an old link" + (let* ((changed-guarantee + (sdlc-design-guarantee "unique-effect" "g1" "At most two commits")) + (changed-contract (contract "r1" (list changed-guarantee))) + (review (sdlc-design-implementation-review + changed-contract current-snapshot (list current-link)))) + (check-equal? (.ref review 'mapping-current?) #f) + (check-equal? (.ref (car (.ref review 'blocked-links)) 'blocker) + 'contract-changed))) + (test-case "source change invalidates exact snapshot binding" + (let* ((new-source + (assurance-artifact "artifact/job-executor" "s1" 'supported digest-b + owner: "lambda-aitia" media-kind: 'scheme-source + provenance: '("repository"))) + (changed (snapshot new-source)) + (review (sdlc-design-implementation-review + baseline changed (list current-link)))) + (check-equal? (.ref review 'mapping-current?) #f) + (check-equal? (.ref review 'unmapped-guarantees) '("unique-effect")) + (check-equal? (.ref (car (.ref review 'blocked-links)) 'blocker) + 'snapshot-changed))) + (test-case "unbound source and unresolved snapshot cannot qualify" + (let* ((unbound (snapshot source source-revisions: '())) + (link (sdlc-design-implementation-link + baseline guarantee source unbound)) + (review (sdlc-design-implementation-review baseline unbound + (list link))) + (frontier (snapshot source unresolved: '("artifact/missing"))) + (frontier-link + (sdlc-design-implementation-link baseline guarantee source frontier)) + (frontier-review + (sdlc-design-implementation-review baseline frontier + (list frontier-link)))) + (check-equal? (.ref (car (.ref review 'blocked-links)) 'blocker) + 'source-unbound) + (check-equal? (.ref frontier-review 'mapping-current?) #f) + (check-equal? (.ref frontier-review 'snapshot-frontier?) #t))) + (test-case "a different source presentation cannot match a canonical cut" + (let* ((different + (assurance-artifact "artifact/job-executor" "s1" 'supported digest-b + owner: "lambda-aitia" media-kind: 'scheme-source + provenance: '("repository"))) + (link (sdlc-design-implementation-link + baseline guarantee different current-snapshot)) + (review (sdlc-design-implementation-review baseline current-snapshot + (list link)))) + (check-equal? (.ref (car (.ref review 'blocked-links)) 'blocker) + 'source-changed))))) diff --git a/t/sdlc/unit/design-verification-test.ss b/t/sdlc/unit/design-verification-test.ss new file mode 100644 index 0000000..eb8e515 --- /dev/null +++ b/t/sdlc/unit/design-verification-test.ss @@ -0,0 +1,323 @@ +;;; SPDX-FileCopyrightText: 2026 tao3k team and Contributors +;;; +;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +(import :std/test + (only-in :std/list/list find) + (only-in :clan/poo/object .o .ref) + (only-in :poo-flow/src/feature-system/source-lock-feature + source-lock-payload-digest) + :poo-flow/lambda-aitia/modules/assurance/interface + :poo-flow/lambda-aitia/modules/sdlc/interface) + +(export design-verification-test) + +(def source-bytes "(def (execute-job) 'committed)\n") +(def digest-a (source-lock-payload-digest source-bytes)) +(def digest-b (string-append "sha256:" (make-string 64 #\b))) +(def guarantee + (sdlc-design-guarantee "unique-effect" "g1" "At most one external commit")) +(def contract + (sdlc-design-contract "job-executor" "r1" "ADR/job-executor" + '("execute-job") '("job-ledger") + '("external-operation") '() (list guarantee) '())) +(def source + (assurance-artifact "artifact/source" "s1" 'supported digest-a + owner: "lambda-aitia" media-kind: 'scheme-source + provenance: '("repository"))) +(def other-source + (assurance-artifact "artifact/other" "s1" 'supported digest-b + owner: "lambda-aitia" media-kind: 'scheme-source + provenance: '("repository"))) +(def claim + (assurance-claim "claim/unique-effect" "r1" 'unknown digest-a + subject: "job-executor" + predicate: "At most one external commit" + support-requirements: '("obligation/test") + scope: "repository" valid-from: "2026-09-24")) +(def obligation + (assurance-obligation "obligation/test" "r1" 'unknown digest-a + subject: "job-executor" claim: "claim/unique-effect" + snapshot: "snapshot/job-executor" + evidence-kind: 'native-test capability: 'gerbil-test + scope: "repository")) +(def evidence + (assurance-evidence "evidence/test" "r1" 'supported digest-b + producer: "gxtest" tool: "gerbil-test" tool-version: "v19" + input-artifacts: '("artifact/source") + obligation: "obligation/test" subject: "job-executor" + scope: "repository" valid-from: "2026-09-24" + admission-state: 'candidate)) +(def other-evidence + (assurance-evidence "evidence/test" "r1" 'supported digest-b + producer: "gxtest" tool: "gerbil-test" tool-version: "v19" + input-artifacts: '("artifact/other") + obligation: "obligation/test" subject: "job-executor" + scope: "repository" valid-from: "2026-09-24" + admission-state: 'candidate)) +(def evidence-link + (assurance-relation "relation/evidence" 'assurance 'discharges + "evidence/test" "obligation/test" 'observed)) +(def requirement-link + (assurance-relation "relation/requirement" 'assurance 'supports + "obligation/test" "claim/unique-effect" 'declared)) +(def source-link + (assurance-relation "relation/source" 'structural 'depends-on + "claim/unique-effect" "artifact/source" 'declared)) + +(def (construct obligation-value evidence-value relation-values) + (assurance-snapshot + "snapshot/job-executor" "cut-1" "graph/job-executor" + '(("artifact/source" . "s1") ("artifact/other" . "s1")) + '(("claim/unique-effect" . "r1")) + "fact-cut-1" "policy/design" "p1" + (list source other-source claim obligation-value evidence-value) relation-values)) +(def (bound-cut relation-values evidence-value) + (let (draft (construct obligation evidence-value relation-values)) + (construct (assurance-bind-obligation-to-snapshot obligation draft) + evidence-value relation-values))) +(def (bound-obligation snapshot-value) + (find (lambda (node) + (equal? (.ref node 'identity) "obligation/test")) + (.ref snapshot-value 'nodes))) +(def (outcome snapshot-value input-source) + (assurance-verifier-outcome + "outcome/test" 'succeeded + producer: "gxtest" tool: "gerbil-test" tool-version: "v19" + obligation: "obligation/test" subject: "job-executor" scope: "repository" + snapshot-revision: (.ref snapshot-value 'revision) + snapshot-context-digest: (.ref snapshot-value 'context-digest) + inputs: (list (assurance-verifier-input input-source)) + output-digest: digest-b)) +(def (host source-procedure source-reader: (reader #f)) + (assurance-verification-host + "host/design-verification" + (if reader + (lambda (subject-value issued-at expires-at inputs) + (and (= (length inputs) 1) + (equal? (.ref (car inputs) 'identity) "artifact/source") + (equal? (.ref (car inputs) 'payload) source-bytes) + (equal? (.ref (.ref subject-value 'outcome) 'output-digest) + digest-b) + (< issued-at expires-at))) + (lambda (subject-value issued-at expires-at) + (and (equal? (.ref (.ref subject-value 'outcome) 'output-digest) + digest-b) + (< issued-at expires-at)))) + (lambda () 11) 9 source-procedure source-reader: reader)) +(def (review snapshot-value host-value input-source evidence-value) + (let* ((bound (bound-obligation snapshot-value)) + (reported (outcome snapshot-value input-source)) + (subject (assurance-verification-subject + snapshot-value bound evidence-value reported)) + (admission (assurance-host-admit + host-value bound evidence-value reported)) + (mapping + (sdlc-design-implementation-link contract guarantee source + snapshot-value))) + (values + (sdlc-design-guarantee-support-review + host-value contract guarantee (list mapping) claim subject + evidence-link admission (if admission (list admission) '())) + admission subject mapping))) + +(def design-verification-test + (test-suite "SDLC design guarantee Host support" + (test-case "source-bound Host admission closes support but not conformance" + (let* ((cut (bound-cut (list evidence-link requirement-link source-link) + evidence)) + (configured (host (lambda () cut)))) + (let-values (((receipt admission subject mapping) + (review cut configured source evidence))) + (check-equal? (.ref receipt 'source-bound-support-current?) #t) + (check-equal? (.ref receipt 'source-bytes-checked-support-current?) #f) + (check-equal? (.ref receipt 'verifier-independence-established?) #f) + (check-equal? (.ref receipt 'implementation-conforms?) #f) + (check-equal? (.ref receipt 'release-authorized?) #f) + (check-equal? (.ref receipt 'runtime-executed?) #f)))) + (test-case "Host-owned source bytes must match the mapped digest" + (let* ((cut (bound-cut (list evidence-link requirement-link source-link) + evidence)) + (configured + (host (lambda () cut) + source-reader: (lambda (identity) + (and (equal? identity "artifact/source") + source-bytes))))) + (let-values (((receipt admission subject mapping) + (review cut configured source evidence))) + (check-equal? (.ref receipt 'source-bytes-checked-support-current?) #t) + (check-equal? (.ref receipt 'verifier-independence-established?) #f) + (check-equal? (.ref receipt 'implementation-conforms?) #f)))) + (test-case "changed Host bytes fail closed" + (let* ((cut (bound-cut (list evidence-link requirement-link source-link) + evidence)) + (payload "different") + (configured + (host (lambda () cut) + source-reader: (lambda (identity) payload)))) + (let-values (((receipt admission subject mapping) + (review cut configured source evidence))) + (check-equal? admission #f) + (check-equal? (.ref receipt 'source-bound-support-current?) #f) + (check-equal? (.ref receipt 'source-bytes-checked-support-current?) #f) + (set! payload source-bytes) + (check-equal? + (.ref (sdlc-design-guarantee-support-review + configured contract guarantee (list mapping) claim subject + evidence-link admission '()) + 'source-bytes-checked-support-current?) + #f) + (let-values (((fresh-receipt fresh-admission _subject _mapping) + (review cut configured source evidence))) + (check-equal? (if fresh-admission #t #f) #t) + (check-equal? + (.ref fresh-receipt 'source-bytes-checked-support-current?) + #t))))) + (test-case "issued source-bound admission cannot revive after byte drift" + (let* ((cut (bound-cut (list evidence-link requirement-link source-link) + evidence)) + (payload source-bytes) + (configured + (host (lambda () cut) + source-reader: (lambda (identity) payload)))) + (let-values (((receipt admission subject mapping) + (review cut configured source evidence))) + (check-equal? (if admission #t #f) #t) + (check-equal? (.ref receipt 'source-bytes-checked-support-current?) #t) + (set! payload "different") + (check-equal? + (assurance-host-admission-current? configured admission) #f) + (set! payload source-bytes) + (check-equal? + (assurance-host-admission-current? configured admission) #f) + (let-values (((fresh-receipt fresh-admission _subject _mapping) + (review cut configured source evidence))) + (check-equal? (if fresh-admission #t #f) #t) + (check-equal? + (.ref fresh-receipt 'source-bytes-checked-support-current?) + #t))))) + (test-case "source bytes moving during verifier execution prevent issuance" + (let* ((cut (bound-cut (list evidence-link requirement-link source-link) + evidence)) + (payload source-bytes) + (configured + (assurance-verification-host + "host/source-race" + (lambda (subject issued-at expires-at inputs) + (check-equal? (.ref (car inputs) 'payload) source-bytes) + (set! payload "different") + #t) + (lambda () 11) 9 (lambda () cut) + source-reader: (lambda (identity) payload))) + (reported (outcome cut source))) + (check-equal? + (assurance-host-admit + configured (bound-obligation cut) evidence reported) + #f))) + (test-case "verifier cannot mutate handed source bytes and keep a seal" + (let* ((cut (bound-cut (list evidence-link requirement-link source-link) + evidence)) + (configured + (assurance-verification-host + "host/handed-input-mutation" + (lambda (subject issued-at expires-at inputs) + (let (input (car inputs)) + (set-car! inputs + (.o identity: (.ref input 'identity) + revision: (.ref input 'revision) + digest: (source-lock-payload-digest "different") + payload: "different"))) + #t) + (lambda () 11) 9 (lambda () cut) + source-reader: (lambda (identity) source-bytes))) + (reported (outcome cut source))) + (check-equal? + (assurance-host-admit + configured (bound-obligation cut) evidence reported) + #f))) + (test-case "a cut moving during Host source read fails closed" + (let* ((cut (bound-cut (list evidence-link requirement-link source-link) + evidence)) + (next (bound-cut (list evidence-link requirement-link) + evidence)) + (current-cut cut) + (configured + (host (lambda () current-cut) + source-reader: + (lambda (identity) + (set! current-cut next) + source-bytes)))) + (let-values (((receipt admission subject mapping) + (review cut configured source evidence))) + (check-equal? admission #f) + (check-equal? (.ref receipt 'source-bytes-checked-support-current?) #f)))) + (test-case "design assumptions cannot disappear from the Assurance Claim" + (let* ((cut (bound-cut (list evidence-link requirement-link source-link) + evidence)) + (configured (host (lambda () cut))) + (assumed + (sdlc-design-contract + "job-executor" "r2" "ADR/job-executor" + '("execute-job") '("job-ledger") '("external-operation") + (list (sdlc-design-assumption + "provider-idempotency" "a1" "Provider deduplicates keys")) + (list guarantee) '())) + (reported (outcome cut source)) + (bound (bound-obligation cut)) + (subject (assurance-verification-subject + cut bound evidence reported)) + (admission (assurance-host-admit configured bound evidence reported)) + (mapping (sdlc-design-implementation-link + assumed guarantee source cut)) + (receipt + (sdlc-design-guarantee-support-review + configured assumed guarantee (list mapping) claim subject + evidence-link admission (list admission)))) + (check-equal? (.ref receipt 'mapping-current?) #t) + (check-equal? (.ref receipt 'claim-current?) #f) + (check-equal? (.ref receipt 'source-bound-support-current?) #f))) + (test-case "missing claim-to-source dependency blocks design support" + (let* ((cut (bound-cut (list evidence-link requirement-link) evidence)) + (configured (host (lambda () cut)))) + (let-values (((receipt admission subject mapping) + (review cut configured source evidence))) + (check-equal? (.ref receipt 'host-admission-current?) #t) + (check-equal? (.ref receipt 'source-input-bound?) #f) + (check-equal? (.ref receipt 'source-bound-support-current?) #f)))) + (test-case "admission over another current source does not cover design source" + (let* ((cut (bound-cut (list evidence-link requirement-link source-link) + other-evidence)) + (configured (host (lambda () cut)))) + (let-values (((receipt admission subject mapping) + (review cut configured other-source other-evidence))) + (check-equal? (.ref receipt 'host-admission-current?) #t) + (check-equal? (.ref receipt 'source-input-bound?) #f) + (check-equal? (.ref receipt 'source-bound-support-current?) #f)))) + (test-case "revoked or copied admission cannot close support" + (let* ((cut (bound-cut (list evidence-link requirement-link source-link) + evidence)) + (configured (host (lambda () cut)))) + (let-values (((receipt admission subject mapping) + (review cut configured source evidence))) + (let ((copied (.o (:: @ admission))) + (before (.ref receipt 'source-bound-support-current?))) + (check-equal? before #t) + (check-equal? + (.ref (sdlc-design-guarantee-support-review + configured contract guarantee (list mapping) claim subject + evidence-link admission '()) + 'source-bound-support-current?) + #f) + (check-equal? + (.ref (sdlc-design-guarantee-support-review + configured contract guarantee (list mapping) claim subject + evidence-link copied (list copied)) + 'source-bound-support-current?) + #f) + (assurance-host-revoke-admission! configured admission) + (check-equal? + (.ref (sdlc-design-guarantee-support-review + configured contract guarantee (list mapping) claim subject + evidence-link admission (list admission)) + 'source-bound-support-current?) + #f))))))) diff --git a/t/sdlc/unit/job-retirement-impact-test.ss b/t/sdlc/unit/job-retirement-impact-test.ss new file mode 100644 index 0000000..487410d --- /dev/null +++ b/t/sdlc/unit/job-retirement-impact-test.ss @@ -0,0 +1,91 @@ +;;; SPDX-FileCopyrightText: 2026 tao3k team and Contributors +;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +(import :std/test + (only-in :clan/poo/object .ref) + (only-in :poo-flow/src/graph/types poo-flow-graph-id) + (only-in :poo-flow/modules/temporal-causality/interface + poo-flow-structural-impact-analyze + poo-flow-causal-cut poo-flow-temporal-causal-classify) + :poo-flow/lambda-aitia/user-interface/scenarios/job-executor-retirement) + +(export job-retirement-impact-test) + +(def (impact graph (complete? #t)) + (poo-flow-structural-impact-analyze + graph '(automatic-retry) job-retirement-relations 'dependents complete?)) + +(def (classify graph) + (poo-flow-temporal-causal-classify + graph (poo-flow-causal-cut graph 4) "retry-enabled" 7)) + +(def job-retirement-impact-test + (test-suite "Job-executor retirement Impact qualification" + (test-case "old dependents survive analysis of a proposed removal" + (let* ((receipt (impact job-retirement-before)) + (affected (.ref receipt 'affected-node-ids)) + (witness + (car (filter (lambda (value) + (eq? (.ref value 'target-node-id) 'verify-queued-retry)) + (.ref receipt 'relation-trajectories))))) + (check (length affected) => 5) + (for-each (lambda (id) (check (if (memq id affected) #t #f) => #t)) + '(automatic-retry historical-result active-job queued-job + verify-queued-retry)) + (check (memq 'unrelated-export affected) => #f) + (check (.ref witness 'node-path) + => '(automatic-retry queued-job verify-queued-retry)) + (check (.ref witness 'relation-path) => '(USES_CAPABILITY VERIFIES)) + (check (.ref receipt 'release-authorized?) => #f) + (check (.ref receipt 'runtime-executed?) => #f))) + (test-case "candidate graph cannot turn missing retired identity into no impact" + (let (before (impact job-retirement-before)) + (check-exception (impact job-retirement-candidate) true) + (check (poo-flow-graph-id job-retirement-before) + => 'job-executor/before-retirement) + (check (.ref (impact job-retirement-before) 'affected-node-ids) + => (.ref before 'affected-node-ids)))) + (test-case "unknown external inventory prevents scoped completeness" + (let (receipt (impact job-retirement-before #f)) + (check (.ref receipt 'status) => 'partial-impact) + (check (.ref receipt 'release-authorized?) => #f))) + (test-case "past occurrence and current activity remain separate facts" + (let* ((cut (poo-flow-causal-cut job-retirement-event-graph 4)) + (receipt (classify job-retirement-event-graph))) + (check (.ref receipt 'past-event-ids) + => '("retry-enabled" "historical-result" "job-started")) + (check (.ref receipt 'current-event-ids) + => '("job-still-active" "retirement-review")) + (check (.ref receipt 'future-event-ids) => '("queued-retry")) + (check (.ref receipt 'counterfactual-event-ids) => '("retire-retry")) + (check (.ref receipt 'outside-horizon-event-ids) => '("later-retry")) + (check (.ref receipt 'hypothesized-event-ids) => '()) + (check (member "retire-retry" + (map (lambda (event) (.ref event 'identity)) + (.ref cut 'events))) => #f) + (check (.ref receipt 'assurance-closed?) => #f) + (check (.ref receipt 'runtime-executed?) => #f))) + (test-case "later review does not rewrite the earlier historical cut" + (let* ((old (poo-flow-causal-cut job-retirement-event-graph 2)) + (current (poo-flow-causal-cut job-retirement-event-graph 4)) + (old-events (.ref old 'events)) + (historical (cadr old-events))) + (check (map (lambda (event) (.ref event 'identity)) old-events) + => '("retry-enabled" "historical-result" "job-started")) + (check (.ref historical 'payload-identity) => "historical-result/payload") + (check (.ref historical 'modality) => 'observed) + (check (.ref historical 'committed?) => #t) + (check (if (memq historical (.ref current 'events)) #t #f) => #t) + (check (map (lambda (event) (.ref event 'identity)) (.ref old 'events)) + => '("retry-enabled" "historical-result" "job-started")))) + (test-case "a missing causal parent remains an unknown frontier" + (let (receipt (classify job-retirement-incomplete-event-graph)) + (check (.ref receipt 'status) => 'partial-temporal-classification) + (check (.ref receipt 'unknown-frontier) => '("unknown-consumer-origin")) + (check (.ref receipt 'release-authorized?) => #f))) + (test-case "a proposal cannot be promoted to the observed change trigger" + (check-exception + (poo-flow-temporal-causal-classify + job-retirement-event-graph (poo-flow-causal-cut job-retirement-event-graph 4) + "retire-retry" 7) + true)))) diff --git a/t/sdlc/unit/nasa-catalog-test.ss b/t/sdlc/unit/nasa-catalog-test.ss index f66763a..82231d7 100644 --- a/t/sdlc/unit/nasa-catalog-test.ss +++ b/t/sdlc/unit/nasa-catalog-test.ss @@ -3,7 +3,7 @@ ;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later (import :std/test :std/error - :poo-flow/src/module-system/contribution/interface + (only-in :clan/poo/object .ref .slot?) :poo-flow/lambda-aitia/modules/sdlc/standards/nasa-7150-2d :std/list/list) (export nasa-catalog-test) @@ -15,7 +15,7 @@ (check-equal? (length (delete-duplicates/hash (map (lambda (r) (.ref r 'identity)) rows))) 130) (check-equal? (length (filter (lambda (r) (.slot? r 'class-matrix)) rows)) 100) (for-each (lambda (r) - (check-equal? (.ref r 'standard) "nasa/npr-7150.2d") + (check-equal? (.ref r 'standard) "nasa/sdlc/npr-7150.2") (check-equal? (.ref r 'assessment) 'not-evaluated)) rows))) (test-case "matrix blanks never become unconditional exemptions" (check-equal? (nasa-matrix-invocation "SWE-013" 'e) 'invoked) diff --git a/t/sdlc/unit/nasa-certification-test.ss b/t/sdlc/unit/nasa-certification-test.ss index 01d8469..35a5654 100644 --- a/t/sdlc/unit/nasa-certification-test.ss +++ b/t/sdlc/unit/nasa-certification-test.ss @@ -4,7 +4,7 @@ (import :std/test :std/error (only-in :clan/poo/object .o .ref) - :poo-flow/src/module-system/contribution/verification + :poo-flow/core/contribution/verification :poo-flow/lambda-aitia/modules/sdlc/objects :poo-flow/lambda-aitia/modules/sdlc/standards/nasa-policy :poo-flow/lambda-aitia/modules/sdlc/standards/nasa-certification) diff --git a/t/sdlc/unit/nasa-lifecycle-test.ss b/t/sdlc/unit/nasa-lifecycle-test.ss index 7d95eb5..81eda22 100644 --- a/t/sdlc/unit/nasa-lifecycle-test.ss +++ b/t/sdlc/unit/nasa-lifecycle-test.ss @@ -4,7 +4,7 @@ (import :std/test :std/error (only-in :clan/poo/object .o .ref) - :poo-flow/src/module-system/contribution/verification + :poo-flow/core/contribution/verification :poo-flow/lambda-aitia/modules/sdlc/objects :poo-flow/lambda-aitia/modules/sdlc/standards/nasa-7150-2d :poo-flow/lambda-aitia/modules/sdlc/standards/nasa-coverage diff --git a/t/sdlc/unit/nasa-review-test.ss b/t/sdlc/unit/nasa-review-test.ss index a28c7de..4d52b5f 100644 --- a/t/sdlc/unit/nasa-review-test.ss +++ b/t/sdlc/unit/nasa-review-test.ss @@ -3,7 +3,7 @@ ;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later (import :std/test :std/error - :poo-flow/src/module-system/contribution/interface + (only-in :clan/poo/object .o .ref) :poo-flow/lambda-aitia/modules/sdlc/interface :poo-flow/lambda-aitia/modules/sdlc/standards/nasa-7150-2d :poo-flow/lambda-aitia/modules/sdlc/standards/nasa-7150-2d-catalog diff --git a/t/sdlc/unit/nasa-standard-provider-test.ss b/t/sdlc/unit/nasa-standard-provider-test.ss index 1298283..b5779a8 100644 --- a/t/sdlc/unit/nasa-standard-provider-test.ss +++ b/t/sdlc/unit/nasa-standard-provider-test.ss @@ -12,7 +12,7 @@ (only-in :poo-flow/src/feature-system/interface sources-lock-freeze write-sources-lock-module) - (only-in :poo-flow/src/modules/standards/interface + (only-in :poo-flow/modules/standards/interface poo-flow-standard-digest poo-flow-standard-materialization-receipt-valid? poo-flow-standard-resolution-receipt-bundle @@ -41,6 +41,11 @@ (def nasa-standard-provider-test (test-suite "NASA NPR 7150.2D Standards Core Provider" + (test-case + "Standards family shares the public NASA SDLC identity" + (check-equal? (.ref Nasa7150_2DStandardFamily 'identity) + "nasa/sdlc/npr-7150.2") + (check-equal? (.ref Nasa7150_2DStandardFamily 'semantic-kind) 'sdlc)) (test-case "declares all immutable NASA source facts through Sources Lock Feature" (check-equal? (.ref Nasa7150_2DSourcesLock 'feature-id) 'sources-lock) @@ -50,7 +55,7 @@ "source.lock.ss is the deterministic product of declarations and bytes" (let* ((generated-lock (sources-lock-freeze - "lambda-aitia/nasa-npr-7150.2d/sources" + "lambda-aitia/nasa/sdlc/npr-7150.2/D/sources" "2022-03-08" Nasa7150_2DSources read-lambda-aitia-source diff --git a/t/sdlc/unit/nasa-structured-test.ss b/t/sdlc/unit/nasa-structured-test.ss index 4fe282f..2aa90a6 100644 --- a/t/sdlc/unit/nasa-structured-test.ss +++ b/t/sdlc/unit/nasa-structured-test.ss @@ -3,8 +3,10 @@ ;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later (import :std/test :std/error - :poo-flow/src/module-system/contribution/interface - :poo-flow/src/module-system/contribution/verification + (only-in :clan/poo/object .o .ref) + (only-in :poo-flow/core/contribution/verification + poo-flow-verify poo-flow-revoke-verification!) + :poo-flow/core/contribution/verification :poo-flow/lambda-aitia/modules/sdlc/interface :poo-flow/lambda-aitia/modules/sdlc/standards/nasa-review :poo-flow/lambda-aitia/modules/sdlc/standards/nasa-structured diff --git a/t/sdlc/unit/sdlc-config-test.ss b/t/sdlc/unit/sdlc-config-test.ss index 138b716..b4baf8f 100644 --- a/t/sdlc/unit/sdlc-config-test.ss +++ b/t/sdlc/unit/sdlc-config-test.ss @@ -3,28 +3,30 @@ ;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later (import :std/test :std/error - :poo-flow/src/module-system/load - :poo-flow/src/module-system/declaration/interface - :poo-flow/src/module-system/contribution/interface + (only-in :poo-flow/src/user-interface/module-selection-syntax + poo-flow-modules!) + :poo-flow/src/user-interface/module-selection + (only-in :clan/poo/object .ref) + (only-in :poo-flow/core/contribution/objects admit-contributions) :poo-flow/lambda-aitia/modules/sdlc/config) (def (selected . flags) (sdlc-config (poo-flow-user-module-selection 'custom 'sdlc flags))) (export sdlc-config-test) (def sdlc-config-test (test-suite "SDLC init selection projection" - (test-case "existing custom module syntax selects the standard" + (test-case "custom SDLC module is standard-free" (let* ((bundles (poo-flow-modules! - :custom (sdlc @ "../lambda-aitia/modules/sdlc" +nasa-7150-2d))) + :custom (sdlc @ "../lambda-aitia/modules/sdlc"))) (selection (caar bundles)) (contribution (sdlc-config selection)) (standards (.ref (.ref contribution 'profile) 'standards))) (check-equal? (poo-flow-user-module-selection-entrypoint selection) "../lambda-aitia/modules/sdlc/interface.ss") - (check-equal? (map (lambda (s) (.ref s 'identity)) standards) '("nasa/npr-7150.2d")) + (check-equal? standards '()) (check-equal? (.ref (admit-contributions (list contribution) '()) 'accepted?) #t))) (test-case "no feature means no NASA selection" (check-equal? (.ref (.ref (selected) 'profile) 'standards) '())) (test-case "unsupported features and unrelated modules are rejected" - (check-exception (selected '+nasa-7150-2c) Error?) - (check-exception (selected '+nasa-7150-2d '+nasa-7150-2d) Error?) + (check-exception (selected '+nasa) Error?) + (check-exception (selected '+npr-7150.2) Error?) (check-exception (sdlc-config (poo-flow-user-module-selection 'custom 'other '())) Error?)))) diff --git a/t/sdlc/unit/sdlc-model-test.ss b/t/sdlc/unit/sdlc-model-test.ss index 9869221..63824dc 100644 --- a/t/sdlc/unit/sdlc-model-test.ss +++ b/t/sdlc/unit/sdlc-model-test.ss @@ -3,7 +3,7 @@ ;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later (import :std/test :std/error - :poo-flow/src/module-system/contribution/interface + (only-in :clan/poo/object .o .ref) :poo-flow/lambda-aitia/modules/sdlc/interface) (def project (sdlc-project "flight" "r2" "software" #t)) (def evidence (sdlc-evidence "ev" "flight" "r2" "software" "nasa" "SWE-052" "test" 'pass)) diff --git a/t/sdlc/unit/sdlc-test.ss b/t/sdlc/unit/sdlc-test.ss index 204f3a5..535d963 100644 --- a/t/sdlc/unit/sdlc-test.ss +++ b/t/sdlc/unit/sdlc-test.ss @@ -3,7 +3,8 @@ ;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later (import :std/test :std/error - :poo-flow/src/module-system/contribution/interface + (only-in :clan/poo/object .o .ref) + (only-in :poo-flow/core/contribution/objects admit-contributions) :poo-flow/lambda-aitia/modules/sdlc/interface :poo-flow/lambda-aitia/modules/sdlc/standards/nasa-7150-2d) (def ProjectSdlcProfile (sdlc-with-standards SdlcProfile (list Nasa7150_2D))) @@ -24,7 +25,7 @@ (test-case "clause references preserve edition and source location" (let ((clause (nasa-requirement-by-id "SWE-013"))) (check-equal? (.ref clause 'identity) "SWE-013") - (check-equal? (.ref clause 'standard) "nasa/npr-7150.2d") + (check-equal? (.ref clause 'standard) "nasa/sdlc/npr-7150.2") (check-equal? (.ref clause 'section) "3.1.3") (check-equal? (.ref clause 'topic) 'lifecycle-planning))) (test-case "other standards compose without changing SDLC or NASA" diff --git a/t/sdlc/unit/sdlc-trace-test.ss b/t/sdlc/unit/sdlc-trace-test.ss index 788b09c..792f5a0 100644 --- a/t/sdlc/unit/sdlc-trace-test.ss +++ b/t/sdlc/unit/sdlc-trace-test.ss @@ -3,7 +3,7 @@ ;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later (import :std/test :std/error - :poo-flow/src/module-system/contribution/interface + (only-in :clan/poo/object .o .ref) :poo-flow/lambda-aitia/modules/sdlc/interface :poo-flow/lambda-aitia/modules/sdlc/standards/nasa-7150-2d) (def project (sdlc-project "flight" "r2" "software" #t)) diff --git a/testing-interface.ss b/testing-interface.ss index 5a71dd0..d5d5865 100644 --- a/testing-interface.ss +++ b/testing-interface.ss @@ -13,7 +13,7 @@ testing-interface-map-profile testing-test-selector) :std/list/list - (only-in :poo-flow/src/module-system/observability/testing-extension + (only-in :poo-flow/src/testing/testing-extension make-poo-flow-testing-observability-profile poo-flow-testing-observability-extension)) diff --git a/tools/build-native-library.py b/tools/build-native-library.py index 54a89c9..874c905 100644 --- a/tools/build-native-library.py +++ b/tools/build-native-library.py @@ -120,6 +120,27 @@ def configure_gambit_compiler(home: Path) -> str: return str(compiler) +def configure_orgize_header() -> None: + """Make Orgize's installed public header visible to Gambit's C phase.""" + + gerbil_path = os.environ.get("GERBIL_PATH") + if not gerbil_path: + raise RuntimeError("GERBIL_PATH must identify the installed Gerbil packages") + header_root = ( + Path(gerbil_path).resolve() + / "pkg/github.com/tao3k/orgize/bindings/c/include" + ) + if not (header_root / "orgize.h").is_file(): + raise RuntimeError( + "Orgize's public C header is absent; run gxpkg deps --install " + f"for the pinned POO Flow dependencies: {header_root}" + ) + previous = os.environ.get("C_INCLUDE_PATH") + os.environ["C_INCLUDE_PATH"] = os.pathsep.join( + part for part in (str(header_root), previous) if part + ) + + def module_closure(project: Path) -> tuple[list[tuple[str, Path]], Path]: expression = r'''(let* ((ctx (import-module "bindings/c/aitia-native.ss")) (deps (gxc#find-runtime-module-deps ctx))) @@ -166,6 +187,7 @@ def generated_define(link_source: Path, name: str) -> str: def main() -> int: configure_darwin_toolchain() + configure_orgize_header() parser = argparse.ArgumentParser() parser.add_argument("--output", required=True, type=Path) arguments = parser.parse_args() diff --git a/tools/import-nasa-catalog.py b/tools/import-nasa-catalog.py index bdc39aa..ce65310 100644 --- a/tools/import-nasa-catalog.py +++ b/tools/import-nasa-catalog.py @@ -55,7 +55,7 @@ (page (list-ref row 2)) (excerpt (list-ref row 3)) (parts (list-ref row 4)) (notes (list-ref row 5)) (matrix (list-ref row 6)) - (value (.o identity: id standard: "nasa/npr-7150.2d" + (value (.o identity: id standard: "nasa/sdlc/npr-7150.2" section: section-value source-url: (string-append "https://nodis3.gsfc.nasa.gov/displayDir.cfm?Internal_ID=N_PR_7150_002D_&page_name=" page) source-excerpt: excerpt source-parts: parts source-notes: notes diff --git a/tools/nasa-implementation-report.ss b/tools/nasa-implementation-report.ss index ffd4e4d..10e0592 100644 --- a/tools/nasa-implementation-report.ss +++ b/tools/nasa-implementation-report.ss @@ -3,7 +3,7 @@ ;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later ;;; Render the executable ledger; no separately maintained requirement table. -(import :poo-flow/src/module-system/contribution/interface +(import (only-in :clan/poo/object .ref) :poo-flow/lambda-aitia/modules/sdlc/standards/nasa-coverage) (def (main . args) (display "#+title: NASA implementation coverage ledger\n\n") diff --git a/tools/update-sources-lock.ss b/tools/update-sources-lock.ss index c5df6e8..ef9f3dd 100644 --- a/tools/update-sources-lock.ss +++ b/tools/update-sources-lock.ss @@ -19,7 +19,7 @@ (def lock (sources-lock-freeze - "lambda-aitia/nasa-npr-7150.2d/sources" + "lambda-aitia/nasa/sdlc/npr-7150.2/D/sources" "2022-03-08" Nasa7150_2DSources read-source diff --git a/user-interface/compositions/aitia.ss b/user-interface/compositions/aitia.ss index 7793f8f..1a8efdd 100644 --- a/user-interface/compositions/aitia.ss +++ b/user-interface/compositions/aitia.ss @@ -3,52 +3,33 @@ ;;; ;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later -;;; Boundary: official maintained Aitia Composition and its catalog entry. -;;; Invariant: Aitia owns domain assembly only; selection, loading, planning and -;;; identity rules come from POO Flow. - -(import (only-in :poo-flow/src/module-system/profile-composition/interface - poo-flow-composition - poo-flow-composition-catalog - poo-flow-official-composition-identity - poo-flow-scenario-case - poo-flow-scenario-clause - poo-flow-scenario-profile-binding - poo-flow-scenario-stage) - :poo-flow/lambda-aitia/modules/formal-methods/interface +;;; Official Aitia composition over POO Flow's current Profile algebra. +;;; Domain selection stays in the maintained Project Profile; POO Flow owns +;;; module selection, composition, Case lowering and runtime admission. +(import (only-in :clan/poo/object .o) + (only-in :poo-flow/core/module-system/schema/relations + poo-flow-semantic-identity) + (only-in :poo-flow/src/authoring/semantic-module + poo-flow-semantic-module) + (only-in :poo-flow/core/profile-composition/profile-bundle + poo-flow-profile-export poo-flow-module-profiles + profiles compose) + (only-in :poo-flow/core/profile-composition/selection-syntax use-module) + (only-in :poo-flow/src/scenario/composition-syntax user-composition) :poo-flow/lambda-aitia/user-interface/profiles/software-engineering/project) -(export aitia-composition-identity - aitia-composition - aitia-composition-catalog) - -(def aitia-composition-identity - (poo-flow-official-composition-identity 'aitia)) - -(def (aitia-default-case project-profile) - (poo-flow-scenario-case - 'aitia - '() - (list project-profile) - (list - (poo-flow-scenario-stage - 'software-engineering-project - (list - (poo-flow-scenario-clause - 'profile - '(default-software-engineering-project))))) - (list - (poo-flow-scenario-profile-binding - 'aitia - 'default-software-engineering-project)))) +(export AitiaModule aitia) -(def aitia-composition - (poo-flow-composition - aitia-composition-identity - default-software-engineering-project - aitia-default-case)) +(def AitiaModule + (poo-flow-semantic-module + (poo-flow-semantic-identity 'software-engineering 'aitia) + profiles: + (poo-flow-module-profiles + (poo-flow-profile-export + 'default + (.o (:: @ default-software-engineering-project) + stages: (.o software-engineering-project: (.o))))))) -(def aitia-composition-catalog - (poo-flow-composition-catalog - (list aitia-composition) - '(gitops sdlc ADR assurance formal-methods))) +(user-composition aitia + (compose profiles + (use-module AitiaModule default))) diff --git a/user-interface/config.ss b/user-interface/config.ss index 2fb966c..1ec2abb 100644 --- a/user-interface/config.ss +++ b/user-interface/config.ss @@ -3,21 +3,35 @@ ;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later ;;; -*- Gerbil -*- -;;; Final User Composition: provider + environment Profiles + SDLC Standard. -(import :poo-flow/src/user-interface/config-discovery-syntax) -(export github-gitops-sdlc) +;;; Maintained GitHub/GitOps/SDLC Case. Selection and Case lowering belong to +;;; POO Flow; the concrete policy Profiles remain owned by Aitia. +(import (only-in :poo-flow/core/profile-composition/profile-bundle + profiles compose + poo-flow-profile-export poo-flow-module-profiles) + (only-in :poo-flow/core/profile-composition/selection-syntax use-module) + (only-in :poo-flow/src/scenario/composition-syntax user-composition) + (only-in :poo-flow/core/module-system/schema/relations + poo-flow-semantic-identity) + (only-in :poo-flow/src/authoring/semantic-module + poo-flow-semantic-module) + :poo-flow/lambda-aitia/user-interface/profiles/github/actions + :poo-flow/lambda-aitia/user-interface/profiles/gitops/dev + :poo-flow/lambda-aitia/user-interface/profiles/gitops/staging + :poo-flow/lambda-aitia/user-interface/profiles/gitops/production + :poo-flow/lambda-aitia/user-interface/profiles/nasa/sdlc) +(export AitiaDeliveryModule github-gitops-sdlc) -(use-composition github-gitops-sdlc - (modules - (use-module funflow as github - (profile actions)) - (use-module gitops as delivery - (profile dev) - (profile staging) - (profile production)) - (use-module sdlc as sdlc - (profile nasa-7150-2d))) - (compose - (profile github actions) - (profiles delivery dev staging production) - (profile sdlc nasa-7150-2d))) +(def AitiaDeliveryModule + (poo-flow-semantic-module + (poo-flow-semantic-identity 'software-engineering 'delivery) + profiles: + (poo-flow-module-profiles + (poo-flow-profile-export 'actions actions) + (poo-flow-profile-export 'dev dev) + (poo-flow-profile-export 'staging staging) + (poo-flow-profile-export 'production production)))) + +(user-composition github-gitops-sdlc + (compose profiles + (use-module AitiaDeliveryModule actions dev staging production) + (use-module NasaSdlcModule npr-7150.2))) diff --git a/user-interface/contracts/engineering-workspace.org b/user-interface/contracts/engineering-workspace.org new file mode 100644 index 0000000..3af78a5 --- /dev/null +++ b/user-interface/contracts/engineering-workspace.org @@ -0,0 +1,95 @@ +# SPDX-FileCopyrightText: 2026 tao3k team and Contributors +# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +#+title: Aitia engineering workspace — Scheme-owned Org contracts + +:PROPERTIES: +:ID: aitia-engineering-workspace-contract +:STATUS: draft +:END: + +Orgize's Scheme/POO Org Elements and Org Contract modules own these declarations. +The tagged Scheme blocks lower to admitted POO values, then to Rust AOT plans +consumed over the Scheme-declared Rowan graph. No legacy CLI evaluator or +Python implementation defines the semantics. CONTRACT_ID and CONTRACT_SCOPE +remain native Org properties; :org-contract is the upstream block selector, +not a replacement metadata channel. + +These contracts check reviewable structure only. They do not resolve identity, +verify the content of evidence, complete a review or grant execution authority. +Consumer generation/evaluation must be qualified on the Scheme/Rowan path. + +* Engineering workspace +:PROPERTIES: +:CONTRACT_ID: lambda-aitia.engineering-workspace +:CONTRACT_SCOPE: document +:END: + +#+begin_src scheme :org-contract +(assert-org-element "workspace.organization" error + (bindings) + (org-elements headline (property title "Organization") (child-of scope)) + (expect at-least 1)) +(assert-org-element "workspace.requirements" error + (bindings) + (org-elements headline (property title "Requirements") (descendant-of scope)) + (expect at-least 1)) +(assert-org-element "workspace.assumptions" error + (bindings) + (org-elements headline (property title "Assumptions") (descendant-of scope)) + (expect at-least 1)) +(assert-org-element "workspace.alternatives" error + (bindings) + (org-elements headline (property title "Alternatives") (descendant-of scope)) + (expect at-least 1)) +(assert-org-element "workspace.review" error + (bindings) + (org-elements headline (property title "Review") (descendant-of scope)) + (expect at-least 1)) +(assert-org-element "workspace.receipts" error + (bindings) + (org-elements headline (property title "Receipts") (descendant-of scope)) + (expect at-least 1)) +#+end_src + +* Design decision review +:PROPERTIES: +:CONTRACT_ID: lambda-aitia.design-review +:CONTRACT_SCOPE: subtree +:END: + +The section bindings are local to the selected decision. A required next gate +is proving that another decision's content cannot satisfy these assertions. + +#+begin_src scheme :org-contract +(assert-org-element "decision.requirements" error + (bindings + (bind requirements + (org-elements headline (property title "Requirements") (child-of scope)))) + (org-elements headline (child-of requirements)) + (expect at-least 1)) +(assert-org-element "decision.assumptions" error + (bindings + (bind assumptions + (org-elements headline (property title "Assumptions") (child-of scope)))) + (org-elements headline (child-of assumptions)) + (expect at-least 1)) +(assert-org-element "decision.alternatives" error + (bindings + (bind alternatives + (org-elements headline (property title "Alternatives") (child-of scope)))) + (org-elements headline (child-of alternatives)) + (expect at-least 2)) +(assert-org-element "decision.review" error + (bindings + (bind review + (org-elements headline (property title "Review") (child-of scope)))) + (org-elements headline (child-of review)) + (expect at-least 1)) +(assert-org-element "decision.receipts" error + (bindings + (bind receipts + (org-elements headline (property title "Receipts") (child-of scope)))) + (org-elements link (descendant-of receipts)) + (expect at-least 1)) +#+end_src diff --git a/user-interface/profiles/github/actions.ss b/user-interface/profiles/github/actions.ss index 1a4de3a..e39d487 100644 --- a/user-interface/profiles/github/actions.ss +++ b/user-interface/profiles/github/actions.ss @@ -6,7 +6,7 @@ ;;; Reusable GitHub workflow Profiles; no scenario or runtime execution lives here. (import (only-in :clan/poo/object .def) - :poo-flow/src/modules/funflow/github-ci-contract) + :poo-flow/modules/funflow/github-ci-contract) (export actions) (.def (actions @ poo-flow-funflow-github-ci-profile) diff --git a/user-interface/profiles/nasa/sdlc.ss b/user-interface/profiles/nasa/sdlc.ss new file mode 100644 index 0000000..1c56435 --- /dev/null +++ b/user-interface/profiles/nasa/sdlc.ss @@ -0,0 +1,31 @@ +;;; -*- Gerbil -*- +;;; SPDX-FileCopyrightText: 2026 tao3k team and Contributors +;;; +;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +;;; Organization NASA / domain SDLC / standard family NPR 7150.2. +;;; POO Flow owns the typed namespace, Module and Profile selection. +(import (only-in :clan/poo/object .def) + (only-in :poo-flow/core/module-system/schema/relations + poo-flow-semantic-identity) + (only-in :poo-flow/src/authoring/semantic-module + poo-flow-semantic-module) + (only-in :poo-flow/core/profile-composition/profile-bundle + poo-flow-profile-export poo-flow-module-profiles) + :poo-flow/lambda-aitia/modules/sdlc/config) +(export NasaSdlcModule nasa-sdlc-npr7150_2) + +(.def (nasa-sdlc-npr7150_2 @ sdlc-nasa-7150-profile) + (name 'nasa/sdlc/npr-7150.2) + (role 'standard) + (standard "nasa/sdlc/npr-7150.2") + (owner 'lambda-aitia) + (extends sdlc-nasa-7150-profile) + (runtime-executed #f)) + +(def NasaSdlcModule + (poo-flow-semantic-module + (poo-flow-semantic-identity 'nasa 'sdlc) + profiles: + (poo-flow-module-profiles + (poo-flow-profile-export 'npr-7150.2 nasa-sdlc-npr7150_2)))) diff --git a/user-interface/profiles/sdlc/nasa-7150-2d.ss b/user-interface/profiles/sdlc/nasa-7150-2d.ss deleted file mode 100644 index 2040d2b..0000000 --- a/user-interface/profiles/sdlc/nasa-7150-2d.ss +++ /dev/null @@ -1,15 +0,0 @@ -;;; SPDX-FileCopyrightText: 2026 tao3k team and Contributors -;;; -;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later - -;;; -*- Gerbil -*- -(import (only-in :clan/poo/object .def) - :poo-flow/lambda-aitia/modules/sdlc/config) -(export nasa-7150-2d) -(.def (nasa-7150-2d @ sdlc-nasa-7150-profile) - (name 'nasa-7150-2d) - (role 'standard) - (standard "nasa/npr-7150.2d") - (owner 'lambda-aitia) - (extends sdlc-nasa-7150-profile) - (runtime-executed #f)) diff --git a/user-interface/profiles/software-engineering/project.ss b/user-interface/profiles/software-engineering/project.ss index ecd4aef..86e2a9c 100644 --- a/user-interface/profiles/software-engineering/project.ss +++ b/user-interface/profiles/software-engineering/project.ss @@ -7,7 +7,7 @@ ;;; Invariant: the Profile selects Aitia knowledge and obligations only; module ;;; realization and Runtime execution remain separate POO Flow phases. -(import (only-in :poo-flow/src/module-system/declaration/interface +(import (only-in :poo-flow/src/user-interface/module-selection poo-flow-settings poo-flow-user-module-selection) (only-in :poo-flow/src/user-interface/profile-core @@ -23,17 +23,19 @@ (poo-flow-user-module-selection 'software-engineering 'gitops '(+open-gitops)) (poo-flow-user-module-selection - 'software-engineering 'sdlc '(+nasa-7150-2d)) + 'software-engineering 'sdlc '()) + (poo-flow-user-module-selection + 'nasa 'sdlc '(+npr-7150.2)) (poo-flow-user-module-selection 'software-engineering 'ADR '(+rfc-relations)) (poo-flow-user-module-selection 'software-engineering 'assurance '(+causal-evidence)) (poo-flow-user-module-selection - 'software-engineering 'formal-methods '(+tla+ +lean +cedar)))) + 'software-engineering 'formal-methods '(+quint +lean +cedar)))) (poo-flow-settings project-kind: 'software-engineering composition: 'aitia - formal-method-obligations: '(tla+ lean cedar) + formal-method-obligations: '(quint lean cedar) runtime-executed?: #f) '(project-kind composition diff --git a/user-interface/scenarios/github-gitops.ss b/user-interface/scenarios/github-gitops.ss index e8c4866..0070f80 100644 --- a/user-interface/scenarios/github-gitops.ss +++ b/user-interface/scenarios/github-gitops.ss @@ -8,7 +8,11 @@ (export github-change github-check github-run-gitops) (def (github-change event repository revision source-ref target-ref pull-request) (gitops-change 'github event repository revision source-ref target-ref pull-request)) -(def (github-check name repository revision conclusion) - (gitops-check name repository revision conclusion)) +(def (github-check name repository revision conclusion + standard-edition: (standard-edition #f) + source-lock-digest: (source-lock-digest #f)) + (gitops-check name repository revision conclusion + standard-edition: standard-edition + source-lock-digest: source-lock-digest)) (def (github-run-gitops composition change checks) (gitops-evaluate composition change checks)) diff --git a/user-interface/scenarios/job-executor-retirement.ss b/user-interface/scenarios/job-executor-retirement.ss new file mode 100644 index 0000000..9bb216e --- /dev/null +++ b/user-interface/scenarios/job-executor-retirement.ss @@ -0,0 +1,81 @@ +;;; SPDX-FileCopyrightText: 2026 tao3k team and Contributors +;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +;;; Synthetic qualification facts, not observations from a production service. +;;; Algorithms and receipt authority stay with POO Flow's public owners. +(import (only-in :poo-flow/src/graph/types + poo-flow-graph poo-flow-graph-node poo-flow-graph-edge) + (only-in :poo-flow/modules/temporal-causality/interface + poo-flow-temporal-observation poo-flow-causal-event + poo-flow-causal-event-graph)) + +(export job-retirement-before job-retirement-candidate + job-retirement-relations job-retirement-events + job-retirement-event-graph job-retirement-incomplete-event-graph) + +(def job-retirement-relations '(PRODUCED_UNDER USES_CAPABILITY VERIFIES)) + +(def job-retirement-before + (poo-flow-graph + 'job-executor/before-retirement + (map poo-flow-graph-node + '(automatic-retry historical-result active-job queued-job + verify-queued-retry unrelated-export)) + (list + (poo-flow-graph-edge 'historical-result 'automatic-retry 'PRODUCED_UNDER) + (poo-flow-graph-edge 'active-job 'automatic-retry 'USES_CAPABILITY) + (poo-flow-graph-edge 'queued-job 'automatic-retry 'USES_CAPABILITY) + (poo-flow-graph-edge 'verify-queued-retry 'queued-job 'VERIFIES)))) + +;;; Candidate topology only: moving these edges is not a migration receipt. +;;; Historical interpretation still binds job-retirement-before, not this graph. +(def job-retirement-candidate + (poo-flow-graph + 'job-executor/candidate-after-retirement + (map poo-flow-graph-node + '(reviewed-recovery historical-result active-job queued-job + verify-queued-retry unrelated-export)) + (list + (poo-flow-graph-edge 'active-job 'reviewed-recovery 'USES_CAPABILITY) + (poo-flow-graph-edge 'queued-job 'reviewed-recovery 'USES_CAPABILITY) + (poo-flow-graph-edge 'verify-queued-retry 'queued-job 'VERIFIES)))) + +(def (fixture-event id kind position parents modality committed?) + (poo-flow-causal-event + id "job-executor/retirement-fixture" kind + (poo-flow-temporal-observation + (string-append id "/time") 'logical-version position + "synthetic-retirement-fixture") + (string-append id "/payload") parents modality committed?)) + +(def job-retirement-events + (list + (fixture-event "retry-enabled" 'capability-enabled 1 '() 'observed #t) + (fixture-event "historical-result" 'job-completed 2 + '("retry-enabled") 'observed #t) + (fixture-event "job-started" 'job-started 2 + '("retry-enabled") 'observed #t) + ;; Current activity is explicitly observed; old creation time cannot infer it. + (fixture-event "job-still-active" 'job-state-observed 4 + '("job-started") 'observed #t) + (fixture-event "retirement-review" 'review-requested 4 + '("retry-enabled") 'observed #t) + (fixture-event "queued-retry" 'planned-retry 6 + '("retry-enabled") 'declared #f) + (fixture-event "retire-retry" 'proposed-retirement 5 + '("retirement-review") 'counterfactual #f) + (fixture-event "later-retry" 'planned-retry 10 + '("retry-enabled") 'declared #f) + (fixture-event "unrelated-export" 'export-completed 4 '() 'observed #t))) + +(def job-retirement-event-graph + (poo-flow-causal-event-graph + "job-executor/retirement-fixture" job-retirement-events)) + +(def job-retirement-incomplete-event-graph + (poo-flow-causal-event-graph + "job-executor/retirement-fixture" + (append job-retirement-events + (list (fixture-event "external-observation" 'consumer-observed 4 + '("retry-enabled" "unknown-consumer-origin") + 'observed #t))))) diff --git a/user-interface/scenarios/job-retirement-contract.ss b/user-interface/scenarios/job-retirement-contract.ss new file mode 100644 index 0000000..ded0719 --- /dev/null +++ b/user-interface/scenarios/job-retirement-contract.ss @@ -0,0 +1,55 @@ +;;; SPDX-FileCopyrightText: 2026 tao3k team and Contributors +;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +;;; Maintained POO design declaration for the bounded formal experiment. +;;; Source bindings are not Host evidence admission or implementation refinement. +(import (only-in :poo-flow/lambda-aitia/modules/sdlc/design + sdlc-design-contract sdlc-design-assumption sdlc-design-guarantee)) +(export job-retirement-contract job-retirement-safety + job-retirement-quint-sources job-retirement-lean-sources + job-retirement-scheme-sources) + +(def job-retirement-safety + (sdlc-design-guarantee + "retirement-safety" "g1" + "Retirement leaves no active or queued work; a cached grant cannot start a later effect.")) +(def job-retirement-contract + (sdlc-design-contract + "job-executor/retirement" "r1" "RFC/0004/retirement-ingress" + '("review-retirement" "invalidate-cached-authority" "drain-known-work") + '("retirement-review-cut") '() + (list + (sdlc-design-assumption "atomic-epoch-fence" "a1" + "Closing issuance and advancing the authority epoch is one atomic step.") + (sdlc-design-assumption "known-inventory" "a1" + "Inventory at finalization covers all active and queued consumers.") + (sdlc-design-assumption "no-new-grant" "a1" + "No new grant is issued after closure in the modeled scope.") + (sdlc-design-assumption "bounded-client" "a1" + "The Quint composition models one cached grant, one request and one attempt.")) + (list job-retirement-safety) '())) + +;;; Dependency closure for the retirement slice, not unrelated assurance gates. +;;; Lists name repository source assets; they are not a second workflow DSL. +(def job-retirement-quint-sources + '("proof/quint/EffectAdmission.qnt" + "proof/quint/WorkCore.qnt" + "proof/quint/WorkLifecycle.qnt" + "proof/quint/RetirementReview.qnt" + "proof/quint/RetirementIngress.qnt" + "proof/quint/package.json" + "proof/quint/package-lock.json" + "proof/quint/tlc-config.json" + "proof/quint/check_contract.py" + "proof/quint/check_scheme_replay.py" + "proof/quint/process_runner.py")) +(def job-retirement-lean-sources + '("proof/lean/LambdaAitiaProof/Modules/Sdlc/Retirement.lean" + "proof/lean/LambdaAitiaProof/Modules/Sdlc/RetirementIngress.lean")) +(def job-retirement-scheme-sources + '("modules/sdlc/retirement-host.ss" + "t/formal/retirement-execution-test.ss" + "t/formal/retirement-replay-test.ss" + "justfile" + ".github/workflows/ci.yml" + "build.ss")) diff --git a/user-interface/workspaces/job-executor.org b/user-interface/workspaces/job-executor.org new file mode 100644 index 0000000..0799f5b --- /dev/null +++ b/user-interface/workspaces/job-executor.org @@ -0,0 +1,289 @@ +# SPDX-FileCopyrightText: 2026 tao3k team and Contributors +# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +#+title: Job executor — engineering design workspace +#+startup: overview +#+TODO: TODO REVIEW BLOCKED | DONE CANCELLED +#+category: aitia-design + +:PROPERTIES: +:ID: aitia-job-executor-workspace +:REVISION: r2 +:STATUS: draft +:CONTRACT_ORG: lambda-aitia.engineering-workspace +:END: + +* Design question +:PROPERTIES: +:ID: aitia-job-executor-question +:END: + +How can one logical job finish after acknowledgement loss without creating a +second external effect? This maintained Aitia work surface connects people, +Agent review roles, design alternatives and bounded evidence in native Org. +Its starting decision is [[file:../../docs/adrs/0008-stable-operation-key-after-lost-ack.org][ADR 0008]], which remains proposed. + +This file targets the Scheme-owned Org Elements query path. Its consumer +qualification is pending; legacy CLI queries do not close that gate. +Role references below are proposed +assignments, not registered principals, running Agents or granted capabilities. +The Aitia Org-to-Scheme projection and controlled edit/execution loop remain +pending under [[file:../../docs/rfcs/0005-org-native-engineering-workspace.org][RFC 0005]]. Reading this file executes no source block. + +| Dimension | Current state | Authority / source | +|-----------+---------------+--------------------| +| Design choice | proposed: stable logical identity | ADR 0008; independent review pending | +| Local qualification | bounded pass and known-fault replay | linked source-fixture receipt; not provider certification | +| Guarantee support | full support unresolved | retention, intent, concurrency and external commit boundaries open | +| Effect authority | not granted by this workspace | existing Cedar / Runtime admission required | +| Production outcome | not observed here | no production execution receipt | + +* Organization +:PROPERTIES: +:ID: aitia-job-executor-organization +:END: + +Roles refer to the existing POO Flow Organization Bundle concepts. An +application must resolve actual principal/role/Agent identities, visibility +and delegations before scheduling. Org nesting does not confer authority. + +| Proposed role | Responsibility | Suggested context projection | Not granted by this document | +|---------------+----------------+------------------------------+------------------------------| +| design-owner | owns requirements, alternatives and ADR proposal | whole design and review findings | self-certification of external guarantees | +| case-reviewer | reviews historical relevance and counterevidence | source inventory, assumptions and alternatives | mutation of provider or release policy | +| guarantee-reviewer | checks gaps across component contracts | requirements, assumptions, owner matrix and scoped receipts | conversion of a hypothesis into proof | +| verification-runner | requests bounded checks through Runtime | selected obligation, bound inputs and tool contract | arbitrary Org code execution or provider credentials | +| decision-reviewer | reviews the proposed choice and its conditions | alternatives, objections, obligations and evidence references | bypassing Cedar / Runtime effect gates | + +The same person or Agent may fill more than one role only when the application +permits it; a required independent review must retain actual principal +separation. No Agent is spawned by a heading or an =OWNER_ROLE= property. + +* Design decision — stable logical identity +:PROPERTIES: +:ID: aitia-job-executor-design-decision +:STATUS: proposed +:CONTRACT_ORG: lambda-aitia.design-review +:END: + +This subtree is the review context for ADR 0008. Its contract checks local +requirements, assumptions, alternatives, review work and receipt references; +passing does not accept the decision or establish any external guarantee. + +** Requirements +:PROPERTIES: +:ID: aitia-job-executor-requirements +:END: + +*** Unique logical effect +:PROPERTIES: +:ID: aitia-job-executor-unique-effect +:AITIA_KIND: requirement +:OWNER_ROLE: design-owner +:END: + +One logical job operation must not commit a duplicate effect within the +qualified provider contract. Per-key uniqueness is insufficient when a retry +can change its key. A completed review must state the scope rather than claim +unconditional exactly-once delivery. + +*** Completion after transient response loss +:PROPERTIES: +:ID: aitia-job-executor-completion +:AITIA_KIND: requirement +:OWNER_ROLE: design-owner +:END: + +The job must be able to complete after transient acknowledgement loss under +explicit service-availability and retry/reconciliation conditions. Turning +off retries cannot silently remove this requirement. + +** Assumptions +:PROPERTIES: +:ID: aitia-job-executor-assumptions +:END: + +*** Provider retains logical-operation identity +:PROPERTIES: +:ID: aitia-job-executor-retention +:AITIA_KIND: assumption +:OWNER_ROLE: design-owner +:STATUS: unqualified +:END: + +The provider's deduplication scope and retention cover permitted retries. +There is no measured retention duration in the current fixture. A shortened +retention interval, identity collision or late request reopens this assumption. + +*** Effect and deduplication survive failure coherently +:PROPERTIES: +:ID: aitia-job-executor-atomicity +:AITIA_KIND: assumption +:OWNER_ROLE: guarantee-reviewer +:STATUS: unqualified +:END: + +The actual provider supplies atomic commit or a qualified recovery protocol. +The local SQLite row stands for both the key and the effect; it does not test +an independent external side effect. Two Cedar permits are not a uniqueness +mechanism. A lost ACK leaves the caller uncertain, not entitled to assume no +commit occurred. + +** Alternatives +:PROPERTIES: +:ID: aitia-job-executor-alternatives +:END: + +#+name: retry-design-alternatives +| Option | Unique logical effect | Completion | Additional obligations | Current judgment | +|--------+-----------------------+------------+------------------------+------------------| +| Stop after lost ACK | avoids a retry-induced duplicate, but does not establish original outcome | not guaranteed | explicit unknown outcome and recovery owner | conflicts with unattended completion unless recovery is added | +| Retry with stable identity | conditional on scope, retention and provider semantics | conditional on availability and retry policy | parameter binding, persistence, concurrent attempts and crash checks | proposed by ADR 0008; one bounded fixture passes | +| Query / reconcile before further action | depends on authoritative correlation and reconciliation semantics | depends on query availability and outcome resolution | bind original identity; absence must not be mistaken for failure when history is incomplete | candidate; no local implementation or receipt | + +These are comparisons under the same requirements, not a numeric ranking. +The first and third options are not claimed to have been executed. Reconciliation +may complement stable identity; the alternatives are not inherently exclusive. + +*** Stop after lost ACK +:PROPERTIES: +:ID: aitia-job-executor-option-stop +:END: + +The comparison above identifies the missing completion/recovery guarantee. + +*** Retry with stable identity +:PROPERTIES: +:ID: aitia-job-executor-option-stable-identity +:END: + +ADR 0008 proposes this option under the assumptions in this decision subtree. + +*** Query and reconcile +:PROPERTIES: +:ID: aitia-job-executor-option-reconcile +:END: + +The comparison above retains this unimplemented candidate and its unknowns. + +** Historical material +:PROPERTIES: +:ID: aitia-job-executor-history +:END: + +[[file:../../docs/plans/engineering-case-source-inventory.org][The source inventory]] retains attribution and applicability boundaries for +the AWS idempotency article, the etcd watch report and the GitHub incident +analysis. The first informs identity/retention questions. The other two remain +candidate questions about missing history and cross-component assumptions; +they are not evidence of an Aitia defect. + +** Review +:PROPERTIES: +:ID: aitia-job-executor-review +:END: + +*** TODO Review retention and retry horizon :assumption:review: +:PROPERTIES: +:ID: aitia-job-executor-review-retention +:OWNER_ROLE: case-reviewer +:SUBJECT_ID: aitia-job-executor-retention +:END: + +- [ ] Name the actual provider contract and identity scope. +- [ ] Compare retry horizon with retention, including late requests. +- [ ] Record contrary evidence and the required action outside the guarantee. + +Expected Agent contribution: an attributed applicability finding attached to +this node, with proposed changes and explicit unknowns. It cannot accept the ADR. + +*** TODO Review guarantee composition :guarantee:review: +:PROPERTIES: +:ID: aitia-job-executor-review-composition +:OWNER_ROLE: guarantee-reviewer +:SUBJECT_ID: aitia-job-executor-atomicity +:END: + +- [ ] Bind authorized intent to dispatched parameters. +- [ ] Qualify mismatched-parameter rejection and concurrent attempts. +- [ ] Qualify crash boundaries with the actual effect provider. + +Use the [[file:../../docs/adrs/0008-stable-operation-key-after-lost-ack.org][existing owner matrix]]: Host supplies trusted context, Cedar evaluates +authorization, Runtime enforces at use, and provider mechanisms establish +effect uniqueness. Do not add a second policy evaluator. + +*** BLOCKED Connect the source-bound Org review projection :integration: +:PROPERTIES: +:ID: aitia-job-executor-review-projection +:OWNER_ROLE: design-owner +:SUBJECT_ID: aitia-job-executor-workspace +:END: + +The Scheme sources are located on Orgize's =codex/scheme-rowan-org-cutover= +branch. Qualify an installable package boundary before wiring this projection: +the current gerbil-parser Scheme dependency points back to POO Flow, and Orgize +does not yet have a Gerbil package entrypoint. See RFC 0005's package audit. +Reuse parser-owned identities/ranges and POO objects. Do not implement an Org +parser in Python or infer supported bindings from the existence of the CLI. + +*** TODO Review the proposed ADR :decision:review: +:PROPERTIES: +:ID: aitia-job-executor-review-decision +:OWNER_ROLE: decision-reviewer +:SUBJECT_ID: lambda-aitia-adr-0008 +:END: + +- [ ] Review the same requirements across alternatives and retained objections. +- [ ] Confirm the missing qualifications and responsible owners. +- [ ] Record a decision revision; editing this TODO does not accept the ADR. + +** Receipts +:PROPERTIES: +:ID: aitia-job-executor-receipts +:END: + +The [[file:../../bindings/python/qualification/job_executor/README.org][source-fixture replay receipt]] records the 2026-09-24 run at code commit +=f3438241780e863db6c0b518d86628b540f96cd1=: fixed mode passed, while buggy mode +failed specifically with two commits. This is a linked historical receipt, not +a new run produced by opening this document or a trusted Host-issued admission. + +| Observation | Scope | Does not establish | +|-------------+-------+--------------------| +| Fixed strategy passed | single job, fixed payload, sequential restart and local provider | retention, multi-tenant or independent external-effect guarantees | +| Buggy strategy failed with two commits | deliberate known-fault regression | novel bug discovery or causal proof for a production service | +| Org contract check | workspace sections and decision-local review structure | identity resolution, role authority, evidence freshness or execution | + +The separate [[file:../../docs/receipts/2026-09-24-retirement-impact.org][retirement Impact receipt]] qualifies a synthetic automatic-retry +removal analysis through POO Flow. It is not a new run of the persistent Python +worker or evidence that queued tasks were migrated or cancelled. + +* Revision journal +:PROPERTIES: +:ID: aitia-job-executor-journal +:END: +:LOGBOOK: +- Note taken on [2026-09-24 Thu] \\ + Created the native authoring/query work surface. No role assignment or + production authorization was issued. Existing receipts remain linked. +:END: + +Keep later judgments as new revisions with source and reason. An Agent edit +proposal must bind the document digest, node ID, original range and expected +old content; a concurrent edit requires review/replanning, not a blind offset +replacement. A moved heading retains identity but gains no delegated rights. + +* Workflow + +#+begin_export mermaid +flowchart TD + O["Org design node
authored"] --> Q["Orgize projection
located and queryable"] + Q --> R["Role-scoped review
proposed"] + R --> P["Node edit
pending owner acceptance"] + P --> C["Revised design
obligations reopened"] + C --> G["Cedar / Runtime handoff
qualification pending"] + G --> E["Actual execution receipt
required before claiming results"] + E --> O +#+end_export + +This document is an authored work surface, not a qualification receipt. +Scheme admission, AOT generation and actual consumer evaluation remain required. +The diagram describes the intended interaction, not completed execution.