From 2e69ec370a6b36dc6b0d6ac78b00c82211453303 Mon Sep 17 00:00:00 2001 From: guangtao Date: Wed, 23 Sep 2026 11:00:26 +0800 Subject: [PATCH 01/64] Start snapshot-bound verification planning in Aitia --- ROADMAP.org | 14 ++- modules/assurance/config.ss | 20 +++- modules/assurance/funs.ss | 119 ++++++++++++++++++- modules/assurance/types.ss | 88 ++++++++++++++- t/assurance/unit/assurance-kernel-test.ss | 132 ++++++++++++++++++++++ 5 files changed, 369 insertions(+), 4 deletions(-) diff --git a/ROADMAP.org b/ROADMAP.org index df0eec8..c67cf26 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -133,7 +133,19 @@ Exit gates: * Phase 3 — Obligation derivation and verification planning -Status: =planned= +Status: =active= (selection evidence only; replacement derivation remains open) + +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. +An obligation bound to an older snapshot is blocked rather than silently +promoted to a replacement obligation. This slice does not implement the +ordered dependency plan, replacement-obligation derivation, verifier choice +receipts or composition obligations below. Deliverables: diff --git a/modules/assurance/config.ss b/modules/assurance/config.ss index b9e2d69..1addd54 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/src/module-system/contribution/model :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-flow-check-model + AssuranceVerificationPolicy + (.o (:: @ (poo-flow-model-prototype AssuranceVerificationPolicy)) + identity: identity-value revision: revision-value + capabilities: + (list-sort capability= value 0))) (def (one-of values) @@ -219,6 +238,55 @@ (enum-slot 'unresolved text-list?) (enum-slot 'conflicts text-list?)))) +;;; 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 symbol?) + (enum-slot 'capability symbol?) + (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 '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)))))) + (defstruct assurance-invalidation-receipt-record (identity snapshot-digest changed impacted invalidated-evidence required-obligations invalidated-decisions blocked-effects witnesses unresolved @@ -268,6 +336,24 @@ (eq? (.ref value 'plane) (assurance-relation-kind-plane (.ref value 'relation))))) (def (assurance-snapshot? value) (poo-flow-model? AssuranceSnapshot value)) +(def (assurance-verification-policy? value) + (poo-flow-model? AssuranceVerificationPolicy value)) +(def (assurance-verification-request? value) + (and (poo-flow-model? AssuranceVerificationRequest value) + (eq? (.ref value 'selected?) + (not (.ref value 'blocker))))) +(def (assurance-verification-plan? value) + (and (poo-flow-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-invalidation-receipt? value) (assurance-invalidation-receipt-record? value)) (def (assurance-invalidation-receipt-ref receipt name) diff --git a/t/assurance/unit/assurance-kernel-test.ss b/t/assurance/unit/assurance-kernel-test.ss index 38f2ea4..bc44f1c 100644 --- a/t/assurance/unit/assurance-kernel-test.ss +++ b/t/assurance/unit/assurance-kernel-test.ss @@ -359,6 +359,138 @@ (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 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 "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 "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 From dbd4e95fc51123e3363bd3f256e4d4ff2892bf5c Mon Sep 17 00:00:00 2001 From: guangtao Date: Wed, 23 Sep 2026 11:52:33 +0800 Subject: [PATCH 02/64] Constrain assurance obligation evidence kinds --- ROADMAP.org | 5 ++++- modules/assurance/types.ss | 13 +++++++++--- t/assurance/unit/assurance-kernel-test.ss | 26 ++++++++++++++++++++++- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/ROADMAP.org b/ROADMAP.org index c67cf26..2f9d60f 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -133,7 +133,7 @@ Exit gates: * Phase 3 — Obligation derivation and verification planning -Status: =active= (selection evidence only; replacement derivation remains open) +Status: =active= (typed kinds and selection evidence; replacement derivation remains open) The first executable slice reuses the Phase 2 invalidation receipt to select snapshot-bound obligations. A POO-native verification policy declares @@ -142,6 +142,9 @@ 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. An obligation bound to an older snapshot is blocked rather than silently promoted to a replacement obligation. This slice does not implement the ordered dependency plan, replacement-obligation derivation, verifier choice diff --git a/modules/assurance/types.ss b/modules/assurance/types.ss index 0f53423..3f991fe 100644 --- a/modules/assurance/types.ss +++ b/modules/assurance/types.ss @@ -10,9 +10,10 @@ (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 @@ -40,6 +41,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) @@ -77,6 +81,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)))) @@ -170,7 +176,8 @@ (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 'evidence-kind assurance-evidence-kind?) + (enum-slot 'capability symbol?) (text-slot 'scope)))) (def AssuranceEvidence (semantic-node-class @@ -254,7 +261,7 @@ (text-slot 'subject) (text-slot 'claim) (enum-slot 'snapshot-digest assurance-digest?) - (enum-slot 'evidence-kind symbol?) + (enum-slot 'evidence-kind assurance-evidence-kind?) (enum-slot 'capability symbol?) (enum-slot 'selected? boolean?) (enum-slot 'blocker verification-blocker?)))) diff --git a/t/assurance/unit/assurance-kernel-test.ss b/t/assurance/unit/assurance-kernel-test.ss index bc44f1c..dcca61f 100644 --- a/t/assurance/unit/assurance-kernel-test.ss +++ b/t/assurance/unit/assurance-kernel-test.ss @@ -405,9 +405,33 @@ #f) (check-equal? (assurance-verification-plan? - (.o (:: @ left) release-authorized?: #t)) + (.o (:: @ left) release-authorized?: #t)) #f))) + (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 "missing capability remains an explained blocked obligation" (let* ((policy (assurance-verification-policy "policy/release" "r1" '())) From 96dd1311ee0339c8cbe058650b92c11e6bfcde77 Mon Sep 17 00:00:00 2001 From: guangtao Date: Wed, 23 Sep 2026 12:32:58 +0800 Subject: [PATCH 03/64] Order verification obligations with POO Flow Graph --- ROADMAP.org | 12 +- modules/assurance/funs.ss | 114 +++++++++++------ modules/assurance/types.ss | 6 +- modules/assurance/verification-projection.ss | 73 +++++++++++ t/assurance/unit/assurance-kernel-test.ss | 122 +++++++++++++++++++ 5 files changed, 289 insertions(+), 38 deletions(-) create mode 100644 modules/assurance/verification-projection.ss diff --git a/ROADMAP.org b/ROADMAP.org index 2f9d60f..9d6a279 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -133,7 +133,7 @@ Exit gates: * Phase 3 — Obligation derivation and verification planning -Status: =active= (typed kinds and selection evidence; replacement derivation remains open) +Status: =active= (typed kinds and dependency-ordered selection; replacement derivation remains open) The first executable slice reuses the Phase 2 invalidation receipt to select snapshot-bound obligations. A POO-native verification policy declares @@ -145,10 +145,16 @@ 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 withholding an executable order. The plan 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. This slice does not implement the -ordered dependency plan, replacement-obligation derivation, verifier choice -receipts or composition obligations below. +replacement-obligation derivation, verifier choice receipts or composition +obligations below. Deliverables: diff --git a/modules/assurance/funs.ss b/modules/assurance/funs.ss index 45a73be..e37179a 100644 --- a/modules/assurance/funs.ss +++ b/modules/assurance/funs.ss @@ -8,10 +8,16 @@ (only-in :gerbil/core list-sort string->utf8) :std/list/list (only-in :std/encoding/hex hex-encode) + (only-in :poo-flow/src/graph/algorithms + poo-flow-graph-cycle-path + poo-flow-graph-reachable-ids + poo-flow-graph-topological-order) :poo-flow/src/module-system/contribution/model :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 @@ -315,6 +321,21 @@ ;;; 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 cycle-path + prerequisites planned-ids) + (cond + ((pair? conflicts) 'conflicted-snapshot) + ((pair? unresolved) 'unresolved-frontier) + ((pair? cycle-path) 'dependency-cycle) + ((not (string=? (.ref obligation 'snapshot) (.ref snapshot 'identity))) + '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) @@ -343,38 +364,59 @@ (unresolved (assurance-invalidation-receipt-ref invalidation 'unresolved)) (conflicts (.ref snapshot 'conflicts)) - (capabilities (.ref policy 'capabilities)) - (requests - (map - (lambda (obligation-id) - (let* ((obligation - (node-by-identity (.ref snapshot 'nodes) - obligation-id)) - (reason - (cond - ((pair? conflicts) 'conflicted-snapshot) - ((pair? unresolved) 'unresolved-frontier) - ((not (string=? (.ref obligation 'snapshot) - (.ref snapshot 'identity))) - 'stale-obligation) - ((not (memq (.ref obligation 'capability) capabilities)) - 'capability-unavailable) - (else #f))) - (request-id obligation-id) - (request-reason reason)) - (poo-flow-check-model - AssuranceVerificationRequest - (.o (:: @ (poo-flow-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) - selected?: (not request-reason) - blocker: request-reason)))) - obligation-ids)) + (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) '())) + (ordered-ids + (if (null? cycle-path) + (poo-flow-graph-topological-order dependency-graph) + obligation-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 cycle-path + (cdr (assoc obligation-id prerequisites)) obligation-ids))) + ordered-ids)) + (blocked-closure + (poo-flow-graph-reachable-ids + dependency-graph + (map car (filter (lambda (entry) (cdr entry)) + base-blockers)))) + (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-flow-check-model + AssuranceVerificationRequest + (.o (:: @ (poo-flow-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?))) @@ -389,6 +431,7 @@ (.ref request 'subject) (.ref request 'claim) (.ref request 'evidence-kind) (.ref request 'capability) + (.ref request 'dependencies) (.ref request 'selected?) (.ref request 'blocker))) requests)) (digest @@ -398,6 +441,7 @@ (.ref policy 'identity) (.ref policy 'revision) (.ref policy 'capabilities) changed impacted blocked-effects witnesses + cycle-path selected blocked request-projection unresolved conflicts)))) (let ((plan-identity identity) @@ -407,6 +451,7 @@ (plan-changed changed) (plan-impacted impacted) (plan-blocked-effects blocked-effects) + (plan-cycle-path cycle-path) (plan-witnesses witnesses) (plan-requests requests) (plan-unresolved unresolved) @@ -423,8 +468,9 @@ blocked-obligations: plan-blocked changed: plan-changed impacted: plan-impacted blocked-effects: plan-blocked-effects + cycle-path: plan-cycle-path witnesses: plan-witnesses requests: plan-requests unresolved: plan-unresolved conflicts: plan-conflicts verifier-executed?: #f release-authorized?: #f - runtime-executed?: #f))))) + runtime-executed?: #f))))))) diff --git a/modules/assurance/types.ss b/modules/assurance/types.ss index 3f991fe..b4da4f2 100644 --- a/modules/assurance/types.ss +++ b/modules/assurance/types.ss @@ -101,7 +101,9 @@ (def (verification-blocker? value) (or (not value) (memq value '(conflicted-snapshot unresolved-frontier - capability-unavailable stale-obligation)))) + capability-unavailable stale-obligation + dependency-unplanned dependency-blocked + dependency-cycle)))) (def (revision-bindings? values) (and (list? values) (every (lambda (binding) @@ -263,6 +265,7 @@ (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) @@ -286,6 +289,7 @@ (enum-slot 'changed text-list?) (enum-slot 'impacted text-list?) (enum-slot 'blocked-effects text-list?) + (enum-slot 'cycle-path text-list?) (enum-slot 'witnesses invalidation-trajectories?) (enum-slot 'requests verification-request-list?) (enum-slot 'unresolved text-list?) 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 Date: Wed, 23 Sep 2026 12:57:20 +0800 Subject: [PATCH 04/64] Bind verification obligations to exact snapshot context --- ROADMAP.org | 7 +- docs/rfcs/0001-causal-assurance-core.org | 7 +- .../0002-snapshot-and-invalidation-kernel.org | 4 + modules/assurance/funs.ss | 57 ++++++++++++++- modules/assurance/objects.ss | 6 +- modules/assurance/types.ss | 4 + t/assurance/unit/assurance-kernel-test.ss | 73 +++++++++++++++++-- 7 files changed, 145 insertions(+), 13 deletions(-) diff --git a/ROADMAP.org b/ROADMAP.org index 9d6a279..6aef93f 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -152,7 +152,12 @@ prerequisite is never assumed discharged, and a dependency cycle records its path while withholding an executable order. The plan 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. This slice does not implement the +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. +This slice does not implement the replacement-obligation derivation, verifier choice receipts or composition obligations below. diff --git a/docs/rfcs/0001-causal-assurance-core.org b/docs/rfcs/0001-causal-assurance-core.org index 3b49a47..ec35ab1 100644 --- a/docs/rfcs/0001-causal-assurance-core.org +++ b/docs/rfcs/0001-causal-assurance-core.org @@ -106,7 +106,12 @@ 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. ** Evidence 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/modules/assurance/funs.ss b/modules/assurance/funs.ss index e37179a..d760153 100644 --- a/modules/assurance/funs.ss +++ b/modules/assurance/funs.ss @@ -21,6 +21,7 @@ (export assurance-node-canonical assurance-relation-canonical assurance-canonical-digest assurance-snapshot + assurance-bind-obligation-to-snapshot assurance-support-admissible? assurance-invalidation-graph assurance-invalidate assurance-plan-verification) @@ -34,7 +35,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)) @@ -49,6 +51,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)) @@ -227,13 +239,26 @@ (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)) @@ -247,10 +272,29 @@ 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))))))))) +;;; 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-flow-check-model + AssuranceObligation + (.o (:: @ obligation) + snapshot-revision: bound-revision + snapshot-context-digest: bound-context)))) + ;;; 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. @@ -328,7 +372,12 @@ ((pair? conflicts) 'conflicted-snapshot) ((pair? unresolved) 'unresolved-frontier) ((pair? cycle-path) 'dependency-cycle) - ((not (string=? (.ref obligation 'snapshot) (.ref snapshot 'identity))) + ((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) diff --git a/modules/assurance/objects.ss b/modules/assurance/objects.ss index a3c57f8..5d4445d 100644 --- a/modules/assurance/objects.ss +++ b/modules/assurance/objects.ss @@ -80,12 +80,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) + capability: capability-value scope: scope-value + snapshot-revision: (snapshot-revision-value #f) + snapshot-context-digest: (snapshot-context-value #f)) (poo-flow-check-model AssuranceObligation (.o (:: @ (poo-flow-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 diff --git a/modules/assurance/types.ss b/modules/assurance/types.ss index b4da4f2..7dcd5e4 100644 --- a/modules/assurance/types.ss +++ b/modules/assurance/types.ss @@ -94,6 +94,7 @@ (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) @@ -178,6 +179,8 @@ (semantic-node-class 'aitia/obligation (list (text-slot 'subject) (text-slot 'claim) (text-slot 'snapshot) + (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)))) @@ -241,6 +244,7 @@ (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?) diff --git a/t/assurance/unit/assurance-kernel-test.ss b/t/assurance/unit/assurance-kernel-test.ss index f365d14..6ad6ffd 100644 --- a/t/assurance/unit/assurance-kernel-test.ss +++ b/t/assurance/unit/assurance-kernel-test.ss @@ -83,12 +83,25 @@ (append (list artifact claim evidence) obligations) (append (list dependency) relations))) -(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 (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) @@ -402,6 +415,13 @@ (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) '()) @@ -602,6 +622,47 @@ (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 From 05544ef36b7fe7deba6bc30d10f7a9d76356cdab Mon Sep 17 00:00:00 2001 From: guangtao Date: Wed, 23 Sep 2026 13:09:35 +0800 Subject: [PATCH 05/64] Preserve independent verification branches across dependency cycles --- ROADMAP.org | 4 ++- modules/assurance/funs.ss | 38 +++++++++++++++++------ t/assurance/unit/assurance-kernel-test.ss | 18 +++++++---- 3 files changed, 43 insertions(+), 17 deletions(-) diff --git a/ROADMAP.org b/ROADMAP.org index 6aef93f..13e9871 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -149,7 +149,9 @@ 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 withholding an executable order. The plan still does not attest +path while blocking only its strongly connected component and downstream +dependents; an independent acyclic branch retains its own order. The plan +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, diff --git a/modules/assurance/funs.ss b/modules/assurance/funs.ss index d760153..c6b5920 100644 --- a/modules/assurance/funs.ss +++ b/modules/assurance/funs.ss @@ -10,6 +10,7 @@ (only-in :std/encoding/hex hex-encode) (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/src/module-system/contribution/model @@ -366,12 +367,12 @@ ;;; 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 cycle-path + unresolved conflicts cyclic-ids prerequisites planned-ids) (cond ((pair? conflicts) 'conflicted-snapshot) ((pair? unresolved) 'unresolved-frontier) - ((pair? cycle-path) 'dependency-cycle) + ((member (.ref obligation 'identity) cyclic-ids) 'dependency-cycle) ((or (not (string=? (.ref obligation 'snapshot) (.ref snapshot 'identity))) (not (equal? (.ref obligation 'snapshot-revision) @@ -419,10 +420,12 @@ snapshot obligation-ids))) (let* ((cycle-path (or (poo-flow-graph-cycle-path dependency-graph) '())) - (ordered-ids - (if (null? cycle-path) - (poo-flow-graph-topological-order dependency-graph) - obligation-ids)) + (cyclic-ids + (concatenate + (.ref (poo-flow-graph-loop-analysis-receipt dependency-graph) + 'cyclic-components))) + (cycle-blocked + (poo-flow-graph-reachable-ids dependency-graph cyclic-ids)) (base-blockers (map (lambda (obligation-id) @@ -430,14 +433,29 @@ obligation-id (verification-base-blocker (node-by-identity (.ref snapshot 'nodes) obligation-id) - snapshot capabilities unresolved conflicts cycle-path + snapshot capabilities unresolved conflicts cyclic-ids (cdr (assoc obligation-id prerequisites)) obligation-ids))) - ordered-ids)) + obligation-ids)) (blocked-closure (poo-flow-graph-reachable-ids dependency-graph - (map car (filter (lambda (entry) (cdr entry)) - base-blockers)))) + (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) diff --git a/t/assurance/unit/assurance-kernel-test.ss b/t/assurance/unit/assurance-kernel-test.ss index 6ad6ffd..114d426 100644 --- a/t/assurance/unit/assurance-kernel-test.ss +++ b/t/assurance/unit/assurance-kernel-test.ss @@ -549,30 +549,36 @@ (check-equal? (.ref (cadr (.ref plan 'requests)) 'blocker) 'dependency-blocked))) - (test-case "a dependency cycle has a witness and no executable order" + (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) + (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/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) '()) + (check-equal? (.ref plan 'selected-obligations) '("obligation/c")) (check-equal? (.ref plan 'blocked-obligations) - '("obligation/a" "obligation/b")) + '("obligation/a" "obligation/b" "obligation/d")) (check-equal? (.ref plan 'cycle-path) '("obligation/a" "obligation/b" "obligation/a")) (check-equal? (map (lambda (request) (.ref request 'blocker)) (.ref plan 'requests)) - '(dependency-cycle dependency-cycle)))) + '(#f dependency-cycle dependency-cycle + dependency-blocked)))) (test-case "missing capability remains an explained blocked obligation" (let* ((policy From 94a3ebc832977e9f32f91dc3dc53d9823e95819b Mon Sep 17 00:00:00 2001 From: guangtao Date: Wed, 23 Sep 2026 13:21:27 +0800 Subject: [PATCH 06/64] Expose every blocked verification cycle in plans --- ROADMAP.org | 4 +-- modules/assurance/funs.ss | 12 ++++--- modules/assurance/types.ss | 1 + t/assurance/unit/assurance-kernel-test.ss | 43 +++++++++++++++++++++++ 4 files changed, 53 insertions(+), 7 deletions(-) diff --git a/ROADMAP.org b/ROADMAP.org index 13e9871..fbf9321 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -151,8 +151,8 @@ 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 -still does not attest -that any prerequisite has been verified. +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 diff --git a/modules/assurance/funs.ss b/modules/assurance/funs.ss index c6b5920..5e24ce0 100644 --- a/modules/assurance/funs.ss +++ b/modules/assurance/funs.ss @@ -420,10 +420,10 @@ snapshot obligation-ids))) (let* ((cycle-path (or (poo-flow-graph-cycle-path dependency-graph) '())) - (cyclic-ids - (concatenate - (.ref (poo-flow-graph-loop-analysis-receipt dependency-graph) - 'cyclic-components))) + (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 @@ -508,7 +508,7 @@ (.ref policy 'identity) (.ref policy 'revision) (.ref policy 'capabilities) changed impacted blocked-effects witnesses - cycle-path + cycle-path cyclic-components selected blocked request-projection unresolved conflicts)))) (let ((plan-identity identity) @@ -519,6 +519,7 @@ (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) @@ -536,6 +537,7 @@ 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 diff --git a/modules/assurance/types.ss b/modules/assurance/types.ss index 7dcd5e4..4d3cc30 100644 --- a/modules/assurance/types.ss +++ b/modules/assurance/types.ss @@ -294,6 +294,7 @@ (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?) diff --git a/t/assurance/unit/assurance-kernel-test.ss b/t/assurance/unit/assurance-kernel-test.ss index 114d426..98a18e3 100644 --- a/t/assurance/unit/assurance-kernel-test.ss +++ b/t/assurance/unit/assurance-kernel-test.ss @@ -498,6 +498,7 @@ '("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")) @@ -575,11 +576,53 @@ '("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" '())) From 3658d9e300eccb412eae41a0eced8753e9b4075c Mon Sep 17 00:00:00 2001 From: guangtao Date: Wed, 23 Sep 2026 13:39:21 +0800 Subject: [PATCH 07/64] Derive inert replacement obligation requirements --- ROADMAP.org | 13 ++- docs/rfcs/0001-causal-assurance-core.org | 5 + modules/assurance/funs.ss | 114 ++++++++++++++++++++- modules/assurance/types.ss | 46 +++++++++ t/assurance/unit/assurance-kernel-test.ss | 118 ++++++++++++++++++++++ 5 files changed, 291 insertions(+), 5 deletions(-) diff --git a/ROADMAP.org b/ROADMAP.org index fbf9321..c6c1c96 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -133,7 +133,8 @@ Exit gates: * Phase 3 — Obligation derivation and verification planning -Status: =active= (typed kinds and dependency-ordered selection; replacement derivation remains open) +Status: =active= (typed kinds, dependency-ordered selection and replacement +requirements; replacement declaration remains open) The first executable slice reuses the Phase 2 invalidation receipt to select snapshot-bound obligations. A POO-native verification policy declares @@ -159,9 +160,13 @@ 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. -This slice does not implement the -replacement-obligation derivation, verifier choice receipts or composition -obligations below. +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. Materializing +and binding replacement obligations, verifier choice receipts and composition +obligations remain open. Deliverables: diff --git a/docs/rfcs/0001-causal-assurance-core.org b/docs/rfcs/0001-causal-assurance-core.org index ec35ab1..6e3adc6 100644 --- a/docs/rfcs/0001-causal-assurance-core.org +++ b/docs/rfcs/0001-causal-assurance-core.org @@ -112,6 +112,11 @@ 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. ** Evidence diff --git a/modules/assurance/funs.ss b/modules/assurance/funs.ss index 5e24ce0..a03d933 100644 --- a/modules/assurance/funs.ss +++ b/modules/assurance/funs.ss @@ -24,7 +24,8 @@ assurance-canonical-digest assurance-snapshot assurance-bind-obligation-to-snapshot assurance-support-admissible? assurance-invalidation-graph - assurance-invalidate assurance-plan-verification) + assurance-invalidate assurance-derive-replacement-requirements + assurance-plan-verification) (def (assurance-node-semantic-slots kind) (case kind @@ -363,6 +364,117 @@ 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-flow-check-model + AssuranceReplacementRequirement + (.o (:: @ (poo-flow-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-flow-check-model + AssuranceReplacementDerivation + (.o (:: @ (poo-flow-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)))) + ;;; 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. diff --git a/modules/assurance/types.ss b/modules/assurance/types.ss index 4d3cc30..28087d5 100644 --- a/modules/assurance/types.ss +++ b/modules/assurance/types.ss @@ -19,6 +19,7 @@ AssuranceObligation AssuranceEvidence AssuranceCounterexample AssuranceFinding AssuranceDecision AssuranceEffect AssuranceRelation AssuranceSnapshot + AssuranceReplacementRequirement AssuranceReplacementDerivation AssuranceVerificationPolicy AssuranceVerificationRequest AssuranceVerificationPlan make-assurance-invalidation-receipt-record @@ -27,6 +28,8 @@ assurance-action? assurance-obligation? assurance-evidence? assurance-counterexample? assurance-finding? assurance-decision? assurance-effect? assurance-relation? assurance-snapshot? + assurance-replacement-requirement? + assurance-replacement-derivation? assurance-invalidation-receipt? assurance-invalidation-receipt-ref assurance-verification-policy? assurance-verification-request? assurance-verification-plan?) @@ -105,6 +108,13 @@ 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 (revision-bindings? values) (and (list? values) (every (lambda (binding) @@ -251,6 +261,38 @@ (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)))))) + ;;; Public planning values remain POO-native and contain no executable hook. (def AssuranceVerificationPolicy (poo-clos-class 'aitia/verification-policy @@ -352,6 +394,10 @@ (eq? (.ref value 'plane) (assurance-relation-kind-plane (.ref value 'relation))))) (def (assurance-snapshot? value) (poo-flow-model? AssuranceSnapshot value)) +(def (assurance-replacement-requirement? value) + (poo-flow-model? AssuranceReplacementRequirement value)) +(def (assurance-replacement-derivation? value) + (poo-flow-model? AssuranceReplacementDerivation value)) (def (assurance-verification-policy? value) (poo-flow-model? AssuranceVerificationPolicy value)) (def (assurance-verification-request? value) diff --git a/t/assurance/unit/assurance-kernel-test.ss b/t/assurance/unit/assurance-kernel-test.ss index 98a18e3..df03e83 100644 --- a/t/assurance/unit/assurance-kernel-test.ss +++ b/t/assurance/unit/assurance-kernel-test.ss @@ -447,6 +447,124 @@ (.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 "verification obligations have a closed evidence-kind vocabulary" (check-equal? +assurance-evidence-kinds+ From 50bbfed1a4cbedeb90f42cfda7d563fd01c45a78 Mon Sep 17 00:00:00 2001 From: guangtao Date: Wed, 23 Sep 2026 13:57:19 +0800 Subject: [PATCH 08/64] Explain declared verifier choices without execution --- ROADMAP.org | 9 +- docs/rfcs/0001-causal-assurance-core.org | 5 + modules/assurance/funs.ss | 113 +++++++++++++++++++++- modules/assurance/objects.ss | 23 ++++- modules/assurance/types.ss | 56 ++++++++++- t/assurance/unit/assurance-kernel-test.ss | 79 +++++++++++++++ 6 files changed, 279 insertions(+), 6 deletions(-) diff --git a/ROADMAP.org b/ROADMAP.org index c6c1c96..ba93e90 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -165,8 +165,13 @@ 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. Materializing -and binding replacement obligations, verifier choice receipts and composition -obligations remain open. +and binding replacement obligations and composition obligations remain open. +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. Deliverables: diff --git a/docs/rfcs/0001-causal-assurance-core.org b/docs/rfcs/0001-causal-assurance-core.org index 6e3adc6..56cf213 100644 --- a/docs/rfcs/0001-causal-assurance-core.org +++ b/docs/rfcs/0001-causal-assurance-core.org @@ -248,6 +248,11 @@ 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. +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. The first verifier kinds are Lean proof, TLA+/TLC model check, runtime replay, unit test, integration test, Cedar differential, static analysis and human diff --git a/modules/assurance/funs.ss b/modules/assurance/funs.ss index a03d933..ea640d2 100644 --- a/modules/assurance/funs.ss +++ b/modules/assurance/funs.ss @@ -25,7 +25,7 @@ assurance-bind-obligation-to-snapshot assurance-support-admissible? assurance-invalidation-graph assurance-invalidate assurance-derive-replacement-requirements - assurance-plan-verification) + assurance-plan-verification assurance-explain-verifier-choices) (def (assurance-node-semantic-slots kind) (case kind @@ -655,3 +655,114 @@ 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)) + (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 28087d5..bc5cf70 100644 --- a/modules/assurance/types.ss +++ b/modules/assurance/types.ss @@ -21,7 +21,8 @@ AssuranceRelation AssuranceSnapshot AssuranceReplacementRequirement AssuranceReplacementDerivation AssuranceVerificationPolicy AssuranceVerificationRequest - AssuranceVerificationPlan + AssuranceVerificationPlan AssuranceVerifierCandidate + AssuranceVerifierChoice AssuranceVerifierChoiceReceipt make-assurance-invalidation-receipt-record assurance-node? assurance-artifact? assurance-claim? assurance-assumption? assurance-observation? assurance-event? @@ -32,7 +33,8 @@ assurance-replacement-derivation? assurance-invalidation-receipt? assurance-invalidation-receipt-ref assurance-verification-policy? assurance-verification-request? - assurance-verification-plan?) + assurance-verification-plan? assurance-verifier-candidate? + assurance-verifier-choice? assurance-verifier-choice-receipt?) (def +assurance-node-kinds+ '(artifact claim assumption observation event action obligation evidence @@ -102,6 +104,12 @@ (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 (verification-blocker? value) (or (not value) (memq value '(conflicted-snapshot unresolved-frontier @@ -345,6 +353,42 @@ (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)))))) + (defstruct assurance-invalidation-receipt-record (identity snapshot-digest changed impacted invalidated-evidence required-obligations invalidated-decisions blocked-effects witnesses unresolved @@ -416,6 +460,14 @@ (map (lambda (request) (.ref request 'obligation-identity)) (filter (lambda (request) (not (.ref request 'selected?))) (.ref value 'requests)))))) +(def (assurance-verifier-candidate? value) + (poo-flow-model? AssuranceVerifierCandidate value)) +(def (assurance-verifier-choice? value) + (and (poo-flow-model? AssuranceVerifierChoice value) + (eq? (.ref value 'selected?) + (eq? (.ref value 'reason) 'chosen)))) +(def (assurance-verifier-choice-receipt? value) + (poo-flow-model? AssuranceVerifierChoiceReceipt value)) (def (assurance-invalidation-receipt? value) (assurance-invalidation-receipt-record? value)) (def (assurance-invalidation-receipt-ref receipt name) diff --git a/t/assurance/unit/assurance-kernel-test.ss b/t/assurance/unit/assurance-kernel-test.ss index df03e83..ccacfcf 100644 --- a/t/assurance/unit/assurance-kernel-test.ss +++ b/t/assurance/unit/assurance-kernel-test.ss @@ -589,6 +589,85 @@ 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 "POO Graph orders affected prerequisites before dependents" (let* ((a (verification-obligation "obligation/a" 'gerbil-test)) (b (verification-obligation "obligation/b" 'gerbil-test)) From 979daf8fedeedbc96f287673b496f589d7f9c16c Mon Sep 17 00:00:00 2001 From: guangtao Date: Wed, 23 Sep 2026 14:30:31 +0800 Subject: [PATCH 09/64] Require independent obligations for composed claims --- ROADMAP.org | 12 +- docs/rfcs/0001-causal-assurance-core.org | 9 +- modules/assurance/funs.ss | 131 ++++++++++++++++++- modules/assurance/invalidation-projection.ss | 4 +- modules/assurance/types.ss | 42 +++++- schemas/vocabulary-manifest.json | 9 +- t/assurance/unit/assurance-kernel-test.ss | 123 +++++++++++++++++ 7 files changed, 319 insertions(+), 11 deletions(-) diff --git a/ROADMAP.org b/ROADMAP.org index ba93e90..5c46668 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -133,8 +133,8 @@ Exit gates: * Phase 3 — Obligation derivation and verification planning -Status: =active= (typed kinds, dependency-ordered selection and replacement -requirements; replacement declaration remains open) +Status: =active= (typed kinds, dependency-ordered selection, replacement and +composition requirements; replacement declaration remains open) The first executable slice reuses the Phase 2 invalidation receipt to select snapshot-bound obligations. A POO-native verification policy declares @@ -165,13 +165,19 @@ 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. Materializing -and binding replacement obligations and composition obligations remain open. +and binding replacement obligations remain open. 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: diff --git a/docs/rfcs/0001-causal-assurance-core.org b/docs/rfcs/0001-causal-assurance-core.org index 56cf213..965173d 100644 --- a/docs/rfcs/0001-causal-assurance-core.org +++ b/docs/rfcs/0001-causal-assurance-core.org @@ -167,7 +167,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. @@ -273,6 +275,11 @@ 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 diff --git a/modules/assurance/funs.ss b/modules/assurance/funs.ss index ea640d2..810d1b7 100644 --- a/modules/assurance/funs.ss +++ b/modules/assurance/funs.ss @@ -25,7 +25,8 @@ assurance-bind-obligation-to-snapshot assurance-support-admissible? assurance-invalidation-graph assurance-invalidate assurance-derive-replacement-requirements - assurance-plan-verification assurance-explain-verifier-choices) + assurance-plan-verification assurance-explain-verifier-choices + assurance-derive-composition-requirements) (def (assurance-node-semantic-slots kind) (case kind @@ -475,6 +476,134 @@ requirements: derived-requirements digest: derivation-digest verifier-executed?: #f release-authorized?: #f)))) +;;; 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-flow-check-model + AssuranceCompositionRequirement + (.o (:: @ (poo-flow-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-flow-check-model + AssuranceCompositionDerivation + (.o (:: @ (poo-flow-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. diff --git a/modules/assurance/invalidation-projection.ss b/modules/assurance/invalidation-projection.ss index 8792612..a67510c 100644 --- a/modules/assurance/invalidation-projection.ss +++ b/modules/assurance/invalidation-projection.ss @@ -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/types.ss b/modules/assurance/types.ss index bc5cf70..eb6e62f 100644 --- a/modules/assurance/types.ss +++ b/modules/assurance/types.ss @@ -23,6 +23,7 @@ AssuranceVerificationPolicy AssuranceVerificationRequest AssuranceVerificationPlan AssuranceVerifierCandidate AssuranceVerifierChoice AssuranceVerifierChoiceReceipt + AssuranceCompositionRequirement AssuranceCompositionDerivation make-assurance-invalidation-receipt-record assurance-node? assurance-artifact? assurance-claim? assurance-assumption? assurance-observation? assurance-event? @@ -34,7 +35,9 @@ 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-choice? assurance-verifier-choice-receipt? + assurance-composition-requirement? + assurance-composition-derivation?) (def +assurance-node-kinds+ '(artifact claim assumption observation event action obligation evidence @@ -56,6 +59,7 @@ (cites . provenance) (used . provenance) (depends-on . structural) + (composes . structural) (implements . structural) (tests . structural) (refines . structural) @@ -110,6 +114,12 @@ (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 @@ -389,6 +399,32 @@ (enum-slot 'verifier-executed? (one-of '(#f))) (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 @@ -468,6 +504,10 @@ (eq? (.ref value 'reason) 'chosen)))) (def (assurance-verifier-choice-receipt? value) (poo-flow-model? AssuranceVerifierChoiceReceipt value)) +(def (assurance-composition-requirement? value) + (poo-flow-model? AssuranceCompositionRequirement value)) +(def (assurance-composition-derivation? value) + (poo-flow-model? AssuranceCompositionDerivation value)) (def (assurance-invalidation-receipt? value) (assurance-invalidation-receipt-record? value)) (def (assurance-invalidation-receipt-ref receipt name) diff --git a/schemas/vocabulary-manifest.json b/schemas/vocabulary-manifest.json index 0a244ad..bc4e921 100644 --- a/schemas/vocabulary-manifest.json +++ b/schemas/vocabulary-manifest.json @@ -1,6 +1,6 @@ { "manifestSchema": "lambda-aitia.vocabulary-manifest", - "revision": 2, + "revision": 3, "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,9 @@ "lambda-aitia.assurance-snapshot", "lambda-aitia.invalidation-receipt", "lambda-aitia.planning-result", + "lambda-aitia.replacement-requirements", + "lambda-aitia.verifier-choices", + "lambda-aitia.composition-requirements", "lambda-aitia.native-descriptor", "lambda-aitia.native-error" ] diff --git a/t/assurance/unit/assurance-kernel-test.ss b/t/assurance/unit/assurance-kernel-test.ss index ccacfcf..2209616 100644 --- a/t/assurance/unit/assurance-kernel-test.ss +++ b/t/assurance/unit/assurance-kernel-test.ss @@ -134,6 +134,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) @@ -668,6 +674,123 @@ "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)) From 95bcf24b5bce1deef11dcd730796295c9cb28e10 Mon Sep 17 00:00:00 2001 From: guangtao Date: Wed, 23 Sep 2026 15:02:00 +0800 Subject: [PATCH 10/64] Avoid Darwin cross-module keyword symbol crash --- build.ss | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/build.ss b/build.ss index fb9f466..7a7fcfc 100755 --- a/build.ss +++ b/build.ss @@ -37,4 +37,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)))) From 3c75da87cc0ebd654bff78e4d3b799543bd865ee Mon Sep 17 00:00:00 2001 From: guangtao Date: Wed, 23 Sep 2026 15:11:25 +0800 Subject: [PATCH 11/64] Prove independent composition obligation boundary --- ROADMAP.org | 2 +- .../LambdaAitiaProof/Modules/Assurance.lean | 1 + .../Modules/Assurance/Composition.lean | 79 +++++++++++++++++++ proof/lean/README.org | 8 ++ 4 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 proof/lean/LambdaAitiaProof/Modules/Assurance/Composition.lean diff --git a/ROADMAP.org b/ROADMAP.org index 5c46668..246ccc7 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -245,7 +245,7 @@ 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: diff --git a/proof/lean/LambdaAitiaProof/Modules/Assurance.lean b/proof/lean/LambdaAitiaProof/Modules/Assurance.lean index d155daa..8a0da03 100644 --- a/proof/lean/LambdaAitiaProof/Modules/Assurance.lean +++ b/proof/lean/LambdaAitiaProof/Modules/Assurance.lean @@ -3,3 +3,4 @@ -- SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later import LambdaAitiaProof.Modules.Assurance.Invalidation +import LambdaAitiaProof.Modules.Assurance.Composition diff --git a/proof/lean/LambdaAitiaProof/Modules/Assurance/Composition.lean b/proof/lean/LambdaAitiaProof/Modules/Assurance/Composition.lean new file mode 100644 index 0000000..1450e2b --- /dev/null +++ b/proof/lean/LambdaAitiaProof/Modules/Assurance/Composition.lean @@ -0,0 +1,79 @@ +-- SPDX-FileCopyrightText: 2026 tao3k team and Contributors +-- +-- SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +/-! +The composition law is independent of the Graph traversal implementation. +It states the admission boundary: evidence for components does not discharge +the separate, current obligation declared for their composite claim. +-/ + +namespace LambdaAitiaProof.Modules.Assurance.Composition + +structure Contract (Claim Obligation : Type) where + componentOf : Claim → Claim → Prop + componentObligation : Obligation → Claim → Prop + directObligation : Obligation → Claim → Prop + current : Obligation → Prop + discharged : Obligation → Prop + compositeSupported : Claim → Prop + directEvidenceRequired : + ∀ composite, compositeSupported composite → + ∃ obligation, directObligation obligation composite ∧ + current obligation ∧ discharged obligation + independent : + ∀ obligation component composite, + componentOf component composite → + componentObligation obligation component → + ¬ directObligation obligation composite + +def ComponentOnlyEvidence {Claim Obligation : Type} + (contract : Contract Claim Obligation) (composite : Claim) : Prop := + ∀ obligation, + contract.current obligation ∧ contract.discharged obligation → + ∃ component, + contract.componentOf component composite ∧ + contract.componentObligation obligation component + +theorem componentOnlyEvidenceCannotSupportComposite + {Claim Obligation : Type} + (contract : Contract Claim Obligation) (composite : Claim) + (onlyComponents : ComponentOnlyEvidence contract composite) : + ¬ contract.compositeSupported composite := by + intro supported + obtain ⟨obligation, direct, current, discharged⟩ := + contract.directEvidenceRequired composite supported + obtain ⟨component, part, componentEvidence⟩ := + onlyComponents obligation ⟨current, discharged⟩ + exact (contract.independent obligation component composite part componentEvidence) direct + +theorem directObligationNeeded + {Claim Obligation : Type} + (contract : Contract Claim Obligation) (composite : Claim) + (supported : contract.compositeSupported composite) : + ∃ obligation, + contract.directObligation obligation composite ∧ + contract.current obligation ∧ contract.discharged obligation := + contract.directEvidenceRequired composite supported + +theorem directWitnessDiffersFromComponentObligation + {Claim Obligation : Type} + (contract : Contract Claim Obligation) + (component composite : Claim) (componentEvidence : Obligation) + (part : contract.componentOf component composite) + (belongsToComponent : contract.componentObligation componentEvidence component) + (supported : contract.compositeSupported composite) : + ∃ directEvidence, + contract.directObligation directEvidence composite ∧ + contract.current directEvidence ∧ + contract.discharged directEvidence ∧ + directEvidence ≠ componentEvidence := by + obtain ⟨directEvidence, direct, current, discharged⟩ := + contract.directEvidenceRequired composite supported + refine ⟨directEvidence, direct, current, discharged, ?_⟩ + intro same + subst directEvidence + exact (contract.independent componentEvidence component composite + part belongsToComponent) direct + +end LambdaAitiaProof.Modules.Assurance.Composition diff --git a/proof/lean/README.org b/proof/lean/README.org index be6556f..49d82eb 100644 --- a/proof/lean/README.org +++ b/proof/lean/README.org @@ -22,3 +22,11 @@ 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. From 33c4426f6dc75676a7a31cad3cbc85d2fc1d771e Mon Sep 17 00:00:00 2001 From: guangtao Date: Wed, 23 Sep 2026 15:39:34 +0800 Subject: [PATCH 12/64] feat(assurance): bind explicitly declared replacement obligations --- ROADMAP.org | 10 +- docs/rfcs/0001-causal-assurance-core.org | 6 + modules/assurance/funs.ss | 136 ++++++++++++++++++ modules/assurance/types.ss | 35 +++++ schemas/vocabulary-manifest.json | 3 +- t/assurance/unit/assurance-kernel-test.ss | 160 ++++++++++++++++++++++ 6 files changed, 346 insertions(+), 4 deletions(-) diff --git a/ROADMAP.org b/ROADMAP.org index 246ccc7..2ac93dd 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -134,7 +134,7 @@ Exit gates: * Phase 3 — Obligation derivation and verification planning Status: =active= (typed kinds, dependency-ordered selection, replacement and -composition requirements; replacement declaration remains open) +composition requirements, and explicit replacement declaration/binding) The first executable slice reuses the Phase 2 invalidation receipt to select snapshot-bound obligations. A POO-native verification policy declares @@ -164,8 +164,12 @@ 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. Materializing -and binding replacement obligations remain open. +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 diff --git a/docs/rfcs/0001-causal-assurance-core.org b/docs/rfcs/0001-causal-assurance-core.org index 965173d..d30ad55 100644 --- a/docs/rfcs/0001-causal-assurance-core.org +++ b/docs/rfcs/0001-causal-assurance-core.org @@ -117,6 +117,12 @@ 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 diff --git a/modules/assurance/funs.ss b/modules/assurance/funs.ss index 810d1b7..c5306bd 100644 --- a/modules/assurance/funs.ss +++ b/modules/assurance/funs.ss @@ -25,6 +25,7 @@ assurance-bind-obligation-to-snapshot assurance-support-admissible? assurance-invalidation-graph assurance-invalidate assurance-derive-replacement-requirements + assurance-declare-replacement-obligation assurance-plan-verification assurance-explain-verifier-choices assurance-derive-composition-requirements) @@ -476,6 +477,141 @@ 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-flow-check-model + AssuranceReplacementDeclarationReceipt + (.o (:: @ (poo-flow-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. diff --git a/modules/assurance/types.ss b/modules/assurance/types.ss index eb6e62f..355df52 100644 --- a/modules/assurance/types.ss +++ b/modules/assurance/types.ss @@ -20,6 +20,7 @@ AssuranceFinding AssuranceDecision AssuranceEffect AssuranceRelation AssuranceSnapshot AssuranceReplacementRequirement AssuranceReplacementDerivation + AssuranceReplacementDeclarationReceipt AssuranceVerificationPolicy AssuranceVerificationRequest AssuranceVerificationPlan AssuranceVerifierCandidate AssuranceVerifierChoice AssuranceVerifierChoiceReceipt @@ -32,6 +33,7 @@ assurance-effect? assurance-relation? assurance-snapshot? 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? @@ -133,6 +135,15 @@ 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) @@ -311,6 +322,26 @@ (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 @@ -478,6 +509,10 @@ (poo-flow-model? AssuranceReplacementRequirement value)) (def (assurance-replacement-derivation? value) (poo-flow-model? AssuranceReplacementDerivation value)) +(def (assurance-replacement-declaration-receipt? value) + (and (poo-flow-model? AssuranceReplacementDeclarationReceipt value) + (eq? (not (.ref value 'blocker)) + (if (.ref value 'final-snapshot-digest) #t #f)))) (def (assurance-verification-policy? value) (poo-flow-model? AssuranceVerificationPolicy value)) (def (assurance-verification-request? value) diff --git a/schemas/vocabulary-manifest.json b/schemas/vocabulary-manifest.json index bc4e921..4c712e1 100644 --- a/schemas/vocabulary-manifest.json +++ b/schemas/vocabulary-manifest.json @@ -1,6 +1,6 @@ { "manifestSchema": "lambda-aitia.vocabulary-manifest", - "revision": 3, + "revision": 4, "semanticOwner": "lambda-aitia", "nodeKinds": [ "artifact", @@ -67,6 +67,7 @@ "lambda-aitia.invalidation-receipt", "lambda-aitia.planning-result", "lambda-aitia.replacement-requirements", + "lambda-aitia.replacement-declaration", "lambda-aitia.verifier-choices", "lambda-aitia.composition-requirements", "lambda-aitia.native-descriptor", diff --git a/t/assurance/unit/assurance-kernel-test.ss b/t/assurance/unit/assurance-kernel-test.ss index 2209616..d3cd432 100644 --- a/t/assurance/unit/assurance-kernel-test.ss +++ b/t/assurance/unit/assurance-kernel-test.ss @@ -5,6 +5,7 @@ (import :std/test (only-in :std/error Error?) + (only-in :std/list/list find) (only-in :clan/poo/object .o .ref) :poo-flow/lambda-aitia/modules/assurance/interface) @@ -108,6 +109,30 @@ (list dependency discharge obligation-support decision-dependency authorization))) +(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" (test-case "nominal node identity is not granted by matching slots" @@ -571,6 +596,141 @@ (.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+ From 9d3a8522d14efc668d30c5ceb9680953c6d7e044 Mon Sep 17 00:00:00 2001 From: guangtao Date: Wed, 23 Sep 2026 16:21:16 +0800 Subject: [PATCH 13/64] ci: consume POO Flow test exit-status pin --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d70b9f2..31d175b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,7 +89,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: repository: tao3k/poo-flow - ref: da492eea00a7dab502cfb4c7586edefbba96bbec + ref: 13fd70bec1c3f835c41d61bffbf218f8209d839e path: poo-flow submodules: false From 50ae06fdce07a6d0ca244ec0dd1393449ceb8e34 Mon Sep 17 00:00:00 2001 From: guangtao Date: Wed, 23 Sep 2026 17:02:21 +0800 Subject: [PATCH 14/64] ci: pin source-complete POO Flow test mechanism --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31d175b..f150785 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,7 +89,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: repository: tao3k/poo-flow - ref: 13fd70bec1c3f835c41d61bffbf218f8209d839e + ref: 5c55eef6743b310f7fbf37f9555580fc9b3498cf path: poo-flow submodules: false From ed16837985161d6a51f5f4a988fcd2d948869335 Mon Sep 17 00:00:00 2001 From: guangtao Date: Wed, 23 Sep 2026 17:23:24 +0800 Subject: [PATCH 15/64] assurance: classify verifier outcomes before evidence admission --- ROADMAP.org | 8 +- docs/rfcs/0001-causal-assurance-core.org | 5 + modules/assurance/evidence-admission.ss | 150 ++++++++++++++++ modules/assurance/interface.ss | 2 + modules/assurance/types.ss | 58 ++++++ schemas/vocabulary-manifest.json | 3 +- t/assurance/unit/evidence-admission-test.ss | 184 ++++++++++++++++++++ 7 files changed, 406 insertions(+), 4 deletions(-) create mode 100644 modules/assurance/evidence-admission.ss create mode 100644 t/assurance/unit/evidence-admission-test.ss diff --git a/ROADMAP.org b/ROADMAP.org index 2ac93dd..dd62315 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -205,7 +205,8 @@ Exit gates: * Phase 4 — Verifier adapters and evidence admission -Status: =planned= +Status: =in-progress= (typed reported outcomes and pure structural eligibility +are implemented; trusted adapter execution and evidence promotion are open). Deliverables: @@ -213,8 +214,9 @@ Deliverables: 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 current receipt reports structural + eligibility only; it cannot promote evidence or authorize release. 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, diff --git a/docs/rfcs/0001-causal-assurance-core.org b/docs/rfcs/0001-causal-assurance-core.org index d30ad55..139e648 100644 --- a/docs/rfcs/0001-causal-assurance-core.org +++ b/docs/rfcs/0001-causal-assurance-core.org @@ -256,6 +256,11 @@ 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 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 must be +established before any =AssuranceEvidence= may transition to =admitted=. 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 diff --git a/modules/assurance/evidence-admission.ss b/modules/assurance/evidence-admission.ss new file mode 100644 index 0000000..72cecad --- /dev/null +++ b/modules/assurance/evidence-admission.ss @@ -0,0 +1,150 @@ +;;; -*- Gerbil -*- +;;; SPDX-FileCopyrightText: 2026 tao3k team and Contributors +;;; +;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +;;; Pure structural eligibility only. The Host still owns adapter identity, +;;; execution attestation and the transition to admitted evidence. +(import (only-in :clan/poo/object .o .ref) + (only-in :std/list/list find every) + :poo-flow/src/module-system/contribution/model + (only-in :poo-flow/lambda-aitia/modules/assurance/funs + assurance-canonical-digest assurance-node-canonical) + :poo-flow/lambda-aitia/modules/assurance/types) + +(export assurance-verifier-input assurance-verifier-outcome + assurance-evaluate-evidence-admission) + +(def (assurance-verifier-input artifact) + (unless (assurance-artifact? artifact) + (error "verifier input requires an artifact")) + (poo-flow-check-model + AssuranceVerifierInput + (.o (:: @ (poo-flow-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-flow-check-model + AssuranceVerifierOutcome + (.o (:: @ (poo-flow-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-evaluate-evidence-admission snapshot obligation outcome) + (unless (and (assurance-snapshot? snapshot) + (assurance-obligation? obligation) + (assurance-verifier-outcome? outcome)) + (error "invalid evidence admission inputs")) + (let* ((reported-result outcome) + (required-obligation obligation) + (current-snapshot snapshot) + (inputs (.ref outcome 'inputs)) + (current-obligation + (snapshot-node snapshot (.ref obligation 'identity))) + (admission-blocker + (cond + ((not (eq? (.ref outcome 'status) 'succeeded)) + 'outcome-not-successful) + ((pair? (.ref snapshot 'conflicts)) 'snapshot-conflicted) + ((or (not current-obligation) + (not (assurance-obligation? current-obligation)) + (not (equal? (assurance-node-canonical obligation) + (assurance-node-canonical current-obligation))) + (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)))) + 'stale-obligation) + ((or (not (equal? (.ref outcome 'obligation) + (.ref obligation 'identity))) + (not (equal? (.ref outcome 'subject) + (.ref obligation 'subject))) + (not (equal? (.ref outcome 'scope) + (.ref obligation 'scope))) + (not (equal? (.ref outcome 'snapshot-revision) + (.ref snapshot 'revision))) + (not (equal? (.ref outcome 'snapshot-context-digest) + (.ref snapshot 'context-digest)))) + 'outcome-binding-mismatch) + ((not (.ref outcome 'output-digest)) 'missing-output) + ((null? inputs) 'missing-inputs) + ((duplicate-inputs? inputs) 'duplicate-inputs) + ((not (every (lambda (input) (current-input? snapshot input)) + inputs)) + 'input-binding-mismatch) + ((and (eq? (.ref obligation 'evidence-kind) 'human-review) + (or (not (.ref outcome 'accountable-authority)) + (not (.ref outcome 'review-scope)) + (not (equal? (.ref outcome 'review-scope) + (.ref obligation 'scope))))) + 'human-authority-missing) + (else #f))) + (reported-digest + (assurance-canonical-digest (outcome-canonical reported-result))) + (receipt-digest + (assurance-canonical-digest + (list 'lambda-aitia.evidence-admission + reported-digest (assurance-node-canonical required-obligation) + (.ref current-snapshot 'digest) admission-blocker)))) + (poo-flow-check-model + AssuranceEvidenceAdmissionReceipt + (.o (:: @ (poo-flow-model-prototype AssuranceEvidenceAdmissionReceipt)) + schema: "lambda-aitia.evidence-admission" + outcome: (.ref reported-result 'identity) + obligation: (.ref required-obligation 'identity) + outcome-digest: reported-digest + snapshot-digest: (.ref current-snapshot 'digest) + digest: receipt-digest + eligible?: (not admission-blocker) blocker: admission-blocker + verifier-executed?: #f release-authorized?: #f)))) diff --git a/modules/assurance/interface.ss b/modules/assurance/interface.ss index cfe8620..3d07b65 100644 --- a/modules/assurance/interface.ss +++ b/modules/assurance/interface.ss @@ -9,5 +9,7 @@ (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/config) (export (import: :poo-flow/lambda-aitia/modules/assurance/config)) diff --git a/modules/assurance/types.ss b/modules/assurance/types.ss index 355df52..bc9aa4d 100644 --- a/modules/assurance/types.ss +++ b/modules/assurance/types.ss @@ -24,6 +24,8 @@ AssuranceVerificationPolicy AssuranceVerificationRequest AssuranceVerificationPlan AssuranceVerifierCandidate AssuranceVerifierChoice AssuranceVerifierChoiceReceipt + AssuranceVerifierInput AssuranceVerifierOutcome + AssuranceEvidenceAdmissionReceipt AssuranceCompositionRequirement AssuranceCompositionDerivation make-assurance-invalidation-receipt-record assurance-node? assurance-artifact? assurance-claim? @@ -38,6 +40,8 @@ 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-composition-requirement? assurance-composition-derivation?) @@ -430,6 +434,53 @@ (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)))))) + ;;; A composition needs its own snapshot-bound obligation. Component support ;;; remains separate and cannot discharge this structural requirement. (def AssuranceCompositionRequirement @@ -539,6 +590,13 @@ (eq? (.ref value 'reason) 'chosen)))) (def (assurance-verifier-choice-receipt? value) (poo-flow-model? AssuranceVerifierChoiceReceipt value)) +(def (assurance-verifier-input? value) + (poo-flow-model? AssuranceVerifierInput value)) +(def (assurance-verifier-outcome? value) + (poo-flow-model? AssuranceVerifierOutcome value)) +(def (assurance-evidence-admission-receipt? value) + (and (poo-flow-model? AssuranceEvidenceAdmissionReceipt value) + (eq? (.ref value 'eligible?) (not (.ref value 'blocker))))) (def (assurance-composition-requirement? value) (poo-flow-model? AssuranceCompositionRequirement value)) (def (assurance-composition-derivation? value) diff --git a/schemas/vocabulary-manifest.json b/schemas/vocabulary-manifest.json index 4c712e1..b0bc9dd 100644 --- a/schemas/vocabulary-manifest.json +++ b/schemas/vocabulary-manifest.json @@ -1,6 +1,6 @@ { "manifestSchema": "lambda-aitia.vocabulary-manifest", - "revision": 4, + "revision": 5, "semanticOwner": "lambda-aitia", "nodeKinds": [ "artifact", @@ -69,6 +69,7 @@ "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/t/assurance/unit/evidence-admission-test.ss b/t/assurance/unit/evidence-admission-test.ss new file mode 100644 index 0000000..c0a740e --- /dev/null +++ b/t/assurance/unit/evidence-admission-test.ss @@ -0,0 +1,184 @@ +;;; -*- 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 .o .ref) + :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 (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))))) From 0f7a827e4184baf1e76a40af3d87c262ddb7d27c Mon Sep 17 00:00:00 2001 From: guangtao Date: Wed, 23 Sep 2026 17:44:47 +0800 Subject: [PATCH 16/64] assurance: require sealed verifier support for current evidence --- ROADMAP.org | 21 ++-- .../software-sdlc-integration-inventory.org | 12 +- docs/rfcs/0001-causal-assurance-core.org | 13 +- modules/assurance/evidence-admission.ss | 97 ++++++++++++++- modules/assurance/types.ss | 16 ++- t/assurance/unit/evidence-admission-test.ss | 116 +++++++++++++++++- 6 files changed, 256 insertions(+), 19 deletions(-) diff --git a/ROADMAP.org b/ROADMAP.org index dd62315..8e219d8 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -48,13 +48,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 @@ -205,8 +207,9 @@ Exit gates: * Phase 4 — Verifier adapters and evidence admission -Status: =in-progress= (typed reported outcomes and pure structural eligibility -are implemented; trusted adapter execution and evidence promotion are open). +Status: =in-progress= (typed reported outcomes, pure structural eligibility and +sealed-support checks reuse POO Flow's verification adapter. The trusted Host +operation, evidence promotion and effect gate remain open). Deliverables: @@ -217,6 +220,9 @@ Deliverables: 3. [~] Admission rules for successful, failed, timed-out, unavailable and indeterminate verifier outcomes. The current receipt reports structural eligibility only; it cannot promote evidence or authorize release. + Verified support additionally requires an exact Host-issued POO Flow seal; + the existing three-argument support predicate remains structural-only and + must never be used as an authorization gate. 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, @@ -351,8 +357,9 @@ 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. +3. [X] Move the SDLC and GitOps owners into Aitia and publish the Episteme + deletion plus parent pin at the already qualified ownership cut. A newer + Aitia PR head still needs its own parent gitlink update after acceptance. 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. diff --git a/docs/plans/software-sdlc-integration-inventory.org b/docs/plans/software-sdlc-integration-inventory.org index fedddda..c41563e 100644 --- a/docs/plans/software-sdlc-integration-inventory.org +++ b/docs/plans/software-sdlc-integration-inventory.org @@ -17,12 +17,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: diff --git a/docs/rfcs/0001-causal-assurance-core.org b/docs/rfcs/0001-causal-assurance-core.org index 139e648..436b7cc 100644 --- a/docs/rfcs/0001-causal-assurance-core.org +++ b/docs/rfcs/0001-causal-assurance-core.org @@ -259,8 +259,17 @@ a typed execution outcome; evidence admission is another separate function. 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 must be -established before any =AssuranceEvidence= may transition to =admitted=. +attestation. Host-owned adapter identity and effectful admission are required +before an =AssuranceEvidence= may become authoritative. Its caller-supplied +=admission-state= is currently metadata, not proof of that transition. +The older three-argument =assurance-support-admissible?= checks structural +relationships and a declared admission-state only; it is not an authorization +predicate. =assurance-verified-support-admissible?= additionally checks the +current snapshot, exact evidence/outcome bindings and a POO Flow verification +seal over the complete semantic subject. Only a Host-controlled adapter may +issue that seal. A copied receipt, modified presentation, expired interval or +revoked receipt does not verify. This boundary still does not 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 diff --git a/modules/assurance/evidence-admission.ss b/modules/assurance/evidence-admission.ss index 72cecad..fb99230 100644 --- a/modules/assurance/evidence-admission.ss +++ b/modules/assurance/evidence-admission.ss @@ -6,14 +6,21 @@ ;;; Pure structural eligibility only. 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/src/module-system/contribution/model + (only-in :poo-flow/src/module-system/contribution/verification + poo-flow-verification-valid?) (only-in :poo-flow/lambda-aitia/modules/assurance/funs - assurance-canonical-digest assurance-node-canonical) + assurance-canonical-digest assurance-node-canonical + assurance-relation-canonical assurance-support-admissible?) :poo-flow/lambda-aitia/modules/assurance/types) (export assurance-verifier-input assurance-verifier-outcome - assurance-evaluate-evidence-admission) + assurance-evaluate-evidence-admission + assurance-verification-subject + assurance-verification-subject-snapshot + assurance-verified-support-admissible?) (def (assurance-verifier-input artifact) (unless (assurance-artifact? artifact) @@ -79,6 +86,92 @@ (.ref reported-result 'accountable-authority) (.ref reported-result 'review-scope))) +(def (assurance-verification-subject snapshot-value obligation-value + evidence-value outcome-value) + (poo-flow-check-model + AssuranceVerificationSubject + (.o (:: @ (poo-flow-model-prototype AssuranceVerificationSubject)) + snapshot: snapshot-value obligation: obligation-value + evidence: evidence-value outcome: outcome-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")) + (let ((snapshot-value (.ref subject-value 'snapshot))) + (assurance-canonical-digest + (list 'lambda-aitia.verification-subject + (.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) + (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 string Date: Wed, 23 Sep 2026 17:56:36 +0800 Subject: [PATCH 17/64] Make sealed support admission the sole public API --- ROADMAP.org | 6 +-- docs/rfcs/0001-causal-assurance-core.org | 15 +++--- modules/assurance/evidence-admission.ss | 36 +++++++++---- modules/assurance/funs.ss | 21 +------- t/assurance/unit/assurance-kernel-test.ss | 57 ++++++++++++++++----- t/assurance/unit/evidence-admission-test.ss | 10 ++-- 6 files changed, 84 insertions(+), 61 deletions(-) diff --git a/ROADMAP.org b/ROADMAP.org index 8e219d8..b40cdb1 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -220,9 +220,9 @@ Deliverables: 3. [~] Admission rules for successful, failed, timed-out, unavailable and indeterminate verifier outcomes. The current receipt reports structural eligibility only; it cannot promote evidence or authorize release. - Verified support additionally requires an exact Host-issued POO Flow seal; - the existing three-argument support predicate remains structural-only and - must never be used as an authorization gate. + The sole public support-admission entrypoint requires an exact Host-issued + POO Flow seal over the current semantic subject; no structural-only public + support predicate remains. 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, diff --git a/docs/rfcs/0001-causal-assurance-core.org b/docs/rfcs/0001-causal-assurance-core.org index 436b7cc..9609e11 100644 --- a/docs/rfcs/0001-causal-assurance-core.org +++ b/docs/rfcs/0001-causal-assurance-core.org @@ -262,14 +262,13 @@ 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 currently metadata, not proof of that transition. -The older three-argument =assurance-support-admissible?= checks structural -relationships and a declared admission-state only; it is not an authorization -predicate. =assurance-verified-support-admissible?= additionally checks the -current snapshot, exact evidence/outcome bindings and a POO Flow verification -seal over the complete semantic subject. Only a Host-controlled adapter may -issue that seal. A copied receipt, modified presentation, expired interval or -revoked receipt does not verify. This boundary still does not authorize an -effect; Cedar and Runtime Host remain separate. +=assurance-support-admissible?= is the sole public support-admission predicate. +It checks structural relationships, the current snapshot, exact +evidence/outcome bindings and a POO Flow verification seal over the complete +semantic subject. Only a Host-controlled adapter may issue that seal. A +copied receipt, modified presentation, expired interval or revoked receipt +does not verify. This boundary still does not 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 diff --git a/modules/assurance/evidence-admission.ss b/modules/assurance/evidence-admission.ss index fb99230..1236754 100644 --- a/modules/assurance/evidence-admission.ss +++ b/modules/assurance/evidence-admission.ss @@ -3,8 +3,8 @@ ;;; ;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later -;;; Pure structural eligibility only. The Host still owns adapter identity, -;;; execution attestation and the transition to admitted evidence. +;;; 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) @@ -13,14 +13,14 @@ poo-flow-verification-valid?) (only-in :poo-flow/lambda-aitia/modules/assurance/funs assurance-canonical-digest assurance-node-canonical - assurance-relation-canonical assurance-support-admissible?) + assurance-relation-canonical) :poo-flow/lambda-aitia/modules/assurance/types) (export assurance-verifier-input assurance-verifier-outcome assurance-evaluate-evidence-admission assurance-verification-subject assurance-verification-subject-snapshot - assurance-verified-support-admissible?) + assurance-support-admissible?) (def (assurance-verifier-input artifact) (unless (assurance-artifact? artifact) @@ -132,13 +132,29 @@ (and (= (length left) (length right)) (equal? (list-sort string Date: Wed, 23 Sep 2026 18:14:50 +0800 Subject: [PATCH 18/64] Reject forged verification adapters in assurance checks --- .github/workflows/ci.yml | 2 +- ROADMAP.org | 8 +++++--- docs/rfcs/0001-causal-assurance-core.org | 10 ++++++---- t/assurance/unit/evidence-admission-test.ss | 14 ++++++++++++++ 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f150785..535ce8e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,7 +89,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: repository: tao3k/poo-flow - ref: 5c55eef6743b310f7fbf37f9555580fc9b3498cf + ref: 8aa14eafe87ed63d32295f3eac5e5a4eda6658f5 path: poo-flow submodules: false diff --git a/ROADMAP.org b/ROADMAP.org index b40cdb1..8f55080 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -220,9 +220,11 @@ Deliverables: 3. [~] Admission rules for successful, failed, timed-out, unavailable and indeterminate verifier outcomes. The current receipt reports structural eligibility only; it cannot promote evidence or authorize release. - The sole public support-admission entrypoint requires an exact Host-issued - POO Flow seal over the current semantic subject; no structural-only public - support predicate remains. + The sole public support-admission entrypoint requires an exact POO Flow + adapter-issued seal over the current semantic subject; no structural-only + public support predicate remains. A forged adapter presentation is rejected, + but proving that the adapter and evaluation clock belong to the trusted + Host 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, diff --git a/docs/rfcs/0001-causal-assurance-core.org b/docs/rfcs/0001-causal-assurance-core.org index 9609e11..81bdbc4 100644 --- a/docs/rfcs/0001-causal-assurance-core.org +++ b/docs/rfcs/0001-causal-assurance-core.org @@ -265,10 +265,12 @@ before an =AssuranceEvidence= may become authoritative. Its caller-supplied =assurance-support-admissible?= is the sole public support-admission predicate. It checks structural relationships, the current snapshot, exact evidence/outcome bindings and a POO Flow verification seal over the complete -semantic subject. Only a Host-controlled adapter may issue that seal. A -copied receipt, modified presentation, expired interval or revoked receipt -does not verify. This boundary still does not authorize an effect; Cedar and -Runtime Host remain separate. +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 diff --git a/t/assurance/unit/evidence-admission-test.ss b/t/assurance/unit/evidence-admission-test.ss index 31523d0..527f79a 100644 --- a/t/assurance/unit/evidence-admission-test.ss +++ b/t/assurance/unit/evidence-admission-test.ss @@ -247,6 +247,20 @@ #t)) (test-case "copied seal and presentation mutation cannot forge support" + (check-equal? + (assurance-support-admissible? + 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-admissible? + 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 From 611df233a21b78a35be43fbf814880fe4577a2be Mon Sep 17 00:00:00 2001 From: guangtao Date: Wed, 23 Sep 2026 18:23:20 +0800 Subject: [PATCH 19/64] Close pure Phase 3 planning with exact replay receipt --- ROADMAP.org | 5 +- .../2026-09-23-phase3-planning-replay.org | 53 +++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 docs/receipts/2026-09-23-phase3-planning-replay.org diff --git a/ROADMAP.org b/ROADMAP.org index 8f55080..3b2a807 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -135,8 +135,9 @@ Exit gates: * Phase 3 — Obligation derivation and verification planning -Status: =active= (typed kinds, dependency-ordered selection, replacement and -composition requirements, and explicit replacement declaration/binding) +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 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. From 608525c84e853ed5f5b156693c8f72fe58cfcf97 Mon Sep 17 00:00:00 2001 From: guangtao Date: Wed, 23 Sep 2026 18:50:28 +0800 Subject: [PATCH 20/64] Add bounded assurance lifecycle TLA model and counterexamples --- ROADMAP.org | 9 ++++ .../2026-09-23-phase5-lifecycle-model.org | 50 +++++++++++++++++ proof/tla/AssuranceLifecycle.cfg | 11 ++++ proof/tla/AssuranceLifecycle.tla | 54 +++++++++++++++++++ proof/tla/AssuranceLifecycleAfterRevoke.cfg | 7 +++ proof/tla/AssuranceLifecycleEarlyGrant.cfg | 7 +++ proof/tla/AssuranceLifecycleLateAdmit.cfg | 7 +++ proof/tla/AssuranceLifecycleReachable.cfg | 7 +++ proof/tla/AssuranceLifecycleUnsafe.cfg | 7 +++ proof/tla/README.org | 40 ++++++++++++++ 10 files changed, 199 insertions(+) create mode 100644 docs/receipts/2026-09-23-phase5-lifecycle-model.org create mode 100644 proof/tla/AssuranceLifecycle.cfg create mode 100644 proof/tla/AssuranceLifecycle.tla create mode 100644 proof/tla/AssuranceLifecycleAfterRevoke.cfg create mode 100644 proof/tla/AssuranceLifecycleEarlyGrant.cfg create mode 100644 proof/tla/AssuranceLifecycleLateAdmit.cfg create mode 100644 proof/tla/AssuranceLifecycleReachable.cfg create mode 100644 proof/tla/AssuranceLifecycleUnsafe.cfg create mode 100644 proof/tla/README.org diff --git a/ROADMAP.org b/ROADMAP.org index 3b2a807..dd3a211 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -269,6 +269,15 @@ TLA+ owns the dynamic lifecycle: change -> invalidate -> plan -> verify -> admit -> decide -> authorize -> effect #+end_example +A bounded TLC model in =proof/tla/AssuranceLifecycle.tla= 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, liveness, and source-bound CI replay +remain open. + Required model-checking counterexamples include stale cached decisions, evidence admitted after a source change, release before re-verification, counterfactual promotion without transition authority and effect execution 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..87995cd --- /dev/null +++ b/docs/receipts/2026-09-23-phase5-lifecycle-model.org @@ -0,0 +1,50 @@ +# 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: + +* Exact model cut + +- Aitia base commit: =611df233a21b78a35be43fbf814880fe4577a2be=; + the model and this receipt are the candidate change on top of that commit. +- =AssuranceLifecycle.tla= SHA-256: + =94e31fe4e68687f2deb261e50ae543d7e6434ef59afe2f869f63fb0d4267f861=. +- Safe TLC configuration SHA-256: + =984c59bc517d3ae0a4477e7cb578f4a5a553bd966aed577d202e513679df21e9=. +- 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 | +| 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. +Progress/fairness, 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/proof/tla/AssuranceLifecycle.cfg b/proof/tla/AssuranceLifecycle.cfg new file mode 100644 index 0000000..869b430 --- /dev/null +++ b/proof/tla/AssuranceLifecycle.cfg @@ -0,0 +1,11 @@ +\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors +\* +\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +SPECIFICATION Spec +CHECK_DEADLOCK FALSE +INVARIANT TypeInvariant +INVARIANT AdmissionIsCurrent +INVARIANT DecisionRequiresAdmission +INVARIANT GrantRequiresCurrentDecision +INVARIANT NoUnsafeEffectStart diff --git a/proof/tla/AssuranceLifecycle.tla b/proof/tla/AssuranceLifecycle.tla new file mode 100644 index 0000000..14a7445 --- /dev/null +++ b/proof/tla/AssuranceLifecycle.tla @@ -0,0 +1,54 @@ +---- MODULE AssuranceLifecycle ---- +\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors +\* +\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +EXTENDS Naturals, TLC + +(* Bounded control-plane model. A successful report is not a Host verification; + admitted evidence and grants are current only inside their validity cut. *) + +VARIABLES revision, planned, verified, admitted, decided, granted, grantEpoch, epoch, clock, sealUntil, grantUntil, adapterInstalled, reportedSuccess, effectStarted, startSafe + +vars == <> + +Init == 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 + +InstallAdapter == ~adapterInstalled /\ adapterInstalled' = TRUE /\ UNCHANGED <> +ReportSuccess == ~reportedSuccess /\ reportedSuccess' = TRUE /\ UNCHANGED <> +Plan == planned # revision /\ planned' = revision /\ UNCHANGED <> +Verify == adapterInstalled /\ planned = revision /\ clock < 3 /\ verified' = revision /\ sealUntil' = clock + 1 /\ UNCHANGED <> +Admit == verified = revision /\ clock < sealUntil /\ admitted # revision /\ admitted' = revision /\ UNCHANGED <> +Decide == admitted = revision /\ decided # revision /\ decided' = revision /\ UNCHANGED <> +Authorize == decided = revision /\ admitted = revision /\ granted # revision /\ clock < 3 /\ granted' = revision /\ grantEpoch' = epoch /\ grantUntil' = clock + 1 /\ UNCHANGED <> +StartEffect == ~effectStarted /\ granted = revision /\ admitted = revision /\ decided = revision /\ grantEpoch = epoch /\ clock < grantUntil /\ effectStarted' = TRUE /\ startSafe' = (admitted = revision /\ decided = revision /\ granted = revision /\ grantEpoch = epoch /\ clock < grantUntil) /\ UNCHANGED <> +ChangeSource == revision = 1 /\ revision' = 2 /\ planned' = 0 /\ verified' = 0 /\ admitted' = 0 /\ decided' = 0 /\ granted' = 0 /\ sealUntil' = 0 /\ grantUntil' = 0 /\ UNCHANGED <> +Tick == clock < 3 /\ clock' = clock + 1 /\ admitted' = (IF clock + 1 < sealUntil THEN admitted ELSE 0) /\ decided' = (IF clock + 1 < sealUntil THEN decided ELSE 0) /\ granted' = (IF (clock + 1 < sealUntil /\ clock + 1 < grantUntil) THEN granted ELSE 0) /\ UNCHANGED <> +Revoke == epoch < 2 /\ epoch' = epoch + 1 /\ verified' = 0 /\ admitted' = 0 /\ decided' = 0 /\ granted' = 0 /\ sealUntil' = 0 /\ UNCHANGED <> + +Next == InstallAdapter \/ ReportSuccess \/ Plan \/ Verify \/ Admit \/ Decide \/ Authorize \/ StartEffect \/ ChangeSource \/ Tick \/ Revoke +Spec == Init /\ [][Next]_vars + +(* Deliberately unsafe mutant: a reported green result starts an effect without + Host admission or grant. Its counterexample must be found by TLC. *) +UnsafeStart == reportedSuccess /\ ~effectStarted /\ effectStarted' = TRUE /\ startSafe' = (admitted = revision /\ decided = revision /\ granted = revision /\ grantEpoch = epoch /\ clock < grantUntil) /\ UNCHANGED <> +UnsafeNext == Next \/ UnsafeStart +UnsafeSpec == Init /\ [][UnsafeNext]_vars +UnsafeLateAdmit == revision = 2 /\ reportedSuccess /\ admitted = 0 /\ admitted' = 1 /\ UNCHANGED <> +UnsafeLateAdmitNext == Next \/ UnsafeLateAdmit +UnsafeLateAdmitSpec == Init /\ [][UnsafeLateAdmitNext]_vars +UnsafeEarlyGrant == revision = 2 /\ reportedSuccess /\ admitted = 0 /\ granted = 0 /\ clock < 3 /\ granted' = revision /\ grantEpoch' = epoch /\ grantUntil' = clock + 1 /\ UNCHANGED <> +UnsafeEarlyGrantNext == Next \/ UnsafeEarlyGrant +UnsafeEarlyGrantSpec == Init /\ [][UnsafeEarlyGrantNext]_vars +UnsafeAfterRevoke == epoch > grantEpoch /\ grantUntil > clock /\ granted = 0 /\ ~effectStarted /\ effectStarted' = TRUE /\ startSafe' = (admitted = revision /\ decided = revision /\ granted = revision /\ grantEpoch = epoch /\ clock < grantUntil) /\ UNCHANGED <> +UnsafeAfterRevokeNext == Next \/ UnsafeAfterRevoke +UnsafeAfterRevokeSpec == Init /\ [][UnsafeAfterRevokeNext]_vars + +TypeInvariant == revision \in {1, 2} /\ planned \in {0, 1, 2} /\ verified \in {0, 1, 2} /\ admitted \in {0, 1, 2} /\ decided \in {0, 1, 2} /\ granted \in {0, 1, 2} /\ grantEpoch \in 0..2 /\ epoch \in 0..2 /\ clock \in 0..3 /\ sealUntil \in 0..4 /\ grantUntil \in 0..4 /\ adapterInstalled \in BOOLEAN /\ reportedSuccess \in BOOLEAN /\ effectStarted \in BOOLEAN /\ startSafe \in BOOLEAN +AdmissionIsCurrent == admitted = 0 \/ (admitted = revision /\ verified = revision /\ clock < sealUntil) +DecisionRequiresAdmission == decided = 0 \/ (decided = revision /\ admitted = revision) +GrantRequiresCurrentDecision == granted = 0 \/ (granted = revision /\ decided = revision /\ admitted = revision /\ grantEpoch = epoch /\ clock < grantUntil) +NoUnsafeEffectStart == startSafe +NoEffectStarted == ~effectStarted + +==== diff --git a/proof/tla/AssuranceLifecycleAfterRevoke.cfg b/proof/tla/AssuranceLifecycleAfterRevoke.cfg new file mode 100644 index 0000000..9666d2d --- /dev/null +++ b/proof/tla/AssuranceLifecycleAfterRevoke.cfg @@ -0,0 +1,7 @@ +\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors +\* +\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +SPECIFICATION UnsafeAfterRevokeSpec +CHECK_DEADLOCK FALSE +INVARIANT NoUnsafeEffectStart diff --git a/proof/tla/AssuranceLifecycleEarlyGrant.cfg b/proof/tla/AssuranceLifecycleEarlyGrant.cfg new file mode 100644 index 0000000..4e1bdbd --- /dev/null +++ b/proof/tla/AssuranceLifecycleEarlyGrant.cfg @@ -0,0 +1,7 @@ +\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors +\* +\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +SPECIFICATION UnsafeEarlyGrantSpec +CHECK_DEADLOCK FALSE +INVARIANT GrantRequiresCurrentDecision diff --git a/proof/tla/AssuranceLifecycleLateAdmit.cfg b/proof/tla/AssuranceLifecycleLateAdmit.cfg new file mode 100644 index 0000000..e65137b --- /dev/null +++ b/proof/tla/AssuranceLifecycleLateAdmit.cfg @@ -0,0 +1,7 @@ +\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors +\* +\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +SPECIFICATION UnsafeLateAdmitSpec +CHECK_DEADLOCK FALSE +INVARIANT AdmissionIsCurrent diff --git a/proof/tla/AssuranceLifecycleReachable.cfg b/proof/tla/AssuranceLifecycleReachable.cfg new file mode 100644 index 0000000..c4606c9 --- /dev/null +++ b/proof/tla/AssuranceLifecycleReachable.cfg @@ -0,0 +1,7 @@ +\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors +\* +\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +SPECIFICATION Spec +CHECK_DEADLOCK FALSE +INVARIANT NoEffectStarted diff --git a/proof/tla/AssuranceLifecycleUnsafe.cfg b/proof/tla/AssuranceLifecycleUnsafe.cfg new file mode 100644 index 0000000..0bce88d --- /dev/null +++ b/proof/tla/AssuranceLifecycleUnsafe.cfg @@ -0,0 +1,7 @@ +\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors +\* +\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +SPECIFICATION UnsafeSpec +CHECK_DEADLOCK FALSE +INVARIANT NoUnsafeEffectStart diff --git a/proof/tla/README.org b/proof/tla/README.org new file mode 100644 index 0000000..55020b0 --- /dev/null +++ b/proof/tla/README.org @@ -0,0 +1,40 @@ +# SPDX-FileCopyrightText: 2026 tao3k team and Contributors +# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +#+title: Bounded assurance lifecycle model + +This model describes one obligation and one source revision change. It is a +contract for a future trusted Host handoff, not an implementation of that Host. +In particular, =ReportSuccess= is untrusted: it cannot verify, admit, decide, +authorize, or start an effect. =InstallAdapter= merely makes a verifier +available. Admission requires a current verification seal; authorization +requires current admission and decision; starting an effect also requires a +non-expired grant issued in the current revocation epoch. + +The bounded domains are two source revisions, four clock values, and three +revocation epochs. TLC exhaustively checks the safe model within those bounds. +The =Unsafe*= actions are deliberate mutations; they are not implementation +paths. A nonzero TLC exit for each mutant is the expected counterexample. + +From this directory, with =tlc= installed on =PATH=: + +#+begin_src sh +JAVA_TOOL_OPTIONS=-Xmx512m tlc -workers 1 -config AssuranceLifecycle.cfg AssuranceLifecycle +JAVA_TOOL_OPTIONS=-Xmx512m tlc -workers 1 -config AssuranceLifecycleReachable.cfg AssuranceLifecycle +JAVA_TOOL_OPTIONS=-Xmx512m tlc -workers 1 -config AssuranceLifecycleUnsafe.cfg AssuranceLifecycle +JAVA_TOOL_OPTIONS=-Xmx512m tlc -workers 1 -config AssuranceLifecycleLateAdmit.cfg AssuranceLifecycle +JAVA_TOOL_OPTIONS=-Xmx512m tlc -workers 1 -config AssuranceLifecycleEarlyGrant.cfg AssuranceLifecycle +JAVA_TOOL_OPTIONS=-Xmx512m tlc -workers 1 -config AssuranceLifecycleAfterRevoke.cfg AssuranceLifecycle +#+end_src + +Use =-metadir= with a writable temporary directory when running these checks +from a read-only checkout. The safe configuration must exit 0. The reachability +configuration deliberately checks =NoEffectStarted= and must find a valid +=StartEffect= trace. Each other mutant must violate its named invariant; a +mutant passing TLC would be a failed negative contract, not a success. + +This model does not prove a live adapter was installed by a trusted Host, a +clock is actually trustworthy, a grant is cryptographically bound, or the +Scheme/Python/Runtime path enforces the model. It does not yet model +counterfactual promotion, concurrent effects, or liveness/fairness. Those +remain separate Phase 4–6 exit gates. From 742291f3c0bbef5df8a7e6c3fcaba93c0c8afe8c Mon Sep 17 00:00:00 2001 From: guangtao Date: Wed, 23 Sep 2026 18:59:41 +0800 Subject: [PATCH 21/64] Check conditional assurance lifecycle progress with TLC --- ROADMAP.org | 7 ++++++- .../software-sdlc-integration-inventory.org | 2 +- .../2026-09-23-phase5-lifecycle-model.org | 16 +++++++++++----- docs/rfcs/0001-causal-assurance-core.org | 6 ++++++ proof/tla/AssuranceLifecycle.tla | 8 ++++++++ proof/tla/AssuranceLifecycleNoFair.cfg | 7 +++++++ proof/tla/AssuranceLifecycleProgress.cfg | 9 +++++++++ proof/tla/README.org | 12 +++++++++--- 8 files changed, 57 insertions(+), 10 deletions(-) create mode 100644 proof/tla/AssuranceLifecycleNoFair.cfg create mode 100644 proof/tla/AssuranceLifecycleProgress.cfg diff --git a/ROADMAP.org b/ROADMAP.org index dd3a211..cdb3449 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -275,9 +275,14 @@ 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, liveness, and source-bound CI replay +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, counterfactual promotion without transition authority and effect execution diff --git a/docs/plans/software-sdlc-integration-inventory.org b/docs/plans/software-sdlc-integration-inventory.org index c41563e..8611060 100644 --- a/docs/plans/software-sdlc-integration-inventory.org +++ b/docs/plans/software-sdlc-integration-inventory.org @@ -71,7 +71,7 @@ POO Flow execution does not redefine Aitia reasoning. | =bindings/python= | Lambda Aitia | new | Aitia-owned CFFI consumer and wheel for the Scheme-native ABI | Python transports JSON and never duplicates Scheme decisions | | =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/tla/AssuranceLifecycle.tla= | 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 | * Existing semantics to preserve diff --git a/docs/receipts/2026-09-23-phase5-lifecycle-model.org b/docs/receipts/2026-09-23-phase5-lifecycle-model.org index 87995cd..bf26f3a 100644 --- a/docs/receipts/2026-09-23-phase5-lifecycle-model.org +++ b/docs/receipts/2026-09-23-phase5-lifecycle-model.org @@ -13,12 +13,16 @@ * Exact model cut -- Aitia base commit: =611df233a21b78a35be43fbf814880fe4577a2be=; - the model and this receipt are the candidate change on top of that commit. +- Aitia base commit: =608525c84e853ed5f5b156693c8f72fe58cfcf97=; + the progress extension and this updated + receipt are the candidate change on top of that commit. - =AssuranceLifecycle.tla= SHA-256: - =94e31fe4e68687f2deb261e50ae543d7e6434ef59afe2f869f63fb0d4267f861=. + =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 @@ -29,6 +33,8 @@ | 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 | @@ -45,6 +51,6 @@ declared bounds was exhausted. 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. -Progress/fairness, counterfactual transition authority, source-bound CI replay, -and the executable Host handoff remain open. The existing POO Flow Cedar +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/rfcs/0001-causal-assurance-core.org b/docs/rfcs/0001-causal-assurance-core.org index 81bdbc4..f740a6a 100644 --- a/docs/rfcs/0001-causal-assurance-core.org +++ b/docs/rfcs/0001-causal-assurance-core.org @@ -308,6 +308,12 @@ The architecture requires executable formal statements of at least these laws: Lean proves general laws. TLA+/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 diff --git a/proof/tla/AssuranceLifecycle.tla b/proof/tla/AssuranceLifecycle.tla index 14a7445..7af3664 100644 --- a/proof/tla/AssuranceLifecycle.tla +++ b/proof/tla/AssuranceLifecycle.tla @@ -29,6 +29,14 @@ Revoke == epoch < 2 /\ epoch' = epoch + 1 /\ verified' = 0 /\ admitted' = 0 /\ d Next == InstallAdapter \/ ReportSuccess \/ Plan \/ Verify \/ Admit \/ Decide \/ Authorize \/ StartEffect \/ ChangeSource \/ Tick \/ Revoke Spec == Init /\ [][Next]_vars +(* Progress is conditional: no source change, clock tick or revocation occurs, + and the Host eventually schedules each continuously enabled stage. This + does not assert unconditional progress for a changing environment. *) +StableNext == InstallAdapter \/ ReportSuccess \/ Plan \/ Verify \/ Admit \/ Decide \/ Authorize \/ StartEffect +StableSpecNoFair == Init /\ [][StableNext]_vars +StableSpec == Init /\ [][StableNext]_vars /\ WF_vars(InstallAdapter) /\ WF_vars(Plan) /\ WF_vars(Verify) /\ WF_vars(Admit) /\ WF_vars(Decide) /\ WF_vars(Authorize) /\ WF_vars(StartEffect) +EventuallyEffect == <>effectStarted + (* Deliberately unsafe mutant: a reported green result starts an effect without Host admission or grant. Its counterexample must be found by TLC. *) UnsafeStart == reportedSuccess /\ ~effectStarted /\ effectStarted' = TRUE /\ startSafe' = (admitted = revision /\ decided = revision /\ granted = revision /\ grantEpoch = epoch /\ clock < grantUntil) /\ UNCHANGED <> diff --git a/proof/tla/AssuranceLifecycleNoFair.cfg b/proof/tla/AssuranceLifecycleNoFair.cfg new file mode 100644 index 0000000..ad1c41b --- /dev/null +++ b/proof/tla/AssuranceLifecycleNoFair.cfg @@ -0,0 +1,7 @@ +\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors +\* +\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +SPECIFICATION StableSpecNoFair +CHECK_DEADLOCK FALSE +PROPERTY EventuallyEffect diff --git a/proof/tla/AssuranceLifecycleProgress.cfg b/proof/tla/AssuranceLifecycleProgress.cfg new file mode 100644 index 0000000..b659dc8 --- /dev/null +++ b/proof/tla/AssuranceLifecycleProgress.cfg @@ -0,0 +1,9 @@ +\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors +\* +\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +SPECIFICATION StableSpec +CHECK_DEADLOCK FALSE +INVARIANT TypeInvariant +INVARIANT NoUnsafeEffectStart +PROPERTY EventuallyEffect diff --git a/proof/tla/README.org b/proof/tla/README.org index 55020b0..6962877 100644 --- a/proof/tla/README.org +++ b/proof/tla/README.org @@ -20,6 +20,8 @@ From this directory, with =tlc= installed on =PATH=: #+begin_src sh JAVA_TOOL_OPTIONS=-Xmx512m tlc -workers 1 -config AssuranceLifecycle.cfg AssuranceLifecycle +JAVA_TOOL_OPTIONS=-Xmx512m tlc -workers 1 -config AssuranceLifecycleProgress.cfg AssuranceLifecycle +JAVA_TOOL_OPTIONS=-Xmx512m tlc -workers 1 -config AssuranceLifecycleNoFair.cfg AssuranceLifecycle JAVA_TOOL_OPTIONS=-Xmx512m tlc -workers 1 -config AssuranceLifecycleReachable.cfg AssuranceLifecycle JAVA_TOOL_OPTIONS=-Xmx512m tlc -workers 1 -config AssuranceLifecycleUnsafe.cfg AssuranceLifecycle JAVA_TOOL_OPTIONS=-Xmx512m tlc -workers 1 -config AssuranceLifecycleLateAdmit.cfg AssuranceLifecycle @@ -28,7 +30,10 @@ JAVA_TOOL_OPTIONS=-Xmx512m tlc -workers 1 -config AssuranceLifecycleAfterRevoke. #+end_src Use =-metadir= with a writable temporary directory when running these checks -from a read-only checkout. The safe configuration must exit 0. The reachability +from a read-only checkout. The safe and progress configurations must exit 0. +Progress assumes a stable source and clock, no revocation, and weak fairness +for each Host stage. The no-fairness configuration must violate +=EventuallyEffect=: a possible path alone does not prove progress. The reachability configuration deliberately checks =NoEffectStarted= and must find a valid =StartEffect= trace. Each other mutant must violate its named invariant; a mutant passing TLC would be a failed negative contract, not a success. @@ -36,5 +41,6 @@ mutant passing TLC would be a failed negative contract, not a success. This model does not prove a live adapter was installed by a trusted Host, a clock is actually trustworthy, a grant is cryptographically bound, or the Scheme/Python/Runtime path enforces the model. It does not yet model -counterfactual promotion, concurrent effects, or liveness/fairness. Those -remain separate Phase 4–6 exit gates. +counterfactual promotion or concurrent effects. Conditional fairness does not +establish progress when sources change, clocks advance, or grants are revoked. +Those remain separate Phase 4–6 exit gates. From 9ea6385de909bcde784132532e1e3db7a99a53e4 Mon Sep 17 00:00:00 2001 From: guangtao Date: Wed, 23 Sep 2026 19:21:06 +0800 Subject: [PATCH 22/64] Bind assurance verification to configured Host capability --- ROADMAP.org | 16 +-- .../2026-09-23-phase4-host-capability.org | 45 ++++++++ docs/rfcs/0001-causal-assurance-core.org | 5 +- modules/assurance/evidence-admission.ss | 48 +++++---- modules/assurance/host.ss | 83 +++++++++++++++ modules/assurance/interface.ss | 2 + t/assurance/unit/assurance-kernel-test.ss | 2 +- t/assurance/unit/evidence-admission-test.ss | 100 +++++++++++++++++- 8 files changed, 266 insertions(+), 35 deletions(-) create mode 100644 docs/receipts/2026-09-23-phase4-host-capability.org create mode 100644 modules/assurance/host.ss diff --git a/ROADMAP.org b/ROADMAP.org index cdb3449..5fd5481 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -209,8 +209,9 @@ Exit gates: * Phase 4 — Verifier adapters and evidence admission Status: =in-progress= (typed reported outcomes, pure structural eligibility and -sealed-support checks reuse POO Flow's verification adapter. The trusted Host -operation, evidence promotion and effect gate remain open). +sealed-support checks reuse POO Flow's verification adapter. A configured +Scheme Host capability now owns adapter, clock, lease and revocation for its +checks. Trusted deployment, evidence promotion and effect gate remain open). Deliverables: @@ -221,11 +222,12 @@ Deliverables: 3. [~] Admission rules for successful, failed, timed-out, unavailable and indeterminate verifier outcomes. The current receipt reports structural eligibility only; it cannot promote evidence or authorize release. - The sole public support-admission entrypoint requires an exact POO Flow - adapter-issued seal over the current semantic subject; no structural-only - public support predicate remains. A forged adapter presentation is rejected, - but proving that the adapter and evaluation clock belong to the trusted - Host remains open. + 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 is 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, 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/rfcs/0001-causal-assurance-core.org b/docs/rfcs/0001-causal-assurance-core.org index f740a6a..2708dad 100644 --- a/docs/rfcs/0001-causal-assurance-core.org +++ b/docs/rfcs/0001-causal-assurance-core.org @@ -262,7 +262,10 @@ 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 currently metadata, not proof of that transition. -=assurance-support-admissible?= is the sole public support-admission predicate. +=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 diff --git a/modules/assurance/evidence-admission.ss b/modules/assurance/evidence-admission.ss index 1236754..b41ef0d 100644 --- a/modules/assurance/evidence-admission.ss +++ b/modules/assurance/evidence-admission.ss @@ -18,9 +18,10 @@ (export assurance-verifier-input assurance-verifier-outcome assurance-evaluate-evidence-admission + assurance-evidence-outcome-bound? assurance-verification-subject assurance-verification-subject-snapshot - assurance-support-admissible?) + assurance-support-sealed-under-adapter?) (def (assurance-verifier-input artifact) (unless (assurance-artifact? artifact) @@ -132,6 +133,26 @@ (and (= (length left) (length right)) (equal? (list-sort 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 (assurance-verification-host identity-value operation clock lease) + (unless (and (string? identity-value) + (> (string-length identity-value) 0) + (procedure? operation) (procedure? clock) + (exact-integer? lease) (> lease 0)) + (error "invalid assurance verification Host configuration")) + (let* ((adapter + (poo-flow-verification-adapter + identity-value operation assurance-verification-subject-snapshot)) + (host (.o identity: (string-copy identity-value)))) + (hash-put! issued-hosts host + (vector (string-copy identity-value) adapter clock lease -1)) + 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) + (let (now (host-instant entry)) + (poo-flow-verify + (vector-ref entry 1) + (assurance-verification-subject + snapshot obligation evidence outcome) + now (+ now (vector-ref entry 3))))))) + +;;; 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)) + (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)) + (poo-flow-revoke-verification! (vector-ref entry 1) receipt))) diff --git a/modules/assurance/interface.ss b/modules/assurance/interface.ss index 3d07b65..2b781c3 100644 --- a/modules/assurance/interface.ss +++ b/modules/assurance/interface.ss @@ -11,5 +11,7 @@ (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/config) (export (import: :poo-flow/lambda-aitia/modules/assurance/config)) diff --git a/t/assurance/unit/assurance-kernel-test.ss b/t/assurance/unit/assurance-kernel-test.ss index b2010d5..5599f8b 100644 --- a/t/assurance/unit/assurance-kernel-test.ss +++ b/t/assurance/unit/assurance-kernel-test.ss @@ -136,7 +136,7 @@ full-software-snapshot current-obligation evidence support-outcome) 10 20)) (def (kernel-support? relation-value evidence-value) - (assurance-support-admissible? + (assurance-support-sealed-under-adapter? relation-value evidence-value current-obligation full-software-snapshot support-outcome support-adapter support-receipt 11)) diff --git a/t/assurance/unit/evidence-admission-test.ss b/t/assurance/unit/evidence-admission-test.ss index 527f79a..9d3779b 100644 --- a/t/assurance/unit/evidence-admission-test.ss +++ b/t/assurance/unit/evidence-admission-test.ss @@ -4,6 +4,7 @@ ;;; 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/src/module-system/contribution/verification poo-flow-verification-adapter poo-flow-verify @@ -91,7 +92,7 @@ assurance-verification-subject-snapshot)) (def (verified-support relation-value evidence-value obligation-value snapshot-value outcome-value receipt-value now-value) - (assurance-support-admissible? + (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) @@ -248,7 +249,7 @@ (test-case "copied seal and presentation mutation cannot forge support" (check-equal? - (assurance-support-admissible? + (assurance-support-sealed-under-adapter? trusted-discharge trusted-evidence trusted-obligation trusted-snapshot trusted-outcome (.o identity: "host/native-test" @@ -256,7 +257,7 @@ trusted-receipt 11) #f) (check-equal? - (assurance-support-admissible? + (assurance-support-sealed-under-adapter? trusted-discharge trusted-evidence trusted-obligation trusted-snapshot trusted-outcome (.o (:: @ trusted-adapter)) trusted-receipt 11) @@ -288,12 +289,101 @@ trusted-receipt 11) #f) (check-equal? - (assurance-support-admissible? + (assurance-support-sealed-under-adapter? trusted-discharge trusted-evidence trusted-obligation trusted-snapshot trusted-outcome untrusted-adapter - trusted-receipt 11) + 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)) + (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)) + (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)) + (check-equal? + (assurance-host-verify + host trusted-snapshot trusted-obligation trusted-evidence + trusted-outcome) + #f))) + (test-case "expiry and revocation remove verified support" (check-equal? (verified-support From e2bc8284751743cf288e080fc5bbabdd6ae5cdd9 Mon Sep 17 00:00:00 2001 From: guangtao Date: Wed, 23 Sep 2026 20:49:50 +0800 Subject: [PATCH 23/64] Issue source-bound Host admissions without snapshot mutation --- .github/workflows/ci.yml | 2 +- ROADMAP.org | 16 +- .../2026-09-23-phase4-admission-ledger.org | 49 +++++ docs/rfcs/0001-causal-assurance-core.org | 10 +- modules/assurance/evidence-admission.ss | 41 +++-- modules/assurance/host.ss | 168 ++++++++++++++++-- t/assurance/unit/evidence-admission-test.ss | 164 ++++++++++++++++- 7 files changed, 412 insertions(+), 38 deletions(-) create mode 100644 docs/receipts/2026-09-23-phase4-admission-ledger.org diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 535ce8e..c354aa3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,7 +89,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: repository: tao3k/poo-flow - ref: 8aa14eafe87ed63d32295f3eac5e5a4eda6658f5 + ref: 90d1c79f474f89a4f975b282611de55662301b74 path: poo-flow submodules: false diff --git a/ROADMAP.org b/ROADMAP.org index 5fd5481..45241f3 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -210,8 +210,9 @@ Exit gates: 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 and revocation for its -checks. Trusted deployment, evidence promotion and effect gate remain open). +Scheme Host capability now owns adapter, clock, lease, current snapshot source, +revocation and a separate candidate admission ledger. Trusted deployment, +decision consumption and the Runtime effect gate remain open). Deliverables: @@ -220,14 +221,19 @@ Deliverables: 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. The current receipt reports structural - eligibility only; it cannot promote evidence or authorize release. + 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 is trusted remains open. + 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, 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..7757a55 --- /dev/null +++ b/docs/receipts/2026-09-23-phase4-admission-ledger.org @@ -0,0 +1,49 @@ +# 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. + +=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/rfcs/0001-causal-assurance-core.org b/docs/rfcs/0001-causal-assurance-core.org index 2708dad..f83ca3e 100644 --- a/docs/rfcs/0001-causal-assurance-core.org +++ b/docs/rfcs/0001-causal-assurance-core.org @@ -261,7 +261,15 @@ 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 currently metadata, not proof of that transition. +=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; +source changes revoke earlier seals, including if the old cut reappears. +This is a Host-local admission candidate, not a decision or effect grant. =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 diff --git a/modules/assurance/evidence-admission.ss b/modules/assurance/evidence-admission.ss index b41ef0d..be5d496 100644 --- a/modules/assurance/evidence-admission.ss +++ b/modules/assurance/evidence-admission.ss @@ -20,6 +20,7 @@ assurance-evaluate-evidence-admission assurance-evidence-outcome-bound? assurance-verification-subject + assurance-snapshot-semantic-digest assurance-verification-subject-snapshot assurance-support-sealed-under-adapter?) @@ -95,16 +96,8 @@ snapshot: snapshot-value obligation: obligation-value evidence: evidence-value outcome: outcome-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")) - (let ((snapshot-value (.ref subject-value 'snapshot))) - (assurance-canonical-digest - (list 'lambda-aitia.verification-subject - (.ref snapshot-value 'identity) +(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) @@ -120,10 +113,30 @@ (map assurance-relation-canonical (.ref snapshot-value 'relations)) (.ref snapshot-value 'unresolved) - (.ref snapshot-value 'conflicts) - (assurance-node-canonical (.ref subject-value 'obligation)) - (assurance-node-canonical (.ref subject-value 'evidence)) - (outcome-canonical (.ref subject-value 'outcome)))))) + (.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) diff --git a/modules/assurance/host.ss b/modules/assurance/host.ss index b150aa8..64825ce 100644 --- a/modules/assurance/host.ss +++ b/modules/assurance/host.ss @@ -7,20 +7,29 @@ ;;; lease are private to the exact issued Host object; neither an editable POO ;;; presentation nor a per-call time argument selects authority. (import (only-in :clan/poo/object .o .ref .slot? object?) + (only-in :std/hash/misc hash-remove!) (only-in :poo-flow/src/module-system/contribution/verification poo-flow-verification-adapter poo-flow-verify + poo-flow-verification-valid? poo-flow-revoke-verification!) + (only-in :poo-flow/lambda-aitia/modules/assurance/types + assurance-snapshot?) (only-in :poo-flow/lambda-aitia/modules/assurance/evidence-admission assurance-evaluate-evidence-admission assurance-evidence-outcome-bound? assurance-verification-subject + assurance-snapshot-semantic-digest assurance-verification-subject-snapshot assurance-support-sealed-under-adapter?)) (export assurance-verification-host assurance-host-verify - assurance-host-sealed-support? assurance-host-revoke!) + assurance-host-sealed-support? assurance-host-revoke! + assurance-host-admit assurance-host-admission-current? + assurance-host-revoke-admission!) (def issued-hosts (make-hash-table-eq weak-keys: #t)) +(def issued-host-receipts (make-hash-table-eq weak-keys: #t)) +(def issued-admissions (make-hash-table-eq weak-keys: #t)) (def (host-entry host) (let (entry (hash-get issued-hosts host)) @@ -41,18 +50,47 @@ (vector-set! entry 4 now) now)) -(def (assurance-verification-host identity-value operation clock lease) +(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 (assurance-snapshot? snapshot) + (invalidate-host-cut! entry) + (error "assurance Host snapshot source returned an invalid 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) (unless (and (string? identity-value) (> (string-length identity-value) 0) (procedure? operation) (procedure? clock) - (exact-integer? lease) (> lease 0)) + (exact-integer? lease) (> lease 0) + (procedure? snapshot-source)) (error "invalid assurance verification Host configuration")) (let* ((adapter (poo-flow-verification-adapter identity-value operation assurance-verification-subject-snapshot)) (host (.o identity: (string-copy identity-value)))) (hash-put! issued-hosts host - (vector (string-copy identity-value) adapter clock lease -1)) + (vector (string-copy identity-value) adapter clock lease -1 + snapshot-source 0 #f 0)) host)) ;;; An eligible report alone is never an issued seal. The configured operation @@ -63,21 +101,125 @@ (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) (let (now (host-instant entry)) - (poo-flow-verify - (vector-ref entry 1) - (assurance-verification-subject - snapshot obligation evidence outcome) - now (+ now (vector-ref entry 3))))))) + (let (receipt + (poo-flow-verify + (vector-ref entry 1) + (assurance-verification-subject + snapshot obligation evidence outcome) + now (+ now (vector-ref entry 3)))) + (when receipt + (hash-put! issued-host-receipts receipt + (vector host (vector-ref entry 8)))) + receipt))))) ;;; 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)) - (assurance-support-sealed-under-adapter? - relation evidence obligation snapshot outcome - (vector-ref entry 1) receipt (host-instant entry)))) + (let (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)) + (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)) + (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 + (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 (assurance-host-admission-current? host admission) + (let* ((host-state (required-host-entry host)) + (issued (admission-entry host admission))) + (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) + (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)))))) + +(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/t/assurance/unit/evidence-admission-test.ss b/t/assurance/unit/evidence-admission-test.ss index 9d3779b..e6ac52c 100644 --- a/t/assurance/unit/evidence-admission-test.ss +++ b/t/assurance/unit/evidence-admission-test.ss @@ -72,6 +72,25 @@ (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" @@ -306,7 +325,7 @@ (equal? (.ref (.ref subject-value 'evidence) 'content-digest) digest-b))) - (lambda () now) 9)) + (lambda () now) 9 (lambda () trusted-snapshot))) (receipt (assurance-host-verify host trusted-snapshot trusted-obligation trusted-evidence @@ -355,7 +374,7 @@ (assurance-verification-host "host/revocation-test" (lambda (subject-value issued-at expires-at) #t) - (lambda () 11) 9)) + (lambda () 11) 9 (lambda () trusted-snapshot))) (receipt (assurance-host-verify host trusted-snapshot trusted-obligation trusted-evidence @@ -377,11 +396,148 @@ (assurance-verification-host "host/refusal-test" (lambda (subject-value issued-at expires-at) #f) - (lambda () 11) 9)) + (lambda () 11) 9 (lambda () trusted-snapshot))) (check-equal? (assurance-host-verify host trusted-snapshot trusted-obligation trusted-evidence - trusted-outcome) + 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-equal? + (assurance-host-sealed-support? + host trusted-discharge trusted-evidence trusted-obligation + trusted-snapshot trusted-outcome receipt) + #f) + (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 "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-equal? (assurance-host-admission-current? host admission) #f) + (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" From e955a883366314eaa02fc46d073821135b397556 Mon Sep 17 00:00:00 2001 From: guangtao Date: Wed, 23 Sep 2026 20:55:54 +0800 Subject: [PATCH 24/64] Reject noncanonical Host snapshots before admission --- .../2026-09-23-phase4-admission-ledger.org | 5 +++- docs/rfcs/0001-causal-assurance-core.org | 3 ++- modules/assurance/funs.ss | 25 +++++++++++++++++++ modules/assurance/host.ss | 7 ++++-- t/assurance/unit/evidence-admission-test.ss | 7 +++--- 5 files changed, 40 insertions(+), 7 deletions(-) diff --git a/docs/receipts/2026-09-23-phase4-admission-ledger.org b/docs/receipts/2026-09-23-phase4-admission-ledger.org index 7757a55..e0fe9f6 100644 --- a/docs/receipts/2026-09-23-phase4-admission-ledger.org +++ b/docs/receipts/2026-09-23-phase4-admission-ledger.org @@ -26,7 +26,10 @@ 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. +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 diff --git a/docs/rfcs/0001-causal-assurance-core.org b/docs/rfcs/0001-causal-assurance-core.org index f83ca3e..dcc19ae 100644 --- a/docs/rfcs/0001-causal-assurance-core.org +++ b/docs/rfcs/0001-causal-assurance-core.org @@ -268,7 +268,8 @@ 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; -source changes revoke earlier seals, including if the old cut reappears. +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-sealed-support?= is the configured Host seal predicate, not a promotion transition. diff --git a/modules/assurance/funs.ss b/modules/assurance/funs.ss index 3fa45ee..5c9f914 100644 --- a/modules/assurance/funs.ss +++ b/modules/assurance/funs.ss @@ -22,6 +22,7 @@ (export assurance-node-canonical assurance-relation-canonical assurance-canonical-digest assurance-snapshot + assurance-snapshot-canonical? assurance-bind-obligation-to-snapshot assurance-invalidation-graph assurance-invalidate assurance-derive-replacement-requirements @@ -281,6 +282,30 @@ nodes: ordered-nodes relations: ordered-relations unresolved: missing conflicts: all-conflicts))))))))) +;;; 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. diff --git a/modules/assurance/host.ss b/modules/assurance/host.ss index 64825ce..92a13ca 100644 --- a/modules/assurance/host.ss +++ b/modules/assurance/host.ss @@ -14,6 +14,8 @@ poo-flow-revoke-verification!) (only-in :poo-flow/lambda-aitia/modules/assurance/types assurance-snapshot?) + (only-in :poo-flow/lambda-aitia/modules/assurance/funs + assurance-snapshot-canonical?) (only-in :poo-flow/lambda-aitia/modules/assurance/evidence-admission assurance-evaluate-evidence-admission assurance-evidence-outcome-bound? @@ -57,9 +59,10 @@ (def (host-current-snapshot entry) (let (snapshot ((vector-ref entry 5))) - (unless (assurance-snapshot? snapshot) + (unless (and (assurance-snapshot? snapshot) + (assurance-snapshot-canonical? snapshot)) (invalidate-host-cut! entry) - (error "assurance Host snapshot source returned an invalid snapshot")) + (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 diff --git a/t/assurance/unit/evidence-admission-test.ss b/t/assurance/unit/evidence-admission-test.ss index e6ac52c..58ece7a 100644 --- a/t/assurance/unit/evidence-admission-test.ss +++ b/t/assurance/unit/evidence-admission-test.ss @@ -420,11 +420,11 @@ trusted-snapshot trusted-outcome receipt) #t) (set! current (.o (:: @ trusted-snapshot) fact-cut: "event-cut/2")) - (check-equal? + (check-exception (assurance-host-sealed-support? host trusted-discharge trusted-evidence trusted-obligation trusted-snapshot trusted-outcome receipt) - #f) + Error?) (set! current trusted-snapshot) (check-equal? (assurance-host-sealed-support? @@ -482,7 +482,8 @@ (.o (:: @ candidate-snapshot) fact-cut: "event-cut/2")) (check-equal? (.ref current 'digest) (.ref candidate-snapshot 'digest)) - (check-equal? (assurance-host-admission-current? host admission) #f) + (check-exception + (assurance-host-admission-current? host admission) Error?) (set! current candidate-snapshot) (check-equal? (assurance-host-admission-current? host admission) #f))) From 1416c28e5f5bcfd64f90ab83220beda5f0bbe501 Mon Sep 17 00:00:00 2001 From: guangtao Date: Wed, 23 Sep 2026 21:10:17 +0800 Subject: [PATCH 25/64] Bind Host admissions to current assurance support edges --- ROADMAP.org | 6 +- docs/rfcs/0001-causal-assurance-core.org | 7 +++ modules/assurance/evidence-admission.ss | 19 +++++- modules/assurance/host.ss | 64 +++++++++++++-------- t/assurance/unit/evidence-admission-test.ss | 38 ++++++++++++ 5 files changed, 106 insertions(+), 28 deletions(-) diff --git a/ROADMAP.org b/ROADMAP.org index 45241f3..dc89649 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -211,8 +211,10 @@ Exit gates: 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. Trusted deployment, -decision consumption and the Runtime effect gate remain open). +revocation and a separate candidate admission ledger. An exact current +assurance-edge witness can consume that ledger without promoting the snapshot +evidence slot. Full Decision closure, trusted deployment, Cedar policy and the +Runtime effect gate remain open). Deliverables: diff --git a/docs/rfcs/0001-causal-assurance-core.org b/docs/rfcs/0001-causal-assurance-core.org index dcc19ae..b7be346 100644 --- a/docs/rfcs/0001-causal-assurance-core.org +++ b/docs/rfcs/0001-causal-assurance-core.org @@ -271,6 +271,13 @@ 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-sealed-support?= is the configured Host seal predicate, not a promotion transition. The lower-level =assurance-support-sealed-under-adapter?= is conditional on a diff --git a/modules/assurance/evidence-admission.ss b/modules/assurance/evidence-admission.ss index be5d496..6fb9926 100644 --- a/modules/assurance/evidence-admission.ss +++ b/modules/assurance/evidence-admission.ss @@ -22,6 +22,7 @@ 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) @@ -168,7 +169,7 @@ ;;; Shape alone never grants support. This check is private to the sealed ;;; admission boundary and cannot be used as a public authorization shortcut. -(def (structural-support? relation evidence obligation) +(def (structural-support? relation evidence obligation admission-state) (and (assurance-relation? relation) (assurance-evidence? evidence) (assurance-obligation? obligation) @@ -181,14 +182,26 @@ (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) + (eq? (.ref evidence 'admission-state) admission-state) (not (memq (.ref relation 'modality) '(hypothesized counterfactual))))) +;;; A relation is only a structural witness until a Host checks the separate +;;; admission capability. The candidate state is never rewritten in the cut. +(def (assurance-candidate-support-structural? + relation evidence obligation snapshot) + (and (structural-support? relation evidence obligation 'candidate) + (assurance-snapshot? snapshot) + (let (current + (snapshot-relation snapshot (.ref relation 'identity))) + (and current + (equal? (assurance-relation-canonical current) + (assurance-relation-canonical relation)))))) + ;;; Conditional low-level check: this does not establish that the supplied ;;; adapter and clock belong to the application-configured Host. (def (assurance-support-sealed-under-adapter? relation evidence obligation snapshot outcome adapter issued-receipt now) - (and (structural-support? relation evidence obligation) + (and (structural-support? relation evidence obligation 'admitted) (assurance-snapshot? snapshot) (assurance-verifier-outcome? outcome) (let ((current-relation diff --git a/modules/assurance/host.ss b/modules/assurance/host.ss index 92a13ca..f6c7ddc 100644 --- a/modules/assurance/host.ss +++ b/modules/assurance/host.ss @@ -22,11 +22,13 @@ assurance-verification-subject assurance-snapshot-semantic-digest assurance-verification-subject-snapshot + assurance-candidate-support-structural? assurance-support-sealed-under-adapter?)) (export assurance-verification-host assurance-host-verify assurance-host-sealed-support? assurance-host-revoke! assurance-host-admit assurance-host-admission-current? + assurance-host-admitted-support? assurance-host-revoke-admission!) (def issued-hosts (make-hash-table-eq weak-keys: #t)) @@ -192,33 +194,49 @@ (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) + (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* ((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) - (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)))))) + (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 (assurance-host-revoke-admission! host admission) (required-host-entry host) diff --git a/t/assurance/unit/evidence-admission-test.ss b/t/assurance/unit/evidence-admission-test.ss index 58ece7a..e302035 100644 --- a/t/assurance/unit/evidence-admission-test.ss +++ b/t/assurance/unit/evidence-admission-test.ss @@ -467,6 +467,44 @@ (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 From c105cea1839a2de58ec5059df93d042426f01540 Mon Sep 17 00:00:00 2001 From: guangtao Date: Wed, 23 Sep 2026 21:58:04 +0800 Subject: [PATCH 26/64] Assess Host-backed claim support requirements without authority --- ROADMAP.org | 6 +- docs/rfcs/0001-causal-assurance-core.org | 8 + modules/assurance/host.ss | 143 ++++++++++++++++- modules/assurance/types.ss | 48 ++++++ t/assurance/unit/required-support-test.ss | 183 ++++++++++++++++++++++ 5 files changed, 384 insertions(+), 4 deletions(-) create mode 100644 t/assurance/unit/required-support-test.ss diff --git a/ROADMAP.org b/ROADMAP.org index dc89649..f7f5917 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -213,8 +213,10 @@ 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. Full Decision closure, trusted deployment, Cedar policy and the -Runtime effect gate remain open). +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). Deliverables: diff --git a/docs/rfcs/0001-causal-assurance-core.org b/docs/rfcs/0001-causal-assurance-core.org index b7be346..272028a 100644 --- a/docs/rfcs/0001-causal-assurance-core.org +++ b/docs/rfcs/0001-causal-assurance-core.org @@ -278,6 +278,14 @@ 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. =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 diff --git a/modules/assurance/host.ss b/modules/assurance/host.ss index f6c7ddc..7f1a654 100644 --- a/modules/assurance/host.ss +++ b/modules/assurance/host.ss @@ -7,15 +7,18 @@ ;;; lease are private to the exact issued Host object; neither an editable POO ;;; presentation nor a per-call time argument selects authority. (import (only-in :clan/poo/object .o .ref .slot? object?) + (only-in :std/list/list find every filter delete-duplicates/hash) (only-in :std/hash/misc hash-remove!) + :poo-flow/src/module-system/contribution/model (only-in :poo-flow/src/module-system/contribution/verification poo-flow-verification-adapter poo-flow-verify poo-flow-verification-valid? poo-flow-revoke-verification!) (only-in :poo-flow/lambda-aitia/modules/assurance/types - assurance-snapshot?) + assurance-snapshot? assurance-claim? assurance-obligation? + AssuranceRequiredSupport AssessmentRequiredSupport) (only-in :poo-flow/lambda-aitia/modules/assurance/funs - assurance-snapshot-canonical?) + assurance-snapshot-canonical? assurance-node-canonical) (only-in :poo-flow/lambda-aitia/modules/assurance/evidence-admission assurance-evaluate-evidence-admission assurance-evidence-outcome-bound? @@ -29,6 +32,7 @@ assurance-host-sealed-support? assurance-host-revoke! assurance-host-admit assurance-host-admission-current? assurance-host-admitted-support? + assurance-host-required-support assurance-host-revoke-admission!) (def issued-hosts (make-hash-table-eq weak-keys: #t)) @@ -238,6 +242,141 @@ 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))) + +(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-flow-check-model + AssessmentRequiredSupport + (.o (:: @ (poo-flow-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-flow-check-model + AssuranceRequiredSupport + (.o (:: @ (poo-flow-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 (assurance-host-revoke-admission! host admission) (required-host-entry host) (let (issued (admission-entry host admission)) diff --git a/modules/assurance/types.ss b/modules/assurance/types.ss index 0801260..fb91bb3 100644 --- a/modules/assurance/types.ss +++ b/modules/assurance/types.ss @@ -26,6 +26,7 @@ AssuranceVerifierChoice AssuranceVerifierChoiceReceipt AssuranceVerifierInput AssuranceVerifierOutcome AssuranceEvidenceAdmissionReceipt AssuranceVerificationSubject + AssuranceRequiredSupport AssessmentRequiredSupport AssuranceCompositionRequirement AssuranceCompositionDerivation make-assurance-invalidation-receipt-record assurance-node? assurance-artifact? assurance-claim? @@ -42,6 +43,7 @@ assurance-verifier-choice? assurance-verifier-choice-receipt? assurance-verifier-input? assurance-verifier-outcome? assurance-evidence-admission-receipt? + assurance-required-support? assessment-required-support? assurance-verification-subject? assurance-composition-requirement? assurance-composition-derivation?) @@ -493,6 +495,42 @@ (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 composition needs its own snapshot-bound obligation. Component support ;;; remains separate and cannot discharge this structural requirement. (def AssuranceCompositionRequirement @@ -609,6 +647,16 @@ (def (assurance-evidence-admission-receipt? value) (and (poo-flow-model? AssuranceEvidenceAdmissionReceipt value) (eq? (.ref value 'eligible?) (not (.ref value 'blocker))))) +(def (assessment-required-support? value) + (and (poo-flow-model? AssessmentRequiredSupport value) + (eq? (.ref value 'supported?) (not (.ref value 'blocker))))) +(def (assurance-required-support? value) + (and (poo-flow-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-verification-subject? value) (poo-flow-model? AssuranceVerificationSubject value)) (def (assurance-composition-requirement? value) diff --git a/t/assurance/unit/required-support-test.ss b/t/assurance/unit/required-support-test.ss new file mode 100644 index 0000000..676e2da --- /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/src/module-system/contribution/model + poo-flow-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-flow-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))))) From af032f8dc9bbffddb931e58d79daf0397d5f6375 Mon Sep 17 00:00:00 2001 From: guangtao Date: Wed, 23 Sep 2026 22:21:38 +0800 Subject: [PATCH 27/64] Bind undecided Decisions to current Host preflight --- ROADMAP.org | 7 +- docs/rfcs/0001-causal-assurance-core.org | 9 ++ modules/assurance/host.ss | 131 +++++++++++++++- modules/assurance/types.ss | 19 +++ t/assurance/unit/decision-preflight-test.ss | 165 ++++++++++++++++++++ 5 files changed, 328 insertions(+), 3 deletions(-) create mode 100644 t/assurance/unit/decision-preflight-test.ss diff --git a/ROADMAP.org b/ROADMAP.org index f7f5917..6887a45 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -216,7 +216,9 @@ 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). +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: @@ -309,7 +311,8 @@ Exit gates: * Phase 6 — POO Flow integration and effect gate -Status: =planned= +Status: =planned= (the Host Decision preflight is an inert input only; Cedar +evaluation and Runtime effect admission have not landed). Deliverables: diff --git a/docs/rfcs/0001-causal-assurance-core.org b/docs/rfcs/0001-causal-assurance-core.org index 272028a..0a62cdc 100644 --- a/docs/rfcs/0001-causal-assurance-core.org +++ b/docs/rfcs/0001-causal-assurance-core.org @@ -286,6 +286,15 @@ 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-sealed-support?= is the configured Host seal predicate, not a promotion transition. The lower-level =assurance-support-sealed-under-adapter?= is conditional on a diff --git a/modules/assurance/host.ss b/modules/assurance/host.ss index 7f1a654..2557e71 100644 --- a/modules/assurance/host.ss +++ b/modules/assurance/host.ss @@ -16,7 +16,9 @@ poo-flow-revoke-verification!) (only-in :poo-flow/lambda-aitia/modules/assurance/types assurance-snapshot? assurance-claim? assurance-obligation? - AssuranceRequiredSupport AssessmentRequiredSupport) + assurance-decision? assurance-decision-preflight? + AssuranceRequiredSupport AssessmentRequiredSupport + AssuranceDecisionPreflight) (only-in :poo-flow/lambda-aitia/modules/assurance/funs assurance-snapshot-canonical? assurance-node-canonical) (only-in :poo-flow/lambda-aitia/modules/assurance/evidence-admission @@ -33,11 +35,15 @@ assurance-host-admit assurance-host-admission-current? assurance-host-admitted-support? assurance-host-required-support + assurance-host-preflight-decision + assurance-host-decision-preflight-current? + assurance-host-revoke-decision-preflight! assurance-host-revoke-admission!) (def issued-hosts (make-hash-table-eq weak-keys: #t)) (def issued-host-receipts (make-hash-table-eq weak-keys: #t)) (def issued-admissions (make-hash-table-eq weak-keys: #t)) +(def issued-decision-preflights (make-hash-table-eq weak-keys: #t)) (def (host-entry host) (let (entry (hash-get issued-hosts host)) @@ -377,6 +383,129 @@ (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-flow-check-model + AssuranceDecisionPreflight + (.o (:: @ (poo-flow-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))))))) + +(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)) diff --git a/modules/assurance/types.ss b/modules/assurance/types.ss index fb91bb3..178c936 100644 --- a/modules/assurance/types.ss +++ b/modules/assurance/types.ss @@ -27,6 +27,7 @@ AssuranceVerifierInput AssuranceVerifierOutcome AssuranceEvidenceAdmissionReceipt AssuranceVerificationSubject AssuranceRequiredSupport AssessmentRequiredSupport + AssuranceDecisionPreflight AssuranceCompositionRequirement AssuranceCompositionDerivation make-assurance-invalidation-receipt-record assurance-node? assurance-artifact? assurance-claim? @@ -44,6 +45,7 @@ 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?) @@ -531,6 +533,21 @@ (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 @@ -657,6 +674,8 @@ (pair? (.ref value 'requirements)) (every (lambda (item) (.ref item 'supported?)) (.ref value 'requirements)))))) +(def (assurance-decision-preflight? value) + (poo-flow-model? AssuranceDecisionPreflight value)) (def (assurance-verification-subject? value) (poo-flow-model? AssuranceVerificationSubject value)) (def (assurance-composition-requirement? value) diff --git a/t/assurance/unit/decision-preflight-test.ss b/t/assurance/unit/decision-preflight-test.ss new file mode 100644 index 0000000..450f6a2 --- /dev/null +++ b/t/assurance/unit/decision-preflight-test.ss @@ -0,0 +1,165 @@ +;;; -*- 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 .o .ref) + :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 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 "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))) + (old-cut current)) + (check-equal? + (assurance-host-decision-preflight-current? host preflight) #t) + (set! current + (construct (bound-obligation old-cut) relations "event-cut/2")) + (check-equal? + (assurance-host-decision-preflight-current? host preflight) #f) + (set! current old-cut) + (check-equal? + (assurance-host-decision-preflight-current? host preflight) #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))))) From 7814032ba1ebb40dc6006ea375186ef3b90de380 Mon Sep 17 00:00:00 2001 From: guangtao Date: Wed, 23 Sep 2026 23:02:33 +0800 Subject: [PATCH 28/64] Project current Decision preflights into Cedar requests --- ROADMAP.org | 12 ++++-- docs/rfcs/0001-causal-assurance-core.org | 8 ++++ modules/assurance/host.ss | 39 +++++++++++++++++++ t/assurance/unit/decision-preflight-test.ss | 43 ++++++++++++++++++++- 4 files changed, 97 insertions(+), 5 deletions(-) diff --git a/ROADMAP.org b/ROADMAP.org index 6887a45..fcc8428 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -311,8 +311,10 @@ Exit gates: * Phase 6 — POO Flow integration and effect gate -Status: =planned= (the Host Decision preflight is an inert input only; Cedar -evaluation and Runtime effect admission have not landed). +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). Deliverables: @@ -326,8 +328,10 @@ Deliverables: 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. diff --git a/docs/rfcs/0001-causal-assurance-core.org b/docs/rfcs/0001-causal-assurance-core.org index 0a62cdc..86e0549 100644 --- a/docs/rfcs/0001-causal-assurance-core.org +++ b/docs/rfcs/0001-causal-assurance-core.org @@ -295,6 +295,14 @@ 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 must independently bind the source cut at use, evaluate Cedar through +both engines and gate the effect. The Scheme projection does none of these. =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 diff --git a/modules/assurance/host.ss b/modules/assurance/host.ss index 2557e71..da3c3a7 100644 --- a/modules/assurance/host.ss +++ b/modules/assurance/host.ss @@ -14,6 +14,9 @@ poo-flow-verification-adapter poo-flow-verify poo-flow-verification-valid? poo-flow-revoke-verification!) + (only-in :poo-flow/src/modules/authorization/providers/cedar/interface + poo-flow-cedar-runtime-handoff? + poo-flow-cedar-authorization-request) (only-in :poo-flow/lambda-aitia/modules/assurance/types assurance-snapshot? assurance-claim? assurance-obligation? assurance-decision? assurance-decision-preflight? @@ -37,6 +40,7 @@ assurance-host-required-support assurance-host-preflight-decision assurance-host-decision-preflight-current? + assurance-host-cedar-request assurance-host-revoke-decision-preflight! assurance-host-revoke-admission!) @@ -500,6 +504,41 @@ (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 (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" + 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) + request))))) + (def (assurance-host-revoke-decision-preflight! host preflight) (required-host-entry host) (unless (decision-preflight-entry host preflight) diff --git a/t/assurance/unit/decision-preflight-test.ss b/t/assurance/unit/decision-preflight-test.ss index 450f6a2..cbba248 100644 --- a/t/assurance/unit/decision-preflight-test.ss +++ b/t/assurance/unit/decision-preflight-test.ss @@ -3,8 +3,12 @@ ;;; ;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later -(import :std/test +(import :std/test :std/error (only-in :clan/poo/object .o .ref) + (only-in :poo-flow/src/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) @@ -78,6 +82,9 @@ "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" @@ -127,6 +134,40 @@ (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)) + (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? (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? + (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 + 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))) From d5fedd4c480c6530fad9064b5f749aa464249e98 Mon Sep 17 00:00:00 2001 From: guangtao Date: Wed, 23 Sep 2026 23:30:41 +0800 Subject: [PATCH 29/64] Mark Aitia Cedar requests as preflight-only --- ROADMAP.org | 3 ++- docs/rfcs/0001-causal-assurance-core.org | 8 ++++++-- modules/assurance/host.ss | 1 + t/assurance/unit/decision-preflight-test.ss | 1 + 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/ROADMAP.org b/ROADMAP.org index fcc8428..fd9a85d 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -314,7 +314,8 @@ Exit gates: 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). +admission and Runtime effect gating have not landed. The generic Runtime +boundary rejects its preflight-only marker before Cedar evaluation). Deliverables: diff --git a/docs/rfcs/0001-causal-assurance-core.org b/docs/rfcs/0001-causal-assurance-core.org index 86e0549..471edaa 100644 --- a/docs/rfcs/0001-causal-assurance-core.org +++ b/docs/rfcs/0001-causal-assurance-core.org @@ -301,8 +301,12 @@ 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 must independently bind the source cut at use, evaluate Cedar through -both engines and gate the effect. The Scheme projection does none of these. +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 projection does none of these. =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 diff --git a/modules/assurance/host.ss b/modules/assurance/host.ss index da3c3a7..63727ac 100644 --- a/modules/assurance/host.ss +++ b/modules/assurance/host.ss @@ -521,6 +521,7 @@ (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) diff --git a/t/assurance/unit/decision-preflight-test.ss b/t/assurance/unit/decision-preflight-test.ss index cbba248..1a563b3 100644 --- a/t/assurance/unit/decision-preflight-test.ss +++ b/t/assurance/unit/decision-preflight-test.ss @@ -157,6 +157,7 @@ (.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) From d14f7bb3585944d57958d9d65db082f8d89a61ec Mon Sep 17 00:00:00 2001 From: guangtao Date: Wed, 23 Sep 2026 23:45:35 +0800 Subject: [PATCH 30/64] Recheck Host-issued Cedar requests at current source cut --- ROADMAP.org | 4 ++- docs/rfcs/0001-causal-assurance-core.org | 7 +++- modules/assurance/host.ss | 36 ++++++++++++++++++--- t/assurance/unit/decision-preflight-test.ss | 23 ++++++++++++- 4 files changed, 63 insertions(+), 7 deletions(-) diff --git a/ROADMAP.org b/ROADMAP.org index fd9a85d..9965648 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -315,7 +315,9 @@ 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). +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). Deliverables: diff --git a/docs/rfcs/0001-causal-assurance-core.org b/docs/rfcs/0001-causal-assurance-core.org index 471edaa..a1934d5 100644 --- a/docs/rfcs/0001-causal-assurance-core.org +++ b/docs/rfcs/0001-causal-assurance-core.org @@ -306,7 +306,12 @@ 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 projection does none of these. +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. =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 diff --git a/modules/assurance/host.ss b/modules/assurance/host.ss index 63727ac..9c8b9f6 100644 --- a/modules/assurance/host.ss +++ b/modules/assurance/host.ss @@ -6,7 +6,7 @@ ;;; An application-configured capability for verification. Adapter, clock and ;;; lease are private to the exact issued Host object; neither an editable POO ;;; presentation nor a per-call time argument selects authority. -(import (only-in :clan/poo/object .o .ref .slot? object?) +(import (only-in :clan/poo/object .o .ref .slot? .alist object?) (only-in :std/list/list find every filter delete-duplicates/hash) (only-in :std/hash/misc hash-remove!) :poo-flow/src/module-system/contribution/model @@ -16,7 +16,8 @@ poo-flow-revoke-verification!) (only-in :poo-flow/src/modules/authorization/providers/cedar/interface poo-flow-cedar-runtime-handoff? - poo-flow-cedar-authorization-request) + poo-flow-cedar-authorization-request + poo-flow-cedar-authorization-request?) (only-in :poo-flow/lambda-aitia/modules/assurance/types assurance-snapshot? assurance-claim? assurance-obligation? assurance-decision? assurance-decision-preflight? @@ -40,7 +41,7 @@ assurance-host-required-support assurance-host-preflight-decision assurance-host-decision-preflight-current? - assurance-host-cedar-request + assurance-host-cedar-request assurance-host-cedar-request-current? assurance-host-revoke-decision-preflight! assurance-host-revoke-admission!) @@ -48,6 +49,7 @@ (def issued-host-receipts (make-hash-table-eq weak-keys: #t)) (def issued-admissions (make-hash-table-eq weak-keys: #t)) (def issued-decision-preflights (make-hash-table-eq weak-keys: #t)) +(def issued-cedar-requests (make-hash-table-eq weak-keys: #t)) (def (host-entry host) (let (entry (hash-get issued-hosts host)) @@ -507,6 +509,11 @@ ;;; 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) + (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) @@ -538,7 +545,28 @@ (and (= epoch-value (vector-ref issued 7)) (same-host-cut? host-state snapshot-value epoch-value) (assurance-host-decision-preflight-current? host preflight) - request))))) + (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) diff --git a/t/assurance/unit/decision-preflight-test.ss b/t/assurance/unit/decision-preflight-test.ss index 1a563b3..e58d820 100644 --- a/t/assurance/unit/decision-preflight-test.ss +++ b/t/assurance/unit/decision-preflight-test.ss @@ -147,10 +147,20 @@ (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") @@ -165,6 +175,8 @@ (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))) @@ -179,16 +191,25 @@ (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))) + (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)) From ad2abff7ca53492dfa43a3ad28a74bae6e600e33 Mon Sep 17 00:00:00 2001 From: guangtao Date: Thu, 24 Sep 2026 00:12:52 +0800 Subject: [PATCH 31/64] Require source-bound Cedar capability for future Aitia release --- ROADMAP.org | 5 ++++- docs/rfcs/0001-causal-assurance-core.org | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/ROADMAP.org b/ROADMAP.org index 9965648..751f842 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -317,7 +317,10 @@ 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). +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: diff --git a/docs/rfcs/0001-causal-assurance-core.org b/docs/rfcs/0001-causal-assurance-core.org index a1934d5..18be3aa 100644 --- a/docs/rfcs/0001-causal-assurance-core.org +++ b/docs/rfcs/0001-causal-assurance-core.org @@ -312,6 +312,12 @@ 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 From 1d1ddc170daabf34b8b27feb6988293a13f66acd Mon Sep 17 00:00:00 2001 From: guangtao Date: Thu, 24 Sep 2026 00:52:30 +0800 Subject: [PATCH 32/64] Add POO-native SDLC design contract review --- .github/workflows/ci.yml | 15 +- ROADMAP.org | 12 +- bindings/c/aitia-runtime.c | 7 +- .../rfcs/0003-composition-value-interface.org | 10 +- justfile | 3 +- modules/sdlc/README.org | 32 +++ modules/sdlc/design.ss | 229 ++++++++++++++++++ modules/sdlc/interface.ss | 2 + proof/lean/AxiomAudit.lean | 19 ++ proof/lean/LambdaAitiaProof.lean | 1 + proof/lean/LambdaAitiaProof/Modules/Sdlc.lean | 5 + .../LambdaAitiaProof/Modules/Sdlc/Design.lean | 53 ++++ proof/lean/README.org | 14 +- proof/lean/lakefile.lean | 3 + t/fixtures/composition-value/aitia-default.ss | 4 + t/modules/unit/composition-identity-test.ss | 9 + t/sdlc/unit/design-contract-test.ss | 158 ++++++++++++ 17 files changed, 567 insertions(+), 9 deletions(-) create mode 100644 modules/sdlc/design.ss create mode 100644 proof/lean/AxiomAudit.lean create mode 100644 proof/lean/LambdaAitiaProof/Modules/Sdlc.lean create mode 100644 proof/lean/LambdaAitiaProof/Modules/Sdlc/Design.lean create mode 100644 t/fixtures/composition-value/aitia-default.ss create mode 100644 t/sdlc/unit/design-contract-test.ss diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c354aa3..30c08e5 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' diff --git a/ROADMAP.org b/ROADMAP.org index 751f842..7fe46ae 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -350,7 +350,17 @@ Exit gates: * Phase 7 — Software/SDLC reference qualification -Status: =planned= +Status: =active= for an inert design-contract replacement slice; full reference +qualification remains open. + +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, model-to-code +mapping, admitted verifier receipt or release decision. The first reference case is a software change crossing requirement, implementation, verification, release-decision and effect boundaries. It 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/docs/rfcs/0003-composition-value-interface.org b/docs/rfcs/0003-composition-value-interface.org index 92664db..76cafd2 100644 --- a/docs/rfcs/0003-composition-value-interface.org +++ b/docs/rfcs/0003-composition-value-interface.org @@ -196,11 +196,11 @@ not established the desired downstream contract. * Acceptance gates -- [ ] An =aitia.ss= containing only =(use-composition aitia)= evaluates to one +- [X] 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. +- [X] 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. @@ -210,6 +210,12 @@ not established the desired downstream contract. - [ ] Direct compilation, atomic behavior tests, a real Aitia Scenario and the existing composition macro scale gate pass with exact receipts. +The checked items have direct behavior coverage in +=t/modules/unit/composition-identity-test.ss= and POO Flow's +=t/profile-composition-value-test.ss=. Other items remain open; the default +Profile selecting five Modules is not yet evidence that every declared slot +and downstream Scenario has been independently qualified. + * Relationship to other decisions - RFC 0001 defines the causal assurance vocabulary carried by the default diff --git a/justfile b/justfile index 81c7d0e..b60773b 100644 --- a/justfile +++ b/justfile @@ -62,7 +62,8 @@ 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" # Aitia owns its native ABI and Python Runtime checks. [group('check')] diff --git a/modules/sdlc/README.org b/modules/sdlc/README.org index 0dfd17f..0019739 100644 --- a/modules/sdlc/README.org +++ b/modules/sdlc/README.org @@ -13,6 +13,38 @@ 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. + 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 diff --git a/modules/sdlc/design.ss b/modules/sdlc/design.ss new file mode 100644 index 0000000..bd764bf --- /dev/null +++ b/modules/sdlc/design.ss @@ -0,0 +1,229 @@ +;;; 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/src/module-system/contribution/model + :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-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-flow-model? SdlcDesignClause value)) +(def (sdlc-design-variation? value) + (poo-flow-model? SdlcDesignVariation value)) +(def (sdlc-design-contract? value) + (and (poo-flow-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-flow-check-model + SdlcDesignClause + (.o (:: @ (poo-flow-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-flow-check-model + SdlcDesignVariation + (.o (:: @ (poo-flow-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-flow-check-model + SdlcDesignContract + (.o (:: @ (poo-flow-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/README.org b/proof/lean/README.org index 49d82eb..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 @@ -30,3 +30,15 @@ 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/t/fixtures/composition-value/aitia-default.ss b/t/fixtures/composition-value/aitia-default.ss new file mode 100644 index 0000000..2872e41 --- /dev/null +++ b/t/fixtures/composition-value/aitia-default.ss @@ -0,0 +1,4 @@ +;;; SPDX-FileCopyrightText: 2026 tao3k team and Contributors +;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +(use-composition aitia) diff --git a/t/modules/unit/composition-identity-test.ss b/t/modules/unit/composition-identity-test.ss index 01d81ef..2d8daa3 100644 --- a/t/modules/unit/composition-identity-test.ss +++ b/t/modules/unit/composition-identity-test.ss @@ -79,4 +79,13 @@ (check-equal? (.ref (.ref (car (.ref case-value 'profiles)) 'delivery) 'provider) 'github) + (check-equal? (.ref (.ref case-value 'admission) 'accepted?) #t))) + (test-case "zero-override downstream value needs no imports" + (let (case-value + (poo-flow-load-composition-value + "t/fixtures/composition-value/aitia-default.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? (.ref (.ref case-value 'admission) 'accepted?) #t))))) diff --git a/t/sdlc/unit/design-contract-test.ss b/t/sdlc/unit/design-contract-test.ss new file mode 100644 index 0000000..ad1122c --- /dev/null +++ b/t/sdlc/unit/design-contract-test.ss @@ -0,0 +1,158 @@ +;;; 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 "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?)))) From 4b2bfbb09a36e140b6fb065631efcd72496837e0 Mon Sep 17 00:00:00 2001 From: guangtao Date: Thu, 24 Sep 2026 08:53:49 +0800 Subject: [PATCH 33/64] Bind SDLC design guarantees to assurance source snapshots --- ROADMAP.org | 11 +- modules/sdlc/README.org | 11 ++ modules/sdlc/design.ss | 10 ++ modules/sdlc/implementation.ss | 141 ++++++++++++++++++++++ modules/sdlc/interface.ss | 2 + t/sdlc/unit/design-implementation-test.ss | 105 ++++++++++++++++ 6 files changed, 276 insertions(+), 4 deletions(-) create mode 100644 modules/sdlc/implementation.ss create mode 100644 t/sdlc/unit/design-implementation-test.ss diff --git a/ROADMAP.org b/ROADMAP.org index 7fe46ae..dc842e2 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -350,8 +350,8 @@ Exit gates: * Phase 7 — Software/SDLC reference qualification -Status: =active= for an inert design-contract replacement slice; full reference -qualification remains open. +Status: =active= for inert design-contract replacement and snapshot-bound +implementation-mapping slices; full reference qualification remains open. The first job-executor regression now records an ADR-bound POO-native design contract with assumptions, guarantees, state ownership, effects and variation @@ -359,8 +359,11 @@ 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, model-to-code -mapping, admitted verifier receipt or release decision. +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. The first reference case is a software change crossing requirement, implementation, verification, release-decision and effect boundaries. It diff --git a/modules/sdlc/README.org b/modules/sdlc/README.org index 0019739..59c445f 100644 --- a/modules/sdlc/README.org +++ b/modules/sdlc/README.org @@ -45,6 +45,17 @@ 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. + 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 diff --git a/modules/sdlc/design.ss b/modules/sdlc/design.ss index bd764bf..4ba8ab3 100644 --- a/modules/sdlc/design.ss +++ b/modules/sdlc/design.ss @@ -8,12 +8,15 @@ (only-in :gerbil/core list-sort) :std/list/list :poo-flow/src/module-system/contribution/model + (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) @@ -123,6 +126,13 @@ (ordered-keys (.ref value 'assumptions) clause-key) (ordered-keys (.ref value 'guarantees) clause-key) (ordered-keys (.ref value 'variations) variation-key))) +(def (sdlc-design-contract-digest value) + (unless (sdlc-design-contract? value) + (error "invalid design contract for digest")) + (assurance-canonical-digest + (list 'lambda-aitia.sdlc-design-contract + (.ref value 'identity) (.ref value 'revision) + (contract-body value)))) (def (changed-ids old-values new-values key) (ordered-ids (append diff --git a/modules/sdlc/implementation.ss b/modules/sdlc/implementation.ss new file mode 100644 index 0000000..776bc35 --- /dev/null +++ b/modules/sdlc/implementation.ss @@ -0,0 +1,141 @@ +;;; SPDX-FileCopyrightText: 2026 tao3k team and Contributors +;;; +;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +;;; An inert, snapshot-bound design-to-source declaration. Source currentness +;;; is checked against Assurance; semantic conformance is not inferred. +(import (only-in :clan/poo/object .o .ref) + (only-in :gerbil/core list-sort) + :std/list/list + :poo-flow/src/module-system/contribution/model + (only-in :poo-flow/lambda-aitia/modules/assurance/interface + assurance-artifact? assurance-digest? + assurance-snapshot? assurance-snapshot-canonical?) + :poo-flow/lambda-aitia/modules/sdlc/types + :poo-flow/lambda-aitia/modules/sdlc/design) + +(export SdlcDesignImplementationLink sdlc-design-implementation-link? + sdlc-design-implementation-link sdlc-design-implementation-review) + +(def (text-slot name) + (poo-clos-direct-slot-definition name type-predicate: sdlc-text?)) +(def (digest-slot name) + (poo-clos-direct-slot-definition name type-predicate: assurance-digest?)) + +(def SdlcDesignImplementationLink + (poo-clos-class 'sdlc/design-implementation-link + direct-slots: + (list (text-slot 'contract) (text-slot 'contract-revision) + (digest-slot 'contract-digest) + (text-slot 'guarantee) (text-slot 'guarantee-revision) + (text-slot 'source) (text-slot 'source-revision) + (digest-slot 'source-digest) + (text-slot 'snapshot) (text-slot 'snapshot-revision) + (digest-slot 'snapshot-context-digest)))) + +(def (sdlc-design-implementation-link? value) + (poo-flow-model? SdlcDesignImplementationLink value)) + +(def (sdlc-design-implementation-link contract-value guarantee-value + source-value snapshot-value) + (unless (and (sdlc-design-contract? contract-value) + (sdlc-design-clause? guarantee-value) + (eq? (.ref guarantee-value 'kind) 'guarantee) + (assurance-artifact? source-value) + (assurance-snapshot-canonical? snapshot-value)) + (error "invalid design implementation link input")) + (poo-flow-check-model + SdlcDesignImplementationLink + (.o (:: @ (poo-flow-model-prototype SdlcDesignImplementationLink)) + contract: (.ref contract-value 'identity) + contract-revision: (.ref contract-value 'revision) + contract-digest: (sdlc-design-contract-digest contract-value) + guarantee: (.ref guarantee-value 'identity) + guarantee-revision: (.ref guarantee-value 'revision) + source: (.ref source-value 'identity) + source-revision: (.ref source-value 'revision) + source-digest: (.ref source-value 'content-digest) + snapshot: (.ref snapshot-value 'identity) + snapshot-revision: (.ref snapshot-value 'revision) + snapshot-context-digest: (.ref snapshot-value 'context-digest)))) + +(def (ordered-ids values) + (list-sort string Date: Thu, 24 Sep 2026 09:19:46 +0800 Subject: [PATCH 34/64] Project design guarantees onto current Host support --- ROADMAP.org | 6 +- modules/sdlc/README.org | 12 ++ modules/sdlc/interface.ss | 2 + modules/sdlc/verification.ss | 142 +++++++++++++++++ t/sdlc/unit/design-verification-test.ss | 194 ++++++++++++++++++++++++ 5 files changed, 355 insertions(+), 1 deletion(-) create mode 100644 modules/sdlc/verification.ss create mode 100644 t/sdlc/unit/design-verification-test.ss diff --git a/ROADMAP.org b/ROADMAP.org index dc842e2..a7cb823 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -363,7 +363,11 @@ 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. +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 first reference case is a software change crossing requirement, implementation, verification, release-decision and effect boundaries. It diff --git a/modules/sdlc/README.org b/modules/sdlc/README.org index 59c445f..1f7116f 100644 --- a/modules/sdlc/README.org +++ b/modules/sdlc/README.org @@ -56,6 +56,18 @@ 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. + 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 diff --git a/modules/sdlc/interface.ss b/modules/sdlc/interface.ss index bba7924..edf8212 100644 --- a/modules/sdlc/interface.ss +++ b/modules/sdlc/interface.ss @@ -18,3 +18,5 @@ (export (import: :poo-flow/lambda-aitia/modules/sdlc/design)) (import :poo-flow/lambda-aitia/modules/sdlc/implementation) (export (import: :poo-flow/lambda-aitia/modules/sdlc/implementation)) +(import :poo-flow/lambda-aitia/modules/sdlc/verification) +(export (import: :poo-flow/lambda-aitia/modules/sdlc/verification)) diff --git a/modules/sdlc/verification.ss b/modules/sdlc/verification.ss new file mode 100644 index 0000000..14754e8 --- /dev/null +++ b/modules/sdlc/verification.ss @@ -0,0 +1,142 @@ +;;; SPDX-FileCopyrightText: 2026 tao3k team and Contributors +;;; +;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +;;; Project a design guarantee onto existing Assurance Host admission. +;;; This does not issue an admission, interpret a verifier, or authorize effects. +(import (only-in :clan/poo/object .o .ref) + (only-in :gerbil/core list-sort) + :std/list/list + (only-in :poo-flow/lambda-aitia/modules/assurance/interface + assurance-claim? assurance-node-canonical + assurance-relation? assurance-snapshot-canonical? + assurance-verification-subject? + assurance-verification-subject-snapshot + assurance-host-admission-current? + assurance-host-admitted-support? + assurance-host-required-support) + :poo-flow/lambda-aitia/modules/sdlc/design + :poo-flow/lambda-aitia/modules/sdlc/implementation) + +(export sdlc-design-guarantee-support-review) + +(def (ordered-ids values) + (list-sort string Date: Thu, 24 Sep 2026 09:36:14 +0800 Subject: [PATCH 35/64] Bind SDLC support review to supplied source bytes --- modules/sdlc/README.org | 7 +++++ modules/sdlc/verification.ss | 32 +++++++++++++++++++++-- t/sdlc/unit/design-verification-test.ss | 34 ++++++++++++++++++++++++- 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/modules/sdlc/README.org b/modules/sdlc/README.org index 1f7116f..f56aeb0 100644 --- a/modules/sdlc/README.org +++ b/modules/sdlc/README.org @@ -67,6 +67,13 @@ 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. +Callers may additionally supply =source-payloads:= as exact artifact-id/byte +pairs. =source-bytes-checked-support-current?= then requires a unique payload +for a mapped verifier input and compares its SHA-256 using POO Flow's Source +Lock algorithm. A missing, duplicated or changed payload fails this stronger +check. This only verifies caller-supplied bytes against the declared artifact; +it does not authenticate the repository checkout, the verifier's actual inputs +or the Host's independence. Those remain separate application trust duties. SDLC does not own CI providers or deployment environments. It contributes the standard, project, obligation, evidence, traceability, tailoring and lifecycle diff --git a/modules/sdlc/verification.ss b/modules/sdlc/verification.ss index 14754e8..c812244 100644 --- a/modules/sdlc/verification.ss +++ b/modules/sdlc/verification.ss @@ -7,6 +7,8 @@ (import (only-in :clan/poo/object .o .ref) (only-in :gerbil/core list-sort) :std/list/list + (only-in :poo-flow/src/feature-system/source-lock-feature + source-lock-payload-digest) (only-in :poo-flow/lambda-aitia/modules/assurance/interface assurance-claim? assurance-node-canonical assurance-relation? assurance-snapshot-canonical? @@ -52,12 +54,22 @@ (.ref link-value 'source-digest)))) (.ref outcome-value 'inputs))) +(def (source-payload-bound? payloads link-value) + (let (matches + (filter (lambda (entry) + (equal? (car entry) (.ref link-value 'source))) + payloads)) + (and (= (length matches) 1) + (equal? (source-lock-payload-digest (cdar matches)) + (.ref link-value 'source-digest))))) + ;;; One Host admission must bind a verifier outcome to at least one source ;;; declared for this guarantee. All claim obligations still need Host support. ;;; The returned value is an inert assessment, never an authority token. (def (sdlc-design-guarantee-support-review host-value contract-value guarantee-value implementation-links - claim-value subject-value relation-value admission-value admissions) + claim-value subject-value relation-value admission-value admissions + source-payloads: (source-payloads '())) (unless (and (sdlc-design-contract? contract-value) (sdlc-design-clause? guarantee-value) (eq? (.ref guarantee-value 'kind) 'guarantee) @@ -66,7 +78,13 @@ (assurance-claim? claim-value) (assurance-verification-subject? subject-value) (assurance-relation? relation-value) - (list? admissions)) + (list? admissions) + (list? source-payloads) + (every (lambda (entry) + (and (pair? entry) (string? (car entry)) + (or (string? (cdr entry)) + (u8vector? (cdr entry))))) + source-payloads)) (error "invalid design guarantee support review input")) (let* ((snapshot-value (.ref subject-value 'snapshot)) (obligation-value (.ref subject-value 'obligation)) @@ -108,6 +126,13 @@ snapshot-value claim-value (.ref link 'source)) (input-bound? outcome-value link))) source-links)) + (source-bytes-bound? + (find (lambda (link) + (and (source-dependency? + snapshot-value claim-value (.ref link 'source)) + (input-bound? outcome-value link) + (source-payload-bound? source-payloads link))) + source-links)) (admission-current? (and admission-value (memq admission-value admissions) @@ -133,9 +158,12 @@ mapping-current?: (.ref mapping 'mapping-current?) claim-current?: (and guarantee-current? claim-current-value #t) source-input-bound?: (and source-bound? #t) + source-bytes-match?: (and source-bytes-bound? #t) host-admission-current?: (and admission-current? #t) required-support-current?: (and support-current? #t) source-bound-support-current?: (and support-current? #t) + source-bytes-checked-support-current?: + (and support-current? source-bytes-bound? #t) verifier-independence-established?: #f implementation-conforms?: #f release-authorized?: #f diff --git a/t/sdlc/unit/design-verification-test.ss b/t/sdlc/unit/design-verification-test.ss index fc94237..4547d79 100644 --- a/t/sdlc/unit/design-verification-test.ss +++ b/t/sdlc/unit/design-verification-test.ss @@ -5,12 +5,15 @@ (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 digest-a (string-append "sha256:" (make-string 64 #\a))) +(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")) @@ -118,10 +121,39 @@ (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 "actual source bytes must match the mapped digest" + (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 ((matched + (sdlc-design-guarantee-support-review + configured contract guarantee (list mapping) claim subject + evidence-link admission (list admission) + source-payloads: (list (cons "artifact/source" source-bytes)))) + (changed + (sdlc-design-guarantee-support-review + configured contract guarantee (list mapping) claim subject + evidence-link admission (list admission) + source-payloads: (list (cons "artifact/source" "different")))) + (duplicate + (sdlc-design-guarantee-support-review + configured contract guarantee (list mapping) claim subject + evidence-link admission (list admission) + source-payloads: (list (cons "artifact/source" source-bytes) + (cons "artifact/source" source-bytes))))) + (check-equal? (.ref matched 'source-bytes-checked-support-current?) #t) + (check-equal? (.ref matched 'verifier-independence-established?) #f) + (check-equal? (.ref matched 'implementation-conforms?) #f) + (check-equal? (.ref changed 'source-bound-support-current?) #t) + (check-equal? (.ref changed 'source-bytes-checked-support-current?) #f) + (check-equal? (.ref duplicate '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)) From 9ed40262db538076d7aa4fc1357e225eb22a7cb8 Mon Sep 17 00:00:00 2001 From: guangtao Date: Thu, 24 Sep 2026 10:32:53 +0800 Subject: [PATCH 36/64] Model NASA NPR 7150.2 as stable SDLC profile family --- README.org | 10 +- bindings/python/tests/test_native.py | 2 +- build.ss | 2 +- .../rfcs/0003-composition-value-interface.org | 279 ++++++------------ modules/assurance/host.ss | 48 ++- modules/sdlc/README.org | 45 ++- modules/sdlc/config.ss | 12 +- .../sdlc/standards/nasa-7150-2d-catalog.ss | 2 +- .../sdlc/standards/nasa-7150-2d-profile.ss | 2 +- modules/sdlc/standards/nasa-7150-2d.ss | 2 +- modules/sdlc/standards/nasa-certification.ss | 2 +- modules/sdlc/standards/nasa-coverage.ss | 2 +- modules/sdlc/standards/nasa-review.ss | 8 +- .../sdlc/standards/nasa-standard-provider.ss | 2 +- modules/sdlc/standards/source.lock.ss | 16 +- modules/sdlc/standards/sources.ss | 14 +- modules/sdlc/verification.ss | 29 +- t/bindings/unit/aitia-native-test.ss | 2 +- t/fixtures/composition-value/aitia-default.ss | 4 - t/fixtures/composition-value/aitia.ss | 8 - t/integration/github-workflow-test.ss | 29 +- t/modules/unit/composition-identity-test.ss | 75 ++--- t/sdlc/unit/design-verification-test.ss | 68 +++-- t/sdlc/unit/nasa-catalog-test.ss | 2 +- t/sdlc/unit/nasa-standard-provider-test.ss | 2 +- t/sdlc/unit/sdlc-config-test.ss | 10 +- t/sdlc/unit/sdlc-test.ss | 2 +- tools/import-nasa-catalog.py | 2 +- tools/update-sources-lock.ss | 2 +- user-interface/compositions/aitia.ss | 67 ++--- user-interface/config.ss | 44 +-- user-interface/profiles/nasa/sdlc.ss | 29 ++ user-interface/profiles/sdlc/nasa-7150-2d.ss | 15 - .../profiles/software-engineering/project.ss | 4 +- 34 files changed, 370 insertions(+), 472 deletions(-) delete mode 100644 t/fixtures/composition-value/aitia-default.ss delete mode 100644 t/fixtures/composition-value/aitia.ss create mode 100644 user-interface/profiles/nasa/sdlc.ss delete mode 100644 user-interface/profiles/sdlc/nasa-7150-2d.ss diff --git a/README.org b/README.org index b2db2b3..22355ec 100644 --- a/README.org +++ b/README.org @@ -202,11 +202,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 diff --git a/bindings/python/tests/test_native.py b/bindings/python/tests/test_native.py index d9fd888..1d9e938 100644 --- a/bindings/python/tests/test_native.py +++ b/bindings/python/tests/test_native.py @@ -75,7 +75,7 @@ def test_python_delegates_gitops_decision_to_scheme() -> None: {"name": "commit-policy", "conclusion": "success"}, {"name": "build", "conclusion": "success"}, {"name": "unit-test", "conclusion": "success"}, - {"name": "nasa-7150-2d", "conclusion": "success"}, + {"name": "nasa/sdlc/npr-7150.2", "conclusion": "success"}, ], } ) diff --git a/build.ss b/build.ss index 7a7fcfc..ea3e90a 100755 --- a/build.ss +++ b/build.ss @@ -27,7 +27,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) diff --git a/docs/rfcs/0003-composition-value-interface.org b/docs/rfcs/0003-composition-value-interface.org index 76cafd2..bcb8cb7 100644 --- a/docs/rfcs/0003-composition-value-interface.org +++ b/docs/rfcs/0003-composition-value-interface.org @@ -6,221 +6,110 @@ :PROPERTIES: :ID: lambda-aitia-rfc-0003 -:STATUS: active +:STATUS: draft :DATE: 2026-09-18 :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 + +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 maintained GitOps Scenario selects this typed Module Profile with: #+begin_src scheme -(use-composition aitia - (project - repository: "tao3k/example" - languages: '(rust python)) - (delivery - provider: github)) +(use-module NasaSdlcModule npr-7150.2) #+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. +Its check name is a workflow gate keyed to the standard family. A green +check name alone does not establish assessment against edition D: exact +edition, source digest, evidence and accountable Host admission must be +bound separately by Assurance. This binding is an open acceptance gate. -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: +* Composition semantics -#+begin_src scheme -(use-composition aitia - (.def (project @ default-software-engineering-project) - (reviewers =>.+ '(security release)))) -#+end_src +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. + +* 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 | -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. +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 -- [X] 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. -- [X] 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. - -The checked items have direct behavior coverage in -=t/modules/unit/composition-identity-test.ss= and POO Flow's -=t/profile-composition-value-test.ss=. Other items remain open; the default -Profile selecting five Modules is not yet evidence that every declared slot -and downstream Scenario has been independently qualified. - -* 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. +- [ ] NASA assessment evidence binds the exact edition and Source Lock digest; + a same-name green check from another edition cannot satisfy it. +- [ ] 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. +- 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/modules/assurance/host.ss b/modules/assurance/host.ss index 9c8b9f6..ced6a2d 100644 --- a/modules/assurance/host.ss +++ b/modules/assurance/host.ss @@ -14,12 +14,15 @@ poo-flow-verification-adapter poo-flow-verify poo-flow-verification-valid? poo-flow-revoke-verification!) + (only-in :poo-flow/src/feature-system/source-lock-feature + source-lock-payload-digest) (only-in :poo-flow/src/modules/authorization/providers/cedar/interface poo-flow-cedar-runtime-handoff? poo-flow-cedar-authorization-request poo-flow-cedar-authorization-request?) (only-in :poo-flow/lambda-aitia/modules/assurance/types - assurance-snapshot? assurance-claim? assurance-obligation? + assurance-snapshot? assurance-artifact? assurance-claim? + assurance-obligation? assurance-decision? assurance-decision-preflight? AssuranceRequiredSupport AssessmentRequiredSupport AssuranceDecisionPreflight) @@ -37,6 +40,7 @@ (export assurance-verification-host assurance-host-verify assurance-host-sealed-support? assurance-host-revoke! assurance-host-admit assurance-host-admission-current? + assurance-host-source-current? assurance-host-admitted-support? assurance-host-required-support assurance-host-preflight-decision @@ -98,12 +102,14 @@ (host-current-snapshot entry) obligation evidence outcome)))) (def (assurance-verification-host - identity-value operation clock lease snapshot-source) + 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)) + (procedure? snapshot-source) + (or (not source-reader) (procedure? source-reader))) (error "invalid assurance verification Host configuration")) (let* ((adapter (poo-flow-verification-adapter @@ -111,7 +117,7 @@ (host (.o identity: (string-copy identity-value)))) (hash-put! issued-hosts host (vector (string-copy identity-value) adapter clock lease -1 - snapshot-source 0 #f 0)) + snapshot-source 0 #f 0 source-reader)) host)) ;;; An eligible report alone is never an issued seal. The configured operation @@ -258,6 +264,40 @@ (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 (assurance-host-source-current? 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))) + (and (or (string? payload) (u8vector? payload)) + (equal? (source-lock-payload-digest payload) + (.ref artifact 'content-digest)) + (equal? expected-cut + (assurance-snapshot-semantic-digest + (host-current-snapshot entry)))))))))) + (def (required-support-link? snapshot obligation claim) (find (lambda (relation) (and (eq? (.ref relation 'plane) 'assurance) diff --git a/modules/sdlc/README.org b/modules/sdlc/README.org index f56aeb0..ab0175b 100644 --- a/modules/sdlc/README.org +++ b/modules/sdlc/README.org @@ -67,18 +67,20 @@ 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. -Callers may additionally supply =source-payloads:= as exact artifact-id/byte -pairs. =source-bytes-checked-support-current?= then requires a unique payload -for a mapped verifier input and compares its SHA-256 using POO Flow's Source -Lock algorithm. A missing, duplicated or changed payload fails this stronger -check. This only verifies caller-supplied bytes against the declared artifact; -it does not authenticate the repository checkout, the verifier's actual inputs -or the Host's independence. Those remain separate application trust duties. +An application may configure the Assurance Host's =source-reader:= capability. +=source-bytes-checked-support-current?= then requires that Host to read the +mapped verifier input, compare its bytes to the Artifact SHA-256 using POO +Flow's Source Lock algorithm, and preserve the same canonical snapshot before +and after reading. Without the Host reader, or if the bytes or cut change, 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 @@ -91,25 +93,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..cfdfd68 100644 --- a/modules/sdlc/config.ss +++ b/modules/sdlc/config.ss @@ -2,7 +2,8 @@ ;;; ;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later -;;; User selection projection. Importing this module enables no standard. +;;; 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/module-system/declaration/interface :poo-flow/lambda-aitia/modules/sdlc/types :poo-flow/lambda-aitia/modules/sdlc/objects @@ -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/standards/nasa-7150-2d-catalog.ss b/modules/sdlc/standards/nasa-7150-2d-catalog.ss index ef53579..8ef49a5 100644 --- a/modules/sdlc/standards/nasa-7150-2d-catalog.ss +++ b/modules/sdlc/standards/nasa-7150-2d-catalog.ss @@ -20,7 +20,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/modules/sdlc/standards/nasa-7150-2d-profile.ss b/modules/sdlc/standards/nasa-7150-2d-profile.ss index e98f903..66167fd 100644 --- a/modules/sdlc/standards/nasa-7150-2d-profile.ss +++ b/modules/sdlc/standards/nasa-7150-2d-profile.ss @@ -16,7 +16,7 @@ "https://nodis3.gsfc.nasa.gov/displayDir.cfm?Internal_ID=N_PR_7150_002D_&page_name=Chapter3") (def Nasa7150_2D - (.o (:: @ StandardProfile.) identity: "nasa/npr-7150.2d" edition: "D" + (.o (:: @ StandardProfile.) identity: "nasa/sdlc/npr-7150.2" edition: "D" source-url: chapter3-url verified-on: "2026-09-12" effective-date: "2022-03-08" expiration-date: "2027-03-08" applicability-source: diff --git a/modules/sdlc/standards/nasa-7150-2d.ss b/modules/sdlc/standards/nasa-7150-2d.ss index f6f759f..8b467be 100644 --- a/modules/sdlc/standards/nasa-7150-2d.ss +++ b/modules/sdlc/standards/nasa-7150-2d.ss @@ -125,7 +125,7 @@ ((eq? condition-value #t) 'applicable) ((eq? condition-value #f) 'condition-not-triggered) (else 'context-review-required)))) - (.o kind: 'sdlc.applicability standard: "nasa/npr-7150.2d" requirement: identity-value + (.o kind: 'sdlc.applicability standard: "nasa/sdlc/npr-7150.2" requirement: identity-value software-class: class-value status: status-value invocation: invocation-value source-url: (.ref row 'source-url) section: (.ref row 'section) tailoring: 'not-approved assessment: 'not-evaluated))) diff --git a/modules/sdlc/standards/nasa-certification.ss b/modules/sdlc/standards/nasa-certification.ss index 549e256..3f959d3 100644 --- a/modules/sdlc/standards/nasa-certification.ss +++ b/modules/sdlc/standards/nasa-certification.ss @@ -109,7 +109,7 @@ subject: (.ref project 'subject) revision: (.ref project 'revision) scope: (.ref project 'scope) - standard: "nasa/npr-7150.2d" + standard: "nasa/sdlc/npr-7150.2" edition: "NPR 7150.2D" software-class: class-value mapping-matrix-digest: matrix-digest diff --git a/modules/sdlc/standards/nasa-coverage.ss b/modules/sdlc/standards/nasa-coverage.ss index d8dc6ce..2d176e5 100644 --- a/modules/sdlc/standards/nasa-coverage.ss +++ b/modules/sdlc/standards/nasa-coverage.ss @@ -59,7 +59,7 @@ (not (eq? (.ref row 'implementation-status) 'implemented))) rows))) - (.o standard: "nasa/npr-7150.2d" + (.o standard: "nasa/sdlc/npr-7150.2" requirement-count: (length rows) implemented-count: (length (filter (lambda (row) diff --git a/modules/sdlc/standards/nasa-review.ss b/modules/sdlc/standards/nasa-review.ss index 66eb3dd..a2353bd 100644 --- a/modules/sdlc/standards/nasa-review.ss +++ b/modules/sdlc/standards/nasa-review.ss @@ -31,7 +31,7 @@ (.o (:: @ (poo-flow-model-prototype SdlcCriterionEvidence)) identity: id kind: 'sdlc.evidence subject: (.ref project 'subject) revision: (.ref project 'revision) scope: (.ref project 'scope) - standard: "nasa/npr-7150.2d" requirement: requirement-id criterion: criterion-id + standard: "nasa/sdlc/npr-7150.2" requirement: requirement-id criterion: criterion-id source-digest: (row-digest (nasa-requirement-by-id requirement-id)) producer: producer-value outcome: outcome-value))) (def (unique-ids? values) @@ -76,7 +76,7 @@ (error "invalid NASA project assessment inputs")) (for-each (lambda (e) - (unless (and (equal? (.ref e 'standard) "nasa/npr-7150.2d") + (unless (and (equal? (.ref e 'standard) "nasa/sdlc/npr-7150.2") (member (.ref e 'criterion) (map (lambda (c) (.ref c 'identity)) (nasa-requirement-criteria (.ref e 'requirement))))) @@ -147,7 +147,7 @@ (poo-flow-check-model SdlcTailoringPacket (.o (:: @ (poo-flow-model-prototype SdlcTailoringPacket)) identity: id subject: (.ref project 'subject) revision: (.ref project 'revision) scope: (.ref project 'scope) - standard: "nasa/npr-7150.2d" requirement: requirement-id + standard: "nasa/sdlc/npr-7150.2" requirement: requirement-id rationale: rationale-value risk: risk-value mitigations: mitigations-value risk-acceptance: acceptance-value archived-matrix: archive-value))) (def (nasa-authority-attestation id packet authority-value signer-value record-value decision-value) @@ -160,7 +160,7 @@ record: record-value decision: decision-value))) (def (nasa-tailoring-review project class-value context packet attestations) (unless (and (sdlc-project? project) (sdlc-tailoring-packet? packet) - (bound? packet project) (equal? (.ref packet 'standard) "nasa/npr-7150.2d") + (bound? packet project) (equal? (.ref packet 'standard) "nasa/sdlc/npr-7150.2") (list? attestations) (every sdlc-authority-attestation? attestations) (unique-ids? attestations)) (error "invalid tailoring review inputs")) diff --git a/modules/sdlc/standards/nasa-standard-provider.ss b/modules/sdlc/standards/nasa-standard-provider.ss index 05c5777..5b285a9 100644 --- a/modules/sdlc/standards/nasa-standard-provider.ss +++ b/modules/sdlc/standards/nasa-standard-provider.ss @@ -44,7 +44,7 @@ nasa-7150-2d-validation-closure nasa-7150-2d-materialize-sources) -(def +nasa-7150-2d-provider-identity+ "lambda-aitia/nasa-npr-7150.2d") +(def +nasa-7150-2d-provider-identity+ "lambda-aitia/nasa/sdlc/npr-7150.2/D") (def Nasa7150_2DStandardFamily (poo-flow-standard-family diff --git a/modules/sdlc/standards/source.lock.ss b/modules/sdlc/standards/source.lock.ss index a58732e..4c15d85 100644 --- a/modules/sdlc/standards/source.lock.ss +++ b/modules/sdlc/standards/source.lock.ss @@ -8,12 +8,12 @@ (def Nasa7150_2DSourcesLock (sources-lock-value - "lambda-aitia/nasa-npr-7150.2d/sources" + "lambda-aitia/nasa/sdlc/npr-7150.2/D/sources" "2022-03-08" - "sha256:286dbc723fbad0e596ad07593d95ce56f7bbdf6526d30719ab3bebd29bc772bb" + "sha256:cfb963b8cd81fd8e22e9b47251c1dc7927b85fcdf9ec9366057cd38cb94f338c" (list (.o (:: @ SourceLockEntry.) - identity: "nasa/npr-7150.2d/appendix-c" + identity: "nasa/sdlc/npr-7150.2/D/appendix-c" path: "modules/sdlc/standards/sources/appendix-c.html" canonical-uri: "https://nodis3.gsfc.nasa.gov/displayDir.cfm?Internal_ID=N_PR_7150_002D_&page_name=AppendixC" exact-version: "2022-03-08" @@ -23,7 +23,7 @@ metadata: '((section . appendix-c)) ) (.o (:: @ SourceLockEntry.) - identity: "nasa/npr-7150.2d/chapter2" + identity: "nasa/sdlc/npr-7150.2/D/chapter2" path: "modules/sdlc/standards/sources/chapter2.html" canonical-uri: "https://nodis3.gsfc.nasa.gov/displayDir.cfm?Internal_ID=N_PR_7150_002D_&page_name=Chapter2" exact-version: "2022-03-08" @@ -33,7 +33,7 @@ metadata: '((section . chapter-2)) ) (.o (:: @ SourceLockEntry.) - identity: "nasa/npr-7150.2d/chapter3" + identity: "nasa/sdlc/npr-7150.2/D/chapter3" path: "modules/sdlc/standards/sources/chapter3.html" canonical-uri: "https://nodis3.gsfc.nasa.gov/displayDir.cfm?Internal_ID=N_PR_7150_002D_&page_name=Chapter3" exact-version: "2022-03-08" @@ -43,7 +43,7 @@ metadata: '((section . chapter-3)) ) (.o (:: @ SourceLockEntry.) - identity: "nasa/npr-7150.2d/chapter4" + identity: "nasa/sdlc/npr-7150.2/D/chapter4" path: "modules/sdlc/standards/sources/chapter4.html" canonical-uri: "https://nodis3.gsfc.nasa.gov/displayDir.cfm?Internal_ID=N_PR_7150_002D_&page_name=Chapter4" exact-version: "2022-03-08" @@ -53,7 +53,7 @@ metadata: '((section . chapter-4)) ) (.o (:: @ SourceLockEntry.) - identity: "nasa/npr-7150.2d/chapter5" + identity: "nasa/sdlc/npr-7150.2/D/chapter5" path: "modules/sdlc/standards/sources/chapter5.html" canonical-uri: "https://nodis3.gsfc.nasa.gov/displayDir.cfm?Internal_ID=N_PR_7150_002D_&page_name=Chapter5" exact-version: "2022-03-08" @@ -63,7 +63,7 @@ metadata: '((section . chapter-5)) ) (.o (:: @ SourceLockEntry.) - identity: "nasa/npr-7150.2d/chapter6" + identity: "nasa/sdlc/npr-7150.2/D/chapter6" path: "modules/sdlc/standards/sources/chapter6.html" canonical-uri: "https://nodis3.gsfc.nasa.gov/displayDir.cfm?Internal_ID=N_PR_7150_002D_&page_name=Chapter6" exact-version: "2022-03-08" diff --git a/modules/sdlc/standards/sources.ss b/modules/sdlc/standards/sources.ss index f5f3bef..1921359 100644 --- a/modules/sdlc/standards/sources.ss +++ b/modules/sdlc/standards/sources.ss @@ -17,7 +17,7 @@ Nasa7150_2DSources Nasa7150_2DSourcesLock) -(def +nasa-7150-2d-edition-identity+ "nasa/npr-7150.2d/2022-03-08") +(def +nasa-7150-2d-edition-identity+ "nasa/sdlc/npr-7150.2/D/2022-03-08") (def +nasa-7150-2d-edition-canonical-uri+ "https://nodis3.gsfc.nasa.gov/N_PR_7150_002D_") (def +nasa-7150-2d-source-ref+ @@ -34,17 +34,17 @@ metadata: (list (cons 'section section-value)))) (def Nasa7150_2DSources - (list (nasa-source "nasa/npr-7150.2d/chapter2" + (list (nasa-source "nasa/sdlc/npr-7150.2/D/chapter2" "chapter2.html" "Chapter2" 'chapter-2) - (nasa-source "nasa/npr-7150.2d/chapter3" + (nasa-source "nasa/sdlc/npr-7150.2/D/chapter3" "chapter3.html" "Chapter3" 'chapter-3) - (nasa-source "nasa/npr-7150.2d/chapter4" + (nasa-source "nasa/sdlc/npr-7150.2/D/chapter4" "chapter4.html" "Chapter4" 'chapter-4) - (nasa-source "nasa/npr-7150.2d/chapter5" + (nasa-source "nasa/sdlc/npr-7150.2/D/chapter5" "chapter5.html" "Chapter5" 'chapter-5) - (nasa-source "nasa/npr-7150.2d/chapter6" + (nasa-source "nasa/sdlc/npr-7150.2/D/chapter6" "chapter6.html" "Chapter6" 'chapter-6) - (nasa-source "nasa/npr-7150.2d/appendix-c" + (nasa-source "nasa/sdlc/npr-7150.2/D/appendix-c" "appendix-c.html" "AppendixC" 'appendix-c))) (include "source.lock.ss") diff --git a/modules/sdlc/verification.ss b/modules/sdlc/verification.ss index c812244..68f0975 100644 --- a/modules/sdlc/verification.ss +++ b/modules/sdlc/verification.ss @@ -7,14 +7,13 @@ (import (only-in :clan/poo/object .o .ref) (only-in :gerbil/core list-sort) :std/list/list - (only-in :poo-flow/src/feature-system/source-lock-feature - source-lock-payload-digest) (only-in :poo-flow/lambda-aitia/modules/assurance/interface assurance-claim? assurance-node-canonical assurance-relation? assurance-snapshot-canonical? assurance-verification-subject? assurance-verification-subject-snapshot assurance-host-admission-current? + assurance-host-source-current? assurance-host-admitted-support? assurance-host-required-support) :poo-flow/lambda-aitia/modules/sdlc/design @@ -54,22 +53,12 @@ (.ref link-value 'source-digest)))) (.ref outcome-value 'inputs))) -(def (source-payload-bound? payloads link-value) - (let (matches - (filter (lambda (entry) - (equal? (car entry) (.ref link-value 'source))) - payloads)) - (and (= (length matches) 1) - (equal? (source-lock-payload-digest (cdar matches)) - (.ref link-value 'source-digest))))) - ;;; One Host admission must bind a verifier outcome to at least one source ;;; declared for this guarantee. All claim obligations still need Host support. ;;; The returned value is an inert assessment, never an authority token. (def (sdlc-design-guarantee-support-review host-value contract-value guarantee-value implementation-links - claim-value subject-value relation-value admission-value admissions - source-payloads: (source-payloads '())) + claim-value subject-value relation-value admission-value admissions) (unless (and (sdlc-design-contract? contract-value) (sdlc-design-clause? guarantee-value) (eq? (.ref guarantee-value 'kind) 'guarantee) @@ -78,13 +67,7 @@ (assurance-claim? claim-value) (assurance-verification-subject? subject-value) (assurance-relation? relation-value) - (list? admissions) - (list? source-payloads) - (every (lambda (entry) - (and (pair? entry) (string? (car entry)) - (or (string? (cdr entry)) - (u8vector? (cdr entry))))) - source-payloads)) + (list? admissions)) (error "invalid design guarantee support review input")) (let* ((snapshot-value (.ref subject-value 'snapshot)) (obligation-value (.ref subject-value 'obligation)) @@ -131,7 +114,11 @@ (and (source-dependency? snapshot-value claim-value (.ref link 'source)) (input-bound? outcome-value link) - (source-payload-bound? source-payloads link))) + (let (source-value + (node-at snapshot-value (.ref link 'source))) + (and source-value + (assurance-host-source-current? + host-value snapshot-value source-value))))) source-links)) (admission-current? (and admission-value diff --git a/t/bindings/unit/aitia-native-test.ss b/t/bindings/unit/aitia-native-test.ss index f209e52..10ba3ec 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\"}]}") (def aitia-native-test (test-suite "Aitia Scheme-native C ABI" diff --git a/t/fixtures/composition-value/aitia-default.ss b/t/fixtures/composition-value/aitia-default.ss deleted file mode 100644 index 2872e41..0000000 --- a/t/fixtures/composition-value/aitia-default.ss +++ /dev/null @@ -1,4 +0,0 @@ -;;; SPDX-FileCopyrightText: 2026 tao3k team and Contributors -;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later - -(use-composition aitia) 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/integration/github-workflow-test.ss b/t/integration/github-workflow-test.ss index 4c2da9e..5c89566 100644 --- a/t/integration/github-workflow-test.ss +++ b/t/integration/github-workflow-test.ss @@ -3,9 +3,11 @@ ;;; 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) @@ -13,7 +15,7 @@ (def revision "0123456789abcdef") (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)) +(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)) @@ -24,10 +26,21 @@ '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)) + '(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 +49,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 +57,7 @@ "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 "checks for another Git revision cannot authorize promotion" (let (decision (run-github-gitops @@ -58,13 +71,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 2d8daa3..74102e8 100644 --- a/t/modules/unit/composition-identity-test.ss +++ b/t/modules/unit/composition-identity-test.ss @@ -6,18 +6,14 @@ (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 poo-flow-scenario-case?) (only-in :poo-flow/src/module-system/declaration/interface 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/src/modules/proof/interface + poo-flow-proof-module?) :poo-flow/lambda-aitia/modules/formal-methods/interface :poo-flow/lambda-aitia/user-interface/compositions/aitia) @@ -25,67 +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)) + (poo-flow-user-module-selection-flags (list-ref modules 5)) '(+tla+ +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-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))) - (test-case "zero-override downstream value needs no imports" - (let (case-value - (poo-flow-load-composition-value - "t/fixtures/composition-value/aitia-default.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? (.ref (.ref case-value 'admission) 'accepted?) #t))))) + 'authorization-policy-validation)))) diff --git a/t/sdlc/unit/design-verification-test.ss b/t/sdlc/unit/design-verification-test.ss index 4547d79..49c5625 100644 --- a/t/sdlc/unit/design-verification-test.ss +++ b/t/sdlc/unit/design-verification-test.ss @@ -89,13 +89,13 @@ snapshot-context-digest: (.ref snapshot-value 'context-digest) inputs: (list (assurance-verifier-input input-source)) output-digest: digest-b)) -(def (host source-procedure) +(def (host source-procedure source-reader: (reader #f)) (assurance-verification-host "host/design-verification" (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)) + (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)) @@ -126,34 +126,52 @@ (check-equal? (.ref receipt 'implementation-conforms?) #f) (check-equal? (.ref receipt 'release-authorized?) #f) (check-equal? (.ref receipt 'runtime-executed?) #f)))) - (test-case "actual source bytes must match the mapped digest" + (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)))) + (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))) - (let ((matched - (sdlc-design-guarantee-support-review - configured contract guarantee (list mapping) claim subject - evidence-link admission (list admission) - source-payloads: (list (cons "artifact/source" source-bytes)))) - (changed - (sdlc-design-guarantee-support-review - configured contract guarantee (list mapping) claim subject - evidence-link admission (list admission) - source-payloads: (list (cons "artifact/source" "different")))) - (duplicate - (sdlc-design-guarantee-support-review + (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? (.ref receipt 'source-bound-support-current?) #t) + (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 (list admission) - source-payloads: (list (cons "artifact/source" source-bytes) - (cons "artifact/source" source-bytes))))) - (check-equal? (.ref matched 'source-bytes-checked-support-current?) #t) - (check-equal? (.ref matched 'verifier-independence-established?) #f) - (check-equal? (.ref matched 'implementation-conforms?) #f) - (check-equal? (.ref changed 'source-bound-support-current?) #t) - (check-equal? (.ref changed 'source-bytes-checked-support-current?) #f) - (check-equal? (.ref duplicate 'source-bytes-checked-support-current?) #f))))) + evidence-link admission (list admission)) + 'source-bytes-checked-support-current?) + #t)))) + (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? (.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)) diff --git a/t/sdlc/unit/nasa-catalog-test.ss b/t/sdlc/unit/nasa-catalog-test.ss index f66763a..8810a92 100644 --- a/t/sdlc/unit/nasa-catalog-test.ss +++ b/t/sdlc/unit/nasa-catalog-test.ss @@ -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-standard-provider-test.ss b/t/sdlc/unit/nasa-standard-provider-test.ss index 1298283..ea5781d 100644 --- a/t/sdlc/unit/nasa-standard-provider-test.ss +++ b/t/sdlc/unit/nasa-standard-provider-test.ss @@ -50,7 +50,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/sdlc-config-test.ss b/t/sdlc/unit/sdlc-config-test.ss index 138b716..cc62b4f 100644 --- a/t/sdlc/unit/sdlc-config-test.ss +++ b/t/sdlc/unit/sdlc-config-test.ss @@ -12,19 +12,19 @@ (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-test.ss b/t/sdlc/unit/sdlc-test.ss index 204f3a5..2b1a7bd 100644 --- a/t/sdlc/unit/sdlc-test.ss +++ b/t/sdlc/unit/sdlc-test.ss @@ -24,7 +24,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/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/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..35ce9b1 100644 --- a/user-interface/compositions/aitia.ss +++ b/user-interface/compositions/aitia.ss @@ -3,52 +3,29 @@ ;;; ;;; 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/src/module-system/semantic-module/objects + poo-flow-semantic-identity poo-flow-semantic-module) + (only-in :poo-flow/src/module-system/profile-composition/interface + poo-flow-profile-export poo-flow-module-profiles + profiles compose use-module 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..83f6c24 100644 --- a/user-interface/config.ss +++ b/user-interface/config.ss @@ -3,21 +3,31 @@ ;;; 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/src/module-system/profile-composition/interface + profiles compose use-module user-composition + poo-flow-profile-export poo-flow-module-profiles) + (only-in :poo-flow/src/module-system/semantic-module/objects + poo-flow-semantic-identity 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/profiles/nasa/sdlc.ss b/user-interface/profiles/nasa/sdlc.ss new file mode 100644 index 0000000..1958212 --- /dev/null +++ b/user-interface/profiles/nasa/sdlc.ss @@ -0,0 +1,29 @@ +;;; -*- 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/src/module-system/semantic-module/objects + poo-flow-semantic-identity poo-flow-semantic-module) + (only-in :poo-flow/src/module-system/profile-composition/interface + 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..c33464a 100644 --- a/user-interface/profiles/software-engineering/project.ss +++ b/user-interface/profiles/software-engineering/project.ss @@ -23,7 +23,9 @@ (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 From 5e314d3a739700d955c7744f480cfbd26b82d500 Mon Sep 17 00:00:00 2001 From: guangtao Date: Thu, 24 Sep 2026 10:50:40 +0800 Subject: [PATCH 37/64] Bind NASA GitOps checks to selected standard edition and source lock --- .github/workflows/ci.yml | 2 +- bindings/c/aitia-contract.ss | 4 +- bindings/python/tests/test_native.py | 64 ++++++++++++++----- .../rfcs/0003-composition-value-interface.org | 14 ++-- modules/gitops/funs.ss | 37 +++++++++-- modules/gitops/objects.ss | 8 ++- modules/gitops/types.ss | 4 +- .../sdlc/standards/nasa-7150-2d-profile.ss | 7 +- t/bindings/unit/aitia-native-test.ss | 2 +- t/integration/github-workflow-test.ss | 41 +++++++++++- user-interface/scenarios/github-gitops.ss | 8 ++- 11 files changed, 152 insertions(+), 39 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 30c08e5..b27cd03 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -98,7 +98,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: repository: tao3k/poo-flow - ref: 90d1c79f474f89a4f975b282611de55662301b74 + ref: 29be9b24f1eb6a15ff7337a3b2fc7e398770e5de path: poo-flow submodules: false 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/python/tests/test_native.py b/bindings/python/tests/test_native.py index 1d9e938..ba4be33 100644 --- a/bindings/python/tests/test_native.py +++ b/bindings/python/tests/test_native.py @@ -62,23 +62,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/sdlc/npr-7150.2", "conclusion": "success"}, - ], - } - ) + decision = evaluate_gitops(_gitops_change()) assert isinstance(decision, GitOpsDecision) assert decision.accepted is True assert decision.profile == "dev" @@ -86,3 +93,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/docs/rfcs/0003-composition-value-interface.org b/docs/rfcs/0003-composition-value-interface.org index bcb8cb7..a177da8 100644 --- a/docs/rfcs/0003-composition-value-interface.org +++ b/docs/rfcs/0003-composition-value-interface.org @@ -57,9 +57,11 @@ The maintained GitOps Scenario selects this typed Module Profile with: #+end_src Its check name is a workflow gate keyed to the standard family. A green -check name alone does not establish assessment against edition D: exact -edition, source digest, evidence and accountable Host admission must be -bound separately by Assurance. This binding is an open acceptance gate. +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. * Composition semantics @@ -99,8 +101,10 @@ that ordinary users must type. exposing POO internals, through a POO Flow-owned public mechanism. - [ ] An advanced override extends the same maintained Project Profile and preserves selection/provenance receipts. -- [ ] NASA assessment evidence binds the exact edition and Source Lock digest; - a same-name green check from another edition cannot satisfy it. +- [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 diff --git a/modules/gitops/funs.ss b/modules/gitops/funs.ss index c9afd81..efe24a2 100644 --- a/modules/gitops/funs.ss +++ b/modules/gitops/funs.ss @@ -18,12 +18,23 @@ (def (composition-gitops-profiles composition) (filter gitops-profile? (.ref composition 'profiles))) -(def (composition-standard-names composition) - (map (lambda (profile) (.ref profile 'name)) +(def (composition-standard-bindings composition) + (map (lambda (profile) + (let (standards (.ref profile 'standards)) + (unless (and (list? standards) (= (length standards) 1)) + (error "standard Profile must bind exactly one Standard" + (.ref profile 'name))) + (cons (.ref profile 'name) (car standards)))) (filter (lambda (profile) (and (.slot? profile 'role) (eq? (.ref profile 'role) 'standard))) (.ref composition 'profiles)))) +(def (standard-check-current? check standard) + (and (.slot? standard 'edition) + (.slot? standard 'source-lock-digest) + (equal? (.ref check 'standard-edition) (.ref standard 'edition)) + (equal? (.ref check 'source-lock-digest) + (.ref standard 'source-lock-digest)))) (def (check-index checks) (let (index (poo-flow-make-value-index)) (for-each @@ -35,17 +46,26 @@ (poo-flow-value-index-put! index (.ref check 'name) check))) checks) index)) -(def (gitops-profile-decision profile standards change checks) +(def (gitops-profile-decision profile standard-bindings change checks) (let* ((required (.ref profile 'required-checks)) + (standards (map car standard-bindings)) (index (check-index checks)) - (missing '()) (failed '()) (stale '())) + (missing '()) (failed '()) (stale '()) + (revision-mismatch? #f) + (standard-mismatch? #f)) (for-each (lambda (name) (let-values (((present? check) (poo-flow-value-index-ref index name))) (cond ((not present?) (set! missing (cons name missing))) ((or (not (equal? (.ref check 'repository) (.ref change 'repository))) (not (equal? (.ref check 'revision) (.ref change 'revision)))) - (set! stale (cons name stale))) + (set! stale (cons name stale)) + (set! revision-mismatch? #t)) + ((let (binding (assoc name standard-bindings)) + (and binding + (not (standard-check-current? check (cdr binding))))) + (set! stale (cons name stale)) + (set! standard-mismatch? #t)) ((not (eq? (.ref check 'conclusion) 'success)) (set! failed (cons name failed)))))) (append required standards)) @@ -61,7 +81,10 @@ stale-checks: (reverse stale) reasons: (append (if (null? missing) '() '(required-check-missing)) (if (null? failed) '() '(required-check-failed)) - (if (null? stale) '() '(check-revision-mismatch)))))))) + (if revision-mismatch? + '(check-revision-mismatch) '()) + (if standard-mismatch? + '(standard-assessment-mismatch) '()))))))) (def (gitops-unmatched-decision change) (validate GitOpsDecision (.new GitOpsDecision @@ -80,7 +103,7 @@ (composition-gitops-profiles composition))) (if profile (gitops-profile-decision - profile (composition-standard-names composition) change checks) + profile (composition-standard-bindings composition) change checks) (gitops-unmatched-decision change)))) (def GitOpsDefaultEvaluationMethod diff --git a/modules/gitops/objects.ss b/modules/gitops/objects.ss index f46354d..c6432af 100644 --- a/modules/gitops/objects.ss +++ b/modules/gitops/objects.ss @@ -71,6 +71,10 @@ provider: provider-value event: event-value repository: repository-value revision: revision-value source-ref: source-ref-value target-ref: target-ref-value pull-request: pull-request-value)) -(def (gitops-check name-value repository-value revision-value conclusion-value) +(def (gitops-check name-value repository-value revision-value conclusion-value + standard-edition: (standard-edition #f) + source-lock-digest: (source-lock-digest #f)) (.new GitOpsCheck name: name-value repository: repository-value - revision: revision-value conclusion: conclusion-value)) + revision: revision-value conclusion: conclusion-value + standard-edition: standard-edition + source-lock-digest: source-lock-digest)) diff --git a/modules/gitops/types.ss b/modules/gitops/types.ss index cb5aa2c..27f2a5f 100644 --- a/modules/gitops/types.ss +++ b/modules/gitops/types.ss @@ -33,7 +33,9 @@ {name: {type: Symbol} repository: {type: NonEmptyString} revision: {type: NonEmptyString} - conclusion: {type: GitOpsCheckConclusion}}) + conclusion: {type: GitOpsCheckConclusion} + standard-edition: {type: (Or False NonEmptyString)} + source-lock-digest: {type: (Or False NonEmptyString)}}) (define-type (GitOpsDecision @ Class.) slots: =>.+ {repository: {type: NonEmptyString} diff --git a/modules/sdlc/standards/nasa-7150-2d-profile.ss b/modules/sdlc/standards/nasa-7150-2d-profile.ss index 66167fd..6d2bbb0 100644 --- a/modules/sdlc/standards/nasa-7150-2d-profile.ss +++ b/modules/sdlc/standards/nasa-7150-2d-profile.ss @@ -7,9 +7,11 @@ ;;; Applicability, review, and certification algorithms live in their own ;;; standard modules and do not enter the Profile import closure. -(import (only-in :clan/poo/object .o) +(import (only-in :clan/poo/object .o .ref) :poo-flow/lambda-aitia/modules/sdlc/objects - :poo-flow/lambda-aitia/modules/sdlc/standards/nasa-7150-2d-catalog) + :poo-flow/lambda-aitia/modules/sdlc/standards/nasa-7150-2d-catalog + (only-in :poo-flow/lambda-aitia/modules/sdlc/standards/sources + Nasa7150_2DSourcesLock)) (export Nasa7150_2D) (def chapter3-url @@ -17,6 +19,7 @@ (def Nasa7150_2D (.o (:: @ StandardProfile.) identity: "nasa/sdlc/npr-7150.2" edition: "D" + source-lock-digest: (.ref Nasa7150_2DSourcesLock 'digest) source-url: chapter3-url verified-on: "2026-09-12" effective-date: "2022-03-08" expiration-date: "2027-03-08" applicability-source: diff --git a/t/bindings/unit/aitia-native-test.ss b/t/bindings/unit/aitia-native-test.ss index 10ba3ec..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/sdlc/npr-7150.2\",\"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/integration/github-workflow-test.ss b/t/integration/github-workflow-test.ss index 5c89566..3328365 100644 --- a/t/integration/github-workflow-test.ss +++ b/t/integration/github-workflow-test.ss @@ -13,8 +13,20 @@ (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)) + (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)) @@ -58,6 +70,33 @@ (successful-checks '(commit-policy build unit-test)))) (check-equal? (.ref decision 'accepted) #f) (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 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)) From f5328fd9588bfee40e0b863a581986e6dea06baa Mon Sep 17 00:00:00 2001 From: guangtao Date: Thu, 24 Sep 2026 11:06:44 +0800 Subject: [PATCH 38/64] Make source-bound Host admissions fail closed on byte drift --- .../rfcs/0003-composition-value-interface.org | 7 +++ modules/assurance/host.ss | 47 ++++++++++++---- modules/sdlc/README.org | 12 +++-- .../sdlc/standards/nasa-standard-provider.ss | 2 +- t/sdlc/unit/design-verification-test.ss | 54 +++++++++++++++++-- t/sdlc/unit/nasa-standard-provider-test.ss | 5 ++ 6 files changed, 108 insertions(+), 19 deletions(-) diff --git a/docs/rfcs/0003-composition-value-interface.org b/docs/rfcs/0003-composition-value-interface.org index a177da8..6c0bb76 100644 --- a/docs/rfcs/0003-composition-value-interface.org +++ b/docs/rfcs/0003-composition-value-interface.org @@ -63,6 +63,13 @@ 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. +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 diff --git a/modules/assurance/host.ss b/modules/assurance/host.ss index ced6a2d..db4c02b 100644 --- a/modules/assurance/host.ss +++ b/modules/assurance/host.ss @@ -129,6 +129,7 @@ (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 @@ -136,10 +137,16 @@ (assurance-verification-subject snapshot obligation evidence outcome) now (+ now (vector-ref entry 3)))) - (when receipt - (hash-put! issued-host-receipts receipt - (vector host (vector-ref entry 8)))) - receipt))))) + (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? @@ -148,6 +155,7 @@ (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 @@ -235,6 +243,8 @@ (.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)) @@ -291,12 +301,29 @@ (equal? (cdr revision) (.ref artifact 'revision)) (eq? (.ref artifact 'state) 'supported) (let (payload (reader (.ref artifact 'identity))) - (and (or (string? payload) (u8vector? payload)) - (equal? (source-lock-payload-digest payload) - (.ref artifact 'content-digest)) - (equal? expected-cut - (assurance-snapshot-semantic-digest - (host-current-snapshot entry)))))))))) + (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))))))))))) + +;;; 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) diff --git a/modules/sdlc/README.org b/modules/sdlc/README.org index ab0175b..216ec2d 100644 --- a/modules/sdlc/README.org +++ b/modules/sdlc/README.org @@ -68,11 +68,13 @@ 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. -=source-bytes-checked-support-current?= then requires that Host to read the -mapped verifier input, compare its bytes to the Artifact SHA-256 using POO -Flow's Source Lock algorithm, and preserve the same canonical snapshot before -and after reading. Without the Host reader, or if the bytes or cut change, the -stronger result fails closed. SDLC review callers cannot supply replacement +With that reader, Host issuance and currentness check every declared verifier +input against its Artifact SHA-256 using POO Flow's Source Lock algorithm. +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. diff --git a/modules/sdlc/standards/nasa-standard-provider.ss b/modules/sdlc/standards/nasa-standard-provider.ss index 5b285a9..aa095d4 100644 --- a/modules/sdlc/standards/nasa-standard-provider.ss +++ b/modules/sdlc/standards/nasa-standard-provider.ss @@ -48,7 +48,7 @@ (def Nasa7150_2DStandardFamily (poo-flow-standard-family - "nasa/npr-7150.2" "NASA" 'software-engineering + "nasa/sdlc/npr-7150.2" "NASA" 'sdlc '(html scheme) "publicly-available-nasa-directive" '((contributor . lambda-aitia) (authority . nasa-nodis)))) diff --git a/t/sdlc/unit/design-verification-test.ss b/t/sdlc/unit/design-verification-test.ss index 49c5625..37c9358 100644 --- a/t/sdlc/unit/design-verification-test.ss +++ b/t/sdlc/unit/design-verification-test.ss @@ -148,15 +148,62 @@ source-reader: (lambda (identity) payload)))) (let-values (((receipt admission subject mapping) (review cut configured source evidence))) - (check-equal? (.ref receipt 'source-bound-support-current?) #t) + (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 (list admission)) + evidence-link admission '()) 'source-bytes-checked-support-current?) - #t)))) + #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) + (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 "a cut moving during Host source read fails closed" (let* ((cut (bound-cut (list evidence-link requirement-link source-link) evidence)) @@ -171,6 +218,7 @@ 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) diff --git a/t/sdlc/unit/nasa-standard-provider-test.ss b/t/sdlc/unit/nasa-standard-provider-test.ss index ea5781d..e6b5d72 100644 --- a/t/sdlc/unit/nasa-standard-provider-test.ss +++ b/t/sdlc/unit/nasa-standard-provider-test.ss @@ -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) From 32dc5d32561732b1bf1c1013de6ea31ad71961af Mon Sep 17 00:00:00 2001 From: guangtao Date: Thu, 24 Sep 2026 11:30:54 +0800 Subject: [PATCH 39/64] Bind source-reader verification to Host-materialized inputs --- .../rfcs/0003-composition-value-interface.org | 4 ++ modules/assurance/host.ss | 70 +++++++++++++++++-- modules/sdlc/README.org | 5 ++ t/sdlc/unit/design-verification-test.ss | 39 +++++++++-- 4 files changed, 109 insertions(+), 9 deletions(-) diff --git a/docs/rfcs/0003-composition-value-interface.org b/docs/rfcs/0003-composition-value-interface.org index 6c0bb76..5fbb8e7 100644 --- a/docs/rfcs/0003-composition-value-interface.org +++ b/docs/rfcs/0003-composition-value-interface.org @@ -65,6 +65,10 @@ 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 diff --git a/modules/assurance/host.ss b/modules/assurance/host.ss index db4c02b..4ba4122 100644 --- a/modules/assurance/host.ss +++ b/modules/assurance/host.ss @@ -111,10 +111,26 @@ (procedure? snapshot-source) (or (not source-reader) (procedure? source-reader))) (error "invalid assurance verification Host configuration")) - (let* ((adapter + (let* ((host (.o identity: (string-copy identity-value))) + (adapter (poo-flow-verification-adapter - identity-value operation assurance-verification-subject-snapshot)) - (host (.o identity: (string-copy identity-value)))) + 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)) @@ -278,7 +294,7 @@ ;;; 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 (assurance-host-source-current? host snapshot artifact) +(def (host-source-payload host snapshot artifact) (let* ((entry (required-host-entry host)) (reader (vector-ref entry 9))) (and reader @@ -309,7 +325,51 @@ (and matches? (equal? expected-cut (assurance-snapshot-semantic-digest - (host-current-snapshot entry))))))))))) + (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 diff --git a/modules/sdlc/README.org b/modules/sdlc/README.org index 216ec2d..67bce21 100644 --- a/modules/sdlc/README.org +++ b/modules/sdlc/README.org @@ -70,6 +70,11 @@ 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 diff --git a/t/sdlc/unit/design-verification-test.ss b/t/sdlc/unit/design-verification-test.ss index 37c9358..eb8e515 100644 --- a/t/sdlc/unit/design-verification-test.ss +++ b/t/sdlc/unit/design-verification-test.ss @@ -92,9 +92,18 @@ (def (host source-procedure source-reader: (reader #f)) (assurance-verification-host "host/design-verification" - (lambda (subject-value issued-at expires-at) - (and (equal? (.ref (.ref subject-value 'outcome) 'output-digest) digest-b) - (< issued-at expires-at))) + (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)) @@ -194,7 +203,8 @@ (configured (assurance-verification-host "host/source-race" - (lambda (subject issued-at expires-at) + (lambda (subject issued-at expires-at inputs) + (check-equal? (.ref (car inputs) 'payload) source-bytes) (set! payload "different") #t) (lambda () 11) 9 (lambda () cut) @@ -204,6 +214,27 @@ (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)) From ed0d7062c86ff2caf49c1fe445f6024e8e2711f1 Mon Sep 17 00:00:00 2001 From: guangtao Date: Thu, 24 Sep 2026 11:56:49 +0800 Subject: [PATCH 40/64] Qualify installed Python verifier with job executor and TLC contracts --- .github/workflows/ci.yml | 35 ++++ ROADMAP.org | 9 + bindings/python/README.md | 14 ++ .../qualification/job_executor/README.org | 28 +++ .../job_executor/test_unique_effect.py | 72 ++++++++ .../qualification/job_executor/worker.py | 41 +++++ .../python/src/lambda_aitia/verification.py | 166 ++++++++++++++++++ .../tests/installed_wheel_qualification.py | 79 +++++++++ bindings/python/tests/test_verification.py | 64 +++++++ bindings/python/tests/tla_contract.py | 55 ++++++ ...08-stable-operation-key-after-lost-ack.org | 75 ++++++++ docs/adrs/README.org | 1 + justfile | 14 +- proof/tla/README.org | 9 +- t/sdlc/unit/design-contract-test.ss | 21 +++ 15 files changed, 681 insertions(+), 2 deletions(-) create mode 100644 bindings/python/qualification/job_executor/README.org create mode 100644 bindings/python/qualification/job_executor/test_unique_effect.py create mode 100644 bindings/python/qualification/job_executor/worker.py create mode 100644 bindings/python/src/lambda_aitia/verification.py create mode 100644 bindings/python/tests/installed_wheel_qualification.py create mode 100644 bindings/python/tests/test_verification.py create mode 100644 bindings/python/tests/tla_contract.py create mode 100644 docs/adrs/0008-stable-operation-key-after-lost-ack.org diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b27cd03..ee44344 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,6 +85,41 @@ 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') }} + tla-model: + name: TLA+ assurance lifecycle counterexamples + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout Lambda Aitia + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Restore pinned TLC cache + id: tlc-cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: .ci/tools/tla2tools.jar + key: lambda-aitia-tlc-v1.7.2-fa18543e44ed5974a85bd2c60c0dc16620ae117680ea8e693d2691999ed90b22 + + - name: Download pinned TLC release asset + if: steps.tlc-cache.outputs.cache-hit != 'true' + run: | + mkdir -p .ci/tools + curl -fsSL --retry 3 \ + https://github.com/tlaplus/tlaplus/releases/download/v1.7.2/tla2tools.jar \ + -o .ci/tools/tla2tools.jar + + - name: Verify TLC release and replay all model contracts + env: + TLA_TOOLS_JAR: ${{ github.workspace }}/.ci/tools/tla2tools.jar + run: python3 -m unittest discover -s bindings/python/tests -p 'tla_contract.py' + + - name: Save pinned TLC cache + if: steps.tlc-cache.outputs.cache-hit != 'true' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: .ci/tools/tla2tools.jar + key: lambda-aitia-tlc-v1.7.2-fa18543e44ed5974a85bd2c60c0dc16620ae117680ea8e693d2691999ed90b22 + scheme-runtime: name: Scheme, integration and Python runtime runs-on: ubuntu-latest diff --git a/ROADMAP.org b/ROADMAP.org index a7cb823..ff88c8e 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -369,6 +369,15 @@ 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 uses Aitia-owned Software/SDLC knowledge while keeping NASA and provider rules diff --git a/bindings/python/README.md b/bindings/python/README.md index 2aff181..08f454f 100644 --- a/bindings/python/README.md +++ b/bindings/python/README.md @@ -23,6 +23,13 @@ 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. +`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. +The caller must supply an isolated source tree and trusted Python environment. +The Assurance Host still owns admission, revocation and execution authority. + ```python from lambda_aitia import SdlcRuntime @@ -52,6 +59,13 @@ LAMBDA_AITIA_NATIVE_LIBRARY=/absolute/path/to/libpoo_flow_aitia.dylib \ env -u SDKROOT uv build --wheel --offline --no-build-isolation ``` +`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, 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/qualification/job_executor/README.org b/bindings/python/qualification/job_executor/README.org new file mode 100644 index 0000000..3342750 --- /dev/null +++ b/bindings/python/qualification/job_executor/README.org @@ -0,0 +1,28 @@ +# 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; process input provenance, a durable Host session, 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. 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/verification.py b/bindings/python/src/lambda_aitia/verification.py new file mode 100644 index 0000000..e0bb0ee --- /dev/null +++ b/bindings/python/src/lambda_aitia/verification.py @@ -0,0 +1,166 @@ +# 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 +import hashlib +import os +from pathlib import Path +import subprocess +import tempfile +from uuid import uuid4 +import xml.etree.ElementTree as ET + + +def source_digest(root: Path) -> str: + """Hash the exact regular-file tree selected for a verifier run.""" + + root = root.resolve(strict=True) + if not root.is_dir(): + raise ValueError("source root must be a directory") + digest = hashlib.sha256() + for path in sorted(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") + contents = path.read_bytes() + digest.update(len(relative).to_bytes(8, "big")) + digest.update(relative) + digest.update(len(contents).to_bytes(8, "big")) + digest.update(contents) + return "sha256:" + digest.hexdigest() + + +@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 run_pytest( + python: Path, + source_root: Path, + selectors: tuple[str, ...], + *, + timeout_seconds: int = 30, + environment: dict[str, str] | None = None, +) -> PytestObservation: + """Run selected tests in a caller-frozen tree and validate their JUnit report. + + The caller owns the tree, interpreter, dependencies, and external services. + This function detects source drift, but cannot prove process isolation or + turn its result into a Host-issued seal. + """ + + if not selectors or any(not item or item.startswith("-") for item in selectors): + raise ValueError("at least one explicit pytest selector is required") + if timeout_seconds <= 0: + raise ValueError("timeout_seconds must be positive") + root = source_root.resolve(strict=True) + before = source_digest(root) + 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 report_directory: + report = Path(report_directory) / f"{run_id}.xml" + argv = ( + str(interpreter), "-m", "pytest", "-q", + "-p", "no:cacheprovider", f"--junitxml={report}", *selectors, + ) + try: + completed = subprocess.run( + argv, cwd=root, env=safe_environment, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, text=True, errors="replace", + timeout=timeout_seconds, check=False, + ) + exit_code = completed.returncode + output = completed.stdout + except subprocess.TimeoutExpired as error: + exit_code = None + output = (error.stdout or b"").decode("utf-8", "replace") if isinstance(error.stdout, bytes) else (error.stdout or "") + after = source_digest(root) + 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 exit_code is None: + status = "timeout" + 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:], + ) diff --git a/bindings/python/tests/installed_wheel_qualification.py b/bindings/python/tests/installed_wheel_qualification.py new file mode 100644 index 0000000..bbf6df4 --- /dev/null +++ b/bindings/python/tests/installed_wheel_qualification.py @@ -0,0 +1,79 @@ +# 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" + run("uv", "pip", "install", "--offline", "--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 = """ +from pathlib import Path +from lambda_aitia import SdlcRuntime +from lambda_aitia.verification import run_pytest +root = Path('scenario').resolve() +assert SdlcRuntime().plan('qualification/job-42').lifecycle_id == 'qualification/job-42' +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 +fixed = run_pytest(Path(__import__('sys').executable), root, selector, + environment={'AITIA_QUALIFICATION_MODE': 'fixed'}) +assert fixed.passed_obligation and fixed.collected == 1, fixed +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_verification.py b/bindings/python/tests/test_verification.py new file mode 100644 index 0000000..e11bced --- /dev/null +++ b/bindings/python/tests/test_verification.py @@ -0,0 +1,64 @@ +# SPDX-FileCopyrightText: 2026 tao3k team and Contributors +# +# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +from pathlib import Path +import sys + +import pytest + +from lambda_aitia.verification import run_pytest + + +@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 + assert not observed.source_current(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 diff --git a/bindings/python/tests/tla_contract.py b/bindings/python/tests/tla_contract.py new file mode 100644 index 0000000..28383d2 --- /dev/null +++ b/bindings/python/tests/tla_contract.py @@ -0,0 +1,55 @@ +# SPDX-FileCopyrightText: 2026 tao3k team and Contributors +# +# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +"""Project test for bounded TLC contracts and typed counterexamples.""" + +from __future__ import annotations + +import hashlib +import os +from pathlib import Path +import subprocess +import tempfile +import unittest + + +ROOT = Path(__file__).resolve().parents[3] / "proof/tla" +TOOLS_SHA256 = "fa18543e44ed5974a85bd2c60c0dc16620ae117680ea8e693d2691999ed90b22" +CASES = ( + ("AssuranceLifecycle.cfg", 0, "Model checking completed. No error has been found."), + ("AssuranceLifecycleProgress.cfg", 0, "Model checking completed. No error has been found."), + ("AssuranceLifecycleNoFair.cfg", 13, "Error: Temporal properties were violated."), + ("AssuranceLifecycleReachable.cfg", 12, "Error: Invariant NoEffectStarted is violated."), + ("AssuranceLifecycleUnsafe.cfg", 12, "Error: Invariant NoUnsafeEffectStart is violated."), + ("AssuranceLifecycleLateAdmit.cfg", 12, "Error: Invariant AdmissionIsCurrent is violated."), + ("AssuranceLifecycleEarlyGrant.cfg", 12, "Error: Invariant GrantRequiresCurrentDecision is violated."), + ("AssuranceLifecycleAfterRevoke.cfg", 12, "Error: Invariant NoUnsafeEffectStart is violated."), +) + + +class TlaContractTest(unittest.TestCase): + def test_bounded_lifecycle(self) -> None: + configured = os.environ.get("TLA_TOOLS_JAR") + self.assertIsNotNone(configured, "TLA_TOOLS_JAR must name v1.7.2") + jar = Path(configured).resolve(strict=True) + actual = hashlib.sha256(jar.read_bytes()).hexdigest() + self.assertEqual(actual, TOOLS_SHA256, "TLA+ tools digest mismatch") + for config, expected_code, expected_text in CASES: + with self.subTest(config=config): + with tempfile.TemporaryDirectory(prefix="aitia-tlc-") as directory: + result = subprocess.run( + ("java", "-XX:+UseParallelGC", "-Xmx512m", "-cp", str(jar), + "tlc2.TLC", "-workers", "1", "-metadir", directory, + "-config", config, "AssuranceLifecycle"), + cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, errors="replace", timeout=60, check=False, + ) + self.assertEqual(result.returncode, expected_code, result.stdout[-6000:]) + self.assertIn(expected_text, result.stdout[-6000:]) + if expected_code != 0: + self.assertTrue( + "counter-example" in result.stdout + or "behavior up to this point" in result.stdout, + result.stdout[-6000:], + ) 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..9929d6b --- /dev/null +++ b/docs/adrs/0008-stable-operation-key-after-lost-ack.org @@ -0,0 +1,75 @@ +# 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: r1 +: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. + +* 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]] diff --git a/docs/adrs/README.org b/docs/adrs/README.org index ed73d1a..6fb3cf6 100644 --- a/docs/adrs/README.org +++ b/docs/adrs/README.org @@ -67,6 +67,7 @@ assurance closure and authority receipt is required before any effect. - [[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: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/justfile b/justfile index b60773b..05af6bc 100644 --- a/justfile +++ b/justfile @@ -65,6 +65,12 @@ check-proof: cd proof/lean && lake build LambdaAitiaModuleAssuranceProof LambdaAitiaModuleSdlcProof cd proof/lean && output="$(lake env lean AxiomAudit.lean)" && printf '%s\n' "$output" && ! grep -q 'sorryAx' <<< "$output" +# TLA_TOOLS_JAR is an explicit, checksum-verified input; the recipe never +# downloads tools or writes into the user's global Java environment. +[group('check')] +check-tla: + python3 -m unittest discover -s bindings/python/tests -p 'tla_contract.py' + # Aitia owns its native ABI and Python Runtime checks. [group('check')] check-native: @@ -81,6 +87,12 @@ check-native: 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 @@ -93,7 +105,7 @@ build-python-wheel native_library: # 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-tla test-integration check-native [group('check')] check-all: check diff --git a/proof/tla/README.org b/proof/tla/README.org index 6962877..74d0740 100644 --- a/proof/tla/README.org +++ b/proof/tla/README.org @@ -16,7 +16,14 @@ revocation epochs. TLC exhaustively checks the safe model within those bounds. The =Unsafe*= actions are deliberate mutations; they are not implementation paths. A nonzero TLC exit for each mutant is the expected counterexample. -From this directory, with =tlc= installed on =PATH=: +The repository gate is =just check-tla= with =TLA_TOOLS_JAR= set to the +v1.7.2 release JAR (SHA-256 +=fa18543e44ed5974a85bd2c60c0dc16620ae117680ea8e693d2691999ed90b22=). +It runs all eight configurations with a 512 MiB Java heap, checks the exact +expected property for each negative case, and rejects missing tools, parser +errors and timeouts. The CI job downloads and verifies that release asset. + +For manual inspection from this directory, with =tlc= installed on =PATH=: #+begin_src sh JAVA_TOOL_OPTIONS=-Xmx512m tlc -workers 1 -config AssuranceLifecycle.cfg AssuranceLifecycle diff --git a/t/sdlc/unit/design-contract-test.ss b/t/sdlc/unit/design-contract-test.ss index ad1122c..c4eeefb 100644 --- a/t/sdlc/unit/design-contract-test.ss +++ b/t/sdlc/unit/design-contract-test.ss @@ -94,6 +94,27 @@ (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))) From 95f73941c135674391f809f4b3706242cb5933a3 Mon Sep 17 00:00:00 2001 From: guangtao Date: Thu, 24 Sep 2026 12:03:01 +0800 Subject: [PATCH 41/64] Install locked wheel-test dependencies on a cold cache --- bindings/python/tests/installed_wheel_qualification.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bindings/python/tests/installed_wheel_qualification.py b/bindings/python/tests/installed_wheel_qualification.py index bbf6df4..9ce64c5 100644 --- a/bindings/python/tests/installed_wheel_qualification.py +++ b/bindings/python/tests/installed_wheel_qualification.py @@ -47,7 +47,10 @@ def test_installed_wheel_qualification() -> None: cwd=PROJECT / "bindings/python", quiet=True) run("uv", "venv", "--python", str(PYTHON), str(venv), cwd=root) interpreter = venv / "bin/python" - run("uv", "pip", "install", "--offline", "--python", str(interpreter), + # 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) From d2960e197bd0ccb4aff58a59b357891709d0d406 Mon Sep 17 00:00:00 2001 From: guangtao Date: Thu, 24 Sep 2026 12:23:44 +0800 Subject: [PATCH 42/64] Keep Aitia native transport alive across Python calls --- bindings/python/README.md | 7 + bindings/python/src/lambda_aitia/__init__.py | 3 +- .../python/src/lambda_aitia/_native/_build.py | 18 +- .../src/lambda_aitia/_native/aitia_shim.c | 192 +++++++++++------- bindings/python/src/lambda_aitia/gitops.py | 10 +- bindings/python/src/lambda_aitia/native.py | 124 +++++++++-- bindings/python/src/lambda_aitia/runtime.py | 9 +- .../tests/installed_wheel_qualification.py | 7 +- bindings/python/tests/test_native.py | 35 +++- 9 files changed, 306 insertions(+), 99 deletions(-) diff --git a/bindings/python/README.md b/bindings/python/README.md index 08f454f..c53f405 100644 --- a/bindings/python/README.md +++ b/bindings/python/README.md @@ -30,6 +30,13 @@ runs, timeouts, missing reports and source drift do not pass its obligation. The caller must supply an isolated source tree and trusted Python environment. The Assurance Host still owns admission, revocation and execution authority. +`AitiaNativeSession` keeps one Gambit transport runtime alive across calls to +`SdlcRuntime(session=...)` and `evaluate_gitops(..., 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. + ```python from lambda_aitia import SdlcRuntime diff --git a/bindings/python/src/lambda_aitia/__init__.py b/bindings/python/src/lambda_aitia/__init__.py index 93da0ed..bded85c 100644 --- a/bindings/python/src/lambda_aitia/__init__.py +++ b/bindings/python/src/lambda_aitia/__init__.py @@ -5,11 +5,12 @@ """Production Python runtime for Lambda Aitia's Scheme-owned SDLC.""" from .gitops import GitOpsDecision, evaluate_gitops -from .native import AitiaNativeError, descriptor +from .native import AitiaNativeError, AitiaNativeSession, descriptor from .runtime import SdlcFlowPlan, SdlcRuntime __all__ = [ "AitiaNativeError", + "AitiaNativeSession", "GitOpsDecision", "SdlcFlowPlan", "SdlcRuntime", diff --git a/bindings/python/src/lambda_aitia/_native/_build.py b/bindings/python/src/lambda_aitia/_native/_build.py index 96aed4a..8f7353a 100644 --- a/bindings/python/src/lambda_aitia/_native/_build.py +++ b/bindings/python/src/lambda_aitia/_native/_build.py @@ -11,6 +11,19 @@ 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, @@ -34,6 +47,7 @@ if __name__ == "__main__": import sys + import sysconfig python_project = Path(__file__).resolve().parents[3] if str(python_project) not in sys.path: @@ -43,6 +57,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..4ff256e 100644 --- a/bindings/python/src/lambda_aitia/_native/aitia_shim.c +++ b/bindings/python/src/lambda_aitia/_native/aitia_shim.c @@ -4,6 +4,7 @@ #include "lambda_aitia/aitia.h" +#include #include #include #include @@ -32,6 +33,19 @@ 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 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, size_t destination_size) { @@ -57,121 +71,157 @@ 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; } - result_init(&result); + 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; + } + *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_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/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/tests/installed_wheel_qualification.py b/bindings/python/tests/installed_wheel_qualification.py index 9ce64c5..5a57240 100644 --- a/bindings/python/tests/installed_wheel_qualification.py +++ b/bindings/python/tests/installed_wheel_qualification.py @@ -61,10 +61,15 @@ def test_installed_wheel_qualification() -> None: # with cwd outside the checkout and no development path override. code = """ from pathlib import Path -from lambda_aitia import SdlcRuntime +from lambda_aitia import AitiaNativeSession, SdlcRuntime from lambda_aitia.verification import run_pytest 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' +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'}) diff --git a/bindings/python/tests/test_native.py b/bindings/python/tests/test_native.py index ba4be33..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 = { From b439af0dfcd8cedbef71feab4dfd72c374139540 Mon Sep 17 00:00:00 2001 From: guangtao Date: Thu, 24 Sep 2026 12:37:12 +0800 Subject: [PATCH 43/64] Run Python verifier from frozen source bytes --- bindings/python/README.md | 8 +- .../qualification/job_executor/README.org | 5 +- .../python/src/lambda_aitia/verification.py | 180 +++++++++++++++--- .../tests/installed_wheel_qualification.py | 5 +- bindings/python/tests/test_verification.py | 64 ++++++- 5 files changed, 229 insertions(+), 33 deletions(-) diff --git a/bindings/python/README.md b/bindings/python/README.md index c53f405..3cc7ab5 100644 --- a/bindings/python/README.md +++ b/bindings/python/README.md @@ -27,8 +27,12 @@ available; the package will never simulate completion with Python callbacks. 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. -The caller must supply an isolated source tree and trusted Python environment. -The Assurance Host still owns admission, revocation and execution authority. +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. `AitiaNativeSession` keeps one Gambit transport runtime alive across calls to `SdlcRuntime(session=...)` and `evaluate_gitops(..., session=...)`. It is diff --git a/bindings/python/qualification/job_executor/README.org b/bindings/python/qualification/job_executor/README.org index 3342750..bb39246 100644 --- a/bindings/python/qualification/job_executor/README.org +++ b/bindings/python/qualification/job_executor/README.org @@ -18,8 +18,9 @@ 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; process input provenance, a durable Host session, independent -provider trust, and a Runtime effect gate remain open. +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. diff --git a/bindings/python/src/lambda_aitia/verification.py b/bindings/python/src/lambda_aitia/verification.py index e0bb0ee..de46690 100644 --- a/bindings/python/src/lambda_aitia/verification.py +++ b/bindings/python/src/lambda_aitia/verification.py @@ -6,24 +6,39 @@ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, replace import hashlib import os -from pathlib import Path +from pathlib import Path, PurePosixPath, PureWindowsPath import subprocess import tempfile from uuid import uuid4 import xml.etree.ElementTree as ET -def source_digest(root: Path) -> str: - """Hash the exact regular-file tree selected for a verifier run.""" +_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") - digest = hashlib.sha256() - for path in sorted(root.rglob("*")): + 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(): @@ -31,12 +46,71 @@ def source_digest(root: Path) -> str: 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") - contents = path.read_bytes() - digest.update(len(relative).to_bytes(8, "big")) - digest.update(relative) - digest.update(len(contents).to_bytes(8, "big")) - digest.update(contents) - return "sha256:" + digest.hexdigest() + 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) @@ -65,27 +139,44 @@ def source_current(self, root: Path) -> bool: return False -def run_pytest( +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 run_pytest_frozen( python: Path, - source_root: Path, + source_payload: bytes, selectors: tuple[str, ...], *, timeout_seconds: int = 30, environment: dict[str, str] | None = None, ) -> PytestObservation: - """Run selected tests in a caller-frozen tree and validate their JUnit report. + """Run selected tests only from a validated frozen source payload. - The caller owns the tree, interpreter, dependencies, and external services. - This function detects source drift, but cannot prove process isolation or - turn its result into a Host-issued seal. + The caller owns the interpreter, dependencies and external services. The + payload is observable process input, not a Host-issued verification seal. """ - if not selectors or any(not item or item.startswith("-") for item in selectors): - raise ValueError("at least one explicit pytest selector is required") + entries = _source_entries(source_payload) + _validated_selectors(selectors, entries) if timeout_seconds <= 0: raise ValueError("timeout_seconds must be positive") - root = source_root.resolve(strict=True) - before = source_digest(root) + before = _source_payload_digest(source_payload) run_id = uuid4().hex interpreter = python.absolute() if not interpreter.is_file(): @@ -99,8 +190,14 @@ def run_pytest( 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 report_directory: - report = Path(report_directory) / f"{run_id}.xml" + 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", "-p", "no:cacheprovider", f"--junitxml={report}", *selectors, @@ -115,8 +212,15 @@ def run_pytest( output = completed.stdout except subprocess.TimeoutExpired as error: exit_code = None - output = (error.stdout or b"").decode("utf-8", "replace") if isinstance(error.stdout, bytes) else (error.stdout or "") - after = source_digest(root) + output = ( + (error.stdout or b"").decode("utf-8", "replace") + if isinstance(error.stdout, bytes) + else (error.stdout or "") + ) + try: + after = source_digest(root) + except (OSError, ValueError): + after = None collected = passed = skipped = 0 report_digest = None report_valid = False @@ -164,3 +268,27 @@ def run_pytest( run_id, before, argv, exit_code, status, collected, passed, skipped, report_digest, output[-8192:], ) + + +def run_pytest( + python: Path, + source_root: Path, + selectors: tuple[str, ...], + *, + timeout_seconds: int = 30, + 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, 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 index 5a57240..890108c 100644 --- a/bindings/python/tests/installed_wheel_qualification.py +++ b/bindings/python/tests/installed_wheel_qualification.py @@ -62,7 +62,7 @@ def test_installed_wheel_qualification() -> None: code = """ from pathlib import Path from lambda_aitia import AitiaNativeSession, SdlcRuntime -from lambda_aitia.verification import run_pytest +from lambda_aitia.verification import capture_source_tree, run_pytest, run_pytest_frozen root = Path('scenario').resolve() assert SdlcRuntime().plan('qualification/job-42').lifecycle_id == 'qualification/job-42' with AitiaNativeSession() as session: @@ -75,7 +75,8 @@ def test_installed_wheel_qualification() -> None: environment={'AITIA_QUALIFICATION_MODE': 'buggy'}) assert buggy.status == 'failed', buggy assert 'assert 2 == 1' in buggy.output, buggy.output -fixed = run_pytest(Path(__import__('sys').executable), root, selector, +fixed = run_pytest_frozen(Path(__import__('sys').executable), + capture_source_tree(root), selector, environment={'AITIA_QUALIFICATION_MODE': 'fixed'}) assert fixed.passed_obligation and fixed.collected == 1, fixed assert fixed.source_current(root) diff --git a/bindings/python/tests/test_verification.py b/bindings/python/tests/test_verification.py index e11bced..1d0ded9 100644 --- a/bindings/python/tests/test_verification.py +++ b/bindings/python/tests/test_verification.py @@ -3,11 +3,17 @@ # SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later from pathlib import Path +import hashlib import sys import pytest -from lambda_aitia.verification import run_pytest +from lambda_aitia.verification import ( + capture_source_tree, + run_pytest, + run_pytest_frozen, + source_digest, +) @pytest.mark.parametrize( @@ -47,9 +53,65 @@ def test_pytest_observation_detects_source_drift(tmp_path: Path) -> None: 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_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" From 2bf1a64289d9eeb45544b348e9db984132d738bd Mon Sep 17 00:00:00 2001 From: guangtao Date: Thu, 24 Sep 2026 12:52:06 +0800 Subject: [PATCH 44/64] Bound Python verifier output and stop timed-out process groups --- bindings/python/README.md | 4 + .../python/src/lambda_aitia/verification.py | 128 +++++++++++++++--- bindings/python/tests/test_verification.py | 36 +++++ 3 files changed, 149 insertions(+), 19 deletions(-) diff --git a/bindings/python/README.md b/bindings/python/README.md index 3cc7ab5..a501cb2 100644 --- a/bindings/python/README.md +++ b/bindings/python/README.md @@ -33,6 +33,10 @@ from a separately materialized copy; selectors cannot escape that copy. 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. +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=...)` and `evaluate_gitops(..., session=...)`. It is diff --git a/bindings/python/src/lambda_aitia/verification.py b/bindings/python/src/lambda_aitia/verification.py index de46690..e43246e 100644 --- a/bindings/python/src/lambda_aitia/verification.py +++ b/bindings/python/src/lambda_aitia/verification.py @@ -10,8 +10,11 @@ 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 @@ -158,12 +161,107 @@ def _validated_selectors( 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. @@ -176,6 +274,8 @@ def run_pytest_frozen( _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() @@ -199,24 +299,12 @@ def run_pytest_frozen( target.write_bytes(contents) report = Path(workspace) / f"{run_id}.xml" argv = ( - str(interpreter), "-m", "pytest", "-q", + str(interpreter), "-m", "pytest", "-q", "-s", "-p", "no:cacheprovider", f"--junitxml={report}", *selectors, ) - try: - completed = subprocess.run( - argv, cwd=root, env=safe_environment, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, text=True, errors="replace", - timeout=timeout_seconds, check=False, - ) - exit_code = completed.returncode - output = completed.stdout - except subprocess.TimeoutExpired as error: - exit_code = None - output = ( - (error.stdout or b"").decode("utf-8", "replace") - if isinstance(error.stdout, bytes) - else (error.stdout or "") - ) + 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): @@ -242,8 +330,8 @@ def run_pytest_frozen( pass if before != after: status = "source-changed" - elif exit_code is None: - status = "timeout" + elif stop_reason: + status = stop_reason elif exit_code == 2: status = "interrupted" elif exit_code == 3: @@ -276,6 +364,7 @@ def run_pytest( 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. @@ -287,7 +376,8 @@ def run_pytest( payload = capture_source_tree(source_root) observation = run_pytest_frozen( python, payload, selectors, - timeout_seconds=timeout_seconds, environment=environment, + 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") diff --git a/bindings/python/tests/test_verification.py b/bindings/python/tests/test_verification.py index 1d0ded9..36b8b8d 100644 --- a/bindings/python/tests/test_verification.py +++ b/bindings/python/tests/test_verification.py @@ -4,7 +4,9 @@ from pathlib import Path import hashlib +import os import sys +import time import pytest @@ -124,3 +126,37 @@ def test_pytest_observation_rejects_timeout(tmp_path: Path) -> None: 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 From 6d62587701e2f178df3425165d8867dbcb4ba316 Mon Sep 17 00:00:00 2001 From: guangtao Date: Thu, 24 Sep 2026 13:02:38 +0800 Subject: [PATCH 45/64] Scope AnyIO foundation to the full Aitia Python runtime --- README.org | 6 ++-- bindings/python/README.md | 12 +++++-- bindings/python/pyproject.toml | 4 +-- bindings/python/src/lambda_aitia/__init__.py | 2 +- bindings/python/uv.lock | 33 +++++++++++++++++++ .../0006-production-python-sdlc-runtime.org | 17 ++++++---- docs/adrs/README.org | 2 +- 7 files changed, 62 insertions(+), 14 deletions(-) diff --git a/README.org b/README.org index 22355ec..6f1e9d2 100644 --- a/README.org +++ b/README.org @@ -49,13 +49,15 @@ 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. SDLC planning is the first implemented capability, not the scope of +the Python project. AnyIO is its runtime-wide concurrency foundation; generic +Graph/DAG and FunFlow execution mechanisms remain owned by POO Flow. * Core model diff --git a/bindings/python/README.md b/bindings/python/README.md index a501cb2..0e03c7b 100644 --- a/bindings/python/README.md +++ b/bindings/python/README.md @@ -3,10 +3,11 @@ 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's software-engineering capabilities. +SDLC planning is its first implemented lifecycle contract: ```text change -> invalidate -> plan -> verify -> admit -> decide -> authorize -> effect @@ -17,6 +18,13 @@ 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. +AnyIO is a production dependency for the runtime-wide asynchronous execution +boundary: structured task lifetimes, cancellation and bounded concurrency will +apply across Aitia capabilities, not only SDLC and not as a pytest cleanup +mechanism. The current native session is thread-affine, so it must not be +passed into `anyio.to_thread.run_sync`; an async entry point requires a +qualified owner-preserving transport before it can claim execution. + The current production surface publishes and validates the canonical lifecycle DAG through `SdlcRuntime.plan()`. The returned receipt is deliberately inert: it cannot claim release authorization or runtime execution. Stage execution is 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/src/lambda_aitia/__init__.py b/bindings/python/src/lambda_aitia/__init__.py index bded85c..773f259 100644 --- a/bindings/python/src/lambda_aitia/__init__.py +++ b/bindings/python/src/lambda_aitia/__init__.py @@ -2,7 +2,7 @@ # # 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, AitiaNativeSession, descriptor 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/docs/adrs/0006-production-python-sdlc-runtime.org b/docs/adrs/0006-production-python-sdlc-runtime.org index cfd2131..f7194c1 100644 --- a/docs/adrs/0006-production-python-sdlc-runtime.org +++ b/docs/adrs/0006-production-python-sdlc-runtime.org @@ -1,13 +1,13 @@ # 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: r2 :RFC_ID: lambda-aitia-rfc-0001 :DECISION_MAKERS: tao3k-maintainers :CONSULTED: poo-flow-maintainers @@ -23,8 +23,9 @@ 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, not an SDLC-only +project. The SDLC lifecycle is its first implemented contract and carries the +complete vocabulary: #+begin_example change -> invalidate -> plan -> verify -> admit -> decide -> authorize -> effect @@ -38,6 +39,9 @@ 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 @@ -50,8 +54,9 @@ 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 complete lifecycle remains the SDLC capability boundary, not the scope + of the entire Python project. Executable capabilities are delivered only + with owned operations and attributable receipts. * Confirmation diff --git a/docs/adrs/README.org b/docs/adrs/README.org index 6fb3cf6..322d260 100644 --- a/docs/adrs/README.org +++ b/docs/adrs/README.org @@ -65,7 +65,7 @@ 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)]] From f3438241780e863db6c0b518d86628b540f96cd1 Mon Sep 17 00:00:00 2001 From: guangtao Date: Thu, 24 Sep 2026 13:31:18 +0800 Subject: [PATCH 46/64] Document full SDLC design and causal assurance workflows --- README.org | 173 ++++++---- ROADMAP.org | 144 ++++++-- bindings/python/README.md | 45 ++- .../0006-production-python-sdlc-runtime.org | 53 ++- .../software-sdlc-integration-inventory.org | 85 +++-- docs/rfcs/0001-causal-assurance-core.org | 78 +++-- .../rfcs/0003-composition-value-interface.org | 10 + ...04-sdlc-design-and-causal-trajectories.org | 322 ++++++++++++++++++ 8 files changed, 726 insertions(+), 184 deletions(-) create mode 100644 docs/rfcs/0004-sdlc-design-and-causal-trajectories.org diff --git a/README.org b/README.org index 6f1e9d2..c0344cc 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 @@ -55,19 +62,20 @@ The production Aitia Python Runtime is maintained entirely under 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. SDLC planning is the first implemented capability, not the scope of -the Python project. AnyIO is its runtime-wide concurrency foundation; generic -Graph/DAG and FunFlow execution mechanisms remain owned by POO Flow. +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. @@ -89,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 @@ -116,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 @@ -134,10 +142,12 @@ 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/tlaps-bench][TLAPS-Bench]] | TLA+ invariant proof tasks and checking runtime | mechanical proof checking and benchmark qualification | model proof requires separate implementation correspondence before supporting a code claim | qualify existing checker execution paths and tasks; preserve mandatory checks | +| [[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, TLA+/TLC, TLAPS 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 @@ -172,31 +182,45 @@ different propositions and trusted boundaries: | 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 | +| TLAPS | mechanical checking of TLA+ proof obligations under explicit statements and trusted dependencies | implementation correspondence 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 +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 TLA+, 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, a bounded TLA+ lifecycle model 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 @@ -240,6 +264,9 @@ 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/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. @@ -247,8 +274,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 ff88c8e..e85cb48 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -4,6 +4,40 @@ #+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. + +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. + +#+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 @@ -118,12 +152,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: @@ -245,12 +287,13 @@ Deliverables: 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: @@ -277,11 +320,29 @@ Lean owns general laws, including: - [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 +TLA+ 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 TLC model in =proof/tla/AssuranceLifecycle.tla= now checks current admission/decision/grant authority, expiry and revocation. Separate mutant @@ -330,7 +391,7 @@ 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. @@ -353,6 +414,11 @@ Exit gates: 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 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 @@ -421,21 +487,33 @@ 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. [X] Move the SDLC and GitOps owners into Aitia and publish the Episteme - deletion plus parent pin at the already qualified ownership cut. A newer - Aitia PR head still needs its own parent gitlink update after acceptance. -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: + +1. [ ] Extend the job-executor design case with a common requirement set and + comparable alternatives, keeping completion and unique-effect obligations. +2. [ ] Qualify one TLAPS-Bench checker/task boundary and one asset-complete + upstream bug case. Record files, exact revisions, licenses, dependencies, + resource bounds and required upstream checks in the integration inventory. +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. +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. +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/python/README.md b/bindings/python/README.md index 0e03c7b..a0282f6 100644 --- a/bindings/python/README.md +++ b/bindings/python/README.md @@ -6,13 +6,31 @@ SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later # Lambda Aitia Python Runtime This is Lambda Aitia's production Python project, not an example package. It -is the Python runtime surface for Aitia's software-engineering capabilities. -SDLC planning is its first implemented lifecycle contract: - -```text -change -> invalidate -> plan -> verify -> admit -> decide -> authorize -> effect +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: + +```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 @@ -20,17 +38,24 @@ ABI and does not reimplement either owner's semantics. AnyIO is a production dependency for the runtime-wide asynchronous execution boundary: structured task lifetimes, cancellation and bounded concurrency will -apply across Aitia capabilities, not only SDLC and not as a pytest cleanup -mechanism. The current native session is thread-affine, so it must not be -passed into `anyio.to_thread.run_sync`; an async entry point requires a -qualified owner-preserving transport before it can claim execution. +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 +entry point requires a qualified owner-preserving transport before it can +claim execution. -The current production surface publishes and validates the canonical lifecycle +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 diff --git a/docs/adrs/0006-production-python-sdlc-runtime.org b/docs/adrs/0006-production-python-sdlc-runtime.org index f7194c1..7cea091 100644 --- a/docs/adrs/0006-production-python-sdlc-runtime.org +++ b/docs/adrs/0006-production-python-sdlc-runtime.org @@ -7,7 +7,8 @@ :ID: lambda-aitia-adr-0006 :STATUS: accepted :DATE: 2026-09-18 -:REVISION: r2 +:REVISION: r3 +:UPDATED: 2026-09-24 :RFC_ID: lambda-aitia-rfc-0001 :DECISION_MAKERS: tao3k-maintainers :CONSULTED: poo-flow-maintainers @@ -23,13 +24,30 @@ implementations to drift. * Decision -=bindings/python= is Lambda Aitia's production Python Runtime, not an SDLC-only -project. The SDLC lifecycle is its first implemented contract and carries the -complete 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: @@ -43,8 +61,8 @@ The responsibilities remain layered: 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. @@ -54,9 +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 SDLC capability boundary, not the scope - of the entire Python project. Executable capabilities are delivered only - with owned operations and attributable receipts. +- 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 @@ -65,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/plans/software-sdlc-integration-inventory.org b/docs/plans/software-sdlc-integration-inventory.org index 8611060..29c6eda 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, @@ -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,33 @@ 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 | | =proof/tla/AssuranceLifecycle.tla= | 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 + +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 | +|-----------+--------------+------------------------+----------------| +| TLAPS-Bench | proof task and existing checker execution path | preserve target/trusted inputs and regression checks; prove the selected entry point and environment work | unqualified; Euclid/GCD is a candidate smoke task | +| 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 +134,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/rfcs/0001-causal-assurance-core.org b/docs/rfcs/0001-causal-assurance-core.org index 18be3aa..f08130f 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,10 @@ 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 | +| TLAPS | mechanical checking of TLA+ proof obligations | model/code correspondence or runtime authority | | 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 +83,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 @@ -163,6 +174,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 @@ -186,6 +204,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 @@ -229,15 +254,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 @@ -246,11 +271,8 @@ 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 @@ -336,10 +358,14 @@ 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. -The first verifier kinds are Lean proof, TLA+/TLC model check, runtime replay, +Verifier integrations include Lean/TLAPS proof, TLA+/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; +listing TLAPS here does not mean its execution path is already integrated. +Reuse the necessary upstream checker and trusted execution boundary before +adding a new implementation, as specified by RFC 0004. * Assurance laws diff --git a/docs/rfcs/0003-composition-value-interface.org b/docs/rfcs/0003-composition-value-interface.org index 5fbb8e7..4c5ab34 100644 --- a/docs/rfcs/0003-composition-value-interface.org +++ b/docs/rfcs/0003-composition-value-interface.org @@ -8,6 +8,7 @@ :ID: lambda-aitia-rfc-0003 :STATUS: draft :DATE: 2026-09-18 +:UPDATED: 2026-09-24 :PARENT_RFC: lambda-aitia-rfc-0001 :IMPLEMENTS: lambda-aitia-rfc-0001 :END: @@ -89,6 +90,14 @@ 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 | @@ -125,6 +134,7 @@ that ordinary users must type. - 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..a55f063 --- /dev/null +++ b/docs/rfcs/0004-sdlc-design-and-causal-trajectories.org @@ -0,0 +1,322 @@ +# 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 +: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. + +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. + +* 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. + +* 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. + +TLAPS-Bench is a candidate source of proof tasks and checking components. +Retain the target statement, trusted inputs and required checks when qualifying +an entry point. Use TLAPS to check TLA+ proofs; a Lean translation is not a +prerequisite. TLC model checking and TLAPS proof checking retain their distinct +propositions. 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 small TLAPS-Bench task and one sufficiently complete upstream bug +case as reusable methods. These are separate execution qualifications until +their claims actually refer to the same subject: a GCD proof cannot discharge +a job-executor guarantee. A known-answer replay is regression evidence; novel +discovery needs a separate evaluation with unexposed answers. + +* Pending acceptance gates + +- [ ] 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. +- [ ] Frozen Python tool observations reach the actual Scheme Host admission + path; serialized success cannot substitute for that path. +- [ ] 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:../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]] +- 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/tlaps-bench][TLAPS-Bench tasks and runtime]] +- [[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. From de65fa2033ce62569147ee5411c05c50051206dd Mon Sep 17 00:00:00 2001 From: guangtao Date: Thu, 24 Sep 2026 13:42:02 +0800 Subject: [PATCH 47/64] Ground design judgments in historical cases and existing Cedar boundaries --- ROADMAP.org | 6 ++ .../qualification/job_executor/README.org | 30 ++++++ ...08-stable-operation-key-after-lost-ack.org | 52 +++++++++- .../engineering-case-source-inventory.org | 63 ++++++++++++ .../software-sdlc-integration-inventory.org | 5 + ...04-sdlc-design-and-causal-trajectories.org | 98 +++++++++++++++++++ 6 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 docs/plans/engineering-case-source-inventory.org diff --git a/ROADMAP.org b/ROADMAP.org index e85cb48..dd5c0ea 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -492,6 +492,12 @@ foundation work is recorded in the phases above. The current queue is: 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 TLAPS-Bench checker/task boundary and one asset-complete upstream bug case. Record files, exact revisions, licenses, dependencies, resource bounds and required upstream checks in the integration inventory. diff --git a/bindings/python/qualification/job_executor/README.org b/bindings/python/qualification/job_executor/README.org index bb39246..235cb97 100644 --- a/bindings/python/qualification/job_executor/README.org +++ b/bindings/python/qualification/job_executor/README.org @@ -27,3 +27,33 @@ 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/docs/adrs/0008-stable-operation-key-after-lost-ack.org b/docs/adrs/0008-stable-operation-key-after-lost-ack.org index 9929d6b..5897f13 100644 --- a/docs/adrs/0008-stable-operation-key-after-lost-ack.org +++ b/docs/adrs/0008-stable-operation-key-after-lost-ack.org @@ -7,7 +7,8 @@ :ID: lambda-aitia-adr-0008 :STATUS: proposed :DATE: 2026-09-24 -:REVISION: r1 +:REVISION: r2 +:UPDATED: 2026-09-24 :RFC_ID: lambda-aitia-rfc-0001 :DECISION_MAKERS: tao3k-maintainers :CONSULTED: poo-flow-maintainers @@ -69,7 +70,56 @@ by declaration-level compatibility. - 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/plans/engineering-case-source-inventory.org b/docs/plans/engineering-case-source-inventory.org new file mode 100644 index 0000000..9210319 --- /dev/null +++ b/docs/plans/engineering-case-source-inventory.org @@ -0,0 +1,63 @@ +# 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. + +* 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 29c6eda..5591031 100644 --- a/docs/plans/software-sdlc-integration-inventory.org +++ b/docs/plans/software-sdlc-integration-inventory.org @@ -83,6 +83,11 @@ eight-step assurance/execution protocol is one mechanism within this framework. * 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. diff --git a/docs/rfcs/0004-sdlc-design-and-causal-trajectories.org b/docs/rfcs/0004-sdlc-design-and-causal-trajectories.org index a55f063..89c5a73 100644 --- a/docs/rfcs/0004-sdlc-design-and-causal-trajectories.org +++ b/docs/rfcs/0004-sdlc-design-and-causal-trajectories.org @@ -8,6 +8,7 @@ :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: @@ -194,6 +195,96 @@ 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. +* 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 @@ -281,6 +372,12 @@ 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 @@ -311,6 +408,7 @@ and their receipts retain their existing scope. - [[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. From bc168ce33deef2bf8a09f5dc96301e6e4817819c Mon Sep 17 00:00:00 2001 From: guangtao Date: Thu, 24 Sep 2026 19:55:23 +0800 Subject: [PATCH 48/64] feat(org): consume Orgize contract ABI in production Python runtime --- .github/workflows/ci.yml | 2 +- bindings/c/aitia-native.ss | 3 + bindings/python/README.md | 11 +- bindings/python/src/lambda_aitia/__init__.py | 4 + .../python/src/lambda_aitia/_native/_build.py | 30 +++- .../src/lambda_aitia/_native/aitia_shim.c | 36 +++++ .../python/src/lambda_aitia/org_contract.py | 145 ++++++++++++++++++ .../tests/installed_wheel_qualification.py | 9 +- bindings/python/tests/test_org_contract.py | 58 +++++++ tools/build-native-library.py | 22 +++ 10 files changed, 316 insertions(+), 4 deletions(-) create mode 100644 bindings/python/src/lambda_aitia/org_contract.py create mode 100644 bindings/python/tests/test_org_contract.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee44344..94ddcb0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -133,7 +133,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: repository: tao3k/poo-flow - ref: 29be9b24f1eb6a15ff7337a3b2fc7e398770e5de + ref: aeabd8229316d4540e4cd7d0edd701e91c1ae535 path: poo-flow submodules: false diff --git a/bindings/c/aitia-native.ss b/bindings/c/aitia-native.ss index 0062f96..9f5e560 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-native :poo-flow/lambda-aitia/bindings/c/aitia-contract) (export aitia-c-round-trip) diff --git a/bindings/python/README.md b/bindings/python/README.md index a0282f6..500ef33 100644 --- a/bindings/python/README.md +++ b/bindings/python/README.md @@ -72,12 +72,21 @@ 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=...)` and `evaluate_gitops(..., session=...)`. It is +`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 diff --git a/bindings/python/src/lambda_aitia/__init__.py b/bindings/python/src/lambda_aitia/__init__.py index 773f259..7627ea1 100644 --- a/bindings/python/src/lambda_aitia/__init__.py +++ b/bindings/python/src/lambda_aitia/__init__.py @@ -6,14 +6,18 @@ from .gitops import GitOpsDecision, evaluate_gitops from .native import AitiaNativeError, AitiaNativeSession, descriptor +from .org_contract import OrgContractResult, OrgElementFact, evaluate_org_contract from .runtime import SdlcFlowPlan, SdlcRuntime __all__ = [ "AitiaNativeError", "AitiaNativeSession", "GitOpsDecision", + "OrgContractResult", + "OrgElementFact", "SdlcFlowPlan", "SdlcRuntime", "descriptor", "evaluate_gitops", + "evaluate_org_contract", ] diff --git a/bindings/python/src/lambda_aitia/_native/_build.py b/bindings/python/src/lambda_aitia/_native/_build.py index 8f7353a..d150683 100644 --- a/bindings/python/src/lambda_aitia/_native/_build.py +++ b/bindings/python/src/lambda_aitia/_native/_build.py @@ -5,6 +5,7 @@ from __future__ import annotations from pathlib import Path +import os from cffi import FFI @@ -34,15 +35,42 @@ 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__": diff --git a/bindings/python/src/lambda_aitia/_native/aitia_shim.c b/bindings/python/src/lambda_aitia/_native/aitia_shim.c index 4ff256e..c517c3b 100644 --- a/bindings/python/src/lambda_aitia/_native/aitia_shim.c +++ b/bindings/python/src/lambda_aitia/_native/aitia_shim.c @@ -3,6 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later #include "lambda_aitia/aitia.h" +#include "orgize.h" #include #include @@ -32,6 +33,11 @@ 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; @@ -211,6 +217,36 @@ int lambda_aitia_python_session_call( 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, 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/tests/installed_wheel_qualification.py b/bindings/python/tests/installed_wheel_qualification.py index 890108c..e0aa9fa 100644 --- a/bindings/python/tests/installed_wheel_qualification.py +++ b/bindings/python/tests/installed_wheel_qualification.py @@ -61,7 +61,8 @@ def test_installed_wheel_qualification() -> None: # with cwd outside the checkout and no development path override. code = """ from pathlib import Path -from lambda_aitia import AitiaNativeSession, SdlcRuntime +from lambda_aitia import (AitiaNativeSession, OrgElementFact, SdlcRuntime, + evaluate_org_contract) from lambda_aitia.verification import capture_source_tree, run_pytest, run_pytest_frozen root = Path('scenario').resolve() assert SdlcRuntime().plan('qualification/job-42').lifecycle_id == 'qualification/job-42' @@ -69,6 +70,12 @@ def test_installed_wheel_qualification() -> None: 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, 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/tools/build-native-library.py b/tools/build-native-library.py index 54a89c9..43cc5b4 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" + ) + if not (header_root / "include/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() From d42d2aeec2ee7fdd5599dbf95faab77f3c2ca0a2 Mon Sep 17 00:00:00 2001 From: guangtao Date: Mon, 28 Sep 2026 17:19:21 +0800 Subject: [PATCH 49/64] feat(sdlc): add retirement assurance and Org workspace flow --- README.org | 5 + ROADMAP.org | 63 +++ bindings/python/README.md | 40 +- bindings/python/src/lambda_aitia/__init__.py | 4 + .../python/src/lambda_aitia/org_workspace.py | 169 ++++++++ .../python/src/lambda_aitia/verification.py | 38 ++ .../tests/installed_wheel_qualification.py | 22 +- bindings/python/tests/test_org_workspace.py | 178 +++++++++ bindings/python/tests/test_verification.py | 59 +++ bindings/python/tests/tla_contract.py | 61 ++- .../engineering-case-source-inventory.org | 14 + ...2026-09-24-formal-source-qualification.org | 120 ++++++ .../2026-09-24-retirement-formal-review.org | 158 ++++++++ .../receipts/2026-09-24-retirement-impact.org | 102 +++++ .../2026-09-24-retirement-ingress-review.org | 143 +++++++ ...04-sdlc-design-and-causal-trajectories.org | 196 ++++++++++ .../0005-org-native-engineering-workspace.org | 361 ++++++++++++++++++ justfile | 14 +- proof/lean/AxiomAudit.lean | 10 + proof/lean/LambdaAitiaProof/Modules/Sdlc.lean | 2 + .../Modules/Sdlc/Retirement.lean | 81 ++++ .../Modules/Sdlc/RetirementIngress.lean | 92 +++++ proof/tla/AssuranceLifecycle.tla | 4 +- proof/tla/EffectAdmission.tla | 10 + proof/tla/README.org | 132 +++++-- proof/tla/RetirementIngress.cfg | 4 + proof/tla/RetirementIngress.tla | 66 ++++ proof/tla/RetirementIngressCachedGrant.cfg | 4 + proof/tla/RetirementIngressRejected.cfg | 4 + proof/tla/RetirementIngressStarted.cfg | 4 + proof/tla/RetirementIngressUnfenced.cfg | 4 + proof/tla/RetirementReview.cfg | 6 + proof/tla/RetirementReview.tla | 79 ++++ proof/tla/RetirementReviewCancellation.cfg | 5 + proof/tla/RetirementReviewReachable.cfg | 5 + proof/tla/RetirementReviewStale.cfg | 5 + proof/tla/RetirementReviewUnknown.cfg | 5 + proof/tla/WorkCancellation.cfg | 4 + proof/tla/WorkCancellationReachable.cfg | 4 + proof/tla/WorkLifecycle.cfg | 4 + proof/tla/WorkLifecycle.tla | 31 ++ proof/tla/WorkQuiescenceReachable.cfg | 4 + t/formal/retirement-binding-test.ss | 82 ++++ t/formal/tla-poo-interface-test.ss | 44 +++ t/formal/tla-syntax-test.ss | 34 ++ t/sdlc/unit/job-retirement-impact-test.ss | 91 +++++ .../contracts/engineering-workspace.org | 95 +++++ .../scenarios/job-executor-retirement.ss | 81 ++++ .../scenarios/job-retirement-contract.ss | 54 +++ user-interface/workspaces/job-executor.org | 289 ++++++++++++++ 50 files changed, 3047 insertions(+), 39 deletions(-) create mode 100644 bindings/python/src/lambda_aitia/org_workspace.py create mode 100644 bindings/python/tests/test_org_workspace.py create mode 100644 docs/receipts/2026-09-24-formal-source-qualification.org create mode 100644 docs/receipts/2026-09-24-retirement-formal-review.org create mode 100644 docs/receipts/2026-09-24-retirement-impact.org create mode 100644 docs/receipts/2026-09-24-retirement-ingress-review.org create mode 100644 docs/rfcs/0005-org-native-engineering-workspace.org create mode 100644 proof/lean/LambdaAitiaProof/Modules/Sdlc/Retirement.lean create mode 100644 proof/lean/LambdaAitiaProof/Modules/Sdlc/RetirementIngress.lean create mode 100644 proof/tla/EffectAdmission.tla create mode 100644 proof/tla/RetirementIngress.cfg create mode 100644 proof/tla/RetirementIngress.tla create mode 100644 proof/tla/RetirementIngressCachedGrant.cfg create mode 100644 proof/tla/RetirementIngressRejected.cfg create mode 100644 proof/tla/RetirementIngressStarted.cfg create mode 100644 proof/tla/RetirementIngressUnfenced.cfg create mode 100644 proof/tla/RetirementReview.cfg create mode 100644 proof/tla/RetirementReview.tla create mode 100644 proof/tla/RetirementReviewCancellation.cfg create mode 100644 proof/tla/RetirementReviewReachable.cfg create mode 100644 proof/tla/RetirementReviewStale.cfg create mode 100644 proof/tla/RetirementReviewUnknown.cfg create mode 100644 proof/tla/WorkCancellation.cfg create mode 100644 proof/tla/WorkCancellationReachable.cfg create mode 100644 proof/tla/WorkLifecycle.cfg create mode 100644 proof/tla/WorkLifecycle.tla create mode 100644 proof/tla/WorkQuiescenceReachable.cfg create mode 100644 t/formal/retirement-binding-test.ss create mode 100644 t/formal/tla-poo-interface-test.ss create mode 100644 t/formal/tla-syntax-test.ss create mode 100644 t/sdlc/unit/job-retirement-impact-test.ss create mode 100644 user-interface/contracts/engineering-workspace.org create mode 100644 user-interface/scenarios/job-executor-retirement.ss create mode 100644 user-interface/scenarios/job-retirement-contract.ss create mode 100644 user-interface/workspaces/job-executor.org diff --git a/README.org b/README.org index c0344cc..7c75936 100644 --- a/README.org +++ b/README.org @@ -267,6 +267,11 @@ The machine-readable declaration and complete texts are maintained in - [[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. diff --git a/ROADMAP.org b/ROADMAP.org index dd5c0ea..755e8e4 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -18,6 +18,13 @@ 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 @@ -26,6 +33,21 @@ 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"] @@ -419,6 +441,14 @@ 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 @@ -490,6 +520,23 @@ domain branch and the public API remains POO-native. The earlier migration, nominal vocabulary, snapshot/invalidation and CFFI foundation work is recorded in the phases above. The current queue is: +Immediate priority (2026-09-24): defer new persistence integration. First +challenge the design with source-reviewed upstream failure reports and actual +formal checks. [[file:proof/tla/README.org][Modular TLA+ checks]] now separate work/cancellation, retirement review 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. The +[[file:docs/receipts/2026-09-24-formal-source-qualification.org][local source qualification]] now passes native TLA+ parsing/roundtrip and +declared POO Contract/source-cut invalidation. Next gates are publication and +consumer pinning of the parser fix, independent replay, and semantic +correspondence between the Scheme contract and formal views (not just hashes). +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; live 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 @@ -505,10 +552,26 @@ foundation work is recorded in the phases above. The current queue is: 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 diff --git a/bindings/python/README.md b/bindings/python/README.md index 500ef33..95b1e49 100644 --- a/bindings/python/README.md +++ b/bindings/python/README.md @@ -40,8 +40,14 @@ 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 -entry point requires a qualified owner-preserving transport before it can -claim execution. +*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: @@ -66,6 +72,29 @@ from a separately materialized copy; selectors cannot escape that copy. 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 @@ -119,9 +148,10 @@ LAMBDA_AITIA_NATIVE_LIBRARY=/absolute/path/to/libpoo_flow_aitia.dylib \ `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, 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. +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 diff --git a/bindings/python/src/lambda_aitia/__init__.py b/bindings/python/src/lambda_aitia/__init__.py index 7627ea1..05c02b0 100644 --- a/bindings/python/src/lambda_aitia/__init__.py +++ b/bindings/python/src/lambda_aitia/__init__.py @@ -7,6 +7,7 @@ from .gitops import GitOpsDecision, evaluate_gitops 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__ = [ @@ -15,9 +16,12 @@ "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/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/verification.py b/bindings/python/src/lambda_aitia/verification.py index e43246e..13e94fe 100644 --- a/bindings/python/src/lambda_aitia/verification.py +++ b/bindings/python/src/lambda_aitia/verification.py @@ -18,6 +18,8 @@ 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 @@ -358,6 +360,42 @@ def run_pytest_frozen( ) +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, diff --git a/bindings/python/tests/installed_wheel_qualification.py b/bindings/python/tests/installed_wheel_qualification.py index e0aa9fa..205344b 100644 --- a/bindings/python/tests/installed_wheel_qualification.py +++ b/bindings/python/tests/installed_wheel_qualification.py @@ -60,10 +60,11 @@ def test_installed_wheel_qualification() -> 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 +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: @@ -82,10 +83,21 @@ def test_installed_wheel_qualification() -> None: environment={'AITIA_QUALIFICATION_MODE': 'buggy'}) assert buggy.status == 'failed', buggy assert 'assert 2 == 1' in buggy.output, buggy.output -fixed = run_pytest_frozen(Path(__import__('sys').executable), - capture_source_tree(root), selector, - environment={'AITIA_QUALIFICATION_MODE': 'fixed'}) -assert fixed.passed_obligation and fixed.collected == 1, fixed +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') 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 index 36b8b8d..817ee82 100644 --- a/bindings/python/tests/test_verification.py +++ b/bindings/python/tests/test_verification.py @@ -8,12 +8,14 @@ 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, ) @@ -72,6 +74,63 @@ def test_frozen_execution_uses_captured_bytes_not_later_source(tmp_path: 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: diff --git a/bindings/python/tests/tla_contract.py b/bindings/python/tests/tla_contract.py index 28383d2..e148450 100644 --- a/bindings/python/tests/tla_contract.py +++ b/bindings/python/tests/tla_contract.py @@ -6,9 +6,11 @@ from __future__ import annotations +import argparse import hashlib import os from pathlib import Path +import re import subprocess import tempfile import unittest @@ -26,30 +28,85 @@ ("AssuranceLifecycleEarlyGrant.cfg", 12, "Error: Invariant GrantRequiresCurrentDecision is violated."), ("AssuranceLifecycleAfterRevoke.cfg", 12, "Error: Invariant NoUnsafeEffectStart is violated."), ) +WORK_CASES = ( + ("WorkLifecycle.cfg", 0, "Model checking completed. No error has been found."), + ("WorkCancellation.cfg", 0, "Model checking completed. No error has been found."), + ("WorkCancellationReachable.cfg", 12, "Error: Invariant NeverCancelled is violated."), + ("WorkQuiescenceReachable.cfg", 12, "Error: Invariant NeverQuiescent is violated."), +) +RETIREMENT_CASES = ( + ("RetirementReview.cfg", 0, "Model checking completed. No error has been found."), + ("RetirementReviewCancellation.cfg", 12, "Error: Invariant NoOutstandingWork is violated."), + ("RetirementReviewStale.cfg", 12, "Error: Invariant NoOutstandingWork is violated."), + ("RetirementReviewUnknown.cfg", 12, "Error: Invariant InventoryKnown is violated."), + ("RetirementReviewReachable.cfg", 12, "Error: Invariant NeverRetired is violated."), +) +INGRESS_CASES = ( + ("RetirementIngress.cfg", 0, "Model checking completed. No error has been found."), + ("RetirementIngressUnfenced.cfg", 12, "Error: Invariant NoPostRetirementStart is violated."), + ("RetirementIngressCachedGrant.cfg", 12, "Error: Invariant NoPostRetirementStart is violated."), + ("RetirementIngressRejected.cfg", 12, "Error: Invariant NeverRejectAfterRetirement is violated."), + ("RetirementIngressStarted.cfg", 12, "Error: Invariant NeverStarted is violated."), +) class TlaContractTest(unittest.TestCase): def test_bounded_lifecycle(self) -> None: + self.check_cases("AssuranceLifecycle", CASES) + + def test_work(self) -> None: + self.check_cases("WorkLifecycle", WORK_CASES) + + def test_retirement(self) -> None: + self.check_cases("RetirementReview", RETIREMENT_CASES) + + def test_ingress(self) -> None: + self.check_cases("RetirementIngress", INGRESS_CASES) + + def check_cases(self, model: str, cases: tuple) -> None: configured = os.environ.get("TLA_TOOLS_JAR") self.assertIsNotNone(configured, "TLA_TOOLS_JAR must name v1.7.2") jar = Path(configured).resolve(strict=True) actual = hashlib.sha256(jar.read_bytes()).hexdigest() self.assertEqual(actual, TOOLS_SHA256, "TLA+ tools digest mismatch") - for config, expected_code, expected_text in CASES: + for config, expected_code, expected_text in cases: with self.subTest(config=config): with tempfile.TemporaryDirectory(prefix="aitia-tlc-") as directory: result = subprocess.run( ("java", "-XX:+UseParallelGC", "-Xmx512m", "-cp", str(jar), "tlc2.TLC", "-workers", "1", "-metadir", directory, - "-config", config, "AssuranceLifecycle"), + "-config", config, model), cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, errors="replace", timeout=60, check=False, ) self.assertEqual(result.returncode, expected_code, result.stdout[-6000:]) self.assertIn(expected_text, result.stdout[-6000:]) + explored = re.search( + r"(\d+) states generated, (\d+) distinct states found, (\d+) states left on queue", + result.stdout, + ) + self.assertIsNotNone(explored, result.stdout[-6000:]) + self.assertGreater(int(explored[2]), 0, "empty state exploration") + if expected_code == 0: + self.assertEqual(int(explored[3]), 0, "incomplete exploration") + print(f"{config}: generated={explored[1]} distinct={explored[2]} " + f"queued={explored[3]} exit={result.returncode}", flush=True) if expected_code != 0: self.assertTrue( "counter-example" in result.stdout or "behavior up to this point" in result.stdout, result.stdout[-6000:], ) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("module", nargs="?", default="all", + choices=("all", "lifecycle", "work", "retirement", "ingress")) + selected = parser.parse_args().module + methods = {"lifecycle": "test_bounded_lifecycle", "work": "test_work", + "retirement": "test_retirement", "ingress": "test_ingress"} + target = "TlaContractTest" + if selected != "all": + target += "." + methods[selected] + unittest.main(argv=[__file__, target, "-v"]) diff --git a/docs/plans/engineering-case-source-inventory.org b/docs/plans/engineering-case-source-inventory.org index 9210319..01a1d3b 100644 --- a/docs/plans/engineering-case-source-inventory.org +++ b/docs/plans/engineering-case-source-inventory.org @@ -31,6 +31,20 @@ 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 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..8ac9894 --- /dev/null +++ b/docs/receipts/2026-09-24-formal-source-qualification.org @@ -0,0 +1,120 @@ +# 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: + +* 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..b52dbd0 --- /dev/null +++ b/docs/receipts/2026-09-24-retirement-formal-review.org @@ -0,0 +1,158 @@ +# 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: + +* 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..0a440b4 --- /dev/null +++ b/docs/receipts/2026-09-24-retirement-ingress-review.org @@ -0,0 +1,143 @@ +# 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: + +* 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/0004-sdlc-design-and-causal-trajectories.org b/docs/rfcs/0004-sdlc-design-and-causal-trajectories.org index 89c5a73..abfda2b 100644 --- a/docs/rfcs/0004-sdlc-design-and-causal-trajectories.org +++ b/docs/rfcs/0004-sdlc-design-and-causal-trajectories.org @@ -37,6 +37,10 @@ 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 @@ -80,6 +84,83 @@ 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. + +TLA+ is organized into reusable action/contract modules with independently +runnable configurations and a separate 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 first [[file:../../proof/tla/README.org][modular 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 TLA+ refinement. The +[[file:../receipts/2026-09-24-retirement-formal-review.org][formal receipt]] records the actual checks and uncovered parser boundary. + +The [[file:../receipts/2026-09-24-formal-source-qualification.org][local source qualification]] now checks native syntax/byte-roundtrip and +binds both source views to one maintained 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. Portable parser pinning, +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 → TLA+ | 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 | +| TLA+/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= | TLA+ =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 | TLA+ =Review!Retire= requires =known= and =Work!Quiescent=; Lean =Retirement.Step.finalize= requires =inventory=, =idle= and =drained= | Neither formal view proves the inventory is complete in production | +| =no-new-grant= | The TLA+ 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= | TLA+ =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= | TLA+ 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 | @@ -195,6 +276,106 @@ 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 @@ -388,8 +569,22 @@ discovery needs a separate evaluation with unexposed answers. 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 + TLA+/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 @@ -405,6 +600,7 @@ and their receipts retain their existing scope. - [[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]] 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 05af6bc..85fa899 100644 --- a/justfile +++ b/justfile @@ -68,8 +68,18 @@ check-proof: # TLA_TOOLS_JAR is an explicit, checksum-verified input; the recipe never # downloads tools or writes into the user's global Java environment. [group('check')] -check-tla: - python3 -m unittest discover -s bindings/python/tests -p 'tla_contract.py' +check-tla module='all': + uv run --locked --project bindings/python python bindings/python/tests/tla_contract.py '{{module}}' + +# Explicit source/formal lane: requires the qualified native parser dependency. +# Every gate must run; none substitutes for semantic implementation refinement. +[group('check')] +check-formal: + just test-scheme-atomic formal tla-syntax-test.ss + just test-scheme-atomic formal tla-poo-interface-test.ss + just test-scheme-atomic formal retirement-binding-test.ss + just check-tla + just check-proof # Aitia owns its native ABI and Python Runtime checks. [group('check')] diff --git a/proof/lean/AxiomAudit.lean b/proof/lean/AxiomAudit.lean index d43334e..9d871e1 100644 --- a/proof/lean/AxiomAudit.lean +++ b/proof/lean/AxiomAudit.lean @@ -17,3 +17,13 @@ open LambdaAitiaProof.Modules #print axioms Sdlc.Design.refinesTrans #print axioms Sdlc.Design.strongerAssumptionBlocks #print axioms Sdlc.Design.droppedGuaranteeBlocks +#print axioms Sdlc.Retirement.initialSafe +#print axioms Sdlc.Retirement.stepPreservesSafety +#print axioms Sdlc.Retirement.reachableSafe +#print axioms Sdlc.Retirement.cancellationDoesNotJustifyRetirement +#print axioms Sdlc.Retirement.oldReviewDoesNotJustifyRetirement +#print axioms Sdlc.Retirement.unknownInventoryBlocksRetirement +#print axioms Sdlc.RetirementIngress.initialSafe +#print axioms Sdlc.RetirementIngress.currentGrantExcludesRetirement +#print axioms Sdlc.RetirementIngress.stepPreservesSafety +#print axioms Sdlc.RetirementIngress.reachableSafe diff --git a/proof/lean/LambdaAitiaProof/Modules/Sdlc.lean b/proof/lean/LambdaAitiaProof/Modules/Sdlc.lean index 26845c3..18d1818 100644 --- a/proof/lean/LambdaAitiaProof/Modules/Sdlc.lean +++ b/proof/lean/LambdaAitiaProof/Modules/Sdlc.lean @@ -3,3 +3,5 @@ -- SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later import LambdaAitiaProof.Modules.Sdlc.Design +import LambdaAitiaProof.Modules.Sdlc.Retirement +import LambdaAitiaProof.Modules.Sdlc.RetirementIngress diff --git a/proof/lean/LambdaAitiaProof/Modules/Sdlc/Retirement.lean b/proof/lean/LambdaAitiaProof/Modules/Sdlc/Retirement.lean new file mode 100644 index 0000000..2ed44b8 --- /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 TLA/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..9a78929 --- /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, TLA 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/tla/AssuranceLifecycle.tla b/proof/tla/AssuranceLifecycle.tla index 7af3664..499157d 100644 --- a/proof/tla/AssuranceLifecycle.tla +++ b/proof/tla/AssuranceLifecycle.tla @@ -3,7 +3,7 @@ \* \* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later -EXTENDS Naturals, TLC +EXTENDS Naturals, TLC, EffectAdmission (* Bounded control-plane model. A successful report is not a Host verification; admitted evidence and grants are current only inside their validity cut. *) @@ -21,7 +21,7 @@ Verify == adapterInstalled /\ planned = revision /\ clock < 3 /\ verified' = rev Admit == verified = revision /\ clock < sealUntil /\ admitted # revision /\ admitted' = revision /\ UNCHANGED <> Decide == admitted = revision /\ decided # revision /\ decided' = revision /\ UNCHANGED <> Authorize == decided = revision /\ admitted = revision /\ granted # revision /\ clock < 3 /\ granted' = revision /\ grantEpoch' = epoch /\ grantUntil' = clock + 1 /\ UNCHANGED <> -StartEffect == ~effectStarted /\ granted = revision /\ admitted = revision /\ decided = revision /\ grantEpoch = epoch /\ clock < grantUntil /\ effectStarted' = TRUE /\ startSafe' = (admitted = revision /\ decided = revision /\ granted = revision /\ grantEpoch = epoch /\ clock < grantUntil) /\ UNCHANGED <> +StartEffect == ~effectStarted /\ CurrentGrant(granted, revision, admitted, decided, grantEpoch, epoch, clock, grantUntil) /\ effectStarted' = TRUE /\ startSafe' = (admitted = revision /\ decided = revision /\ granted = revision /\ grantEpoch = epoch /\ clock < grantUntil) /\ UNCHANGED <> ChangeSource == revision = 1 /\ revision' = 2 /\ planned' = 0 /\ verified' = 0 /\ admitted' = 0 /\ decided' = 0 /\ granted' = 0 /\ sealUntil' = 0 /\ grantUntil' = 0 /\ UNCHANGED <> Tick == clock < 3 /\ clock' = clock + 1 /\ admitted' = (IF clock + 1 < sealUntil THEN admitted ELSE 0) /\ decided' = (IF clock + 1 < sealUntil THEN decided ELSE 0) /\ granted' = (IF (clock + 1 < sealUntil /\ clock + 1 < grantUntil) THEN granted ELSE 0) /\ UNCHANGED <> Revoke == epoch < 2 /\ epoch' = epoch + 1 /\ verified' = 0 /\ admitted' = 0 /\ decided' = 0 /\ granted' = 0 /\ sealUntil' = 0 /\ UNCHANGED <> diff --git a/proof/tla/EffectAdmission.tla b/proof/tla/EffectAdmission.tla new file mode 100644 index 0000000..7a03512 --- /dev/null +++ b/proof/tla/EffectAdmission.tla @@ -0,0 +1,10 @@ +---- MODULE EffectAdmission ---- +\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors +\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +EXTENDS Naturals + +(* Shared predicate extracted from AssuranceLifecycle.StartEffect. + This is a formal contract, not another Runtime or policy evaluator. *) +CurrentGrant(granted, revision, admitted, decided, grantEpoch, epoch, clock, grantUntil) == granted = revision /\ admitted = revision /\ decided = revision /\ grantEpoch = epoch /\ clock < grantUntil +==== diff --git a/proof/tla/README.org b/proof/tla/README.org index 74d0740..e39bf39 100644 --- a/proof/tla/README.org +++ b/proof/tla/README.org @@ -1,11 +1,102 @@ # SPDX-FileCopyrightText: 2026 tao3k team and Contributors # SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later -#+title: Bounded assurance lifecycle model +#+title: Modular TLA+ design checks -This model describes one obligation and one source revision change. It is a -contract for a future trusted Host handoff, not an implementation of that Host. -In particular, =ReportSuccess= is untrusted: it cannot verify, admit, decide, +:PROPERTIES: +:ID: lambda-aitia-tla-modules +:STATUS: design-validation-partial +:DATE: 2026-09-24 +:END: + +* Boundaries and independent checks + +| Selector | Model owner | Checks | Dependency | +|----------+-------------+--------+------------| +| =work= | [[file:WorkLifecycle.tla][WorkLifecycle]] | cancellation preserves outstanding work; cancellation and quiescence reachable; Boolean state space | TLA standard library | +| =retirement= | [[file:RetirementReview.tla][RetirementReview]] | final-cut work/inventory guards; three unsafe mutants; retirement reachable | =INSTANCE WorkLifecycle= | +| =ingress= | [[file:RetirementIngress.tla][RetirementIngress]] | post-retirement requests, epoch fencing, cached-grant mutants and reachable rejection/start | =INSTANCE RetirementReview= and =EffectAdmission= | +| =lifecycle= | [[file:AssuranceLifecycle.tla][AssuranceLifecycle]] | existing evidence/admission/authorization safety and conditional progress | =EffectAdmission= | +| =all= (default) | all four lanes | 22 configurations | complete regression lane | + +An atomic check is a named contract boundary, not an arbitrary file fragment. +The retirement model reuses the work actions instead of copying cancellation +semantics. Its environment also injects late activity and inventory loss, which +are absent from the isolated work model. Passing atomic checks therefore does +not replace the composition check. A work-module edit requires =work=, +=retirement= and =ingress=; a retirement-only edit requires =retirement= and +=ingress=. An =EffectAdmission= edit requires =lifecycle= and =ingress=; +full CI runs =all=. + +The existing assurance lifecycle remains an integration model, not a completed +modularization of admission, revocation and grants. Its effect-admission guard +is now a shared operator, not copied into the retirement consumer. The full +revocation transition/issuance protocol still requires independent extraction +and composition checks before claiming an entirely modular assurance library. + +* Replay + +Set =TLA_TOOLS_JAR= to the existing checksum-verified v1.7.2 release jar. No +tool download or global environment change occurs in these recipes. + +#+begin_src sh +just check-tla work +just check-tla retirement +just check-tla ingress +just check-tla lifecycle +just check-tla +just check-proof +just check-formal +#+end_src + +The Python project is managed by =uv=; the same unittest checks also run in CI +without requiring production Runtime imports. Unknown selectors are rejected, +not silently treated as zero tests. Each TLC run uses one worker, a 512 MiB Java +heap, a 60-second timeout and a separate temporary state directory. Expected +counterexamples require the exact invariant failure, exit status and trace; +parser or tool errors are not successful negative tests. Heap is not a total +RSS bound. Cache the pinned tool, not an unqualified previous green result. + +* Interpretation + +The new models are design experiments, not a second public configuration API +and not yet generated projections of an admitted Scheme POO Contract. Their +=retired= state is a review decision at a modeled cut, not deletion performed +by the Runtime. No action authority follows from a passing model check. + +The new models keep deadlock checking enabled and have explicit terminal +self-loops. Isolated =RetirementReview= freezes its terminal environment; +=RetirementIngress= explicitly permits a later request and checks rejection +using the same current-grant predicate as =AssuranceLifecycle=. This closes a +bounded model assumption, not a deployed ingress or Runtime guarantee. No +fairness, eventual drain, crash recovery, inventory truth, +unbounded consumer coverage or persistent-state claim is established here. + +[[file:../../docs/receipts/2026-09-24-retirement-formal-review.org][The formal review receipt]] separates TLC exploration, Lean laws, native parser +roundtrip and pending Scheme correspondence. TLC acceptance alone does not +close the three-part formal qualification required by POO Flow. +[[file:../../docs/receipts/2026-09-24-retirement-ingress-review.org][The ingress follow-up]] records the new counterexamples, shared predicate +regression and four independent native parser capability gaps. +[[file:../../docs/receipts/2026-09-24-formal-source-qualification.org][The source qualification follow-up]] closes those four gaps locally and runs +all five native models unchanged. =check-formal= runs native syntax, declared +POO source binding, all TLC configurations and Lean in sequence; it fails if +the installed parser lacks the required syntax. Until the parser patch is +published and its dependency pin updated, this explicit lane requires the +qualified linked source checkout in =GERBIL_LOADPATH=. It is not silently added +to default CI against an older dependency. + +The maintained [[file:../../user-interface/scenarios/job-retirement-contract.ss][retirement POO design contract]] names the assumptions, guarantee and +TLA/Lean source groups. Binding tests use existing source digests, Assurance +snapshots and SDLC implementation links. A changed source cut or premise +invalidates the old links. This establishes declared source correspondence, +not generated projections, semantic refinement, verifier admission or Runtime +authority; those claims remain explicitly false in the review value. + +* Existing assurance lifecycle contract + +=AssuranceLifecycle= describes one obligation and one source revision change. +It is a contract for a future trusted Host handoff, not an implementation of +that Host. =ReportSuccess= is untrusted: it cannot verify, admit, decide, authorize, or start an effect. =InstallAdapter= merely makes a verifier available. Admission requires a current verification seal; authorization requires current admission and decision; starting an effect also requires a @@ -13,15 +104,10 @@ non-expired grant issued in the current revocation epoch. The bounded domains are two source revisions, four clock values, and three revocation epochs. TLC exhaustively checks the safe model within those bounds. -The =Unsafe*= actions are deliberate mutations; they are not implementation -paths. A nonzero TLC exit for each mutant is the expected counterexample. - -The repository gate is =just check-tla= with =TLA_TOOLS_JAR= set to the -v1.7.2 release JAR (SHA-256 -=fa18543e44ed5974a85bd2c60c0dc16620ae117680ea8e693d2691999ed90b22=). -It runs all eight configurations with a 512 MiB Java heap, checks the exact -expected property for each negative case, and rejects missing tools, parser -errors and timeouts. The CI job downloads and verifies that release asset. +The =Unsafe*= actions are deliberate mutations, not implementation paths. +The =lifecycle= selector retains all eight original configurations; the CI +unittest discovery retains these plus fourteen new modular checks. The tool +digest remains =fa18543e44ed5974a85bd2c60c0dc16620ae117680ea8e693d2691999ed90b22=. For manual inspection from this directory, with =tlc= installed on =PATH=: @@ -36,18 +122,18 @@ JAVA_TOOL_OPTIONS=-Xmx512m tlc -workers 1 -config AssuranceLifecycleEarlyGrant.c JAVA_TOOL_OPTIONS=-Xmx512m tlc -workers 1 -config AssuranceLifecycleAfterRevoke.cfg AssuranceLifecycle #+end_src -Use =-metadir= with a writable temporary directory when running these checks -from a read-only checkout. The safe and progress configurations must exit 0. -Progress assumes a stable source and clock, no revocation, and weak fairness -for each Host stage. The no-fairness configuration must violate -=EventuallyEffect=: a possible path alone does not prove progress. The reachability -configuration deliberately checks =NoEffectStarted= and must find a valid -=StartEffect= trace. Each other mutant must violate its named invariant; a -mutant passing TLC would be a failed negative contract, not a success. +Use =-metadir= with a writable temporary directory for read-only checkouts. +Safe and progress configurations must exit 0. Progress assumes a stable source +and clock, no revocation, and weak fairness for each Host stage. No-fairness +must violate =EventuallyEffect=: a possible path alone does not prove progress. +Reachability checks =NoEffectStarted= and must find a valid =StartEffect= trace. +Other mutants must violate their named invariants, not merely fail to run. This model does not prove a live adapter was installed by a trusted Host, a -clock is actually trustworthy, a grant is cryptographically bound, or the +clock is trustworthy, a grant is cryptographically bound, or the actual Scheme/Python/Runtime path enforces the model. It does not yet model counterfactual promotion or concurrent effects. Conditional fairness does not establish progress when sources change, clocks advance, or grants are revoked. -Those remain separate Phase 4–6 exit gates. +Those remain separate Phase 4–6 exit gates. The original configurations disable +deadlock checks; they are retained as baseline regressions, not examples of the +new explicit-terminal-state discipline. diff --git a/proof/tla/RetirementIngress.cfg b/proof/tla/RetirementIngress.cfg new file mode 100644 index 0000000..da830d5 --- /dev/null +++ b/proof/tla/RetirementIngress.cfg @@ -0,0 +1,4 @@ +\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors +\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later +SPECIFICATION Spec +INVARIANTS TypeOK RetiredIsFenced NoPostRetirementStart NoOutstandingWork InventoryKnown diff --git a/proof/tla/RetirementIngress.tla b/proof/tla/RetirementIngress.tla new file mode 100644 index 0000000..eabb163 --- /dev/null +++ b/proof/tla/RetirementIngress.tla @@ -0,0 +1,66 @@ +---- MODULE RetirementIngress ---- +\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors +\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +EXTENDS Naturals, TLC, EffectAdmission + +(* Requests can arrive AFTER retirement. Closing ingress stops new grants and + advances the authority epoch; a client may still hold an old cached grant. + Revision/admission/decision/time are fixed to isolate epoch revocation. + No new grant issuance, persistence or deployed authorization is modeled. *) +VARIABLES active, queued, known, reviewed, cancelRequested, retired, + lateFound, gapFound, ingressOpen, epoch, grantEpoch, + pending, attempted, rejectedAfterRetirement, startedAfterRetirement +Review == INSTANCE RetirementReview +vars == <> +boundary == <> +Current == CurrentGrant(1, 1, 1, 1, grantEpoch, epoch, 0, 1) +Init == /\ Review!Init /\ ingressOpen = TRUE /\ epoch = 0 /\ grantEpoch = 0 + /\ pending = FALSE /\ attempted = FALSE + /\ rejectedAfterRetirement = FALSE /\ startedAfterRetirement = FALSE + +Observe == Review!Observe /\ UNCHANGED boundary +CloseIngress == + /\ ingressOpen /\ ingressOpen' = FALSE /\ epoch' = epoch + 1 + /\ UNCHANGED <> +Request == + /\ ~pending /\ ~attempted /\ pending' = TRUE + /\ UNCHANGED <> +StartBody == + /\ active' = TRUE /\ pending' = FALSE /\ attempted' = TRUE + /\ startedAfterRetirement' = retired + /\ UNCHANGED <> +Start == pending /\ Current /\ StartBody +TrustCachedGrant == pending /\ StartBody +Reject == + /\ pending /\ ~Current /\ pending' = FALSE /\ attempted' = TRUE + /\ rejectedAfterRetirement' = retired + /\ UNCHANGED <> +Retire == ~ingressOpen /\ Review!Retire /\ UNCHANGED boundary +RetireWithoutFence == Review!Retire /\ UNCHANGED boundary +Terminal == retired /\ attempted /\ UNCHANGED vars +Environment == Observe \/ CloseIngress \/ Request \/ Reject \/ Terminal +Next == Environment \/ Start \/ Retire +Spec == Init /\ [][Next]_vars +UnfencedSpec == Init /\ [][Environment \/ Start \/ RetireWithoutFence]_vars +CachedGrantSpec == Init /\ [][Environment \/ TrustCachedGrant \/ Retire]_vars +TypeOK == /\ Review!TypeOK /\ ingressOpen \in BOOLEAN /\ epoch \in 0..1 + /\ grantEpoch = 0 /\ pending \in BOOLEAN /\ attempted \in BOOLEAN + /\ rejectedAfterRetirement \in BOOLEAN /\ startedAfterRetirement \in BOOLEAN +RetiredIsFenced == retired => (~ingressOpen /\ grantEpoch # epoch) +NoPostRetirementStart == ~startedAfterRetirement +NoOutstandingWork == Review!NoOutstandingWork +InventoryKnown == Review!InventoryKnown +NeverRejectAfterRetirement == ~rejectedAfterRetirement +NeverStarted == ~(attempted /\ ~rejectedAfterRetirement /\ epoch = 0) +==== diff --git a/proof/tla/RetirementIngressCachedGrant.cfg b/proof/tla/RetirementIngressCachedGrant.cfg new file mode 100644 index 0000000..bca7038 --- /dev/null +++ b/proof/tla/RetirementIngressCachedGrant.cfg @@ -0,0 +1,4 @@ +\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors +\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later +SPECIFICATION CachedGrantSpec +INVARIANTS TypeOK NoPostRetirementStart diff --git a/proof/tla/RetirementIngressRejected.cfg b/proof/tla/RetirementIngressRejected.cfg new file mode 100644 index 0000000..d248cee --- /dev/null +++ b/proof/tla/RetirementIngressRejected.cfg @@ -0,0 +1,4 @@ +\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors +\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later +SPECIFICATION Spec +INVARIANTS TypeOK NeverRejectAfterRetirement diff --git a/proof/tla/RetirementIngressStarted.cfg b/proof/tla/RetirementIngressStarted.cfg new file mode 100644 index 0000000..663e45b --- /dev/null +++ b/proof/tla/RetirementIngressStarted.cfg @@ -0,0 +1,4 @@ +\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors +\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later +SPECIFICATION Spec +INVARIANTS TypeOK NeverStarted diff --git a/proof/tla/RetirementIngressUnfenced.cfg b/proof/tla/RetirementIngressUnfenced.cfg new file mode 100644 index 0000000..3ef737f --- /dev/null +++ b/proof/tla/RetirementIngressUnfenced.cfg @@ -0,0 +1,4 @@ +\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors +\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later +SPECIFICATION UnfencedSpec +INVARIANTS TypeOK NoPostRetirementStart diff --git a/proof/tla/RetirementReview.cfg b/proof/tla/RetirementReview.cfg new file mode 100644 index 0000000..8de74e8 --- /dev/null +++ b/proof/tla/RetirementReview.cfg @@ -0,0 +1,6 @@ +\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors +\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later +SPECIFICATION Spec +INVARIANT TypeOK +INVARIANT NoOutstandingWork +INVARIANT InventoryKnown diff --git a/proof/tla/RetirementReview.tla b/proof/tla/RetirementReview.tla new file mode 100644 index 0000000..5d36f86 --- /dev/null +++ b/proof/tla/RetirementReview.tla @@ -0,0 +1,79 @@ +---- MODULE RetirementReview ---- +\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors +\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +EXTENDS Naturals, TLC + +(* Design-level experiment, not a generated projection or deployed Runtime. + A review can be followed by late activity or a discovered inventory gap. + Cancellation requests never stand in for completion observations. + No persistence implementation or policy authorization is modeled. *) + +VARIABLES active, queued, known, reviewed, cancelRequested, retired, + lateFound, gapFound + +Work == INSTANCE WorkLifecycle + +vars == <> + +Init == /\ Work!Init /\ known = FALSE + /\ reviewed = FALSE /\ retired = FALSE + /\ lateFound = FALSE /\ gapFound = FALSE + +RequestCancellation == + /\ ~retired /\ Work!RequestCancellation + /\ UNCHANGED <> + +ObserveCompletion == + /\ ~retired /\ Work!ObserveCompletion + /\ UNCHANGED <> + +ObserveQueueDrained == + /\ ~retired /\ Work!ObserveQueueDrained + /\ UNCHANGED <> + +CompleteInventory == + /\ ~retired /\ ~known /\ known' = TRUE + /\ UNCHANGED <> + +Review == + /\ ~retired /\ ~reviewed /\ Work!Quiescent /\ known + /\ reviewed' = TRUE + /\ UNCHANGED <> + +ObserveLateActivity == + /\ ~retired /\ reviewed /\ ~lateFound + /\ active' = TRUE /\ lateFound' = TRUE + /\ UNCHANGED <> + +ObserveInventoryGap == + /\ ~retired /\ reviewed /\ ~gapFound + /\ known' = FALSE /\ gapFound' = TRUE + /\ UNCHANGED <> + +Finish == + /\ retired' = TRUE + /\ UNCHANGED <> + +Retire == ~retired /\ reviewed /\ Work!Quiescent /\ known /\ Finish +TrustCancellation == ~retired /\ cancelRequested /\ known /\ Finish +TrustOldReview == ~retired /\ reviewed /\ known /\ Finish +IgnoreInventoryGap == ~retired /\ reviewed /\ Work!Quiescent /\ Finish + +Observe == RequestCancellation \/ ObserveCompletion \/ ObserveQueueDrained + \/ CompleteInventory \/ Review \/ ObserveLateActivity \/ ObserveInventoryGap +Terminal == retired /\ UNCHANGED vars +Next == Observe \/ Retire \/ Terminal +Spec == Init /\ [][Next]_vars +CancellationSpec == Init /\ [][Observe \/ TrustCancellation \/ Terminal]_vars +OldReviewSpec == Init /\ [][Observe \/ TrustOldReview \/ Terminal]_vars +GapSpec == Init /\ [][Observe \/ IgnoreInventoryGap \/ Terminal]_vars + +TypeOK == /\ Work!TypeOK /\ known \in BOOLEAN /\ reviewed \in BOOLEAN + /\ retired \in BOOLEAN /\ lateFound \in BOOLEAN /\ gapFound \in BOOLEAN +NoOutstandingWork == retired => Work!Quiescent +InventoryKnown == retired => known +NeverRetired == ~retired + +==== diff --git a/proof/tla/RetirementReviewCancellation.cfg b/proof/tla/RetirementReviewCancellation.cfg new file mode 100644 index 0000000..2484973 --- /dev/null +++ b/proof/tla/RetirementReviewCancellation.cfg @@ -0,0 +1,5 @@ +\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors +\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later +SPECIFICATION CancellationSpec +INVARIANT TypeOK +INVARIANT NoOutstandingWork diff --git a/proof/tla/RetirementReviewReachable.cfg b/proof/tla/RetirementReviewReachable.cfg new file mode 100644 index 0000000..5b9f303 --- /dev/null +++ b/proof/tla/RetirementReviewReachable.cfg @@ -0,0 +1,5 @@ +\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors +\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later +SPECIFICATION Spec +INVARIANT TypeOK +INVARIANT NeverRetired diff --git a/proof/tla/RetirementReviewStale.cfg b/proof/tla/RetirementReviewStale.cfg new file mode 100644 index 0000000..a4a1d25 --- /dev/null +++ b/proof/tla/RetirementReviewStale.cfg @@ -0,0 +1,5 @@ +\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors +\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later +SPECIFICATION OldReviewSpec +INVARIANT TypeOK +INVARIANT NoOutstandingWork diff --git a/proof/tla/RetirementReviewUnknown.cfg b/proof/tla/RetirementReviewUnknown.cfg new file mode 100644 index 0000000..0716a34 --- /dev/null +++ b/proof/tla/RetirementReviewUnknown.cfg @@ -0,0 +1,5 @@ +\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors +\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later +SPECIFICATION GapSpec +INVARIANT TypeOK +INVARIANT InventoryKnown diff --git a/proof/tla/WorkCancellation.cfg b/proof/tla/WorkCancellation.cfg new file mode 100644 index 0000000..e8d9a09 --- /dev/null +++ b/proof/tla/WorkCancellation.cfg @@ -0,0 +1,4 @@ +\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors +\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later +SPECIFICATION CancellationSpec +INVARIANTS TypeOK WorkRemains diff --git a/proof/tla/WorkCancellationReachable.cfg b/proof/tla/WorkCancellationReachable.cfg new file mode 100644 index 0000000..20dbb27 --- /dev/null +++ b/proof/tla/WorkCancellationReachable.cfg @@ -0,0 +1,4 @@ +\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors +\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later +SPECIFICATION CancellationSpec +INVARIANTS TypeOK NeverCancelled diff --git a/proof/tla/WorkLifecycle.cfg b/proof/tla/WorkLifecycle.cfg new file mode 100644 index 0000000..75a6de7 --- /dev/null +++ b/proof/tla/WorkLifecycle.cfg @@ -0,0 +1,4 @@ +\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors +\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later +SPECIFICATION Spec +INVARIANT TypeOK diff --git a/proof/tla/WorkLifecycle.tla b/proof/tla/WorkLifecycle.tla new file mode 100644 index 0000000..e95929e --- /dev/null +++ b/proof/tla/WorkLifecycle.tla @@ -0,0 +1,31 @@ +---- MODULE WorkLifecycle ---- +\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors +\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +EXTENDS TLC + +(* Shared action contract: asking to cancel does not observe completion. + The caller owns admission, review and authorization. *) +VARIABLES active, queued, cancelRequested +vars == <> +Init == active = TRUE /\ queued = TRUE /\ cancelRequested = FALSE +RequestCancellation == + /\ ~cancelRequested /\ cancelRequested' = TRUE + /\ UNCHANGED <> +ObserveCompletion == active /\ active' = FALSE + /\ UNCHANGED <> +ObserveQueueDrained == queued /\ queued' = FALSE + /\ UNCHANGED <> +Quiescent == ~active /\ ~queued +TypeOK == active \in BOOLEAN /\ queued \in BOOLEAN /\ cancelRequested \in BOOLEAN +Terminal == Quiescent /\ cancelRequested /\ UNCHANGED vars +Next == RequestCancellation \/ ObserveCompletion \/ ObserveQueueDrained \/ Terminal +Spec == Init /\ [][Next]_vars + +(* Atomic cancellation-only environment; completion is deliberately absent. *) +CancellationSpec == Init /\ [][RequestCancellation \/ + (cancelRequested /\ UNCHANGED vars)]_vars +WorkRemains == active /\ queued +NeverCancelled == ~cancelRequested +NeverQuiescent == ~Quiescent +==== diff --git a/proof/tla/WorkQuiescenceReachable.cfg b/proof/tla/WorkQuiescenceReachable.cfg new file mode 100644 index 0000000..8e33085 --- /dev/null +++ b/proof/tla/WorkQuiescenceReachable.cfg @@ -0,0 +1,4 @@ +\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors +\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later +SPECIFICATION Spec +INVARIANTS TypeOK NeverQuiescent diff --git a/t/formal/retirement-binding-test.ss b/t/formal/retirement-binding-test.ss new file mode 100644 index 0000000..c6975d2 --- /dev/null +++ b/t/formal/retirement-binding-test.ss @@ -0,0 +1,82 @@ +;;; 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 tla-source (source "retirement/tla" (source-group-digest job-retirement-tla-sources))) +(def lean-source (source "retirement/lean" (source-group-digest job-retirement-lean-sources))) +(def (snapshot tla lean) + (assurance-snapshot + "retirement/formal-cut" "cut-1" "retirement/formal-sources" + '(("retirement/tla" . "r1") ("retirement/lean" . "r1")) '() + "retirement/design-r1" "formal-review" "r1" (list tla lean) '())) +(def source-cut (snapshot tla-source lean-source)) +(def links + (map (lambda (artifact) + (sdlc-design-implementation-link + job-retirement-contract job-retirement-safety artifact source-cut)) + (list tla-source lean-source))) + +(def retirement-binding-test + (test-suite "Retirement POO contract to formal source binding" + (test-case "both views 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 TLA source mutation invalidates the joined mapping" + (let* ((changed (source "retirement/tla" (source-lock-payload-digest "changed model"))) + (review (sdlc-design-implementation-review + job-retirement-contract (snapshot changed lean-source) links))) + (check (.ref review 'mapping-current?) => #f) + (check (length (.ref review 'blocked-links)) => 2) + (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 tla-source changed) links))) + (check (.ref review 'mapping-current?) => #f) + (check (length (.ref review 'blocked-links)) => 2) + (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)) => 2) + (for-each (lambda (link) (check (.ref link 'blocker) => 'contract-changed)) + (.ref review 'blocked-links)))))) diff --git a/t/formal/tla-poo-interface-test.ss b/t/formal/tla-poo-interface-test.ss new file mode 100644 index 0000000..e92d332 --- /dev/null +++ b/t/formal/tla-poo-interface-test.ss @@ -0,0 +1,44 @@ +;;; -*- Gerbil -*- +;;; SPDX-FileCopyrightText: 2026 tao3k team and Contributors +;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +;;; Consumer qualification of POO Flow's syntax receipt. The parser-owned +;;; artifact tests remain separate; neither test claims TLC or refinement. +(import (only-in :clan/poo/object .ref) + (only-in :std/misc/ports read-all-as-string) + (only-in :std/test check-equal? check-exception test-case test-suite) + (only-in :gerbil-parser/src/runtime/cst + syntax-node? syntax-node-kind syntax-node-start syntax-node-end) + (only-in :poo-flow/src/modules/tla-plus/interface + PooFlowTlaLanguage. poo-flow-tla-document? + poo-flow-tla-parse-source poo-flow-tla-parser-cst)) +(export tla-poo-interface-test) + +(def tla-poo-interface-test + (test-suite "Aitia TLA+ POO interface consumer" + (test-case "all maintained models bind the same parser-owned syntax tree" + (check-equal? (.ref PooFlowTlaLanguage. 'syntax-contract) + "tla-plus.native-core.v1") + (for-each + (lambda (name) + (let* ((source + (call-with-input-file + (string-append "proof/tla/" name ".tla") + read-all-as-string)) + (document (poo-flow-tla-parse-source source)) + (root (poo-flow-tla-parser-cst document))) + (check-equal? (poo-flow-tla-document? document) #t) + (check-equal? (syntax-node? root) #t) + (check-equal? (syntax-node-kind root) 'SourceFile) + (check-equal? (syntax-node-start root) 0) + (check-equal? (syntax-node-end root) + (u8vector-length (string->utf8 source))) + (check-equal? (.ref document 'exact-roundtrip?) #t) + (check-equal? (.ref document 'model-checking?) #f))) + '("EffectAdmission" "AssuranceLifecycle" "WorkLifecycle" + "RetirementReview" "RetirementIngress"))) + (test-case "rejected syntax cannot produce an accepted document" + (check-exception + (poo-flow-tla-parse-source + "---- MODULE Broken ----\nWork == INSTANCE\n====\n") + true)))) diff --git a/t/formal/tla-syntax-test.ss b/t/formal/tla-syntax-test.ss new file mode 100644 index 0000000..815bd44 --- /dev/null +++ b/t/formal/tla-syntax-test.ss @@ -0,0 +1,34 @@ +;;; SPDX-FileCopyrightText: 2026 tao3k team and Contributors +;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +;;; Source qualification only; neither TLC nor semantic refinement is inferred. +;;; Explicit formal lane; run from the Aitia root with the qualified parser. +(import :std/test + (only-in :std/misc/ports read-all-as-string) + (only-in :gerbil-parser/languages/tla-plus/v1/parser + +tla-plus-syntax-contract+ parse-tla-plus-v1) + (only-in :gerbil-parser/src/runtime/artifact + parse-artifact-success? parse-artifact-valid? + parse-artifact-roundtrip)) +(export tla-syntax-test) + +(def tla-syntax-test + (test-suite "Aitia formal source syntax qualification" + (test-case "all maintained models bind native syntax and original bytes" + (check +tla-plus-syntax-contract+ => "tla-plus.native-core.v1") + (for-each + (lambda (name) + (let* ((path (string-append "proof/tla/" name ".tla")) + (source (call-with-input-file path read-all-as-string)) + (artifact (parse-tla-plus-v1 source))) + (check (list name (parse-artifact-success? artifact) + (parse-artifact-valid? artifact)) + => (list name #t #t)) + (check (parse-artifact-roundtrip artifact) => source))) + '("EffectAdmission" "AssuranceLifecycle" "WorkLifecycle" + "RetirementReview" "RetirementIngress"))) + (test-case "lossless diagnostic artifacts do not imply syntax admission" + (let* ((source "---- MODULE Broken ----\nWork == INSTANCE\n====\n") + (artifact (parse-tla-plus-v1 source))) + (check (parse-artifact-success? artifact) => #f) + (check (parse-artifact-roundtrip artifact) => source))))) 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..5f7f96c --- /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/src/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/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/scenarios/job-executor-retirement.ss b/user-interface/scenarios/job-executor-retirement.ss new file mode 100644 index 0000000..58d10e3 --- /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/src/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..5712ab6 --- /dev/null +++ b/user-interface/scenarios/job-retirement-contract.ss @@ -0,0 +1,54 @@ +;;; 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-tla-sources job-retirement-lean-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 TLA+ 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-tla-sources + '("proof/tla/EffectAdmission.tla" + "proof/tla/WorkLifecycle.tla" + "proof/tla/RetirementReview.tla" + "proof/tla/RetirementIngress.tla" + "proof/tla/WorkLifecycle.cfg" + "proof/tla/WorkCancellation.cfg" + "proof/tla/WorkCancellationReachable.cfg" + "proof/tla/WorkQuiescenceReachable.cfg" + "proof/tla/RetirementReview.cfg" + "proof/tla/RetirementReviewCancellation.cfg" + "proof/tla/RetirementReviewStale.cfg" + "proof/tla/RetirementReviewUnknown.cfg" + "proof/tla/RetirementReviewReachable.cfg" + "proof/tla/RetirementIngress.cfg" + "proof/tla/RetirementIngressUnfenced.cfg" + "proof/tla/RetirementIngressCachedGrant.cfg" + "proof/tla/RetirementIngressRejected.cfg" + "proof/tla/RetirementIngressStarted.cfg")) +(def job-retirement-lean-sources + '("proof/lean/LambdaAitiaProof/Modules/Sdlc/Retirement.lean" + "proof/lean/LambdaAitiaProof/Modules/Sdlc/RetirementIngress.lean")) 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. From 49cbc5d5fd4ec983c98dca8070a0b99bc3fc68b4 Mon Sep 17 00:00:00 2001 From: guangtao Date: Mon, 28 Sep 2026 17:50:41 +0800 Subject: [PATCH 50/64] build(python): clean stale CFFI before native qualification --- bindings/python/README.md | 21 +++++++++++++-------- justfile | 21 +++++++++++++++++++-- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/bindings/python/README.md b/bindings/python/README.md index 95b1e49..b55f600 100644 --- a/bindings/python/README.md +++ b/bindings/python/README.md @@ -129,22 +129,27 @@ 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 diff --git a/justfile b/justfile index 85fa899..5fe7fac 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" } @@ -81,6 +82,21 @@ check-formal: just check-tla 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')] check-native: @@ -94,6 +110,7 @@ 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 }}' @@ -110,8 +127,8 @@ check-installed-wheel: [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')] From 70a8d2e3c42f83f2cba71c60fbd5e7917845a04d Mon Sep 17 00:00:00 2001 From: guangtao Date: Sat, 10 Oct 2026 10:59:01 +0800 Subject: [PATCH 51/64] Migrate Aitia formal models from TLA+ to Quint --- .github/workflows/ci.yml | 49 +- .gitignore | 2 + README.org | 12 +- ROADMAP.org | 28 +- bindings/python/tests/tla_contract.py | 112 --- .../0004-own-assurance-proofs-in-aitia.org | 10 +- .../software-sdlc-integration-inventory.org | 3 +- .../2026-09-23-phase5-lifecycle-model.org | 4 + ...2026-09-24-formal-source-qualification.org | 4 + .../2026-09-24-retirement-formal-review.org | 4 + .../2026-09-24-retirement-ingress-review.org | 4 + docs/rfcs/0001-causal-assurance-core.org | 11 +- ...04-sdlc-design-and-causal-trajectories.org | 54 +- justfile | 15 +- modules/formal-methods/config.ss | 8 +- .../Modules/Sdlc/Retirement.lean | 2 +- .../Modules/Sdlc/RetirementIngress.lean | 2 +- proof/quint/AssuranceLifecycle.qnt | 90 ++ proof/quint/EffectAdmission.qnt | 8 + proof/quint/README.org | 59 ++ proof/quint/RetirementIngress.qnt | 60 ++ proof/quint/RetirementReview.qnt | 45 + proof/quint/WorkCore.qnt | 9 + proof/quint/WorkLifecycle.qnt | 18 + proof/quint/check_contract.py | 95 +++ proof/quint/check_progress.py | 77 ++ proof/quint/package-lock.json | 799 ++++++++++++++++++ proof/quint/package.json | 9 + proof/quint/process_runner.py | 67 ++ proof/quint/tlc-config.json | 1 + proof/tla/AssuranceLifecycle.cfg | 11 - proof/tla/AssuranceLifecycle.tla | 62 -- proof/tla/AssuranceLifecycleAfterRevoke.cfg | 7 - proof/tla/AssuranceLifecycleEarlyGrant.cfg | 7 - proof/tla/AssuranceLifecycleLateAdmit.cfg | 7 - proof/tla/AssuranceLifecycleNoFair.cfg | 7 - proof/tla/AssuranceLifecycleProgress.cfg | 9 - proof/tla/AssuranceLifecycleReachable.cfg | 7 - proof/tla/AssuranceLifecycleUnsafe.cfg | 7 - proof/tla/EffectAdmission.tla | 10 - proof/tla/README.org | 139 --- proof/tla/RetirementIngress.cfg | 4 - proof/tla/RetirementIngress.tla | 66 -- proof/tla/RetirementIngressCachedGrant.cfg | 4 - proof/tla/RetirementIngressRejected.cfg | 4 - proof/tla/RetirementIngressStarted.cfg | 4 - proof/tla/RetirementIngressUnfenced.cfg | 4 - proof/tla/RetirementReview.cfg | 6 - proof/tla/RetirementReview.tla | 79 -- proof/tla/RetirementReviewCancellation.cfg | 5 - proof/tla/RetirementReviewReachable.cfg | 5 - proof/tla/RetirementReviewStale.cfg | 5 - proof/tla/RetirementReviewUnknown.cfg | 5 - proof/tla/WorkCancellation.cfg | 4 - proof/tla/WorkCancellationReachable.cfg | 4 - proof/tla/WorkLifecycle.cfg | 4 - proof/tla/WorkLifecycle.tla | 31 - proof/tla/WorkQuiescenceReachable.cfg | 4 - t/formal/retirement-binding-test.ss | 18 +- t/formal/tla-poo-interface-test.ss | 44 - t/formal/tla-syntax-test.ss | 34 - t/modules/unit/composition-identity-test.ss | 4 +- .../profiles/software-engineering/project.ss | 4 +- .../scenarios/job-retirement-contract.ss | 34 +- 64 files changed, 1473 insertions(+), 837 deletions(-) delete mode 100644 bindings/python/tests/tla_contract.py create mode 100644 proof/quint/AssuranceLifecycle.qnt create mode 100644 proof/quint/EffectAdmission.qnt create mode 100644 proof/quint/README.org create mode 100644 proof/quint/RetirementIngress.qnt create mode 100644 proof/quint/RetirementReview.qnt create mode 100644 proof/quint/WorkCore.qnt create mode 100644 proof/quint/WorkLifecycle.qnt create mode 100644 proof/quint/check_contract.py create mode 100644 proof/quint/check_progress.py create mode 100644 proof/quint/package-lock.json create mode 100644 proof/quint/package.json create mode 100644 proof/quint/process_runner.py create mode 100644 proof/quint/tlc-config.json delete mode 100644 proof/tla/AssuranceLifecycle.cfg delete mode 100644 proof/tla/AssuranceLifecycle.tla delete mode 100644 proof/tla/AssuranceLifecycleAfterRevoke.cfg delete mode 100644 proof/tla/AssuranceLifecycleEarlyGrant.cfg delete mode 100644 proof/tla/AssuranceLifecycleLateAdmit.cfg delete mode 100644 proof/tla/AssuranceLifecycleNoFair.cfg delete mode 100644 proof/tla/AssuranceLifecycleProgress.cfg delete mode 100644 proof/tla/AssuranceLifecycleReachable.cfg delete mode 100644 proof/tla/AssuranceLifecycleUnsafe.cfg delete mode 100644 proof/tla/EffectAdmission.tla delete mode 100644 proof/tla/README.org delete mode 100644 proof/tla/RetirementIngress.cfg delete mode 100644 proof/tla/RetirementIngress.tla delete mode 100644 proof/tla/RetirementIngressCachedGrant.cfg delete mode 100644 proof/tla/RetirementIngressRejected.cfg delete mode 100644 proof/tla/RetirementIngressStarted.cfg delete mode 100644 proof/tla/RetirementIngressUnfenced.cfg delete mode 100644 proof/tla/RetirementReview.cfg delete mode 100644 proof/tla/RetirementReview.tla delete mode 100644 proof/tla/RetirementReviewCancellation.cfg delete mode 100644 proof/tla/RetirementReviewReachable.cfg delete mode 100644 proof/tla/RetirementReviewStale.cfg delete mode 100644 proof/tla/RetirementReviewUnknown.cfg delete mode 100644 proof/tla/WorkCancellation.cfg delete mode 100644 proof/tla/WorkCancellationReachable.cfg delete mode 100644 proof/tla/WorkLifecycle.cfg delete mode 100644 proof/tla/WorkLifecycle.tla delete mode 100644 proof/tla/WorkQuiescenceReachable.cfg delete mode 100644 t/formal/tla-poo-interface-test.ss delete mode 100644 t/formal/tla-syntax-test.ss diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 94ddcb0..1d56905 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,40 +85,33 @@ 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') }} - tla-model: - name: TLA+ assurance lifecycle counterexamples + quint-model: + name: Quint lifecycle, retirement and ingress contracts runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 20 steps: - name: Checkout Lambda Aitia uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - name: Restore pinned TLC cache - id: tlc-cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + - name: Set up Node for pinned Quint + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: - path: .ci/tools/tla2tools.jar - key: lambda-aitia-tlc-v1.7.2-fa18543e44ed5974a85bd2c60c0dc16620ae117680ea8e693d2691999ed90b22 + node-version: '22' - - name: Download pinned TLC release asset - if: steps.tlc-cache.outputs.cache-hit != 'true' - run: | - mkdir -p .ci/tools - curl -fsSL --retry 3 \ - https://github.com/tlaplus/tlaplus/releases/download/v1.7.2/tla2tools.jar \ - -o .ci/tools/tla2tools.jar + - name: Set up Java for Quint TLC backend + uses: actions/setup-java@d7793b545071e98d581d3bf084a51c3213318a07 # v4.9.0 + with: + distribution: temurin + java-version: '21' - - name: Verify TLC release and replay all model contracts + - name: Install locked Quint and check all 22 model contracts env: - TLA_TOOLS_JAR: ${{ github.workspace }}/.ci/tools/tla2tools.jar - run: python3 -m unittest discover -s bindings/python/tests -p 'tla_contract.py' - - - name: Save pinned TLC cache - if: steps.tlc-cache.outputs.cache-hit != 'true' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: .ci/tools/tla2tools.jar - key: lambda-aitia-tlc-v1.7.2-fa18543e44ed5974a85bd2c60c0dc16620ae117680ea8e693d2691999ed90b22 + 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 @@ -133,7 +126,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: repository: tao3k/poo-flow - ref: aeabd8229316d4540e4cd7d0edd701e91c1ae535 + ref: 309396b6364cacf40ae2979dc4671f592547a00d path: poo-flow submodules: false @@ -188,6 +181,10 @@ 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: 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 7c75936..a143e52 100644 --- a/README.org +++ b/README.org @@ -143,11 +143,10 @@ be reused without handing it ownership of Aitia's lifecycle. | 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://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/tlaps-bench][TLAPS-Bench]] | TLA+ invariant proof tasks and checking runtime | mechanical proof checking and benchmark qualification | model proof requires separate implementation correspondence before supporting a code claim | qualify existing checker execution paths and tasks; preserve mandatory checks | | [[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, TLAPS 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 @@ -160,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; @@ -181,8 +180,7 @@ 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 | -| TLAPS | mechanical checking of TLA+ proof obligations under explicit statements and trusted dependencies | implementation correspondence 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 | @@ -211,13 +209,13 @@ flowchart TD 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 TLA+, a TLC completion cannot replace runtime evidence, and a +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, pure planning, -modular Lean laws, a bounded TLA+ lifecycle model and known-fault Python replay. +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. diff --git a/ROADMAP.org b/ROADMAP.org index 755e8e4..5b4bea4 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -92,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: @@ -286,7 +286,7 @@ 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. @@ -328,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= @@ -342,7 +342,7 @@ Lean owns general laws, including: - [X] composition requires a current, independent direct obligation; - unknown or conflicted support cannot grant authority. -TLA+ models the dynamic assurance/execution protocol within the SDLC: +Quint models the dynamic assurance/execution protocol within the SDLC: #+begin_export mermaid stateDiagram-v2 @@ -366,7 +366,7 @@ stateDiagram-v2 Effect --> Changed: relevant new observation #+end_export -A bounded TLC model in =proof/tla/AssuranceLifecycle.tla= now checks current +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 @@ -520,14 +520,14 @@ domain branch and the public API remains POO-native. The earlier migration, nominal vocabulary, snapshot/invalidation and CFFI foundation work is recorded in the phases above. The current queue is: -Immediate priority (2026-09-24): defer new persistence integration. First +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/tla/README.org][Modular TLA+ checks]] now separate work/cancellation, retirement review and +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. The -[[file:docs/receipts/2026-09-24-formal-source-qualification.org][local source qualification]] now passes native TLA+ parsing/roundtrip and -declared POO Contract/source-cut invalidation. Next gates are publication and -consumer pinning of the parser fix, independent replay, and semantic +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. Next gates are independent replay and semantic correspondence between the Scheme contract and formal views (not just hashes). 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; live authority/Runtime @@ -545,9 +545,9 @@ work merely because these design-level tests pass. 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 TLAPS-Bench checker/task boundary and one asset-complete - upstream bug case. Record files, exact revisions, licenses, dependencies, - resource bounds and required upstream checks in the integration inventory. +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 diff --git a/bindings/python/tests/tla_contract.py b/bindings/python/tests/tla_contract.py deleted file mode 100644 index e148450..0000000 --- a/bindings/python/tests/tla_contract.py +++ /dev/null @@ -1,112 +0,0 @@ -# SPDX-FileCopyrightText: 2026 tao3k team and Contributors -# -# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later - -"""Project test for bounded TLC contracts and typed counterexamples.""" - -from __future__ import annotations - -import argparse -import hashlib -import os -from pathlib import Path -import re -import subprocess -import tempfile -import unittest - - -ROOT = Path(__file__).resolve().parents[3] / "proof/tla" -TOOLS_SHA256 = "fa18543e44ed5974a85bd2c60c0dc16620ae117680ea8e693d2691999ed90b22" -CASES = ( - ("AssuranceLifecycle.cfg", 0, "Model checking completed. No error has been found."), - ("AssuranceLifecycleProgress.cfg", 0, "Model checking completed. No error has been found."), - ("AssuranceLifecycleNoFair.cfg", 13, "Error: Temporal properties were violated."), - ("AssuranceLifecycleReachable.cfg", 12, "Error: Invariant NoEffectStarted is violated."), - ("AssuranceLifecycleUnsafe.cfg", 12, "Error: Invariant NoUnsafeEffectStart is violated."), - ("AssuranceLifecycleLateAdmit.cfg", 12, "Error: Invariant AdmissionIsCurrent is violated."), - ("AssuranceLifecycleEarlyGrant.cfg", 12, "Error: Invariant GrantRequiresCurrentDecision is violated."), - ("AssuranceLifecycleAfterRevoke.cfg", 12, "Error: Invariant NoUnsafeEffectStart is violated."), -) -WORK_CASES = ( - ("WorkLifecycle.cfg", 0, "Model checking completed. No error has been found."), - ("WorkCancellation.cfg", 0, "Model checking completed. No error has been found."), - ("WorkCancellationReachable.cfg", 12, "Error: Invariant NeverCancelled is violated."), - ("WorkQuiescenceReachable.cfg", 12, "Error: Invariant NeverQuiescent is violated."), -) -RETIREMENT_CASES = ( - ("RetirementReview.cfg", 0, "Model checking completed. No error has been found."), - ("RetirementReviewCancellation.cfg", 12, "Error: Invariant NoOutstandingWork is violated."), - ("RetirementReviewStale.cfg", 12, "Error: Invariant NoOutstandingWork is violated."), - ("RetirementReviewUnknown.cfg", 12, "Error: Invariant InventoryKnown is violated."), - ("RetirementReviewReachable.cfg", 12, "Error: Invariant NeverRetired is violated."), -) -INGRESS_CASES = ( - ("RetirementIngress.cfg", 0, "Model checking completed. No error has been found."), - ("RetirementIngressUnfenced.cfg", 12, "Error: Invariant NoPostRetirementStart is violated."), - ("RetirementIngressCachedGrant.cfg", 12, "Error: Invariant NoPostRetirementStart is violated."), - ("RetirementIngressRejected.cfg", 12, "Error: Invariant NeverRejectAfterRetirement is violated."), - ("RetirementIngressStarted.cfg", 12, "Error: Invariant NeverStarted is violated."), -) - - -class TlaContractTest(unittest.TestCase): - def test_bounded_lifecycle(self) -> None: - self.check_cases("AssuranceLifecycle", CASES) - - def test_work(self) -> None: - self.check_cases("WorkLifecycle", WORK_CASES) - - def test_retirement(self) -> None: - self.check_cases("RetirementReview", RETIREMENT_CASES) - - def test_ingress(self) -> None: - self.check_cases("RetirementIngress", INGRESS_CASES) - - def check_cases(self, model: str, cases: tuple) -> None: - configured = os.environ.get("TLA_TOOLS_JAR") - self.assertIsNotNone(configured, "TLA_TOOLS_JAR must name v1.7.2") - jar = Path(configured).resolve(strict=True) - actual = hashlib.sha256(jar.read_bytes()).hexdigest() - self.assertEqual(actual, TOOLS_SHA256, "TLA+ tools digest mismatch") - for config, expected_code, expected_text in cases: - with self.subTest(config=config): - with tempfile.TemporaryDirectory(prefix="aitia-tlc-") as directory: - result = subprocess.run( - ("java", "-XX:+UseParallelGC", "-Xmx512m", "-cp", str(jar), - "tlc2.TLC", "-workers", "1", "-metadir", directory, - "-config", config, model), - cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - text=True, errors="replace", timeout=60, check=False, - ) - self.assertEqual(result.returncode, expected_code, result.stdout[-6000:]) - self.assertIn(expected_text, result.stdout[-6000:]) - explored = re.search( - r"(\d+) states generated, (\d+) distinct states found, (\d+) states left on queue", - result.stdout, - ) - self.assertIsNotNone(explored, result.stdout[-6000:]) - self.assertGreater(int(explored[2]), 0, "empty state exploration") - if expected_code == 0: - self.assertEqual(int(explored[3]), 0, "incomplete exploration") - print(f"{config}: generated={explored[1]} distinct={explored[2]} " - f"queued={explored[3]} exit={result.returncode}", flush=True) - if expected_code != 0: - self.assertTrue( - "counter-example" in result.stdout - or "behavior up to this point" in result.stdout, - result.stdout[-6000:], - ) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("module", nargs="?", default="all", - choices=("all", "lifecycle", "work", "retirement", "ingress")) - selected = parser.parse_args().module - methods = {"lifecycle": "test_bounded_lifecycle", "work": "test_work", - "retirement": "test_retirement", "ingress": "test_ingress"} - target = "TlaContractTest" - if selected != "all": - target += "." + methods[selected] - unittest.main(argv=[__file__, target, "-v"]) 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/plans/software-sdlc-integration-inventory.org b/docs/plans/software-sdlc-integration-inventory.org index 5591031..78a91b8 100644 --- a/docs/plans/software-sdlc-integration-inventory.org +++ b/docs/plans/software-sdlc-integration-inventory.org @@ -78,7 +78,7 @@ eight-step assurance/execution protocol is one mechanism within this framework. | =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 | -| =proof/tla/AssuranceLifecycle.tla= | Aitia | bounded candidate | invalidate, plan, reverify, admit, decide, authorize and effect lifecycle | generic model; stable-environment progress is conditional, and Host enforcement remains open | +| =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 @@ -94,7 +94,6 @@ dependencies, applicability assumptions, mandatory checks and replay receipt. | Candidate | Reuse target | Qualification required | Current status | |-----------+--------------+------------------------+----------------| -| TLAPS-Bench | proof task and existing checker execution path | preserve target/trusted inputs and regression checks; prove the selected entry point and environment work | unqualified; Euclid/GCD is a candidate smoke task | | 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 | diff --git a/docs/receipts/2026-09-23-phase5-lifecycle-model.org b/docs/receipts/2026-09-23-phase5-lifecycle-model.org index bf26f3a..8718e0b 100644 --- a/docs/receipts/2026-09-23-phase5-lifecycle-model.org +++ b/docs/receipts/2026-09-23-phase5-lifecycle-model.org @@ -11,6 +11,10 @@ :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=; diff --git a/docs/receipts/2026-09-24-formal-source-qualification.org b/docs/receipts/2026-09-24-formal-source-qualification.org index 8ac9894..55e126d 100644 --- a/docs/receipts/2026-09-24-formal-source-qualification.org +++ b/docs/receipts/2026-09-24-formal-source-qualification.org @@ -11,6 +11,10 @@ :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 diff --git a/docs/receipts/2026-09-24-retirement-formal-review.org b/docs/receipts/2026-09-24-retirement-formal-review.org index b52dbd0..8b811da 100644 --- a/docs/receipts/2026-09-24-retirement-formal-review.org +++ b/docs/receipts/2026-09-24-retirement-formal-review.org @@ -11,6 +11,10 @@ :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 diff --git a/docs/receipts/2026-09-24-retirement-ingress-review.org b/docs/receipts/2026-09-24-retirement-ingress-review.org index 0a440b4..9b00403 100644 --- a/docs/receipts/2026-09-24-retirement-ingress-review.org +++ b/docs/receipts/2026-09-24-retirement-ingress-review.org @@ -11,6 +11,10 @@ :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. diff --git a/docs/rfcs/0001-causal-assurance-core.org b/docs/rfcs/0001-causal-assurance-core.org index f08130f..788a0c7 100644 --- a/docs/rfcs/0001-causal-assurance-core.org +++ b/docs/rfcs/0001-causal-assurance-core.org @@ -61,8 +61,7 @@ through named, checked functions that emit receipts. | POO Flow Python runtime | generic runtime and lifecycle transport | Aitia bindings, Aitia Scheme semantics or a second SDLC implementation | | 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 | -| TLAPS | mechanical checking of TLA+ proof obligations | model/code correspondence or runtime authority | +| 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 | @@ -358,12 +357,12 @@ 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/TLAPS proof, TLA+/TLC model check, runtime replay, +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; -listing TLAPS here does not mean its execution path is already integrated. +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. @@ -391,7 +390,7 @@ The architecture requires executable formal statements of at least these laws: 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, @@ -454,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/0004-sdlc-design-and-causal-trajectories.org b/docs/rfcs/0004-sdlc-design-and-causal-trajectories.org index abfda2b..fc3516c 100644 --- a/docs/rfcs/0004-sdlc-design-and-causal-trajectories.org +++ b/docs/rfcs/0004-sdlc-design-and-causal-trajectories.org @@ -91,26 +91,27 @@ 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. -TLA+ is organized into reusable action/contract modules with independently -runnable configurations and a separate composition lane. Each safety boundary +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 first [[file:../../proof/tla/README.org][modular slice]] separates cancellation/work observations from retirement +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 TLA+ refinement. The -[[file:../receipts/2026-09-24-retirement-formal-review.org][formal receipt]] records the actual checks and uncovered parser boundary. +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 [[file:../receipts/2026-09-24-formal-source-qualification.org][local source qualification]] now checks native syntax/byte-roundtrip and -binds both source views to one maintained POO design declaration, revision and +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. Portable parser pinning, -independent replay and semantic correspondence remain required for closure. +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 @@ -130,9 +131,9 @@ reviewable interpretation, an owner and an adversarial check: | Boundary | Required correspondence | Falsifying control | |----------+-------------------------+--------------------| -| POO design contract → TLA+ | 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 → 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 | -| TLA+/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 | +| 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 @@ -150,11 +151,11 @@ correspondence review has these concrete boundaries: | Declaration | Current formal representation | Remaining gap | |-------------+-------------------------------+---------------| -| =atomic-epoch-fence= | TLA+ =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 | TLA+ =Review!Retire= requires =known= and =Work!Quiescent=; Lean =Retirement.Step.finalize= requires =inventory=, =idle= and =drained= | Neither formal view proves the inventory is complete in production | -| =no-new-grant= | The TLA+ 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= | TLA+ =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= | TLA+ 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 | +| =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, @@ -486,12 +487,10 @@ 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. -TLAPS-Bench is a candidate source of proof tasks and checking components. -Retain the target statement, trusted inputs and required checks when qualifying -an entry point. Use TLAPS to check TLA+ proofs; a Lean translation is not a -prerequisite. TLC model checking and TLAPS proof checking retain their distinct -propositions. Existing proof evidence kinds can name different producers -without adding a parallel semantic schema for each tool. +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 @@ -545,11 +544,9 @@ causal or Host closure. 6. Apply an implementation optimization and check both contract preservation and measured benefit under the same workload and environment scope. -Qualify one small TLAPS-Bench task and one sufficiently complete upstream bug -case as reusable methods. These are separate execution qualifications until -their claims actually refer to the same subject: a GCD proof cannot discharge -a job-executor guarantee. A known-answer replay is regression evidence; novel -discovery needs a separate evaluation with unexposed answers. +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 @@ -582,7 +579,7 @@ discovery needs a separate evaluation with unexposed answers. - [ ] 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 - TLA+/Lean interpretations and falsifying controls, followed by concrete + 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 @@ -609,7 +606,6 @@ and their receipts retain their existing scope. =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/tlaps-bench][TLAPS-Bench tasks and runtime]] - [[https://github.com/specula-org/specula-case-studies][Specula experiment archive]] Upstream references were inspected on 2026-09-24. They identify reuse diff --git a/justfile b/justfile index 5fe7fac..cf46044 100644 --- a/justfile +++ b/justfile @@ -66,20 +66,17 @@ check-proof: cd proof/lean && lake build LambdaAitiaModuleAssuranceProof LambdaAitiaModuleSdlcProof cd proof/lean && output="$(lake env lean AxiomAudit.lean)" && printf '%s\n' "$output" && ! grep -q 'sorryAx' <<< "$output" -# TLA_TOOLS_JAR is an explicit, checksum-verified input; the recipe never -# downloads tools or writes into the user's global Java environment. +# 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-tla module='all': - uv run --locked --project bindings/python python bindings/python/tests/tla_contract.py '{{module}}' +check-quint module='all': + python3 proof/quint/check_contract.py '{{module}}' -# Explicit source/formal lane: requires the qualified native parser dependency. # Every gate must run; none substitutes for semantic implementation refinement. [group('check')] check-formal: - just test-scheme-atomic formal tla-syntax-test.ss - just test-scheme-atomic formal tla-poo-interface-test.ss just test-scheme-atomic formal retirement-binding-test.ss - just check-tla + just check-quint just check-proof # Refresh the in-place Python extension without rebuilding POO Flow. @@ -132,7 +129,7 @@ build-python-wheel native_library: # Current repository closure. No recipe grants runtime authority. [group('check')] -check: check-license-contract check-semantic check-proof check-tla 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/formal-methods/config.ss b/modules/formal-methods/config.ss index 038f90f..c338ad6 100644 --- a/modules/formal-methods/config.ss +++ b/modules/formal-methods/config.ss @@ -14,14 +14,14 @@ "objects.ss" "funs.ss") -(export aitia-tla-plus-provider +(export aitia-quint-provider aitia-lean-provider aitia-cedar-provider aitia-formal-methods-module) -(def aitia-tla-plus-provider +(def aitia-quint-provider (aitia-formal-method-provider - 'tla+ + 'quint 'state-transition-model-checking '(module model-checking-configuration trace receipt))) @@ -40,6 +40,6 @@ (def aitia-formal-methods-module (poo-flow-proof-module "lambda-aitia/modules/formal-methods" - (.o (tla+ aitia-tla-plus-provider) + (.o (quint aitia-quint-provider) (lean aitia-lean-provider) (cedar aitia-cedar-provider)))) diff --git a/proof/lean/LambdaAitiaProof/Modules/Sdlc/Retirement.lean b/proof/lean/LambdaAitiaProof/Modules/Sdlc/Retirement.lean index 2ed44b8..e01cee2 100644 --- a/proof/lean/LambdaAitiaProof/Modules/Sdlc/Retirement.lean +++ b/proof/lean/LambdaAitiaProof/Modules/Sdlc/Retirement.lean @@ -5,7 +5,7 @@ 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 TLA/Scheme correspondence, inventory truth, liveness, +This is not a proof of Quint/Scheme correspondence, inventory truth, liveness, post-retirement ingress exclusion, persistence, or effect authorization. -/ diff --git a/proof/lean/LambdaAitiaProof/Modules/Sdlc/RetirementIngress.lean b/proof/lean/LambdaAitiaProof/Modules/Sdlc/RetirementIngress.lean index 9a78929..98a76fe 100644 --- a/proof/lean/LambdaAitiaProof/Modules/Sdlc/RetirementIngress.lean +++ b/proof/lean/LambdaAitiaProof/Modules/Sdlc/RetirementIngress.lean @@ -7,7 +7,7 @@ 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, TLA correspondence, +invalidated cached epoch. No proof of deployed revocation, Quint correspondence, grant issuance, fairness or persistent state is claimed. -/ diff --git a/proof/quint/AssuranceLifecycle.qnt b/proof/quint/AssuranceLifecycle.qnt new file mode 100644 index 0000000..f4ea97b --- /dev/null +++ b/proof/quint/AssuranceLifecycle.qnt @@ -0,0 +1,90 @@ +// 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} + 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} + 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, + startSafe: currentGrant(q.granted, q.revision, q.admitted, q.decided, + q.grantEpoch, q.epoch, q.clock, q.grantUntil)}} + 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} + action step = any {stableStep, ChangeSource, Tick, Revoke} + + // 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} + + 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 + 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) + + 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) + 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() +} 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..33310a8 --- /dev/null +++ b/proof/quint/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: Quint design-model qualification + +:PROPERTIES: +:STATUS: design-validation-partial +:DATE: 2026-10-10 +: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 preserve 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 and safe effect start | untrusted report, late admission, early grant, after-revoke start, valid start reachable, no-fair progress 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 +=EventuallyEffect= 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 + +These are bounded design experiments, not generated projections of the POO +Contract, proof of Scheme/Runtime refinement, a live Host handoff, or deployed +authority. 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 retirement binding test hashes this source cut together with the Lean +view, but a current mapping is not semantic correspondence or implementation +conformance. Those obligations 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..ebbf8fc --- /dev/null +++ b/proof/quint/check_contract.py @@ -0,0 +1,95 @@ +# 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": 2202} +# 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", 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), +) + + +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_progress.py b/proof/quint/check_progress.py new file mode 100644 index 0000000..782ed76 --- /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", "EventuallyEffect"), + 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 EventuallyEffect was violated."), + ): + print(f"QUINT-PROGRESS: checking {name}", flush=True) + config = work / f"{name}.cfg" + config.write_text(f"SPECIFICATION {spec}\nPROPERTY EventuallyEffect\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]) != 18 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/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..ae10096 --- /dev/null +++ b/proof/quint/process_runner.py @@ -0,0 +1,67 @@ +# 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() + raise TimeoutError(f"{label}: {reason} after {now - last_output:.1f}s without output") + 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/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/proof/tla/AssuranceLifecycle.cfg b/proof/tla/AssuranceLifecycle.cfg deleted file mode 100644 index 869b430..0000000 --- a/proof/tla/AssuranceLifecycle.cfg +++ /dev/null @@ -1,11 +0,0 @@ -\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors -\* -\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later - -SPECIFICATION Spec -CHECK_DEADLOCK FALSE -INVARIANT TypeInvariant -INVARIANT AdmissionIsCurrent -INVARIANT DecisionRequiresAdmission -INVARIANT GrantRequiresCurrentDecision -INVARIANT NoUnsafeEffectStart diff --git a/proof/tla/AssuranceLifecycle.tla b/proof/tla/AssuranceLifecycle.tla deleted file mode 100644 index 499157d..0000000 --- a/proof/tla/AssuranceLifecycle.tla +++ /dev/null @@ -1,62 +0,0 @@ ----- MODULE AssuranceLifecycle ---- -\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors -\* -\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later - -EXTENDS Naturals, TLC, EffectAdmission - -(* Bounded control-plane model. A successful report is not a Host verification; - admitted evidence and grants are current only inside their validity cut. *) - -VARIABLES revision, planned, verified, admitted, decided, granted, grantEpoch, epoch, clock, sealUntil, grantUntil, adapterInstalled, reportedSuccess, effectStarted, startSafe - -vars == <> - -Init == 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 - -InstallAdapter == ~adapterInstalled /\ adapterInstalled' = TRUE /\ UNCHANGED <> -ReportSuccess == ~reportedSuccess /\ reportedSuccess' = TRUE /\ UNCHANGED <> -Plan == planned # revision /\ planned' = revision /\ UNCHANGED <> -Verify == adapterInstalled /\ planned = revision /\ clock < 3 /\ verified' = revision /\ sealUntil' = clock + 1 /\ UNCHANGED <> -Admit == verified = revision /\ clock < sealUntil /\ admitted # revision /\ admitted' = revision /\ UNCHANGED <> -Decide == admitted = revision /\ decided # revision /\ decided' = revision /\ UNCHANGED <> -Authorize == decided = revision /\ admitted = revision /\ granted # revision /\ clock < 3 /\ granted' = revision /\ grantEpoch' = epoch /\ grantUntil' = clock + 1 /\ UNCHANGED <> -StartEffect == ~effectStarted /\ CurrentGrant(granted, revision, admitted, decided, grantEpoch, epoch, clock, grantUntil) /\ effectStarted' = TRUE /\ startSafe' = (admitted = revision /\ decided = revision /\ granted = revision /\ grantEpoch = epoch /\ clock < grantUntil) /\ UNCHANGED <> -ChangeSource == revision = 1 /\ revision' = 2 /\ planned' = 0 /\ verified' = 0 /\ admitted' = 0 /\ decided' = 0 /\ granted' = 0 /\ sealUntil' = 0 /\ grantUntil' = 0 /\ UNCHANGED <> -Tick == clock < 3 /\ clock' = clock + 1 /\ admitted' = (IF clock + 1 < sealUntil THEN admitted ELSE 0) /\ decided' = (IF clock + 1 < sealUntil THEN decided ELSE 0) /\ granted' = (IF (clock + 1 < sealUntil /\ clock + 1 < grantUntil) THEN granted ELSE 0) /\ UNCHANGED <> -Revoke == epoch < 2 /\ epoch' = epoch + 1 /\ verified' = 0 /\ admitted' = 0 /\ decided' = 0 /\ granted' = 0 /\ sealUntil' = 0 /\ UNCHANGED <> - -Next == InstallAdapter \/ ReportSuccess \/ Plan \/ Verify \/ Admit \/ Decide \/ Authorize \/ StartEffect \/ ChangeSource \/ Tick \/ Revoke -Spec == Init /\ [][Next]_vars - -(* Progress is conditional: no source change, clock tick or revocation occurs, - and the Host eventually schedules each continuously enabled stage. This - does not assert unconditional progress for a changing environment. *) -StableNext == InstallAdapter \/ ReportSuccess \/ Plan \/ Verify \/ Admit \/ Decide \/ Authorize \/ StartEffect -StableSpecNoFair == Init /\ [][StableNext]_vars -StableSpec == Init /\ [][StableNext]_vars /\ WF_vars(InstallAdapter) /\ WF_vars(Plan) /\ WF_vars(Verify) /\ WF_vars(Admit) /\ WF_vars(Decide) /\ WF_vars(Authorize) /\ WF_vars(StartEffect) -EventuallyEffect == <>effectStarted - -(* Deliberately unsafe mutant: a reported green result starts an effect without - Host admission or grant. Its counterexample must be found by TLC. *) -UnsafeStart == reportedSuccess /\ ~effectStarted /\ effectStarted' = TRUE /\ startSafe' = (admitted = revision /\ decided = revision /\ granted = revision /\ grantEpoch = epoch /\ clock < grantUntil) /\ UNCHANGED <> -UnsafeNext == Next \/ UnsafeStart -UnsafeSpec == Init /\ [][UnsafeNext]_vars -UnsafeLateAdmit == revision = 2 /\ reportedSuccess /\ admitted = 0 /\ admitted' = 1 /\ UNCHANGED <> -UnsafeLateAdmitNext == Next \/ UnsafeLateAdmit -UnsafeLateAdmitSpec == Init /\ [][UnsafeLateAdmitNext]_vars -UnsafeEarlyGrant == revision = 2 /\ reportedSuccess /\ admitted = 0 /\ granted = 0 /\ clock < 3 /\ granted' = revision /\ grantEpoch' = epoch /\ grantUntil' = clock + 1 /\ UNCHANGED <> -UnsafeEarlyGrantNext == Next \/ UnsafeEarlyGrant -UnsafeEarlyGrantSpec == Init /\ [][UnsafeEarlyGrantNext]_vars -UnsafeAfterRevoke == epoch > grantEpoch /\ grantUntil > clock /\ granted = 0 /\ ~effectStarted /\ effectStarted' = TRUE /\ startSafe' = (admitted = revision /\ decided = revision /\ granted = revision /\ grantEpoch = epoch /\ clock < grantUntil) /\ UNCHANGED <> -UnsafeAfterRevokeNext == Next \/ UnsafeAfterRevoke -UnsafeAfterRevokeSpec == Init /\ [][UnsafeAfterRevokeNext]_vars - -TypeInvariant == revision \in {1, 2} /\ planned \in {0, 1, 2} /\ verified \in {0, 1, 2} /\ admitted \in {0, 1, 2} /\ decided \in {0, 1, 2} /\ granted \in {0, 1, 2} /\ grantEpoch \in 0..2 /\ epoch \in 0..2 /\ clock \in 0..3 /\ sealUntil \in 0..4 /\ grantUntil \in 0..4 /\ adapterInstalled \in BOOLEAN /\ reportedSuccess \in BOOLEAN /\ effectStarted \in BOOLEAN /\ startSafe \in BOOLEAN -AdmissionIsCurrent == admitted = 0 \/ (admitted = revision /\ verified = revision /\ clock < sealUntil) -DecisionRequiresAdmission == decided = 0 \/ (decided = revision /\ admitted = revision) -GrantRequiresCurrentDecision == granted = 0 \/ (granted = revision /\ decided = revision /\ admitted = revision /\ grantEpoch = epoch /\ clock < grantUntil) -NoUnsafeEffectStart == startSafe -NoEffectStarted == ~effectStarted - -==== diff --git a/proof/tla/AssuranceLifecycleAfterRevoke.cfg b/proof/tla/AssuranceLifecycleAfterRevoke.cfg deleted file mode 100644 index 9666d2d..0000000 --- a/proof/tla/AssuranceLifecycleAfterRevoke.cfg +++ /dev/null @@ -1,7 +0,0 @@ -\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors -\* -\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later - -SPECIFICATION UnsafeAfterRevokeSpec -CHECK_DEADLOCK FALSE -INVARIANT NoUnsafeEffectStart diff --git a/proof/tla/AssuranceLifecycleEarlyGrant.cfg b/proof/tla/AssuranceLifecycleEarlyGrant.cfg deleted file mode 100644 index 4e1bdbd..0000000 --- a/proof/tla/AssuranceLifecycleEarlyGrant.cfg +++ /dev/null @@ -1,7 +0,0 @@ -\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors -\* -\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later - -SPECIFICATION UnsafeEarlyGrantSpec -CHECK_DEADLOCK FALSE -INVARIANT GrantRequiresCurrentDecision diff --git a/proof/tla/AssuranceLifecycleLateAdmit.cfg b/proof/tla/AssuranceLifecycleLateAdmit.cfg deleted file mode 100644 index e65137b..0000000 --- a/proof/tla/AssuranceLifecycleLateAdmit.cfg +++ /dev/null @@ -1,7 +0,0 @@ -\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors -\* -\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later - -SPECIFICATION UnsafeLateAdmitSpec -CHECK_DEADLOCK FALSE -INVARIANT AdmissionIsCurrent diff --git a/proof/tla/AssuranceLifecycleNoFair.cfg b/proof/tla/AssuranceLifecycleNoFair.cfg deleted file mode 100644 index ad1c41b..0000000 --- a/proof/tla/AssuranceLifecycleNoFair.cfg +++ /dev/null @@ -1,7 +0,0 @@ -\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors -\* -\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later - -SPECIFICATION StableSpecNoFair -CHECK_DEADLOCK FALSE -PROPERTY EventuallyEffect diff --git a/proof/tla/AssuranceLifecycleProgress.cfg b/proof/tla/AssuranceLifecycleProgress.cfg deleted file mode 100644 index b659dc8..0000000 --- a/proof/tla/AssuranceLifecycleProgress.cfg +++ /dev/null @@ -1,9 +0,0 @@ -\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors -\* -\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later - -SPECIFICATION StableSpec -CHECK_DEADLOCK FALSE -INVARIANT TypeInvariant -INVARIANT NoUnsafeEffectStart -PROPERTY EventuallyEffect diff --git a/proof/tla/AssuranceLifecycleReachable.cfg b/proof/tla/AssuranceLifecycleReachable.cfg deleted file mode 100644 index c4606c9..0000000 --- a/proof/tla/AssuranceLifecycleReachable.cfg +++ /dev/null @@ -1,7 +0,0 @@ -\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors -\* -\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later - -SPECIFICATION Spec -CHECK_DEADLOCK FALSE -INVARIANT NoEffectStarted diff --git a/proof/tla/AssuranceLifecycleUnsafe.cfg b/proof/tla/AssuranceLifecycleUnsafe.cfg deleted file mode 100644 index 0bce88d..0000000 --- a/proof/tla/AssuranceLifecycleUnsafe.cfg +++ /dev/null @@ -1,7 +0,0 @@ -\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors -\* -\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later - -SPECIFICATION UnsafeSpec -CHECK_DEADLOCK FALSE -INVARIANT NoUnsafeEffectStart diff --git a/proof/tla/EffectAdmission.tla b/proof/tla/EffectAdmission.tla deleted file mode 100644 index 7a03512..0000000 --- a/proof/tla/EffectAdmission.tla +++ /dev/null @@ -1,10 +0,0 @@ ----- MODULE EffectAdmission ---- -\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors -\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later - -EXTENDS Naturals - -(* Shared predicate extracted from AssuranceLifecycle.StartEffect. - This is a formal contract, not another Runtime or policy evaluator. *) -CurrentGrant(granted, revision, admitted, decided, grantEpoch, epoch, clock, grantUntil) == granted = revision /\ admitted = revision /\ decided = revision /\ grantEpoch = epoch /\ clock < grantUntil -==== diff --git a/proof/tla/README.org b/proof/tla/README.org deleted file mode 100644 index e39bf39..0000000 --- a/proof/tla/README.org +++ /dev/null @@ -1,139 +0,0 @@ -# SPDX-FileCopyrightText: 2026 tao3k team and Contributors -# SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later - -#+title: Modular TLA+ design checks - -:PROPERTIES: -:ID: lambda-aitia-tla-modules -:STATUS: design-validation-partial -:DATE: 2026-09-24 -:END: - -* Boundaries and independent checks - -| Selector | Model owner | Checks | Dependency | -|----------+-------------+--------+------------| -| =work= | [[file:WorkLifecycle.tla][WorkLifecycle]] | cancellation preserves outstanding work; cancellation and quiescence reachable; Boolean state space | TLA standard library | -| =retirement= | [[file:RetirementReview.tla][RetirementReview]] | final-cut work/inventory guards; three unsafe mutants; retirement reachable | =INSTANCE WorkLifecycle= | -| =ingress= | [[file:RetirementIngress.tla][RetirementIngress]] | post-retirement requests, epoch fencing, cached-grant mutants and reachable rejection/start | =INSTANCE RetirementReview= and =EffectAdmission= | -| =lifecycle= | [[file:AssuranceLifecycle.tla][AssuranceLifecycle]] | existing evidence/admission/authorization safety and conditional progress | =EffectAdmission= | -| =all= (default) | all four lanes | 22 configurations | complete regression lane | - -An atomic check is a named contract boundary, not an arbitrary file fragment. -The retirement model reuses the work actions instead of copying cancellation -semantics. Its environment also injects late activity and inventory loss, which -are absent from the isolated work model. Passing atomic checks therefore does -not replace the composition check. A work-module edit requires =work=, -=retirement= and =ingress=; a retirement-only edit requires =retirement= and -=ingress=. An =EffectAdmission= edit requires =lifecycle= and =ingress=; -full CI runs =all=. - -The existing assurance lifecycle remains an integration model, not a completed -modularization of admission, revocation and grants. Its effect-admission guard -is now a shared operator, not copied into the retirement consumer. The full -revocation transition/issuance protocol still requires independent extraction -and composition checks before claiming an entirely modular assurance library. - -* Replay - -Set =TLA_TOOLS_JAR= to the existing checksum-verified v1.7.2 release jar. No -tool download or global environment change occurs in these recipes. - -#+begin_src sh -just check-tla work -just check-tla retirement -just check-tla ingress -just check-tla lifecycle -just check-tla -just check-proof -just check-formal -#+end_src - -The Python project is managed by =uv=; the same unittest checks also run in CI -without requiring production Runtime imports. Unknown selectors are rejected, -not silently treated as zero tests. Each TLC run uses one worker, a 512 MiB Java -heap, a 60-second timeout and a separate temporary state directory. Expected -counterexamples require the exact invariant failure, exit status and trace; -parser or tool errors are not successful negative tests. Heap is not a total -RSS bound. Cache the pinned tool, not an unqualified previous green result. - -* Interpretation - -The new models are design experiments, not a second public configuration API -and not yet generated projections of an admitted Scheme POO Contract. Their -=retired= state is a review decision at a modeled cut, not deletion performed -by the Runtime. No action authority follows from a passing model check. - -The new models keep deadlock checking enabled and have explicit terminal -self-loops. Isolated =RetirementReview= freezes its terminal environment; -=RetirementIngress= explicitly permits a later request and checks rejection -using the same current-grant predicate as =AssuranceLifecycle=. This closes a -bounded model assumption, not a deployed ingress or Runtime guarantee. No -fairness, eventual drain, crash recovery, inventory truth, -unbounded consumer coverage or persistent-state claim is established here. - -[[file:../../docs/receipts/2026-09-24-retirement-formal-review.org][The formal review receipt]] separates TLC exploration, Lean laws, native parser -roundtrip and pending Scheme correspondence. TLC acceptance alone does not -close the three-part formal qualification required by POO Flow. -[[file:../../docs/receipts/2026-09-24-retirement-ingress-review.org][The ingress follow-up]] records the new counterexamples, shared predicate -regression and four independent native parser capability gaps. -[[file:../../docs/receipts/2026-09-24-formal-source-qualification.org][The source qualification follow-up]] closes those four gaps locally and runs -all five native models unchanged. =check-formal= runs native syntax, declared -POO source binding, all TLC configurations and Lean in sequence; it fails if -the installed parser lacks the required syntax. Until the parser patch is -published and its dependency pin updated, this explicit lane requires the -qualified linked source checkout in =GERBIL_LOADPATH=. It is not silently added -to default CI against an older dependency. - -The maintained [[file:../../user-interface/scenarios/job-retirement-contract.ss][retirement POO design contract]] names the assumptions, guarantee and -TLA/Lean source groups. Binding tests use existing source digests, Assurance -snapshots and SDLC implementation links. A changed source cut or premise -invalidates the old links. This establishes declared source correspondence, -not generated projections, semantic refinement, verifier admission or Runtime -authority; those claims remain explicitly false in the review value. - -* Existing assurance lifecycle contract - -=AssuranceLifecycle= describes one obligation and one source revision change. -It is a contract for a future trusted Host handoff, not an implementation of -that Host. =ReportSuccess= is untrusted: it cannot verify, admit, decide, -authorize, or start an effect. =InstallAdapter= merely makes a verifier -available. Admission requires a current verification seal; authorization -requires current admission and decision; starting an effect also requires a -non-expired grant issued in the current revocation epoch. - -The bounded domains are two source revisions, four clock values, and three -revocation epochs. TLC exhaustively checks the safe model within those bounds. -The =Unsafe*= actions are deliberate mutations, not implementation paths. -The =lifecycle= selector retains all eight original configurations; the CI -unittest discovery retains these plus fourteen new modular checks. The tool -digest remains =fa18543e44ed5974a85bd2c60c0dc16620ae117680ea8e693d2691999ed90b22=. - -For manual inspection from this directory, with =tlc= installed on =PATH=: - -#+begin_src sh -JAVA_TOOL_OPTIONS=-Xmx512m tlc -workers 1 -config AssuranceLifecycle.cfg AssuranceLifecycle -JAVA_TOOL_OPTIONS=-Xmx512m tlc -workers 1 -config AssuranceLifecycleProgress.cfg AssuranceLifecycle -JAVA_TOOL_OPTIONS=-Xmx512m tlc -workers 1 -config AssuranceLifecycleNoFair.cfg AssuranceLifecycle -JAVA_TOOL_OPTIONS=-Xmx512m tlc -workers 1 -config AssuranceLifecycleReachable.cfg AssuranceLifecycle -JAVA_TOOL_OPTIONS=-Xmx512m tlc -workers 1 -config AssuranceLifecycleUnsafe.cfg AssuranceLifecycle -JAVA_TOOL_OPTIONS=-Xmx512m tlc -workers 1 -config AssuranceLifecycleLateAdmit.cfg AssuranceLifecycle -JAVA_TOOL_OPTIONS=-Xmx512m tlc -workers 1 -config AssuranceLifecycleEarlyGrant.cfg AssuranceLifecycle -JAVA_TOOL_OPTIONS=-Xmx512m tlc -workers 1 -config AssuranceLifecycleAfterRevoke.cfg AssuranceLifecycle -#+end_src - -Use =-metadir= with a writable temporary directory for read-only checkouts. -Safe and progress configurations must exit 0. Progress assumes a stable source -and clock, no revocation, and weak fairness for each Host stage. No-fairness -must violate =EventuallyEffect=: a possible path alone does not prove progress. -Reachability checks =NoEffectStarted= and must find a valid =StartEffect= trace. -Other mutants must violate their named invariants, not merely fail to run. - -This model does not prove a live adapter was installed by a trusted Host, a -clock is trustworthy, a grant is cryptographically bound, or the actual -Scheme/Python/Runtime path enforces the model. It does not yet model -counterfactual promotion or concurrent effects. Conditional fairness does not -establish progress when sources change, clocks advance, or grants are revoked. -Those remain separate Phase 4–6 exit gates. The original configurations disable -deadlock checks; they are retained as baseline regressions, not examples of the -new explicit-terminal-state discipline. diff --git a/proof/tla/RetirementIngress.cfg b/proof/tla/RetirementIngress.cfg deleted file mode 100644 index da830d5..0000000 --- a/proof/tla/RetirementIngress.cfg +++ /dev/null @@ -1,4 +0,0 @@ -\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors -\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later -SPECIFICATION Spec -INVARIANTS TypeOK RetiredIsFenced NoPostRetirementStart NoOutstandingWork InventoryKnown diff --git a/proof/tla/RetirementIngress.tla b/proof/tla/RetirementIngress.tla deleted file mode 100644 index eabb163..0000000 --- a/proof/tla/RetirementIngress.tla +++ /dev/null @@ -1,66 +0,0 @@ ----- MODULE RetirementIngress ---- -\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors -\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later - -EXTENDS Naturals, TLC, EffectAdmission - -(* Requests can arrive AFTER retirement. Closing ingress stops new grants and - advances the authority epoch; a client may still hold an old cached grant. - Revision/admission/decision/time are fixed to isolate epoch revocation. - No new grant issuance, persistence or deployed authorization is modeled. *) -VARIABLES active, queued, known, reviewed, cancelRequested, retired, - lateFound, gapFound, ingressOpen, epoch, grantEpoch, - pending, attempted, rejectedAfterRetirement, startedAfterRetirement -Review == INSTANCE RetirementReview -vars == <> -boundary == <> -Current == CurrentGrant(1, 1, 1, 1, grantEpoch, epoch, 0, 1) -Init == /\ Review!Init /\ ingressOpen = TRUE /\ epoch = 0 /\ grantEpoch = 0 - /\ pending = FALSE /\ attempted = FALSE - /\ rejectedAfterRetirement = FALSE /\ startedAfterRetirement = FALSE - -Observe == Review!Observe /\ UNCHANGED boundary -CloseIngress == - /\ ingressOpen /\ ingressOpen' = FALSE /\ epoch' = epoch + 1 - /\ UNCHANGED <> -Request == - /\ ~pending /\ ~attempted /\ pending' = TRUE - /\ UNCHANGED <> -StartBody == - /\ active' = TRUE /\ pending' = FALSE /\ attempted' = TRUE - /\ startedAfterRetirement' = retired - /\ UNCHANGED <> -Start == pending /\ Current /\ StartBody -TrustCachedGrant == pending /\ StartBody -Reject == - /\ pending /\ ~Current /\ pending' = FALSE /\ attempted' = TRUE - /\ rejectedAfterRetirement' = retired - /\ UNCHANGED <> -Retire == ~ingressOpen /\ Review!Retire /\ UNCHANGED boundary -RetireWithoutFence == Review!Retire /\ UNCHANGED boundary -Terminal == retired /\ attempted /\ UNCHANGED vars -Environment == Observe \/ CloseIngress \/ Request \/ Reject \/ Terminal -Next == Environment \/ Start \/ Retire -Spec == Init /\ [][Next]_vars -UnfencedSpec == Init /\ [][Environment \/ Start \/ RetireWithoutFence]_vars -CachedGrantSpec == Init /\ [][Environment \/ TrustCachedGrant \/ Retire]_vars -TypeOK == /\ Review!TypeOK /\ ingressOpen \in BOOLEAN /\ epoch \in 0..1 - /\ grantEpoch = 0 /\ pending \in BOOLEAN /\ attempted \in BOOLEAN - /\ rejectedAfterRetirement \in BOOLEAN /\ startedAfterRetirement \in BOOLEAN -RetiredIsFenced == retired => (~ingressOpen /\ grantEpoch # epoch) -NoPostRetirementStart == ~startedAfterRetirement -NoOutstandingWork == Review!NoOutstandingWork -InventoryKnown == Review!InventoryKnown -NeverRejectAfterRetirement == ~rejectedAfterRetirement -NeverStarted == ~(attempted /\ ~rejectedAfterRetirement /\ epoch = 0) -==== diff --git a/proof/tla/RetirementIngressCachedGrant.cfg b/proof/tla/RetirementIngressCachedGrant.cfg deleted file mode 100644 index bca7038..0000000 --- a/proof/tla/RetirementIngressCachedGrant.cfg +++ /dev/null @@ -1,4 +0,0 @@ -\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors -\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later -SPECIFICATION CachedGrantSpec -INVARIANTS TypeOK NoPostRetirementStart diff --git a/proof/tla/RetirementIngressRejected.cfg b/proof/tla/RetirementIngressRejected.cfg deleted file mode 100644 index d248cee..0000000 --- a/proof/tla/RetirementIngressRejected.cfg +++ /dev/null @@ -1,4 +0,0 @@ -\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors -\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later -SPECIFICATION Spec -INVARIANTS TypeOK NeverRejectAfterRetirement diff --git a/proof/tla/RetirementIngressStarted.cfg b/proof/tla/RetirementIngressStarted.cfg deleted file mode 100644 index 663e45b..0000000 --- a/proof/tla/RetirementIngressStarted.cfg +++ /dev/null @@ -1,4 +0,0 @@ -\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors -\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later -SPECIFICATION Spec -INVARIANTS TypeOK NeverStarted diff --git a/proof/tla/RetirementIngressUnfenced.cfg b/proof/tla/RetirementIngressUnfenced.cfg deleted file mode 100644 index 3ef737f..0000000 --- a/proof/tla/RetirementIngressUnfenced.cfg +++ /dev/null @@ -1,4 +0,0 @@ -\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors -\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later -SPECIFICATION UnfencedSpec -INVARIANTS TypeOK NoPostRetirementStart diff --git a/proof/tla/RetirementReview.cfg b/proof/tla/RetirementReview.cfg deleted file mode 100644 index 8de74e8..0000000 --- a/proof/tla/RetirementReview.cfg +++ /dev/null @@ -1,6 +0,0 @@ -\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors -\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later -SPECIFICATION Spec -INVARIANT TypeOK -INVARIANT NoOutstandingWork -INVARIANT InventoryKnown diff --git a/proof/tla/RetirementReview.tla b/proof/tla/RetirementReview.tla deleted file mode 100644 index 5d36f86..0000000 --- a/proof/tla/RetirementReview.tla +++ /dev/null @@ -1,79 +0,0 @@ ----- MODULE RetirementReview ---- -\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors -\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later - -EXTENDS Naturals, TLC - -(* Design-level experiment, not a generated projection or deployed Runtime. - A review can be followed by late activity or a discovered inventory gap. - Cancellation requests never stand in for completion observations. - No persistence implementation or policy authorization is modeled. *) - -VARIABLES active, queued, known, reviewed, cancelRequested, retired, - lateFound, gapFound - -Work == INSTANCE WorkLifecycle - -vars == <> - -Init == /\ Work!Init /\ known = FALSE - /\ reviewed = FALSE /\ retired = FALSE - /\ lateFound = FALSE /\ gapFound = FALSE - -RequestCancellation == - /\ ~retired /\ Work!RequestCancellation - /\ UNCHANGED <> - -ObserveCompletion == - /\ ~retired /\ Work!ObserveCompletion - /\ UNCHANGED <> - -ObserveQueueDrained == - /\ ~retired /\ Work!ObserveQueueDrained - /\ UNCHANGED <> - -CompleteInventory == - /\ ~retired /\ ~known /\ known' = TRUE - /\ UNCHANGED <> - -Review == - /\ ~retired /\ ~reviewed /\ Work!Quiescent /\ known - /\ reviewed' = TRUE - /\ UNCHANGED <> - -ObserveLateActivity == - /\ ~retired /\ reviewed /\ ~lateFound - /\ active' = TRUE /\ lateFound' = TRUE - /\ UNCHANGED <> - -ObserveInventoryGap == - /\ ~retired /\ reviewed /\ ~gapFound - /\ known' = FALSE /\ gapFound' = TRUE - /\ UNCHANGED <> - -Finish == - /\ retired' = TRUE - /\ UNCHANGED <> - -Retire == ~retired /\ reviewed /\ Work!Quiescent /\ known /\ Finish -TrustCancellation == ~retired /\ cancelRequested /\ known /\ Finish -TrustOldReview == ~retired /\ reviewed /\ known /\ Finish -IgnoreInventoryGap == ~retired /\ reviewed /\ Work!Quiescent /\ Finish - -Observe == RequestCancellation \/ ObserveCompletion \/ ObserveQueueDrained - \/ CompleteInventory \/ Review \/ ObserveLateActivity \/ ObserveInventoryGap -Terminal == retired /\ UNCHANGED vars -Next == Observe \/ Retire \/ Terminal -Spec == Init /\ [][Next]_vars -CancellationSpec == Init /\ [][Observe \/ TrustCancellation \/ Terminal]_vars -OldReviewSpec == Init /\ [][Observe \/ TrustOldReview \/ Terminal]_vars -GapSpec == Init /\ [][Observe \/ IgnoreInventoryGap \/ Terminal]_vars - -TypeOK == /\ Work!TypeOK /\ known \in BOOLEAN /\ reviewed \in BOOLEAN - /\ retired \in BOOLEAN /\ lateFound \in BOOLEAN /\ gapFound \in BOOLEAN -NoOutstandingWork == retired => Work!Quiescent -InventoryKnown == retired => known -NeverRetired == ~retired - -==== diff --git a/proof/tla/RetirementReviewCancellation.cfg b/proof/tla/RetirementReviewCancellation.cfg deleted file mode 100644 index 2484973..0000000 --- a/proof/tla/RetirementReviewCancellation.cfg +++ /dev/null @@ -1,5 +0,0 @@ -\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors -\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later -SPECIFICATION CancellationSpec -INVARIANT TypeOK -INVARIANT NoOutstandingWork diff --git a/proof/tla/RetirementReviewReachable.cfg b/proof/tla/RetirementReviewReachable.cfg deleted file mode 100644 index 5b9f303..0000000 --- a/proof/tla/RetirementReviewReachable.cfg +++ /dev/null @@ -1,5 +0,0 @@ -\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors -\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later -SPECIFICATION Spec -INVARIANT TypeOK -INVARIANT NeverRetired diff --git a/proof/tla/RetirementReviewStale.cfg b/proof/tla/RetirementReviewStale.cfg deleted file mode 100644 index a4a1d25..0000000 --- a/proof/tla/RetirementReviewStale.cfg +++ /dev/null @@ -1,5 +0,0 @@ -\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors -\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later -SPECIFICATION OldReviewSpec -INVARIANT TypeOK -INVARIANT NoOutstandingWork diff --git a/proof/tla/RetirementReviewUnknown.cfg b/proof/tla/RetirementReviewUnknown.cfg deleted file mode 100644 index 0716a34..0000000 --- a/proof/tla/RetirementReviewUnknown.cfg +++ /dev/null @@ -1,5 +0,0 @@ -\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors -\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later -SPECIFICATION GapSpec -INVARIANT TypeOK -INVARIANT InventoryKnown diff --git a/proof/tla/WorkCancellation.cfg b/proof/tla/WorkCancellation.cfg deleted file mode 100644 index e8d9a09..0000000 --- a/proof/tla/WorkCancellation.cfg +++ /dev/null @@ -1,4 +0,0 @@ -\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors -\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later -SPECIFICATION CancellationSpec -INVARIANTS TypeOK WorkRemains diff --git a/proof/tla/WorkCancellationReachable.cfg b/proof/tla/WorkCancellationReachable.cfg deleted file mode 100644 index 20dbb27..0000000 --- a/proof/tla/WorkCancellationReachable.cfg +++ /dev/null @@ -1,4 +0,0 @@ -\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors -\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later -SPECIFICATION CancellationSpec -INVARIANTS TypeOK NeverCancelled diff --git a/proof/tla/WorkLifecycle.cfg b/proof/tla/WorkLifecycle.cfg deleted file mode 100644 index 75a6de7..0000000 --- a/proof/tla/WorkLifecycle.cfg +++ /dev/null @@ -1,4 +0,0 @@ -\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors -\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later -SPECIFICATION Spec -INVARIANT TypeOK diff --git a/proof/tla/WorkLifecycle.tla b/proof/tla/WorkLifecycle.tla deleted file mode 100644 index e95929e..0000000 --- a/proof/tla/WorkLifecycle.tla +++ /dev/null @@ -1,31 +0,0 @@ ----- MODULE WorkLifecycle ---- -\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors -\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later - -EXTENDS TLC - -(* Shared action contract: asking to cancel does not observe completion. - The caller owns admission, review and authorization. *) -VARIABLES active, queued, cancelRequested -vars == <> -Init == active = TRUE /\ queued = TRUE /\ cancelRequested = FALSE -RequestCancellation == - /\ ~cancelRequested /\ cancelRequested' = TRUE - /\ UNCHANGED <> -ObserveCompletion == active /\ active' = FALSE - /\ UNCHANGED <> -ObserveQueueDrained == queued /\ queued' = FALSE - /\ UNCHANGED <> -Quiescent == ~active /\ ~queued -TypeOK == active \in BOOLEAN /\ queued \in BOOLEAN /\ cancelRequested \in BOOLEAN -Terminal == Quiescent /\ cancelRequested /\ UNCHANGED vars -Next == RequestCancellation \/ ObserveCompletion \/ ObserveQueueDrained \/ Terminal -Spec == Init /\ [][Next]_vars - -(* Atomic cancellation-only environment; completion is deliberately absent. *) -CancellationSpec == Init /\ [][RequestCancellation \/ - (cancelRequested /\ UNCHANGED vars)]_vars -WorkRemains == active /\ queued -NeverCancelled == ~cancelRequested -NeverQuiescent == ~Quiescent -==== diff --git a/proof/tla/WorkQuiescenceReachable.cfg b/proof/tla/WorkQuiescenceReachable.cfg deleted file mode 100644 index 8e33085..0000000 --- a/proof/tla/WorkQuiescenceReachable.cfg +++ /dev/null @@ -1,4 +0,0 @@ -\* SPDX-FileCopyrightText: 2026 tao3k team and Contributors -\* SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later -SPECIFICATION Spec -INVARIANTS TypeOK NeverQuiescent diff --git a/t/formal/retirement-binding-test.ss b/t/formal/retirement-binding-test.ss index c6975d2..7b318d5 100644 --- a/t/formal/retirement-binding-test.ss +++ b/t/formal/retirement-binding-test.ss @@ -22,19 +22,19 @@ (assurance-artifact id "r1" 'supported digest owner: "lambda-aitia" media-kind: 'formal-source provenance: '("repository-source-bytes"))) -(def tla-source (source "retirement/tla" (source-group-digest job-retirement-tla-sources))) +(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 (snapshot tla lean) +(def (snapshot quint lean) (assurance-snapshot "retirement/formal-cut" "cut-1" "retirement/formal-sources" - '(("retirement/tla" . "r1") ("retirement/lean" . "r1")) '() - "retirement/design-r1" "formal-review" "r1" (list tla lean) '())) -(def source-cut (snapshot tla-source lean-source)) + '(("retirement/quint" . "r1") ("retirement/lean" . "r1")) '() + "retirement/design-r1" "formal-review" "r1" (list quint lean) '())) +(def source-cut (snapshot quint-source lean-source)) (def links (map (lambda (artifact) (sdlc-design-implementation-link job-retirement-contract job-retirement-safety artifact source-cut)) - (list tla-source lean-source))) + (list quint-source lean-source))) (def retirement-binding-test (test-suite "Retirement POO contract to formal source binding" @@ -51,8 +51,8 @@ (check (.ref review 'implementation-conforms?) => #f) (check (.ref review 'evidence-admitted?) => #f) (check (.ref review 'release-authorized?) => #f))) - (test-case "same-revision TLA source mutation invalidates the joined mapping" - (let* ((changed (source "retirement/tla" (source-lock-payload-digest "changed model"))) + (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) links))) (check (.ref review 'mapping-current?) => #f) @@ -61,7 +61,7 @@ (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 tla-source changed) links))) + job-retirement-contract (snapshot quint-source changed) links))) (check (.ref review 'mapping-current?) => #f) (check (length (.ref review 'blocked-links)) => 2) (check (.ref (car (.ref review 'blocked-links)) 'blocker) => 'snapshot-changed))) diff --git a/t/formal/tla-poo-interface-test.ss b/t/formal/tla-poo-interface-test.ss deleted file mode 100644 index e92d332..0000000 --- a/t/formal/tla-poo-interface-test.ss +++ /dev/null @@ -1,44 +0,0 @@ -;;; -*- Gerbil -*- -;;; SPDX-FileCopyrightText: 2026 tao3k team and Contributors -;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later - -;;; Consumer qualification of POO Flow's syntax receipt. The parser-owned -;;; artifact tests remain separate; neither test claims TLC or refinement. -(import (only-in :clan/poo/object .ref) - (only-in :std/misc/ports read-all-as-string) - (only-in :std/test check-equal? check-exception test-case test-suite) - (only-in :gerbil-parser/src/runtime/cst - syntax-node? syntax-node-kind syntax-node-start syntax-node-end) - (only-in :poo-flow/src/modules/tla-plus/interface - PooFlowTlaLanguage. poo-flow-tla-document? - poo-flow-tla-parse-source poo-flow-tla-parser-cst)) -(export tla-poo-interface-test) - -(def tla-poo-interface-test - (test-suite "Aitia TLA+ POO interface consumer" - (test-case "all maintained models bind the same parser-owned syntax tree" - (check-equal? (.ref PooFlowTlaLanguage. 'syntax-contract) - "tla-plus.native-core.v1") - (for-each - (lambda (name) - (let* ((source - (call-with-input-file - (string-append "proof/tla/" name ".tla") - read-all-as-string)) - (document (poo-flow-tla-parse-source source)) - (root (poo-flow-tla-parser-cst document))) - (check-equal? (poo-flow-tla-document? document) #t) - (check-equal? (syntax-node? root) #t) - (check-equal? (syntax-node-kind root) 'SourceFile) - (check-equal? (syntax-node-start root) 0) - (check-equal? (syntax-node-end root) - (u8vector-length (string->utf8 source))) - (check-equal? (.ref document 'exact-roundtrip?) #t) - (check-equal? (.ref document 'model-checking?) #f))) - '("EffectAdmission" "AssuranceLifecycle" "WorkLifecycle" - "RetirementReview" "RetirementIngress"))) - (test-case "rejected syntax cannot produce an accepted document" - (check-exception - (poo-flow-tla-parse-source - "---- MODULE Broken ----\nWork == INSTANCE\n====\n") - true)))) diff --git a/t/formal/tla-syntax-test.ss b/t/formal/tla-syntax-test.ss deleted file mode 100644 index 815bd44..0000000 --- a/t/formal/tla-syntax-test.ss +++ /dev/null @@ -1,34 +0,0 @@ -;;; SPDX-FileCopyrightText: 2026 tao3k team and Contributors -;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later - -;;; Source qualification only; neither TLC nor semantic refinement is inferred. -;;; Explicit formal lane; run from the Aitia root with the qualified parser. -(import :std/test - (only-in :std/misc/ports read-all-as-string) - (only-in :gerbil-parser/languages/tla-plus/v1/parser - +tla-plus-syntax-contract+ parse-tla-plus-v1) - (only-in :gerbil-parser/src/runtime/artifact - parse-artifact-success? parse-artifact-valid? - parse-artifact-roundtrip)) -(export tla-syntax-test) - -(def tla-syntax-test - (test-suite "Aitia formal source syntax qualification" - (test-case "all maintained models bind native syntax and original bytes" - (check +tla-plus-syntax-contract+ => "tla-plus.native-core.v1") - (for-each - (lambda (name) - (let* ((path (string-append "proof/tla/" name ".tla")) - (source (call-with-input-file path read-all-as-string)) - (artifact (parse-tla-plus-v1 source))) - (check (list name (parse-artifact-success? artifact) - (parse-artifact-valid? artifact)) - => (list name #t #t)) - (check (parse-artifact-roundtrip artifact) => source))) - '("EffectAdmission" "AssuranceLifecycle" "WorkLifecycle" - "RetirementReview" "RetirementIngress"))) - (test-case "lossless diagnostic artifacts do not imply syntax admission" - (let* ((source "---- MODULE Broken ----\nWork == INSTANCE\n====\n") - (artifact (parse-tla-plus-v1 source))) - (check (parse-artifact-success? artifact) => #f) - (check (parse-artifact-roundtrip artifact) => source))))) diff --git a/t/modules/unit/composition-identity-test.ss b/t/modules/unit/composition-identity-test.ss index 74102e8..dac1466 100644 --- a/t/modules/unit/composition-identity-test.ss +++ b/t/modules/unit/composition-identity-test.ss @@ -47,10 +47,10 @@ (software-engineering . formal-methods))) (check-equal? (poo-flow-user-module-selection-flags (list-ref modules 5)) - '(+tla+ +lean +cedar)))) + '(+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)))) diff --git a/user-interface/profiles/software-engineering/project.ss b/user-interface/profiles/software-engineering/project.ss index c33464a..7dc99c9 100644 --- a/user-interface/profiles/software-engineering/project.ss +++ b/user-interface/profiles/software-engineering/project.ss @@ -31,11 +31,11 @@ (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/job-retirement-contract.ss b/user-interface/scenarios/job-retirement-contract.ss index 5712ab6..082bded 100644 --- a/user-interface/scenarios/job-retirement-contract.ss +++ b/user-interface/scenarios/job-retirement-contract.ss @@ -6,7 +6,7 @@ (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-tla-sources job-retirement-lean-sources) + job-retirement-quint-sources job-retirement-lean-sources) (def job-retirement-safety (sdlc-design-guarantee @@ -25,30 +25,22 @@ (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 TLA+ composition models one cached grant, one request and one attempt.")) + "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-tla-sources - '("proof/tla/EffectAdmission.tla" - "proof/tla/WorkLifecycle.tla" - "proof/tla/RetirementReview.tla" - "proof/tla/RetirementIngress.tla" - "proof/tla/WorkLifecycle.cfg" - "proof/tla/WorkCancellation.cfg" - "proof/tla/WorkCancellationReachable.cfg" - "proof/tla/WorkQuiescenceReachable.cfg" - "proof/tla/RetirementReview.cfg" - "proof/tla/RetirementReviewCancellation.cfg" - "proof/tla/RetirementReviewStale.cfg" - "proof/tla/RetirementReviewUnknown.cfg" - "proof/tla/RetirementReviewReachable.cfg" - "proof/tla/RetirementIngress.cfg" - "proof/tla/RetirementIngressUnfenced.cfg" - "proof/tla/RetirementIngressCachedGrant.cfg" - "proof/tla/RetirementIngressRejected.cfg" - "proof/tla/RetirementIngressStarted.cfg")) +(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/process_runner.py")) (def job-retirement-lean-sources '("proof/lean/LambdaAitiaProof/Modules/Sdlc/Retirement.lean" "proof/lean/LambdaAitiaProof/Modules/Sdlc/RetirementIngress.lean")) From 9367d7ac0416acbea10c0dfdb91ce7d8e8ac3ffd Mon Sep 17 00:00:00 2001 From: guangtao Date: Sat, 10 Oct 2026 11:24:39 +0800 Subject: [PATCH 52/64] Adapt Aitia imports to POO Flow POO-native owners --- .github/workflows/ci.yml | 2 +- modules/ADR/config.ss | 2 +- modules/ADR/funs.ss | 2 +- modules/ADR/objects.ss | 20 +++--- modules/ADR/types.ss | 12 ++-- modules/assurance/config.ss | 6 +- modules/assurance/evidence-admission.ss | 20 +++--- modules/assurance/funs.ss | 44 ++++++------ modules/assurance/host.ss | 18 ++--- modules/assurance/invalidation-projection.ss | 2 +- modules/assurance/objects.ss | 58 ++++++++-------- modules/assurance/types.ss | 68 +++++++++---------- modules/formal-methods/config.ss | 2 +- modules/gitops/config.ss | 2 +- modules/gitops/funs.ss | 2 +- modules/gitops/objects.ss | 4 +- modules/sdlc/config.ss | 2 +- modules/sdlc/design.ss | 20 +++--- modules/sdlc/funs.ss | 2 +- modules/sdlc/impact.ss | 2 +- modules/sdlc/implementation.ss | 8 +-- modules/sdlc/objects.ss | 28 ++++---- modules/sdlc/standards/nasa-certification.ss | 20 +++--- modules/sdlc/standards/nasa-lifecycle.ss | 12 ++-- modules/sdlc/standards/nasa-policy.ss | 6 +- modules/sdlc/standards/nasa-review.ss | 18 ++--- .../sdlc/standards/nasa-standard-provider.ss | 2 +- modules/sdlc/standards/nasa-structured.ss | 8 +-- modules/sdlc/types.ss | 40 +++++------ t/ADR/unit/ADR-module-test.ss | 2 +- t/assurance/unit/assurance-kernel-test.ss | 2 +- t/assurance/unit/decision-preflight-test.ss | 2 +- t/assurance/unit/evidence-admission-test.ss | 2 +- t/assurance/unit/required-support-test.ss | 6 +- t/gitops/unit/gitops-protocol-test.ss | 2 +- t/integration/github-workflow-test.ss | 5 +- t/modules/unit/composition-identity-test.ss | 6 +- t/modules/unit/source-collection-test.ss | 4 +- t/sdlc/source-admission-test.ss | 2 +- t/sdlc/unit/job-retirement-impact-test.ss | 2 +- t/sdlc/unit/nasa-catalog-test.ss | 2 +- t/sdlc/unit/nasa-certification-test.ss | 2 +- t/sdlc/unit/nasa-lifecycle-test.ss | 2 +- t/sdlc/unit/nasa-review-test.ss | 2 +- t/sdlc/unit/nasa-standard-provider-test.ss | 2 +- t/sdlc/unit/nasa-structured-test.ss | 6 +- t/sdlc/unit/sdlc-config-test.ss | 8 ++- t/sdlc/unit/sdlc-model-test.ss | 2 +- t/sdlc/unit/sdlc-test.ss | 3 +- t/sdlc/unit/sdlc-trace-test.ss | 2 +- testing-interface.ss | 2 +- tools/nasa-implementation-report.ss | 2 +- user-interface/compositions/aitia.ss | 12 ++-- user-interface/config.ss | 12 ++-- user-interface/profiles/github/actions.ss | 2 +- user-interface/profiles/nasa/sdlc.ss | 8 ++- .../profiles/software-engineering/project.ss | 2 +- .../scenarios/job-executor-retirement.ss | 2 +- 58 files changed, 277 insertions(+), 263 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1d56905..d16678c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,7 +126,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: repository: tao3k/poo-flow - ref: 309396b6364cacf40ae2979dc4671f592547a00d + ref: 1ba1443c63d91441eb15f24f32202dfda562d69e path: poo-flow submodules: false 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 1addd54..4960c6f 100644 --- a/modules/assurance/config.ss +++ b/modules/assurance/config.ss @@ -6,7 +6,7 @@ (import (only-in :clan/poo/object .o) (only-in :gerbil/core list-sort) :std/list/list - :poo-flow/src/module-system/contribution/model + :poo-flow/core/poo-clos/interface :poo-flow/lambda-aitia/modules/assurance/types) (export AssurancePolicy assurance-verification-policy) @@ -29,9 +29,9 @@ (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-flow-check-model + (poo-clos-check-model AssuranceVerificationPolicy - (.o (:: @ (poo-flow-model-prototype AssuranceVerificationPolicy)) + (.o (:: @ (poo-clos-model-prototype AssuranceVerificationPolicy)) identity: identity-value revision: revision-value capabilities: (list-sort capabilityruntime) diff --git a/t/assurance/unit/evidence-admission-test.ss b/t/assurance/unit/evidence-admission-test.ss index e302035..de8a929 100644 --- a/t/assurance/unit/evidence-admission-test.ss +++ b/t/assurance/unit/evidence-admission-test.ss @@ -6,7 +6,7 @@ (import :std/test (only-in :std/error Error?) (only-in :clan/poo/object .o .ref) - (only-in :poo-flow/src/module-system/contribution/verification + (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) diff --git a/t/assurance/unit/required-support-test.ss b/t/assurance/unit/required-support-test.ss index 676e2da..a721fd7 100644 --- a/t/assurance/unit/required-support-test.ss +++ b/t/assurance/unit/required-support-test.ss @@ -6,8 +6,8 @@ (import :std/test (only-in :std/list/list find) (only-in :clan/poo/object .o .ref) - (only-in :poo-flow/src/module-system/contribution/model - poo-flow-model-prototype) + (only-in :poo-flow/core/poo-clos/interface + poo-clos-model-prototype) :poo-flow/lambda-aitia/modules/assurance/interface) (export required-support-test) @@ -78,7 +78,7 @@ (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-flow-model-prototype AssessmentRequiredSupport)) + (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" 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 3328365..f46c69e 100644 --- a/t/integration/github-workflow-test.ss +++ b/t/integration/github-workflow-test.ss @@ -5,7 +5,6 @@ (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) @@ -34,10 +33,10 @@ (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)) + (.ref github-gitops-sdlc 'profiles)) '(actions dev staging production nasa/sdlc/npr-7150.2)) (check-equal? (map (lambda (standard) (.ref standard 'identity)) (.ref nasa-sdlc-npr7150_2 'standards)) diff --git a/t/modules/unit/composition-identity-test.ss b/t/modules/unit/composition-identity-test.ss index dac1466..e5ceea2 100644 --- a/t/modules/unit/composition-identity-test.ss +++ b/t/modules/unit/composition-identity-test.ss @@ -5,14 +5,14 @@ (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 + (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/user-interface/profile-core poo-flow-user-profile-modules) - (only-in :poo-flow/src/modules/proof/interface + (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) 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/job-retirement-impact-test.ss b/t/sdlc/unit/job-retirement-impact-test.ss index 5f7f96c..487410d 100644 --- a/t/sdlc/unit/job-retirement-impact-test.ss +++ b/t/sdlc/unit/job-retirement-impact-test.ss @@ -4,7 +4,7 @@ (import :std/test (only-in :clan/poo/object .ref) (only-in :poo-flow/src/graph/types poo-flow-graph-id) - (only-in :poo-flow/src/modules/temporal-causality/interface + (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) diff --git a/t/sdlc/unit/nasa-catalog-test.ss b/t/sdlc/unit/nasa-catalog-test.ss index 8810a92..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) 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 e6b5d72..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 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 cc62b4f..b4baf8f 100644 --- a/t/sdlc/unit/sdlc-config-test.ss +++ b/t/sdlc/unit/sdlc-config-test.ss @@ -3,9 +3,11 @@ ;;; 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))) 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 2b1a7bd..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))) 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/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/user-interface/compositions/aitia.ss b/user-interface/compositions/aitia.ss index 35ce9b1..1a8efdd 100644 --- a/user-interface/compositions/aitia.ss +++ b/user-interface/compositions/aitia.ss @@ -7,11 +7,15 @@ ;;; 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/src/module-system/semantic-module/objects - poo-flow-semantic-identity poo-flow-semantic-module) - (only-in :poo-flow/src/module-system/profile-composition/interface + (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 use-module user-composition) + 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 AitiaModule aitia) diff --git a/user-interface/config.ss b/user-interface/config.ss index 83f6c24..1ec2abb 100644 --- a/user-interface/config.ss +++ b/user-interface/config.ss @@ -5,11 +5,15 @@ ;;; -*- Gerbil -*- ;;; 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/src/module-system/profile-composition/interface - profiles compose use-module user-composition +(import (only-in :poo-flow/core/profile-composition/profile-bundle + profiles compose poo-flow-profile-export poo-flow-module-profiles) - (only-in :poo-flow/src/module-system/semantic-module/objects - poo-flow-semantic-identity poo-flow-semantic-module) + (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 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 index 1958212..1c56435 100644 --- a/user-interface/profiles/nasa/sdlc.ss +++ b/user-interface/profiles/nasa/sdlc.ss @@ -6,9 +6,11 @@ ;;; 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/src/module-system/semantic-module/objects - poo-flow-semantic-identity poo-flow-semantic-module) - (only-in :poo-flow/src/module-system/profile-composition/interface + (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) diff --git a/user-interface/profiles/software-engineering/project.ss b/user-interface/profiles/software-engineering/project.ss index 7dc99c9..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 diff --git a/user-interface/scenarios/job-executor-retirement.ss b/user-interface/scenarios/job-executor-retirement.ss index 58d10e3..9bb216e 100644 --- a/user-interface/scenarios/job-executor-retirement.ss +++ b/user-interface/scenarios/job-executor-retirement.ss @@ -5,7 +5,7 @@ ;;; 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/src/modules/temporal-causality/interface + (only-in :poo-flow/modules/temporal-causality/interface poo-flow-temporal-observation poo-flow-causal-event poo-flow-causal-event-graph)) From f0ba0572492c180270ee47d6a695349f87cb9067 Mon Sep 17 00:00:00 2001 From: guangtao Date: Sat, 10 Oct 2026 11:32:47 +0800 Subject: [PATCH 53/64] Track latest POO Flow PR 34 revision in CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d16678c..f059273 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,7 +126,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: repository: tao3k/poo-flow - ref: 1ba1443c63d91441eb15f24f32202dfda562d69e + ref: ad1524c3b7a5ce0aedb7130cca457b96652e4b9d path: poo-flow submodules: false From cb3ed96969d5314147dd26334cb63363a9950d7c Mon Sep 17 00:00:00 2001 From: guangtao Date: Sat, 10 Oct 2026 11:42:42 +0800 Subject: [PATCH 54/64] Execute bounded retirement ingress through local Scheme Host --- .github/workflows/ci.yml | 6 +- build.ss | 1 + modules/sdlc/retirement-host.ss | 225 ++++++++++++++++++ t/formal/retirement-binding-test.ss | 31 ++- t/formal/retirement-execution-test.ss | 96 ++++++++ .../scenarios/job-retirement-contract.ss | 7 +- 6 files changed, 353 insertions(+), 13 deletions(-) create mode 100644 modules/sdlc/retirement-host.ss create mode 100644 t/formal/retirement-execution-test.ss diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f059273..1466226 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,7 +126,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: repository: tao3k/poo-flow - ref: ad1524c3b7a5ce0aedb7130cca457b96652e4b9d + ref: 120817135c7153465caf19825813c201e1ba2f5e path: poo-flow submodules: false @@ -185,6 +185,10 @@ jobs: 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: 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/build.ss b/build.ss index ea3e90a..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" diff --git a/modules/sdlc/retirement-host.ss b/modules/sdlc/retirement-host.ss new file mode 100644 index 0000000..6721f43 --- /dev/null +++ b/modules/sdlc/retirement-host.ss @@ -0,0 +1,225 @@ +;;; -*- Gerbil -*- +;;; SPDX-FileCopyrightText: 2026 tao3k team and Contributors +;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +;;; Explicit, single-owner Scheme Host for the bounded retirement-ingress cut. +;;; Pure POO state transitions are committed only at this local Host boundary. +;;; This does not issue grants or claim durable, distributed, or Marlin authority. +(import (only-in :clan/poo/object .o .ref .slot? object?)) +(export aitia-retirement-host + aitia-retirement-host-snapshot + aitia-retirement-effect-handoff + aitia-retirement-host-request-cancellation! + aitia-retirement-host-observe-completion! + aitia-retirement-host-observe-queue-drained! + aitia-retirement-host-complete-inventory! + aitia-retirement-host-review-cut! + aitia-retirement-host-observe-late-activity! + aitia-retirement-host-observe-inventory-gap! + aitia-retirement-host-close-ingress! + aitia-retirement-host-request-start! + aitia-retirement-host-attempt-start! + aitia-retirement-host-retire!) + +;;; The live state is keyed by host identity, never returned to callers. +(def retirement-host-states (make-hash-table-eq weak-keys: #t)) + +(def (host-state host) + (let (state (and (object? host) (hash-get retirement-host-states host))) + (unless (and state + (eq? (.ref state 'owner-thread) (current-thread))) + (error "retirement Host requires its serialized owner thread")) + state)) + +(def (state-view state) + (.o kind: 'aitia.retirement-ingress-state.v1 + active?: (.ref state 'active?) + queued?: (.ref state 'queued?) + inventory-known?: (.ref state 'inventory-known?) + reviewed?: (.ref state 'reviewed?) + cancellation-requested?: (.ref state 'cancellation-requested?) + late-activity-found?: (.ref state 'late-activity-found?) + inventory-gap-found?: (.ref state 'inventory-gap-found?) + retired?: (.ref state 'retired?) + ingress-open?: (.ref state 'ingress-open?) + authority-epoch: (.ref state 'authority-epoch) + cached-grant-epoch: (.ref state 'cached-grant-epoch) + pending?: (.ref state 'pending?) + attempted?: (.ref state 'attempted?))) + +(def (receipt action state accepted? reason handoff-executed?) + (.o kind: 'aitia.retirement-ingress-receipt.v1 + action: action accepted?: accepted? reason: reason + state: (state-view state) + handoff-executed?: handoff-executed? + durable?: #f distributed-authority?: #f)) + +(def (publish host action state accepted? reason) + (hash-put! retirement-host-states host state) + (receipt action state accepted? reason #f)) + +(def (aitia-retirement-host) + (let ((host (.o kind: 'aitia.retirement-ingress-host.v1)) + (state (.o owner-thread: (current-thread) + active?: #t queued?: #t inventory-known?: #f + reviewed?: #f cancellation-requested?: #f + late-activity-found?: #f inventory-gap-found?: #f + retired?: #f ingress-open?: #t + authority-epoch: 0 cached-grant-epoch: 0 + pending?: #f attempted?: #f))) + (hash-put! retirement-host-states host state) + host)) + +(def (aitia-retirement-host-snapshot host) + (state-view (host-state host))) + +;;; The named POO handoff is the only effect escape hatch. Its callback is +;;; invoked only after the Host has consumed a current pending request. +(def (aitia-retirement-effect-handoff run) + (unless (procedure? run) (error "retirement handoff requires a procedure")) + (.o kind: 'aitia.retirement-effect-handoff.v1 run: run)) + +(def (aitia-retirement-host-request-cancellation! host) + (let (state (host-state host)) + (cond + ((.ref state 'retired?) + (receipt 'request-cancellation state #f 'already-retired #f)) + ((.ref state 'cancellation-requested?) + (receipt 'request-cancellation state #f 'already-requested #f)) + (else + (publish host 'request-cancellation + (.o (:: @ state) cancellation-requested?: #t) + #t 'recorded))))) + +(def (aitia-retirement-host-observe-completion! host) + (let (state (host-state host)) + (cond + ((.ref state 'retired?) + (receipt 'observe-completion state #f 'already-retired #f)) + ((not (.ref state 'active?)) + (receipt 'observe-completion state #f 'not-active #f)) + (else + (publish host 'observe-completion + (.o (:: @ state) active?: #f) #t 'observed))))) + +(def (aitia-retirement-host-observe-queue-drained! host) + (let (state (host-state host)) + (cond + ((.ref state 'retired?) + (receipt 'observe-queue-drained state #f 'already-retired #f)) + ((not (.ref state 'queued?)) + (receipt 'observe-queue-drained state #f 'already-drained #f)) + (else + (publish host 'observe-queue-drained + (.o (:: @ state) queued?: #f) #t 'observed))))) + +(def (aitia-retirement-host-complete-inventory! host) + (let (state (host-state host)) + (cond + ((.ref state 'retired?) + (receipt 'complete-inventory state #f 'already-retired #f)) + ((.ref state 'inventory-known?) + (receipt 'complete-inventory state #f 'already-known #f)) + (else + (publish host 'complete-inventory + (.o (:: @ state) inventory-known?: #t) #t 'observed))))) + +(def (aitia-retirement-host-review-cut! host) + (let (state (host-state host)) + (cond + ((.ref state 'retired?) + (receipt 'review-cut state #f 'already-retired #f)) + ((.ref state 'active?) + (receipt 'review-cut state #f 'active-work #f)) + ((.ref state 'queued?) + (receipt 'review-cut state #f 'queued-work #f)) + ((not (.ref state 'inventory-known?)) + (receipt 'review-cut state #f 'inventory-unknown #f)) + ((.ref state 'reviewed?) + (receipt 'review-cut state #f 'already-reviewed #f)) + (else + (publish host 'review-cut (.o (:: @ state) reviewed?: #t) + #t 'reviewed))))) + +(def (aitia-retirement-host-observe-late-activity! host) + (let (state (host-state host)) + (cond + ((or (.ref state 'retired?) (not (.ref state 'reviewed?)) + (.ref state 'late-activity-found?)) + (receipt 'observe-late-activity state #f 'not-admissible #f)) + (else + (publish host 'observe-late-activity + (.o (:: @ state) active?: #t late-activity-found?: #t) + #t 'observed))))) + +(def (aitia-retirement-host-observe-inventory-gap! host) + (let (state (host-state host)) + (cond + ((or (.ref state 'retired?) (not (.ref state 'reviewed?)) + (.ref state 'inventory-gap-found?)) + (receipt 'observe-inventory-gap state #f 'not-admissible #f)) + (else + (publish host 'observe-inventory-gap + (.o (:: @ state) inventory-known?: #f inventory-gap-found?: #t) + #t 'observed))))) + +;;; Closure and epoch advancement are one synchronous Host state publication. +(def (aitia-retirement-host-close-ingress! host) + (let (state (host-state host)) + (if (.ref state 'ingress-open?) + (publish host 'close-ingress + (.o (:: @ state) ingress-open?: #f + authority-epoch: (+ 1 (.ref state 'authority-epoch))) + #t 'fenced) + (receipt 'close-ingress state #f 'already-closed #f)))) + +(def (aitia-retirement-host-request-start! host) + (let (state (host-state host)) + (if (or (.ref state 'pending?) (.ref state 'attempted?)) + (receipt 'request-start state #f 'request-already-consumed #f) + (publish host 'request-start (.o (:: @ state) pending?: #t) + #t 'pending)))) + +(def (aitia-retirement-host-attempt-start! host handoff) + (let (state (host-state host)) + (unless (and (object? handoff) (.slot? handoff 'kind) + (eq? (.ref handoff 'kind) + 'aitia.retirement-effect-handoff.v1) + (.slot? handoff 'run) (procedure? (.ref handoff 'run))) + (error "invalid retirement effect handoff")) + (unless (.ref state 'pending?) + (error "retirement start requires a pending request")) + (let (consumed (.o (:: @ state) pending?: #f attempted?: #t)) + (cond + ((not (= (.ref state 'cached-grant-epoch) + (.ref state 'authority-epoch))) + (publish host 'attempt-start consumed #f 'stale-grant)) + ((or (.ref state 'retired?) (not (.ref state 'ingress-open?))) + (publish host 'attempt-start consumed #f 'ingress-closed)) + (else + ;; Consume before handoff so a reentrant call cannot start twice. + (let (started (.o (:: @ consumed) active?: #t)) + (hash-put! retirement-host-states host started) + ((.ref handoff 'run)) + (receipt 'attempt-start started #t 'started #t))))))) + +(def (aitia-retirement-host-retire! host) + (let (state (host-state host)) + (cond + ((.ref state 'retired?) + (receipt 'retire state #f 'already-retired #f)) + ((.ref state 'active?) + (receipt 'retire state #f 'active-work #f)) + ((.ref state 'queued?) + (receipt 'retire state #f 'queued-work #f)) + ((not (.ref state 'inventory-known?)) + (receipt 'retire state #f 'inventory-unknown #f)) + ((not (.ref state 'reviewed?)) + (receipt 'retire state #f 'review-missing #f)) + ((.ref state 'ingress-open?) + (receipt 'retire state #f 'ingress-open #f)) + ((= (.ref state 'cached-grant-epoch) (.ref state 'authority-epoch)) + (receipt 'retire state #f 'grant-current #f)) + (else + (publish host 'retire (.o (:: @ state) retired?: #t) + #t 'retired))))) diff --git a/t/formal/retirement-binding-test.ss b/t/formal/retirement-binding-test.ss index 7b318d5..8a6bce1 100644 --- a/t/formal/retirement-binding-test.ss +++ b/t/formal/retirement-binding-test.ss @@ -24,21 +24,23 @@ 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 (snapshot quint lean) +(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/design-r1" "formal-review" "r1" (list quint lean) '())) -(def source-cut (snapshot quint-source lean-source)) + '(("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))) + (list quint-source lean-source scheme-source))) (def retirement-binding-test (test-suite "Retirement POO contract to formal source binding" - (test-case "both views bind one declaration without granting conformance" + (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) @@ -54,16 +56,23 @@ (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) links))) + job-retirement-contract (snapshot changed lean-source scheme-source) links))) (check (.ref review 'mapping-current?) => #f) - (check (length (.ref review 'blocked-links)) => 2) + (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) links))) + job-retirement-contract (snapshot quint-source changed scheme-source) links))) (check (.ref review 'mapping-current?) => #f) - (check (length (.ref review 'blocked-links)) => 2) + (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 @@ -77,6 +86,6 @@ (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)) => 2) + (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..a79dff9 --- /dev/null +++ b/t/formal/retirement-execution-test.ss @@ -0,0 +1,96 @@ +;;; -*- 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 "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 "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/user-interface/scenarios/job-retirement-contract.ss b/user-interface/scenarios/job-retirement-contract.ss index 082bded..b4895f1 100644 --- a/user-interface/scenarios/job-retirement-contract.ss +++ b/user-interface/scenarios/job-retirement-contract.ss @@ -6,7 +6,8 @@ (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-quint-sources job-retirement-lean-sources + job-retirement-scheme-sources) (def job-retirement-safety (sdlc-design-guarantee @@ -44,3 +45,7 @@ (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" + "build.ss")) From 60a07645d5b74163f06861e1b42041f579cfe788 Mon Sep 17 00:00:00 2001 From: guangtao Date: Sat, 10 Oct 2026 11:51:03 +0800 Subject: [PATCH 55/64] Fix retirement Host lazy slots and reentrant handoff --- modules/sdlc/retirement-host.ss | 62 ++++++++++++++++----------- t/formal/retirement-execution-test.ss | 18 ++++++++ 2 files changed, 55 insertions(+), 25 deletions(-) diff --git a/modules/sdlc/retirement-host.ss b/modules/sdlc/retirement-host.ss index 6721f43..29f3e8a 100644 --- a/modules/sdlc/retirement-host.ss +++ b/modules/sdlc/retirement-host.ss @@ -31,6 +31,12 @@ (error "retirement Host requires its serialized owner thread")) state)) +(def (mutation-state host) + (let (current-state (host-state host)) + (when (.ref current-state 'handoff-in-progress?) + (error "retirement Host mutation during effect handoff")) + current-state)) + (def (state-view state) (.o kind: 'aitia.retirement-ingress-state.v1 active?: (.ref state 'active?) @@ -45,13 +51,14 @@ authority-epoch: (.ref state 'authority-epoch) cached-grant-epoch: (.ref state 'cached-grant-epoch) pending?: (.ref state 'pending?) - attempted?: (.ref state 'attempted?))) + attempted?: (.ref state 'attempted?) + handoff-in-progress?: (.ref state 'handoff-in-progress?))) -(def (receipt action state accepted? reason handoff-executed?) +(def (receipt action-name current-state was-accepted? rejection-reason did-handoff?) (.o kind: 'aitia.retirement-ingress-receipt.v1 - action: action accepted?: accepted? reason: reason - state: (state-view state) - handoff-executed?: handoff-executed? + action: action-name accepted?: was-accepted? reason: rejection-reason + state: (state-view current-state) + handoff-executed?: did-handoff? durable?: #f distributed-authority?: #f)) (def (publish host action state accepted? reason) @@ -60,27 +67,29 @@ (def (aitia-retirement-host) (let ((host (.o kind: 'aitia.retirement-ingress-host.v1)) - (state (.o owner-thread: (current-thread) + (creator-thread (current-thread))) + (let (state (.o owner-thread: creator-thread active?: #t queued?: #t inventory-known?: #f reviewed?: #f cancellation-requested?: #f late-activity-found?: #f inventory-gap-found?: #f retired?: #f ingress-open?: #t authority-epoch: 0 cached-grant-epoch: 0 - pending?: #f attempted?: #f))) - (hash-put! retirement-host-states host state) - host)) + pending?: #f attempted?: #f + handoff-in-progress?: #f)) + (hash-put! retirement-host-states host state) + host))) (def (aitia-retirement-host-snapshot host) (state-view (host-state host))) ;;; The named POO handoff is the only effect escape hatch. Its callback is ;;; invoked only after the Host has consumed a current pending request. -(def (aitia-retirement-effect-handoff run) - (unless (procedure? run) (error "retirement handoff requires a procedure")) - (.o kind: 'aitia.retirement-effect-handoff.v1 run: run)) +(def (aitia-retirement-effect-handoff callback) + (unless (procedure? callback) (error "retirement handoff requires a procedure")) + (.o kind: 'aitia.retirement-effect-handoff.v1 run: callback)) (def (aitia-retirement-host-request-cancellation! host) - (let (state (host-state host)) + (let (state (mutation-state host)) (cond ((.ref state 'retired?) (receipt 'request-cancellation state #f 'already-retired #f)) @@ -92,7 +101,7 @@ #t 'recorded))))) (def (aitia-retirement-host-observe-completion! host) - (let (state (host-state host)) + (let (state (mutation-state host)) (cond ((.ref state 'retired?) (receipt 'observe-completion state #f 'already-retired #f)) @@ -103,7 +112,7 @@ (.o (:: @ state) active?: #f) #t 'observed))))) (def (aitia-retirement-host-observe-queue-drained! host) - (let (state (host-state host)) + (let (state (mutation-state host)) (cond ((.ref state 'retired?) (receipt 'observe-queue-drained state #f 'already-retired #f)) @@ -114,7 +123,7 @@ (.o (:: @ state) queued?: #f) #t 'observed))))) (def (aitia-retirement-host-complete-inventory! host) - (let (state (host-state host)) + (let (state (mutation-state host)) (cond ((.ref state 'retired?) (receipt 'complete-inventory state #f 'already-retired #f)) @@ -125,7 +134,7 @@ (.o (:: @ state) inventory-known?: #t) #t 'observed))))) (def (aitia-retirement-host-review-cut! host) - (let (state (host-state host)) + (let (state (mutation-state host)) (cond ((.ref state 'retired?) (receipt 'review-cut state #f 'already-retired #f)) @@ -142,7 +151,7 @@ #t 'reviewed))))) (def (aitia-retirement-host-observe-late-activity! host) - (let (state (host-state host)) + (let (state (mutation-state host)) (cond ((or (.ref state 'retired?) (not (.ref state 'reviewed?)) (.ref state 'late-activity-found?)) @@ -153,7 +162,7 @@ #t 'observed))))) (def (aitia-retirement-host-observe-inventory-gap! host) - (let (state (host-state host)) + (let (state (mutation-state host)) (cond ((or (.ref state 'retired?) (not (.ref state 'reviewed?)) (.ref state 'inventory-gap-found?)) @@ -165,7 +174,7 @@ ;;; Closure and epoch advancement are one synchronous Host state publication. (def (aitia-retirement-host-close-ingress! host) - (let (state (host-state host)) + (let (state (mutation-state host)) (if (.ref state 'ingress-open?) (publish host 'close-ingress (.o (:: @ state) ingress-open?: #f @@ -174,14 +183,14 @@ (receipt 'close-ingress state #f 'already-closed #f)))) (def (aitia-retirement-host-request-start! host) - (let (state (host-state host)) + (let (state (mutation-state host)) (if (or (.ref state 'pending?) (.ref state 'attempted?)) (receipt 'request-start state #f 'request-already-consumed #f) (publish host 'request-start (.o (:: @ state) pending?: #t) #t 'pending)))) (def (aitia-retirement-host-attempt-start! host handoff) - (let (state (host-state host)) + (let (state (mutation-state host)) (unless (and (object? handoff) (.slot? handoff 'kind) (eq? (.ref handoff 'kind) 'aitia.retirement-effect-handoff.v1) @@ -198,13 +207,16 @@ (publish host 'attempt-start consumed #f 'ingress-closed)) (else ;; Consume before handoff so a reentrant call cannot start twice. - (let (started (.o (:: @ consumed) active?: #t)) + (let (started (.o (:: @ consumed) active?: #t + handoff-in-progress?: #t)) (hash-put! retirement-host-states host started) ((.ref handoff 'run)) - (receipt 'attempt-start started #t 'started #t))))))) + (let (finished (.o (:: @ started) handoff-in-progress?: #f)) + (hash-put! retirement-host-states host finished) + (receipt 'attempt-start finished #t 'started #t)))))))) (def (aitia-retirement-host-retire! host) - (let (state (host-state host)) + (let (state (mutation-state host)) (cond ((.ref state 'retired?) (receipt 'retire state #f 'already-retired #f)) diff --git a/t/formal/retirement-execution-test.ss b/t/formal/retirement-execution-test.ss index a79dff9..a8929d3 100644 --- a/t/formal/retirement-execution-test.ss +++ b/t/formal/retirement-execution-test.ss @@ -78,6 +78,24 @@ '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) From b373c365ef5328ae1b6030db426848c50ea52b42 Mon Sep 17 00:00:00 2001 From: guangtao Date: Sat, 10 Oct 2026 11:58:25 +0800 Subject: [PATCH 56/64] Reject reentrant starts with receipt and bound Host claims --- ROADMAP.org | 8 +++++--- modules/sdlc/retirement-host.ss | 5 +++-- proof/quint/README.org | 16 +++++++++++----- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/ROADMAP.org b/ROADMAP.org index 5b4bea4..fd61e92 100644 --- a/ROADMAP.org +++ b/ROADMAP.org @@ -527,10 +527,12 @@ the existing assurance lifecycle; [[file:docs/receipts/2026-09-24-retirement-for 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. Next gates are independent replay and semantic -correspondence between the Scheme contract and formal views (not just hashes). +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; live authority/Runtime +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 diff --git a/modules/sdlc/retirement-host.ss b/modules/sdlc/retirement-host.ss index 29f3e8a..070a037 100644 --- a/modules/sdlc/retirement-host.ss +++ b/modules/sdlc/retirement-host.ss @@ -183,8 +183,9 @@ (receipt 'close-ingress state #f 'already-closed #f)))) (def (aitia-retirement-host-request-start! host) - (let (state (mutation-state host)) - (if (or (.ref state 'pending?) (.ref state 'attempted?)) + (let (state (host-state host)) + (if (or (.ref state 'handoff-in-progress?) + (.ref state 'pending?) (.ref state 'attempted?)) (receipt 'request-start state #f 'request-already-consumed #f) (publish host 'request-start (.o (:: @ state) pending?: #t) #t 'pending)))) diff --git a/proof/quint/README.org b/proof/quint/README.org index 33310a8..16d0cfa 100644 --- a/proof/quint/README.org +++ b/proof/quint/README.org @@ -46,14 +46,20 @@ is introduced here: ASCENT belongs to the parent POO Flow dependency graph. * Claim boundary These are bounded design experiments, not generated projections of the POO -Contract, proof of Scheme/Runtime refinement, a live Host handoff, or deployed -authority. The lifecycle bounds are two source revisions, four clock values +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 retirement binding test hashes this source cut together with the Lean -view, but a current mapping is not semantic correspondence or implementation -conformance. Those obligations remain open. +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. From cc243a7c5a5ff033a92345f5d55a89fbe3b76c47 Mon Sep 17 00:00:00 2001 From: guangtao Date: Sat, 10 Oct 2026 12:19:55 +0800 Subject: [PATCH 57/64] Qualify Aitia against latest parent Orgize pin --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1466226..315ea29 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,7 +126,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: repository: tao3k/poo-flow - ref: 120817135c7153465caf19825813c201e1ba2f5e + ref: d8e65865aa8dfbc396771ee08f7f9341ff8cb5b0 path: poo-flow submodules: false From f4a11c11ca7c6de4c32773ac395c98ea26e688d0 Mon Sep 17 00:00:00 2001 From: guangtao Date: Sun, 11 Oct 2026 08:17:43 +0800 Subject: [PATCH 58/64] ci: qualify against current POO Flow PR head --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 315ea29..24536ff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,7 +126,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: repository: tao3k/poo-flow - ref: d8e65865aa8dfbc396771ee08f7f9341ff8cb5b0 + ref: 3a6330f9135b7e5d7a96f9c2de7b5fb90e9ad399 path: poo-flow submodules: false From c7b69e595a2a5882173694048ae3fc6c501ac786 Mon Sep 17 00:00:00 2001 From: guangtao Date: Sun, 11 Oct 2026 08:30:01 +0800 Subject: [PATCH 59/64] fix(native): follow Orgize owned C module rename --- bindings/c/aitia-native.ss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/c/aitia-native.ss b/bindings/c/aitia-native.ss index 9f5e560..0afb03b 100644 --- a/bindings/c/aitia-native.ss +++ b/bindings/c/aitia-native.ss @@ -9,7 +9,7 @@ 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-native + :orgize/bindings/c/orgize :poo-flow/lambda-aitia/bindings/c/aitia-contract) (export aitia-c-round-trip) From 60e78e6328f2193a8c565efa604dfda93002c47c Mon Sep 17 00:00:00 2001 From: guangtao Date: Sun, 11 Oct 2026 08:37:27 +0800 Subject: [PATCH 60/64] fix(native): pass Orgize public include directory to Gambit --- scripts/tests/test_build_native_library.py | 19 +++++++++++++++++++ tools/build-native-library.py | 4 ++-- 2 files changed, 21 insertions(+), 2 deletions(-) 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/tools/build-native-library.py b/tools/build-native-library.py index 43cc5b4..874c905 100644 --- a/tools/build-native-library.py +++ b/tools/build-native-library.py @@ -128,9 +128,9 @@ def configure_orgize_header() -> None: raise RuntimeError("GERBIL_PATH must identify the installed Gerbil packages") header_root = ( Path(gerbil_path).resolve() - / "pkg/github.com/tao3k/orgize/bindings/c" + / "pkg/github.com/tao3k/orgize/bindings/c/include" ) - if not (header_root / "include/orgize.h").is_file(): + 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}" From 8bc2ce9bf049ecd9ff93ea6aeb385a6cba72102b Mon Sep 17 00:00:00 2001 From: guangtao Date: Sun, 11 Oct 2026 15:56:57 +0800 Subject: [PATCH 61/64] test(formal): replay Quint ingress traces through Scheme Host --- .github/workflows/ci.yml | 11 ++ justfile | 7 + modules/sdlc/retirement-host.ss | 14 +- proof/quint/README.org | 36 ++++- proof/quint/check_scheme_replay.py | 128 ++++++++++++++++++ scripts/tests/test_quint_replay.py | 74 ++++++++++ t/formal/retirement-execution-test.ss | 43 ++++++ t/formal/retirement-replay-test.ss | 85 ++++++++++++ .../scenarios/job-retirement-contract.ss | 4 + 9 files changed, 397 insertions(+), 5 deletions(-) create mode 100644 proof/quint/check_scheme_replay.py create mode 100644 scripts/tests/test_quint_replay.py create mode 100644 t/formal/retirement-replay-test.ss diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24536ff..ac851d0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -189,6 +189,17 @@ jobs: 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: 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/justfile b/justfile index cf46044..8b9c8f0 100644 --- a/justfile +++ b/justfile @@ -72,10 +72,17 @@ check-proof: 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 + # 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-quint just check-proof diff --git a/modules/sdlc/retirement-host.ss b/modules/sdlc/retirement-host.ss index 070a037..b65a486 100644 --- a/modules/sdlc/retirement-host.ss +++ b/modules/sdlc/retirement-host.ss @@ -211,10 +211,16 @@ (let (started (.o (:: @ consumed) active?: #t handoff-in-progress?: #t)) (hash-put! retirement-host-states host started) - ((.ref handoff 'run)) - (let (finished (.o (:: @ started) handoff-in-progress?: #f)) - (hash-put! retirement-host-states host finished) - (receipt 'attempt-start finished #t 'started #t)))))))) + ;; A failed callback is not completion. Keep work active and the + ;; request consumed, but release the transient reentrancy fence even + ;; when the callback raises. The original failure still propagates. + (dynamic-wind + void + (lambda () ((.ref handoff 'run))) + (lambda () + (hash-put! retirement-host-states host + (.o (:: @ started) handoff-in-progress?: #f)))) + (receipt 'attempt-start (host-state host) #t 'started #t))))))) (def (aitia-retirement-host-retire! host) (let (state (mutation-state host)) diff --git a/proof/quint/README.org b/proof/quint/README.org index 16d0cfa..00fd319 100644 --- a/proof/quint/README.org +++ b/proof/quint/README.org @@ -5,7 +5,7 @@ :PROPERTIES: :STATUS: design-validation-partial -:DATE: 2026-10-10 +:DATE: 2026-10-11 :END: * Model boundaries @@ -45,6 +45,40 @@ 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. + +| 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 | =evidence-admission-test.ss= and =decision-preflight-test.ss= cover source changes, expiry, revocation and preflight | no state-by-state lifecycle replay; authorization and effect delivery remain a POO Flow/runtime boundary | + +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 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/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/formal/retirement-execution-test.ss b/t/formal/retirement-execution-test.ss index a8929d3..72710a5 100644 --- a/t/formal/retirement-execution-test.ss +++ b/t/formal/retirement-execution-test.ss @@ -34,6 +34,49 @@ (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) 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/user-interface/scenarios/job-retirement-contract.ss b/user-interface/scenarios/job-retirement-contract.ss index b4895f1..ded0719 100644 --- a/user-interface/scenarios/job-retirement-contract.ss +++ b/user-interface/scenarios/job-retirement-contract.ss @@ -41,6 +41,7 @@ "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" @@ -48,4 +49,7 @@ (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")) From 1927f085f77e07c924ee398a048bdeb9755ab708 Mon Sep 17 00:00:00 2001 From: guangtao Date: Sun, 11 Oct 2026 16:42:00 +0800 Subject: [PATCH 62/64] Verify bounded lifecycle authorization and delivery through Scheme --- .github/workflows/ci.yml | 12 +- justfile | 7 + modules/assurance/effect-host.ss | 139 +++++++++++ modules/assurance/host.ss | 93 ++++--- modules/assurance/interface.ss | 2 + proof/quint/AssuranceLifecycle.qnt | 52 +++- proof/quint/README.org | 64 ++++- proof/quint/check_contract.py | 7 +- proof/quint/check_lifecycle_replay.py | 253 ++++++++++++++++++++ proof/quint/check_progress.py | 8 +- proof/quint/test_lifecycle_replay.py | 63 +++++ t/assurance/unit/lifecycle-boundary-test.ss | 171 +++++++++++++ t/formal/lifecycle-fixture.ss | 186 ++++++++++++++ t/formal/lifecycle-replay-test.ss | 41 ++++ 14 files changed, 1044 insertions(+), 54 deletions(-) create mode 100644 modules/assurance/effect-host.ss create mode 100644 proof/quint/check_lifecycle_replay.py create mode 100644 proof/quint/test_lifecycle_replay.py create mode 100644 t/assurance/unit/lifecycle-boundary-test.ss create mode 100644 t/formal/lifecycle-fixture.ss create mode 100644 t/formal/lifecycle-replay-test.ss diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ac851d0..006a07a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -104,7 +104,7 @@ jobs: distribution: temurin java-version: '21' - - name: Install locked Quint and check all 22 model contracts + - 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 @@ -200,6 +200,16 @@ jobs: 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/justfile b/justfile index 8b9c8f0..c56e4dc 100644 --- a/justfile +++ b/justfile @@ -77,12 +77,19 @@ check-quint module='all': 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 diff --git a/modules/assurance/effect-host.ss b/modules/assurance/effect-host.ss new file mode 100644 index 0000000..7045994 --- /dev/null +++ b/modules/assurance/effect-host.ss @@ -0,0 +1,139 @@ +;;; -*- Gerbil -*- +;;; SPDX-FileCopyrightText: 2026 tao3k team and Contributors +;;; SPDX-License-Identifier: Apache-2.0 AND LGPL-2.1-or-later + +;;; Local serialized integration boundary, not a Cedar evaluator or distributed +;;; Runtime Host. Only configured runtime operations evaluate policy / deliver. +;;; Editable preflights, requests, tokens and reports never issue capabilities. +(import (only-in :clan/poo/object .o .ref) + :poo-flow/lambda-aitia/modules/assurance/host + :poo-flow/src/policy/authorized-effect-token + (only-in :poo-flow/modules/authorization/providers/cedar/decision + poo-flow-cedar-decision-permit? + poo-flow-cedar-decision->semantic-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)) + (.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) + (instant state) (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/host.ss b/modules/assurance/host.ss index 8c76e20..0921611 100644 --- a/modules/assurance/host.ss +++ b/modules/assurance/host.ss @@ -27,7 +27,8 @@ AssuranceRequiredSupport AssessmentRequiredSupport AssuranceDecisionPreflight) (only-in :poo-flow/lambda-aitia/modules/assurance/funs - assurance-snapshot-canonical? assurance-node-canonical) + assurance-snapshot-canonical? assurance-node-canonical + assurance-canonical-digest) (only-in :poo-flow/lambda-aitia/modules/assurance/evidence-admission assurance-evaluate-evidence-admission assurance-evidence-outcome-bound? @@ -39,7 +40,8 @@ (export assurance-verification-host assurance-host-verify assurance-host-sealed-support? assurance-host-revoke! - assurance-host-admit assurance-host-admission-current? + assurance-host-admit assurance-host-admit-verified + assurance-host-verification-current? assurance-host-admission-current? assurance-host-source-current? assurance-host-admitted-support? assurance-host-required-support @@ -192,39 +194,54 @@ (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)) + (let (receipt (assurance-host-verify host snapshot obligation evidence outcome)) (and receipt - (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))))))) + (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)) @@ -637,9 +654,11 @@ ;;; 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) - (list (.ref request 'principal) (.ref request 'action) - (.ref request 'resource) (.alist (.ref request 'context)) - (.ref request 'intent) (.alist (.ref request 'handoff)))) + ;; 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) diff --git a/modules/assurance/interface.ss b/modules/assurance/interface.ss index 2b781c3..286ed2d 100644 --- a/modules/assurance/interface.ss +++ b/modules/assurance/interface.ss @@ -13,5 +13,7 @@ (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/proof/quint/AssuranceLifecycle.qnt b/proof/quint/AssuranceLifecycle.qnt index f4ea97b..a70a685 100644 --- a/proof/quint/AssuranceLifecycle.qnt +++ b/proof/quint/AssuranceLifecycle.qnt @@ -7,12 +7,16 @@ module AssuranceLifecycle { 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} + 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} + 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}} @@ -30,8 +34,23 @@ module AssuranceLifecycle { 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}} @@ -45,8 +64,12 @@ module AssuranceLifecycle { 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} - action step = any {stableStep, ChangeSource, Tick, Revoke} + 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), @@ -64,13 +87,26 @@ module AssuranceLifecycle { 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 + 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 @@ -80,11 +116,15 @@ module AssuranceLifecycle { 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) + 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/README.org b/proof/quint/README.org index 00fd319..912c060 100644 --- a/proof/quint/README.org +++ b/proof/quint/README.org @@ -12,21 +12,22 @@ 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 preserve the former TLA+ lane's 22 cases: +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 and safe effect start | untrusted report, late admission, early grant, after-revoke start, valid start reachable, no-fair progress counterexample | +| 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 -=EventuallyEffect= under the Quint-defined =stableSpec=. The same property +=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=. @@ -65,12 +66,50 @@ 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 7,383 spanning +paths through real Scheme APIs. Their shared prefixes cover 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. 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. + +| 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. + | 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 | =evidence-admission-test.ss= and =decision-preflight-test.ss= cover source changes, expiry, revocation and preflight | no state-by-state lifecycle replay; authorization and effect delivery remain a POO Flow/runtime boundary | +| 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 @@ -92,6 +131,23 @@ 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 +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. 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 diff --git a/proof/quint/check_contract.py b/proof/quint/check_contract.py index ebbf8fc..50c9c29 100644 --- a/proof/quint/check_contract.py +++ b/proof/quint/check_contract.py @@ -20,7 +20,7 @@ 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": 2202} + "lifecycle-safe": 19042} # lane, name, model, transition, invariant(s), expected counterexample CASES = ( ("work", "work-safe", "WorkLifecycle", "step", "TypeOK", False), @@ -37,12 +37,15 @@ ("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", False), + ("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), ) diff --git a/proof/quint/check_lifecycle_replay.py b/proof/quint/check_lifecycle_replay.py new file mode 100644 index 0000000..0fac4b1 --- /dev/null +++ b/proof/quint/check_lifecycle_replay.py @@ -0,0 +1,253 @@ +# 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", + "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 spanning_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") + # All spanning-tree states occur in its leaf paths. Share prefixes within + # a path instead of restarting a Host for every intermediate state. + branches = {origin for origin, targets in adjacency.items() + if any(paths[target] == paths[origin] + [(action, nodes[target])] + for target, action in targets if target != origin)} + traces = [paths[node] for node in nodes if node not in branches] + covered = {action for trace in traces for action, _ in trace} + for action in ACTIONS: + if action not in covered: + origin, target, _ = next(edge for edge in edges if edge[2] == action) + 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("spanning paths do not cover the complete state space") + return traces + + +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") + environment = dict(os.environ, AITIA_LIFECYCLE_REPLAY=str(fixture)) + result = subprocess.run(["just", "test-scheme-atomic", "formal", "lifecycle-replay-test.ss"], + cwd=REPO, env=environment, timeout=65, check=False, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + if result.returncode or not re.search(r"^OK$", result.stdout, re.MULTILINE): + raise RuntimeError(f"Scheme replay failed: chunk={chunk} exit={result.returncode}\n{result.stdout[-8000:]}") + 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 = spanning_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, traces[offset:offset + 32]) + for chunk, offset in enumerate(range(0, len(traces), 32))] + 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 index 782ed76..8517394 100644 --- a/proof/quint/check_progress.py +++ b/proof/quint/check_progress.py @@ -37,7 +37,7 @@ def main() -> int: compiled_status, compiled_output = run( (QUINT, "compile", str(ROOT / "AssuranceLifecycle.qnt"), "--target", "tlaplus", "--step", "stableStep", - "--temporal", "EventuallyEffect"), + "--temporal", "EventuallyDelivery"), ROOT, "quint-compile", ) module_start = compiled_output.find("-------------------------- MODULE AssuranceLifecycle") @@ -49,11 +49,11 @@ def main() -> int: (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 EventuallyEffect was violated."), + ("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 EventuallyEffect\n") + config.write_text(f"SPECIFICATION {spec}\nPROPERTY EventuallyDelivery\n") meta = work / f"meta-{name}" meta.mkdir() status, output = run( @@ -65,7 +65,7 @@ def main() -> int: 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]) != 18 or int(counts[3]) != 0: + 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}") diff --git a/proof/quint/test_lifecycle_replay.py b/proof/quint/test_lifecycle_replay.py new file mode 100644 index 0000000..34d53ea --- /dev/null +++ b/proof/quint/test_lifecycle_replay.py @@ -0,0 +1,63 @@ +# 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.spanning_traces({'1': {'clock': 0}, '2': {'clock': 1}}, + [('1', '1', 'Terminal')], '1') + + def test_spanning_paths_cover_all_states_and_self_loop_action(self): + with patch.object(checker, 'ACTIONS', ('Verify', 'Terminal')): + traces = checker.spanning_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'}) + + +if __name__ == '__main__': + unittest.main() diff --git a/t/assurance/unit/lifecycle-boundary-test.ss b/t/assurance/unit/lifecycle-boundary-test.ss new file mode 100644 index 0000000..4e2ab64 --- /dev/null +++ b/t/assurance/unit/lifecycle-boundary-test.ss @@ -0,0 +1,171 @@ +;;; -*- 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 "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 "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 "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/formal/lifecycle-fixture.ss b/t/formal/lifecycle-fixture.ss new file mode 100644 index 0000000..95ecc7a --- /dev/null +++ b/t/formal/lifecycle-fixture.ss @@ -0,0 +1,186 @@ +;;; -*- 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)) + (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 () (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..9e81309 --- /dev/null +++ b/t/formal/lifecycle-replay-test.ss @@ -0,0 +1,41 @@ +;;; -*- 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))) traces)))))) From 2f37655be2c42158f6d77984868777a7ca554a77 Mon Sep 17 00:00:00 2001 From: guangtao Date: Sun, 11 Oct 2026 17:19:24 +0800 Subject: [PATCH 63/64] Bound lifecycle replay by state work and preserve complete coverage --- proof/quint/README.org | 25 ++++- proof/quint/check_lifecycle_replay.py | 105 ++++++++++++++++---- proof/quint/process_runner.py | 3 +- proof/quint/test_lifecycle_replay.py | 22 +++- t/assurance/unit/lifecycle-boundary-test.ss | 9 ++ t/formal/lifecycle-replay-test.ss | 4 +- 6 files changed, 141 insertions(+), 27 deletions(-) diff --git a/proof/quint/README.org b/proof/quint/README.org index 912c060..48cb8df 100644 --- a/proof/quint/README.org +++ b/proof/quint/README.org @@ -67,13 +67,17 @@ 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 7,383 spanning -paths through real Scheme APIs. Their shared prefixes cover every reachable +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. Four native processes retain the existing per-test -1G heap / 60-second bound; failed batches, missing final =OK=, incomplete +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. @@ -84,6 +88,10 @@ 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 | |-----------------+-------------------------+--------------------------------| @@ -103,6 +111,8 @@ 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 | |-----------+-----------------+--------------------| @@ -137,6 +147,13 @@ 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. diff --git a/proof/quint/check_lifecycle_replay.py b/proof/quint/check_lifecycle_replay.py index 0fac4b1..94f89a4 100644 --- a/proof/quint/check_lifecycle_replay.py +++ b/proof/quint/check_lifecycle_replay.py @@ -36,6 +36,7 @@ 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") @@ -104,7 +105,7 @@ def read_graph(path, expected): return nodes, edges, initial[0] -def spanning_traces(nodes, edges, initial): +def covering_traces(nodes, edges, initial): adjacency = {} for origin, target, action in edges: adjacency.setdefault(origin, []).append((target, action)) @@ -118,23 +119,83 @@ def spanning_traces(nodes, edges, initial): pending.append(target) if set(paths) != set(nodes): raise ValueError("TLC dump contains unreachable or missing nodes") - # All spanning-tree states occur in its leaf paths. Share prefixes within - # a path instead of restarting a Host for every intermediate state. - branches = {origin for origin, targets in adjacency.items() - if any(paths[target] == paths[origin] + [(action, nodes[target])] - for target, action in targets if target != origin)} - traces = [paths[node] for node in nodes if node not in branches] + # 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: + 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("spanning paths do not cover the complete state space") + 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})" @@ -203,12 +264,20 @@ def negative_traces(quint, work, safe): def replay(work, chunk, traces): fixture = work / f"traces-{chunk}.scm" fixture.write_text("(" + "\n".join(map(sexpr_trace, traces)) + ")\n") - environment = dict(os.environ, AITIA_LIFECYCLE_REPLAY=str(fixture)) - result = subprocess.run(["just", "test-scheme-atomic", "formal", "lifecycle-replay-test.ss"], - cwd=REPO, env=environment, timeout=65, check=False, - stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) - if result.returncode or not re.search(r"^OK$", result.stdout, re.MULTILINE): - raise RuntimeError(f"Scheme replay failed: chunk={chunk} exit={result.returncode}\n{result.stdout[-8000:]}") + 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) @@ -223,7 +292,7 @@ def main(): if count != 19042: raise RuntimeError(f"lifecycle state quotient changed: {count} != 19042") nodes, edges, initial = read_graph(graph, count) - traces = spanning_traces(nodes, edges, initial) + 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)}") @@ -233,8 +302,8 @@ def main(): 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, traces[offset:offset + 32]) - for chunk, offset in enumerate(range(0, len(traces), 32))] + jobs = [pool.submit(replay, work, chunk, batch) + for chunk, batch in enumerate(batches(traces))] try: for job in as_completed(jobs): completed += job.result() diff --git a/proof/quint/process_runner.py b/proof/quint/process_runner.py index ae10096..f9fda54 100644 --- a/proof/quint/process_runner.py +++ b/proof/quint/process_runner.py @@ -44,7 +44,8 @@ def run(command: Sequence[str], cwd: Path, label: str, *, timeout: float = 60, except subprocess.TimeoutExpired: os.killpg(process.pid, signal.SIGKILL) process.wait() - raise TimeoutError(f"{label}: {reason} after {now - last_output:.1f}s without output") + 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) diff --git a/proof/quint/test_lifecycle_replay.py b/proof/quint/test_lifecycle_replay.py index 34d53ea..49e2ccd 100644 --- a/proof/quint/test_lifecycle_replay.py +++ b/proof/quint/test_lifecycle_replay.py @@ -48,16 +48,32 @@ def test_incomplete_tlc_count_fails(self): def test_unreachable_state_fails(self): with patch.object(checker, 'ACTIONS', ('Terminal',)): with self.assertRaisesRegex(ValueError, 'unreachable'): - checker.spanning_traces({'1': {'clock': 0}, '2': {'clock': 1}}, + checker.covering_traces({'1': {'clock': 0}, '2': {'clock': 1}}, [('1', '1', 'Terminal')], '1') - def test_spanning_paths_cover_all_states_and_self_loop_action(self): + def test_covering_paths_cover_all_states_and_self_loop_action(self): with patch.object(checker, 'ACTIONS', ('Verify', 'Terminal')): - traces = checker.spanning_traces({'1': {'clock': 0}, '2': {'clock': 1}}, + 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/t/assurance/unit/lifecycle-boundary-test.ss b/t/assurance/unit/lifecycle-boundary-test.ss index 4e2ab64..83e492b 100644 --- a/t/assurance/unit/lifecycle-boundary-test.ss +++ b/t/assurance/unit/lifecycle-boundary-test.ss @@ -55,6 +55,15 @@ (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) diff --git a/t/formal/lifecycle-replay-test.ss b/t/formal/lifecycle-replay-test.ss index 9e81309..840545b 100644 --- a/t/formal/lifecycle-replay-test.ss +++ b/t/formal/lifecycle-replay-test.ss @@ -38,4 +38,6 @@ (when (eq? action 'StartEffect) (check (lifecycle-get fixture 'delivery-calls) => 0) (check (lifecycle-get fixture 'service-effects) => 0)))) - trace))) traces)))))) + trace) + (displayln "LIFECYCLE-TRACE-COMPLETE steps=" (length trace)) + (force-output))) traces)))))) From 17d93bdc738ff7b76cdf8d99924f02d7e9e0fe06 Mon Sep 17 00:00:00 2001 From: guangtao Date: Sun, 11 Oct 2026 19:31:53 +0800 Subject: [PATCH 64/64] Enforce strict expiry at final effect grant validation instant --- modules/assurance/effect-host.ss | 17 +++++++---- proof/quint/README.org | 8 +++++- t/assurance/unit/lifecycle-boundary-test.ss | 32 +++++++++++++++++++++ t/formal/lifecycle-fixture.ss | 7 +++-- 4 files changed, 55 insertions(+), 9 deletions(-) diff --git a/modules/assurance/effect-host.ss b/modules/assurance/effect-host.ss index 7045994..d8a51b6 100644 --- a/modules/assurance/effect-host.ss +++ b/modules/assurance/effect-host.ss @@ -66,12 +66,17 @@ (= (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)) - (.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) - (instant state) (vector-ref state 5) '())) 'accepted?))) + ;; 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))) diff --git a/proof/quint/README.org b/proof/quint/README.org index 48cb8df..df04332 100644 --- a/proof/quint/README.org +++ b/proof/quint/README.org @@ -161,7 +161,13 @@ 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. Native dual-engine Cedar evaluation, distributed +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. diff --git a/t/assurance/unit/lifecycle-boundary-test.ss b/t/assurance/unit/lifecycle-boundary-test.ss index 83e492b..3f99e2d 100644 --- a/t/assurance/unit/lifecycle-boundary-test.ss +++ b/t/assurance/unit/lifecycle-boundary-test.ss @@ -78,6 +78,20 @@ (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))) @@ -161,6 +175,24 @@ (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) diff --git a/t/formal/lifecycle-fixture.ss b/t/formal/lifecycle-fixture.ss index 95ecc7a..a577d19 100644 --- a/t/formal/lifecycle-fixture.ss +++ b/t/formal/lifecycle-fixture.ss @@ -57,7 +57,7 @@ (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)) + 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)) @@ -86,7 +86,10 @@ (unless (eq? (lifecycle-get fixture 'delivery-mode) 'failed) (increment! fixture 'service-effects)) (lifecycle-get fixture 'delivery-mode)) - (lambda () (lifecycle-get fixture 'clock)) 1)) + (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)