platformforge 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- platformforge/__init__.py +7 -0
- platformforge/__main__.py +5 -0
- platformforge/_forge_portal.py +913 -0
- platformforge/_graphstudio.py +376 -0
- platformforge/_graphview.py +181 -0
- platformforge/_installkit.py +1706 -0
- platformforge/agents/__init__.py +156 -0
- platformforge/agents/bench.py +61 -0
- platformforge/agents/contextpack.py +137 -0
- platformforge/agents/contracts.py +394 -0
- platformforge/agents/coordinators.py +98 -0
- platformforge/agents/critic.py +113 -0
- platformforge/agents/debate.py +161 -0
- platformforge/agents/executors.py +160 -0
- platformforge/agents/guardian.py +48 -0
- platformforge/agents/mirrors.py +240 -0
- platformforge/agents/orchestrator.py +295 -0
- platformforge/agents/planner.py +116 -0
- platformforge/agents/playbook.py +51 -0
- platformforge/agents/referee.py +132 -0
- platformforge/agents/reviewers.py +113 -0
- platformforge/agents/roster.py +893 -0
- platformforge/agents/runledger.py +85 -0
- platformforge/agents/runstore.py +74 -0
- platformforge/agents/specialists.py +73 -0
- platformforge/agents/taskspec.py +94 -0
- platformforge/agents/uniqueness.py +115 -0
- platformforge/agents/verifier.py +106 -0
- platformforge/aiplat/__init__.py +4 -0
- platformforge/aiplat/models.py +175 -0
- platformforge/analytics/__init__.py +6 -0
- platformforge/analytics/capacity.py +109 -0
- platformforge/analytics/debt.py +92 -0
- platformforge/analytics/dx.py +69 -0
- platformforge/analytics/finops_v4.py +171 -0
- platformforge/analytics/goldenpath.py +131 -0
- platformforge/analytics/history.py +233 -0
- platformforge/analytics/measurement.py +149 -0
- platformforge/analytics/models.py +127 -0
- platformforge/analytics/opsanalytics.py +119 -0
- platformforge/analytics/policyintel.py +139 -0
- platformforge/analytics/reliability.py +106 -0
- platformforge/analytics/soak.py +135 -0
- platformforge/analytics/store.py +254 -0
- platformforge/bench.py +174 -0
- platformforge/cases/__init__.py +13 -0
- platformforge/cases/casefile.py +106 -0
- platformforge/cases/context_audit.py +134 -0
- platformforge/cases/ledgers.py +174 -0
- platformforge/cases/replay.py +142 -0
- platformforge/cases/routing_audit.py +162 -0
- platformforge/caveman/__init__.py +11 -0
- platformforge/caveman/compress.py +156 -0
- platformforge/cicd/__init__.py +16 -0
- platformforge/cicd/backstage.py +51 -0
- platformforge/cicd/dockerfile.py +64 -0
- platformforge/cicd/github_actions.py +122 -0
- platformforge/cicd/gitlab_ci.py +56 -0
- platformforge/cicd/gitops.py +130 -0
- platformforge/cli/__init__.py +0 -0
- platformforge/cli/fleetcmd.py +457 -0
- platformforge/cli/main.py +3832 -0
- platformforge/cloud/__init__.py +8 -0
- platformforge/cloud/aws.py +254 -0
- platformforge/cloud/common.py +69 -0
- platformforge/cloud/multicloud.py +143 -0
- platformforge/collect/__init__.py +11 -0
- platformforge/collect/detect.py +254 -0
- platformforge/context/__init__.py +10 -0
- platformforge/context/capsule.py +60 -0
- platformforge/context/gateway.py +227 -0
- platformforge/context/refs.py +49 -0
- platformforge/context/sufficiency.py +50 -0
- platformforge/core/__init__.py +1 -0
- platformforge/core/hashing.py +39 -0
- platformforge/core/ownership.py +220 -0
- platformforge/core/receipts.py +74 -0
- platformforge/core/redaction.py +97 -0
- platformforge/core/store.py +119 -0
- platformforge/core/workspace.py +89 -0
- platformforge/data/contracts/agent-handoff.schema.json +26 -0
- platformforge/data/contracts/agent-run.schema.json +27 -0
- platformforge/data/contracts/agent-spec.schema.json +43 -0
- platformforge/data/contracts/capability.schema.json +23 -0
- platformforge/data/contracts/change-event.schema.json +22 -0
- platformforge/data/contracts/change-intent.schema.json +62 -0
- platformforge/data/contracts/change-plan.schema.json +43 -0
- platformforge/data/contracts/collector-receipt.schema.json +36 -0
- platformforge/data/contracts/context-pack.schema.json +29 -0
- platformforge/data/contracts/debate.schema.json +31 -0
- platformforge/data/contracts/distribution-manifest-v1.schema.json +81 -0
- platformforge/data/contracts/drift-event.schema.json +21 -0
- platformforge/data/contracts/fact.schema.json +35 -0
- platformforge/data/contracts/finding.schema.json +20 -0
- platformforge/data/contracts/graph-v2.schema.json +67 -0
- platformforge/data/contracts/graph.schema.json +40 -0
- platformforge/data/contracts/install-receipt-v1.schema.json +49 -0
- platformforge/data/contracts/observation.schema.json +93 -0
- platformforge/data/contracts/receipt.schema.json +21 -0
- platformforge/data/contracts/recommendation.schema.json +32 -0
- platformforge/data/contracts/refusal.schema.json +16 -0
- platformforge/data/contracts/slo-contract.schema.json +25 -0
- platformforge/data/contracts/task-spec.schema.json +37 -0
- platformforge/data/contracts/workspace-manifest-v1.schema.json +62 -0
- platformforge/data/evals/cases/agent-budget-exhaustion/case.yaml +6 -0
- platformforge/data/evals/cases/agent-conflicting-specialists/case.yaml +11 -0
- platformforge/data/evals/cases/agent-fleet-optimization/case.yaml +13 -0
- platformforge/data/evals/cases/agent-host-no-subagents/case.yaml +6 -0
- platformforge/data/evals/cases/agent-incident-multi/case.yaml +13 -0
- platformforge/data/evals/cases/agent-missing-evidence/case.yaml +6 -0
- platformforge/data/evals/cases/agent-multi-domain-audit/case.yaml +14 -0
- platformforge/data/evals/cases/agent-production-change/case.yaml +15 -0
- platformforge/data/evals/cases/agent-security-mandatory/case.yaml +12 -0
- platformforge/data/evals/cases/agent-simple-iac/case.yaml +14 -0
- platformforge/data/evals/cases/approval-integrity-seal/case.yaml +6 -0
- platformforge/data/evals/cases/capacity-hot/case.yaml +7 -0
- platformforge/data/evals/cases/capacity-hot/fixture/capacity.json +5 -0
- platformforge/data/evals/cases/catalog-noowner/case.yaml +7 -0
- platformforge/data/evals/cases/catalog-noowner/fixture/catalog-info.yaml +6 -0
- platformforge/data/evals/cases/contract-fact/case.yaml +5 -0
- platformforge/data/evals/cases/contract-fact/fixture/doc.json +2 -0
- platformforge/data/evals/cases/cosign-unverified/case.yaml +8 -0
- platformforge/data/evals/cases/cosign-unverified/fixture/sig.json +33 -0
- platformforge/data/evals/cases/expected-delta-refusal/case.yaml +6 -0
- platformforge/data/evals/cases/finops-noattr/case.yaml +9 -0
- platformforge/data/evals/cases/finops-noattr/fixture/costs.json +7 -0
- platformforge/data/evals/cases/fleet-inv-absent-allocation-unknown/case.yaml +7 -0
- platformforge/data/evals/cases/fleet-inv-centrality-not-criticality/case.yaml +7 -0
- platformforge/data/evals/cases/fleet-inv-correlation-not-causality/case.yaml +7 -0
- platformforge/data/evals/cases/fleet-inv-dx-no-person-metrics/case.yaml +7 -0
- platformforge/data/evals/cases/fleet-inv-federation-restricted-denied/case.yaml +7 -0
- platformforge/data/evals/cases/fleet-inv-federation-secret-denied/case.yaml +7 -0
- platformforge/data/evals/cases/fleet-inv-history-window-deterministic/case.yaml +7 -0
- platformforge/data/evals/cases/fleet-inv-low-util-not-deletable/case.yaml +7 -0
- platformforge/data/evals/cases/fleet-inv-member-key-deterministic/case.yaml +7 -0
- platformforge/data/evals/cases/fleet-inv-missing-denominator/case.yaml +7 -0
- platformforge/data/evals/cases/fleet-inv-partial-coverage-visible/case.yaml +7 -0
- platformforge/data/evals/cases/fleet-inv-plan-changeintent-only/case.yaml +7 -0
- platformforge/data/evals/cases/fleet-inv-report-all-cited/case.yaml +7 -0
- platformforge/data/evals/cases/fleet-inv-small-sample-confidence/case.yaml +7 -0
- platformforge/data/evals/cases/fleet-inv-uncertain-optimization-suppressed/case.yaml +7 -0
- platformforge/data/evals/cases/gha-dangerous/case.yaml +12 -0
- platformforge/data/evals/cases/gha-dangerous/fixture/.github/workflows/ci.yaml +11 -0
- platformforge/data/evals/cases/gitops-v2/case.yaml +11 -0
- platformforge/data/evals/cases/gitops-v2/fixture/argo.yaml +41 -0
- platformforge/data/evals/cases/graph-k8s-blast/case.yaml +7 -0
- platformforge/data/evals/cases/graph-k8s-blast/fixture/deploy.yaml +15 -0
- platformforge/data/evals/cases/iac-module-nopin/case.yaml +8 -0
- platformforge/data/evals/cases/iac-module-nopin/fixture/main.tf +3 -0
- platformforge/data/evals/cases/iac-plan-replace/case.yaml +8 -0
- platformforge/data/evals/cases/iac-plan-replace/fixture/plan.json +12 -0
- platformforge/data/evals/cases/iac-sg-open/case.yaml +8 -0
- platformforge/data/evals/cases/iac-sg-open/fixture/main.tf +9 -0
- platformforge/data/evals/cases/iam-oidc/case.yaml +8 -0
- platformforge/data/evals/cases/iam-oidc/fixture/policy.json +11 -0
- platformforge/data/evals/cases/iam-wildcard-positive/case.yaml +8 -0
- platformforge/data/evals/cases/iam-wildcard-positive/fixture/policy.json +3 -0
- platformforge/data/evals/cases/k8s-bad-actor/case.yaml +19 -0
- platformforge/data/evals/cases/k8s-bad-actor/fixture/app.yaml +97 -0
- platformforge/data/evals/cases/k8s-beta-api/case.yaml +8 -0
- platformforge/data/evals/cases/k8s-beta-api/fixture/cr.yaml +7 -0
- platformforge/data/evals/cases/k8s-dep-api-boundary/case.yaml +15 -0
- platformforge/data/evals/cases/k8s-dep-api-boundary/fixture/app.yaml +25 -0
- platformforge/data/evals/cases/k8s-dep-api-clean/case.yaml +12 -0
- platformforge/data/evals/cases/k8s-dep-api-clean/fixture/app.yaml +29 -0
- platformforge/data/evals/cases/k8s-dep-api-unknown/case.yaml +11 -0
- platformforge/data/evals/cases/k8s-dep-api-unknown/fixture/app.yaml +25 -0
- platformforge/data/evals/cases/k8s-dep-api-v120/case.yaml +12 -0
- platformforge/data/evals/cases/k8s-dep-api-v120/fixture/app.yaml +25 -0
- platformforge/data/evals/cases/k8s-dep-api-v129/case.yaml +9 -0
- platformforge/data/evals/cases/k8s-dep-api-v129/fixture/app.yaml +25 -0
- platformforge/data/evals/cases/k8s-hpa-noreq/case.yaml +8 -0
- platformforge/data/evals/cases/k8s-hpa-noreq/fixture/app.yaml +28 -0
- platformforge/data/evals/cases/k8s-limits-boundary/case.yaml +10 -0
- platformforge/data/evals/cases/k8s-limits-boundary/fixture/app.yaml +15 -0
- platformforge/data/evals/cases/k8s-limits-negative/case.yaml +8 -0
- platformforge/data/evals/cases/k8s-limits-negative/fixture/deploy.yaml +15 -0
- platformforge/data/evals/cases/k8s-limits-positive/case.yaml +8 -0
- platformforge/data/evals/cases/k8s-limits-positive/fixture/deploy.yaml +14 -0
- platformforge/data/evals/cases/k8s-nopdb/case.yaml +7 -0
- platformforge/data/evals/cases/k8s-nopdb/fixture/app.yaml +34 -0
- platformforge/data/evals/cases/knowledge-registry/case.yaml +10 -0
- platformforge/data/evals/cases/kyverno-dep/case.yaml +10 -0
- platformforge/data/evals/cases/kyverno-dep/fixture/pol.yaml +18 -0
- platformforge/data/evals/cases/kyverno-legacy/case.yaml +8 -0
- platformforge/data/evals/cases/kyverno-legacy/fixture/pol.yaml +7 -0
- platformforge/data/evals/cases/live-aws-collect/case.yaml +11 -0
- platformforge/data/evals/cases/live-aws-collect/fixture/aws/ec2_describe-vpcs.json +15 -0
- platformforge/data/evals/cases/live-aws-collect/fixture/aws/iam_list-users.denied.txt +1 -0
- platformforge/data/evals/cases/live-aws-collect/fixture/aws/sts_get-caller-identity.json +5 -0
- platformforge/data/evals/cases/live-drift/case.yaml +8 -0
- platformforge/data/evals/cases/live-drift/fixture/after.json +22 -0
- platformforge/data/evals/cases/live-drift/fixture/before.json +22 -0
- platformforge/data/evals/cases/live-incident/case.yaml +7 -0
- platformforge/data/evals/cases/live-incident/fixture/changes.json +8 -0
- platformforge/data/evals/cases/live-incident/fixture/incident.json +7 -0
- platformforge/data/evals/cases/live-k8s-collect/case.yaml +10 -0
- platformforge/data/evals/cases/live-k8s-collect/fixture/kubectl/get_configmaps.denied.txt +1 -0
- platformforge/data/evals/cases/live-k8s-collect/fixture/kubectl/get_deployments.apps.json +25 -0
- platformforge/data/evals/cases/live-k8s-collect/fixture/kubectl/get_pods.json +17 -0
- platformforge/data/evals/cases/live-k8s-collect/fixture/kubectl/get_secrets.json +13 -0
- platformforge/data/evals/cases/live-multicluster/case.yaml +7 -0
- platformforge/data/evals/cases/live-multicluster/fixture/clusters.json +6 -0
- platformforge/data/evals/cases/live-plan/case.yaml +7 -0
- platformforge/data/evals/cases/live-plan/fixture/drift.json +8 -0
- platformforge/data/evals/cases/live-reconcile/case.yaml +7 -0
- platformforge/data/evals/cases/live-reconcile/fixture/desired.json +7 -0
- platformforge/data/evals/cases/live-reconcile/fixture/observed.json +18 -0
- platformforge/data/evals/cases/live-secrets-boundary/case.yaml +8 -0
- platformforge/data/evals/cases/live-secrets-boundary/fixture/envelope.json +16 -0
- platformforge/data/evals/cases/live-temporal-graph/case.yaml +10 -0
- platformforge/data/evals/cases/live-temporal-graph/fixture/graph.json +22 -0
- platformforge/data/evals/cases/ops-approval/case.yaml +6 -0
- platformforge/data/evals/cases/ops-convergence/case.yaml +6 -0
- platformforge/data/evals/cases/ops-execution-safety/case.yaml +6 -0
- platformforge/data/evals/cases/ops-intent/case.yaml +6 -0
- platformforge/data/evals/cases/ops-planning/case.yaml +6 -0
- platformforge/data/evals/cases/ops-policy/case.yaml +6 -0
- platformforge/data/evals/cases/ops-rollback/case.yaml +6 -0
- platformforge/data/evals/cases/ops-verification/case.yaml +6 -0
- platformforge/data/evals/cases/rollback-argocd-revision/case.yaml +6 -0
- platformforge/data/evals/cases/rollback-blind-inverse/case.yaml +6 -0
- platformforge/data/evals/cases/rollback-git-commit/case.yaml +6 -0
- platformforge/data/evals/cases/rollback-impossible/case.yaml +6 -0
- platformforge/data/evals/cases/rollback-k8s-annotation-restore/case.yaml +6 -0
- platformforge/data/evals/cases/rollback-k8s-scale/case.yaml +6 -0
- platformforge/data/evals/cases/rollback-material-tamper/case.yaml +6 -0
- platformforge/data/evals/cases/rollback-terraform-replan/case.yaml +6 -0
- platformforge/data/evals/cases/rollback-unknown/case.yaml +6 -0
- platformforge/data/evals/cases/routing-specialist/case.yaml +9 -0
- platformforge/data/evals/cases/rtk-roundtrip/case.yaml +5 -0
- platformforge/data/evals/cases/rtk-roundtrip/fixture/verbose.txt +300 -0
- platformforge/data/evals/cases/sbom-vuln/case.yaml +9 -0
- platformforge/data/evals/cases/sbom-vuln/fixture/sbom.json +9 -0
- platformforge/data/evals/cases/secret-prose-mixed/fixture/app.env +1 -0
- platformforge/data/evals/cases/secret-prose-mixed/fixture/policy-notes.md +7 -0
- platformforge/data/evals/cases/secrets-noleak/case.yaml +5 -0
- platformforge/data/evals/cases/secrets-noleak/fixture/app.env +2 -0
- platformforge/data/evals/cases/slo-fastburn/case.yaml +8 -0
- platformforge/data/evals/cases/slo-fastburn/fixture/events.yaml +2 -0
- platformforge/data/evals/cases/slo-fastburn/fixture/slo.yaml +4 -0
- platformforge/data/evals/cases/slo-noinput/case.yaml +8 -0
- platformforge/data/evals/cases/slo-noinput/fixture/events.yaml +1 -0
- platformforge/data/evals/cases/slo-noinput/fixture/slo.yaml +4 -0
- platformforge/data/evals/cases/sot-conflict/case.yaml +6 -0
- platformforge/data/evals/cases/token-budget/case.yaml +6 -0
- platformforge/data/evals/cases/token-budget/fixture/big.py +50 -0
- platformforge/data/evals/cases/xr-delete/case.yaml +8 -0
- platformforge/data/evals/cases/xr-delete/fixture/xr.yaml +10 -0
- platformforge/data/evals/cases/xr-graph-chain/case.yaml +8 -0
- platformforge/data/evals/cases/xr-graph-chain/fixture/xrd.yaml +90 -0
- platformforge/data/evals/cases/xr-negative-crd/case.yaml +8 -0
- platformforge/data/evals/cases/xr-negative-crd/fixture/widget.yaml +19 -0
- platformforge/data/evals/cases/xr-upbound-mr/case.yaml +8 -0
- platformforge/data/evals/cases/xr-upbound-mr/fixture/bucket.yaml +12 -0
- platformforge/data/evals/qpt-bench/change-review/case.yaml +5 -0
- platformforge/data/evals/qpt-bench/change-review/fixture/plan.json +9 -0
- platformforge/data/evals/qpt-bench/gha-security/case.yaml +5 -0
- platformforge/data/evals/qpt-bench/gha-security/fixture/ci.yml +10 -0
- platformforge/data/evals/qpt-bench/iam-investigate/case.yaml +5 -0
- platformforge/data/evals/qpt-bench/iam-investigate/fixture/policy.json +6 -0
- platformforge/data/evals/qpt-bench/incident-analysis/case.yaml +5 -0
- platformforge/data/evals/qpt-bench/incident-analysis/fixture/query.json +5 -0
- platformforge/data/evals/qpt-bench/incident-analysis/fixture/rules.yaml +6 -0
- platformforge/data/evals/qpt-bench/k8s-review/case.yaml +6 -0
- platformforge/data/evals/qpt-bench/k8s-review/fixture/deploy.yaml +15 -0
- platformforge/data/evals/qpt-bench/tf-security/case.yaml +5 -0
- platformforge/data/evals/qpt-bench/tf-security/fixture/main.tf +17 -0
- platformforge/data/knowledge/aws/eks.yaml +20 -0
- platformforge/data/knowledge/aws/iam.yaml +28 -0
- platformforge/data/knowledge/aws/network-exposure.yaml +19 -0
- platformforge/data/knowledge/aws/organizations.yaml +19 -0
- platformforge/data/knowledge/crossplane/composition-providers.yaml +20 -0
- platformforge/data/knowledge/crossplane/managed-resources.yaml +26 -0
- platformforge/data/knowledge/crossplane/v1-v2.yaml +25 -0
- platformforge/data/knowledge/kubernetes/api-deprecations.yaml +33 -0
- platformforge/data/knowledge/kubernetes/autoscaling-rbac.yaml +27 -0
- platformforge/data/knowledge/kubernetes/gateway-api.yaml +20 -0
- platformforge/data/knowledge/kubernetes/pod-security.yaml +26 -0
- platformforge/data/knowledge/sources.yaml +516 -0
- platformforge/data/knowledge/terraform/lifecycle.yaml +18 -0
- platformforge/data/knowledge/terraform/plan-semantics.yaml +26 -0
- platformforge/data/lab/chaos-pod-kill/chaos.yaml +3 -0
- platformforge/data/lab/chaos-pod-kill/facts.json +13 -0
- platformforge/data/lab/fleets/acme/capacity.yaml +35 -0
- platformforge/data/lab/fleets/acme/costs.yaml +25 -0
- platformforge/data/lab/fleets/acme/events.yaml +41 -0
- platformforge/data/lab/fleets/acme/fleet.yaml +51 -0
- platformforge/data/lab/fleets/acme/graph.yaml +45 -0
- platformforge/data/lab/fleets/acme/policies.yaml +28 -0
- platformforge/data/lab/fleets/acme/requests.yaml +24 -0
- platformforge/data/lab/scenarios/argo-bad-rollout/expected.yaml +6 -0
- platformforge/data/lab/scenarios/argo-bad-rollout/fixture/app.yaml +9 -0
- platformforge/data/lab/scenarios/aws-dump/expected.yaml +19 -0
- platformforge/data/lab/scenarios/aws-dump/fixture/compute.json +4 -0
- platformforge/data/lab/scenarios/aws-dump/fixture/iam.json +7 -0
- platformforge/data/lab/scenarios/aws-dump/fixture/network.json +6 -0
- platformforge/data/lab/scenarios/aws-dump/fixture/org.json +4 -0
- platformforge/data/lab/scenarios/aws-dump/fixture/storage.json +4 -0
- platformforge/data/lab/scenarios/container-unsigned/expected.yaml +5 -0
- platformforge/data/lab/scenarios/container-unsigned/fixture/supply.json +11 -0
- platformforge/data/lab/scenarios/cost-idle-workload/expected.yaml +5 -0
- platformforge/data/lab/scenarios/cost-idle-workload/fixture/costs.json +12 -0
- platformforge/data/lab/scenarios/crossplane-platform/expected.yaml +16 -0
- platformforge/data/lab/scenarios/crossplane-platform/fixture/negative.yaml +18 -0
- platformforge/data/lab/scenarios/crossplane-platform/fixture/xrd.yaml +90 -0
- platformforge/data/lab/scenarios/demo-platform/expected.yaml +16 -0
- platformforge/data/lab/scenarios/demo-platform/fixture/.github/workflows/ci.yml +10 -0
- platformforge/data/lab/scenarios/demo-platform/fixture/deploy.yaml +16 -0
- platformforge/data/lab/scenarios/demo-platform/fixture/infra.tf +5 -0
- platformforge/data/lab/scenarios/fleet-ai/expected.yaml +12 -0
- platformforge/data/lab/scenarios/fleet-capacity/expected.yaml +12 -0
- platformforge/data/lab/scenarios/fleet-cost/expected.yaml +18 -0
- platformforge/data/lab/scenarios/fleet-federation/expected.yaml +11 -0
- platformforge/data/lab/scenarios/fleet-golden-path/expected.yaml +18 -0
- platformforge/data/lab/scenarios/fleet-op-hotspot/expected.yaml +11 -0
- platformforge/data/lab/scenarios/fleet-ownership/expected.yaml +17 -0
- platformforge/data/lab/scenarios/fleet-partial/expected.yaml +10 -0
- platformforge/data/lab/scenarios/fleet-policy-friction/expected.yaml +10 -0
- platformforge/data/lab/scenarios/fleet-recurring-incident/expected.yaml +12 -0
- platformforge/data/lab/scenarios/fleet-report/expected.yaml +13 -0
- platformforge/data/lab/scenarios/fleet-risk/expected.yaml +18 -0
- platformforge/data/lab/scenarios/gitops-drift/expected.yaml +6 -0
- platformforge/data/lab/scenarios/gitops-drift/fixture/app.yaml +12 -0
- platformforge/data/lab/scenarios/k8s-bad-readiness/expected.yaml +6 -0
- platformforge/data/lab/scenarios/k8s-bad-readiness/fixture/deploy.yaml +23 -0
- platformforge/data/lab/scenarios/k8s-bad-readiness/fixture/netpol.yaml +9 -0
- platformforge/data/lab/scenarios/k8s-missing-limits/expected.yaml +6 -0
- platformforge/data/lab/scenarios/k8s-missing-limits/fixture/deploy.yaml +26 -0
- platformforge/data/lab/scenarios/k8s-missing-limits/fixture/netpol.yaml +9 -0
- platformforge/data/lab/scenarios/k8s-single-replica/expected.yaml +5 -0
- platformforge/data/lab/scenarios/k8s-single-replica/fixture/deploy.yaml +26 -0
- platformforge/data/lab/scenarios/k8s-single-replica/fixture/netpol.yaml +4 -0
- platformforge/data/lab/scenarios/live-aws-collect/expected.yaml +4 -0
- platformforge/data/lab/scenarios/live-aws-collect/fixture/aws/ec2_describe-vpcs.json +15 -0
- platformforge/data/lab/scenarios/live-aws-collect/fixture/aws/iam_list-users.denied.txt +1 -0
- platformforge/data/lab/scenarios/live-aws-collect/fixture/aws/sts_get-caller-identity.json +5 -0
- platformforge/data/lab/scenarios/live-k8s-collect/expected.yaml +4 -0
- platformforge/data/lab/scenarios/live-k8s-collect/fixture/kubectl/get_configmaps.denied.txt +1 -0
- platformforge/data/lab/scenarios/live-k8s-collect/fixture/kubectl/get_deployments.apps.json +25 -0
- platformforge/data/lab/scenarios/live-k8s-collect/fixture/kubectl/get_pods.json +17 -0
- platformforge/data/lab/scenarios/live-k8s-collect/fixture/kubectl/get_secrets.json +13 -0
- platformforge/data/lab/scenarios/live-kind/expected.yaml +14 -0
- platformforge/data/lab/scenarios/live-kind/fixture/kubectl/get_configmaps.denied.txt +1 -0
- platformforge/data/lab/scenarios/live-kind/fixture/kubectl/get_deployments.apps.json +25 -0
- platformforge/data/lab/scenarios/live-kind/fixture/kubectl/get_pods.json +17 -0
- platformforge/data/lab/scenarios/live-kind/fixture/kubectl/get_secrets.json +13 -0
- platformforge/data/lab/scenarios/network-policy-missing/expected.yaml +6 -0
- platformforge/data/lab/scenarios/network-policy-missing/fixture/deploy.yaml +26 -0
- platformforge/data/lab/scenarios/ops-bad-rollout/expected.yaml +3 -0
- platformforge/data/lab/scenarios/ops-bad-rollout/fixture/ops.yaml +33 -0
- platformforge/data/lab/scenarios/ops-break-glass/expected.yaml +3 -0
- platformforge/data/lab/scenarios/ops-break-glass/fixture/ops.yaml +21 -0
- platformforge/data/lab/scenarios/ops-broken-selector/expected.yaml +3 -0
- platformforge/data/lab/scenarios/ops-broken-selector/fixture/ops.yaml +21 -0
- platformforge/data/lab/scenarios/ops-changed-plan-hash/expected.yaml +3 -0
- platformforge/data/lab/scenarios/ops-changed-plan-hash/fixture/ops.yaml +20 -0
- platformforge/data/lab/scenarios/ops-conflicting-operation/expected.yaml +3 -0
- platformforge/data/lab/scenarios/ops-conflicting-operation/fixture/ops.yaml +21 -0
- platformforge/data/lab/scenarios/ops-expired-approval/expected.yaml +3 -0
- platformforge/data/lab/scenarios/ops-expired-approval/fixture/ops.yaml +20 -0
- platformforge/data/lab/scenarios/ops-partial-failure/expected.yaml +3 -0
- platformforge/data/lab/scenarios/ops-partial-failure/fixture/ops.yaml +24 -0
- platformforge/data/lab/scenarios/ops-policy-denial/expected.yaml +3 -0
- platformforge/data/lab/scenarios/ops-policy-denial/fixture/ops.yaml +17 -0
- platformforge/data/lab/scenarios/ops-replica-drift/expected.yaml +3 -0
- platformforge/data/lab/scenarios/ops-replica-drift/fixture/ops.yaml +28 -0
- platformforge/data/lab/scenarios/ops-rollback-annotate/expected.yaml +3 -0
- platformforge/data/lab/scenarios/ops-rollback-annotate/fixture/ops.yaml +41 -0
- platformforge/data/lab/scenarios/ops-rollback-gitops/expected.yaml +3 -0
- platformforge/data/lab/scenarios/ops-rollback-gitops/fixture/ops.yaml +38 -0
- platformforge/data/lab/scenarios/ops-rollback-impossible/expected.yaml +3 -0
- platformforge/data/lab/scenarios/ops-rollback-impossible/fixture/ops.yaml +34 -0
- platformforge/data/lab/scenarios/ops-rollback-scale/expected.yaml +3 -0
- platformforge/data/lab/scenarios/ops-rollback-scale/fixture/ops.yaml +40 -0
- platformforge/data/lab/scenarios/ops-rollback-terraform/expected.yaml +3 -0
- platformforge/data/lab/scenarios/ops-rollback-terraform/fixture/ops.yaml +38 -0
- platformforge/data/lab/scenarios/ops-rollback-terraform/fixture/tf/tfplan.bin +1 -0
- platformforge/data/lab/scenarios/ops-stale-observation/expected.yaml +3 -0
- platformforge/data/lab/scenarios/ops-stale-observation/fixture/ops.yaml +20 -0
- platformforge/data/lab/scenarios/secret-exposure/expected.yaml +5 -0
- platformforge/data/lab/scenarios/secret-exposure/fixture/app.env +2 -0
- platformforge/data/lab/scenarios/slo-burn/expected.yaml +5 -0
- platformforge/data/lab/scenarios/slo-burn/fixture/events.yaml +2 -0
- platformforge/data/lab/scenarios/slo-burn/fixture/slo.yaml +4 -0
- platformforge/data/lab/scenarios/terraform-iam-wildcard/expected.yaml +5 -0
- platformforge/data/lab/scenarios/terraform-iam-wildcard/fixture/policy.json +14 -0
- platformforge/data/lab/scenarios/terraform-public-bucket/expected.yaml +6 -0
- platformforge/data/lab/scenarios/terraform-public-bucket/fixture/main.tf +5 -0
- platformforge/data/portable_skills/platformforge-agents/SKILL.md +47 -0
- platformforge/data/portable_skills/platformforge-change/SKILL.md +41 -0
- platformforge/data/portable_skills/platformforge-core/SKILL.md +62 -0
- platformforge/data/portable_skills/platformforge-economy/SKILL.md +42 -0
- platformforge/data/portable_skills/platformforge-finops/SKILL.md +34 -0
- platformforge/data/portable_skills/platformforge-fleet/SKILL.md +39 -0
- platformforge/data/portable_skills/platformforge-governance/SKILL.md +76 -0
- platformforge/data/portable_skills/platformforge-graph/SKILL.md +46 -0
- platformforge/data/portable_skills/platformforge-lab/SKILL.md +39 -0
- platformforge/data/portable_skills/platformforge-sdd/SKILL.md +39 -0
- platformforge/data/portable_skills/platformforge-security/SKILL.md +38 -0
- platformforge/data/portable_skills/platformforge-sre/SKILL.md +41 -0
- platformforge/data/rules/catalog/finops.yaml +26 -0
- platformforge/data/rules/catalog/gitops.yaml +161 -0
- platformforge/data/rules/catalog/iac.yaml +84 -0
- platformforge/data/rules/catalog/k8s.yaml +322 -0
- platformforge/data/rules/catalog/orchestration.yaml +99 -0
- platformforge/data/rules/catalog/product.yaml +42 -0
- platformforge/data/rules/catalog/routing.yaml +122 -0
- platformforge/data/rules/catalog/security.yaml +159 -0
- platformforge/data/rules/catalog/sre.yaml +67 -0
- platformforge/diagnose/__init__.py +75 -0
- platformforge/distribution/__init__.py +18 -0
- platformforge/distribution/bundle.py +127 -0
- platformforge/distribution/hostflow.py +247 -0
- platformforge/distribution/models.py +80 -0
- platformforge/distribution/service.py +225 -0
- platformforge/economy/__init__.py +27 -0
- platformforge/economy/bench.py +66 -0
- platformforge/economy/budget.py +198 -0
- platformforge/economy/cache.py +287 -0
- platformforge/economy/checkpoint.py +112 -0
- platformforge/economy/collection.py +142 -0
- platformforge/economy/doctor.py +207 -0
- platformforge/economy/engine.py +158 -0
- platformforge/economy/envelope.py +61 -0
- platformforge/economy/fleetpack.py +86 -0
- platformforge/economy/ledger.py +232 -0
- platformforge/economy/plan.py +131 -0
- platformforge/economy/pricing.py +101 -0
- platformforge/economy/qpt.py +286 -0
- platformforge/economy/reconcile.py +86 -0
- platformforge/economy/verifyplan.py +107 -0
- platformforge/economy/waste.py +123 -0
- platformforge/evals/__init__.py +16 -0
- platformforge/evals/coverage.py +140 -0
- platformforge/evals/fleet_invariants.py +260 -0
- platformforge/evals/ops_invariants.py +158 -0
- platformforge/evals/runner.py +541 -0
- platformforge/federation/__init__.py +4 -0
- platformforge/federation/node.py +135 -0
- platformforge/finops/__init__.py +14 -0
- platformforge/finops/alloc.py +31 -0
- platformforge/finops/costs.py +79 -0
- platformforge/finops/focus.py +100 -0
- platformforge/finops/graph_cost.py +51 -0
- platformforge/finops/ingest.py +144 -0
- platformforge/finops/insights.py +193 -0
- platformforge/finops/unit.py +36 -0
- platformforge/fleet/__init__.py +5 -0
- platformforge/fleet/loader.py +73 -0
- platformforge/fleet/models.py +155 -0
- platformforge/fleet/orggraph.py +172 -0
- platformforge/fleet/query.py +154 -0
- platformforge/forge/__init__.py +9 -0
- platformforge/forge/collect.py +88 -0
- platformforge/forge/delegate.py +77 -0
- platformforge/forge/manifest.py +106 -0
- platformforge/freeze/__init__.py +22 -0
- platformforge/freeze/exceptions.py +56 -0
- platformforge/freeze/manifest.py +167 -0
- platformforge/freeze/snapshots.py +108 -0
- platformforge/graph/__init__.py +39 -0
- platformforge/graph/backend.py +174 -0
- platformforge/graph/bench.py +245 -0
- platformforge/graph/build.py +130 -0
- platformforge/graph/delivery.py +183 -0
- platformforge/graph/diff.py +292 -0
- platformforge/graph/events.py +116 -0
- platformforge/graph/migrate.py +44 -0
- platformforge/graph/model.py +264 -0
- platformforge/graph/persist.py +97 -0
- platformforge/graph/query.py +290 -0
- platformforge/graph/temporal.py +78 -0
- platformforge/graph/vocab.py +63 -0
- platformforge/graphview.py +94 -0
- platformforge/iac/__init__.py +7 -0
- platformforge/iac/drift.py +53 -0
- platformforge/iac/plan.py +84 -0
- platformforge/iac/terraform.py +123 -0
- platformforge/install/__init__.py +1 -0
- platformforge/install/service.py +421 -0
- platformforge/k8s/__init__.py +8 -0
- platformforge/k8s/helm.py +152 -0
- platformforge/k8s/hubble.py +87 -0
- platformforge/k8s/manifests.py +513 -0
- platformforge/knowledge/__init__.py +1 -0
- platformforge/knowledge/packs.py +157 -0
- platformforge/knowledge/registry.py +249 -0
- platformforge/knowledge/select.py +127 -0
- platformforge/lab/__init__.py +5 -0
- platformforge/lab/chaos.py +71 -0
- platformforge/lab/fleetlab.py +351 -0
- platformforge/lab/opslab.py +271 -0
- platformforge/lab/runner.py +219 -0
- platformforge/live/__init__.py +36 -0
- platformforge/live/budget.py +92 -0
- platformforge/live/capability.py +80 -0
- platformforge/live/collectors/__init__.py +6 -0
- platformforge/live/collectors/aws.py +370 -0
- platformforge/live/collectors/aws_transport.py +98 -0
- platformforge/live/collectors/k8s.py +389 -0
- platformforge/live/collectors/k8s_transport.py +85 -0
- platformforge/live/cursors.py +48 -0
- platformforge/live/drift.py +130 -0
- platformforge/live/envelope.py +85 -0
- platformforge/live/federation.py +113 -0
- platformforge/live/fixtures.py +168 -0
- platformforge/live/identity.py +258 -0
- platformforge/live/incident.py +200 -0
- platformforge/live/models.py +435 -0
- platformforge/live/reconcile.py +382 -0
- platformforge/live/remediate.py +169 -0
- platformforge/live/store.py +256 -0
- platformforge/live/topology.py +249 -0
- platformforge/mcp/__init__.py +7 -0
- platformforge/mcp/parity.py +69 -0
- platformforge/mcp/registry.py +427 -0
- platformforge/mcp/server.py +84 -0
- platformforge/mcp/tools.py +732 -0
- platformforge/models/__init__.py +21 -0
- platformforge/models/base.py +214 -0
- platformforge/observe/__init__.py +26 -0
- platformforge/observe/capacity.py +39 -0
- platformforge/observe/dr.py +67 -0
- platformforge/observe/grafana.py +76 -0
- platformforge/observe/incident.py +58 -0
- platformforge/observe/otel.py +135 -0
- platformforge/observe/postmortem.py +72 -0
- platformforge/observe/prometheus.py +88 -0
- platformforge/observe/slo.py +125 -0
- platformforge/ops/__init__.py +8 -0
- platformforge/ops/actions.py +215 -0
- platformforge/ops/analytics.py +83 -0
- platformforge/ops/approval.py +275 -0
- platformforge/ops/autorem.py +101 -0
- platformforge/ops/config.py +175 -0
- platformforge/ops/engine.py +758 -0
- platformforge/ops/envelope.py +135 -0
- platformforge/ops/executors/__init__.py +1 -0
- platformforge/ops/executors/base.py +211 -0
- platformforge/ops/executors/git.py +77 -0
- platformforge/ops/executors/gitops.py +134 -0
- platformforge/ops/executors/terraform.py +84 -0
- platformforge/ops/gates.py +135 -0
- platformforge/ops/goldenpath.py +180 -0
- platformforge/ops/material.py +181 -0
- platformforge/ops/models.py +421 -0
- platformforge/ops/operation.py +335 -0
- platformforge/ops/opgraph.py +233 -0
- platformforge/ops/policy.py +275 -0
- platformforge/ops/preconditions.py +109 -0
- platformforge/ops/registry.py +101 -0
- platformforge/ops/risk.py +216 -0
- platformforge/ops/rollback.py +287 -0
- platformforge/ops/rollback_builders.py +163 -0
- platformforge/ops/runbook.py +269 -0
- platformforge/ops/simulate.py +125 -0
- platformforge/ops/source_of_truth.py +198 -0
- platformforge/ops/store.py +157 -0
- platformforge/ops/verify.py +190 -0
- platformforge/optimize/__init__.py +4 -0
- platformforge/optimize/engine.py +109 -0
- platformforge/optimize/models.py +68 -0
- platformforge/optimize/scan.py +196 -0
- platformforge/plan/__init__.py +107 -0
- platformforge/portable_cli.py +199 -0
- platformforge/portal_adapter.py +740 -0
- platformforge/portal_assets/chunk-7_l-b4Tr.js +1 -0
- platformforge/portal_assets/chunk-B0Iad6pD.js +1 -0
- platformforge/portal_assets/chunk-B605XnCF.js +1 -0
- platformforge/portal_assets/chunk-B7N3TXcn.js +1 -0
- platformforge/portal_assets/chunk-BAj6yGem.js +1 -0
- platformforge/portal_assets/chunk-BGUitn2r.js +1 -0
- platformforge/portal_assets/chunk-BVKnCv85.js +1 -0
- platformforge/portal_assets/chunk-BYyM9V_w.js +1 -0
- platformforge/portal_assets/chunk-BaQMhq-G.js +1 -0
- platformforge/portal_assets/chunk-BavOrxVm.js +1 -0
- platformforge/portal_assets/chunk-Bb0C24YV.js +1 -0
- platformforge/portal_assets/chunk-BfCFGPzs.js +1 -0
- platformforge/portal_assets/chunk-C2t4OUVu.js +1 -0
- platformforge/portal_assets/chunk-C3pI4QjU.js +1 -0
- platformforge/portal_assets/chunk-C4gpbtbP.js +1 -0
- platformforge/portal_assets/chunk-CGWeHhKY.js +1 -0
- platformforge/portal_assets/chunk-CJCT86Dr.js +1 -0
- platformforge/portal_assets/chunk-CXcsLDPJ.js +1 -0
- platformforge/portal_assets/chunk-CcjxoLxw.js +1 -0
- platformforge/portal_assets/chunk-D-gBPXHa.js +1 -0
- platformforge/portal_assets/chunk-D3eZogIe.js +1 -0
- platformforge/portal_assets/chunk-DQU0VyoQ.js +1 -0
- platformforge/portal_assets/chunk-DqWNkF1O.js +1 -0
- platformforge/portal_assets/chunk-DuPPkSIE.js +3 -0
- platformforge/portal_assets/chunk-DyIHOW_a.js +1 -0
- platformforge/portal_assets/chunk-Fx4zSGL5.js +1 -0
- platformforge/portal_assets/chunk-UBKt05Rg.js +1 -0
- platformforge/portal_assets/chunk-htnJDQEY.js +1 -0
- platformforge/portal_assets/chunk-kXRFMGqe.js +1 -0
- platformforge/portal_assets/chunk-qe3u763a.js +1 -0
- platformforge/portal_assets/favicon.ico +0 -0
- platformforge/portal_assets/index.html +13 -0
- platformforge/portal_assets/main-QDFVZK3T.js +5 -0
- platformforge/portal_assets/styles-MQEBDFDX.css +1 -0
- platformforge/product/__init__.py +10 -0
- platformforge/product/backstage.py +49 -0
- platformforge/product/catalog.py +74 -0
- platformforge/product/crossplane.py +341 -0
- platformforge/product/golden_paths/__init__.py +5 -0
- platformforge/product/golden_paths/engine.py +97 -0
- platformforge/product/golden_paths/library.yaml +198 -0
- platformforge/product/golden_paths/model.py +84 -0
- platformforge/product/maturity.py +107 -0
- platformforge/product/scorecards.py +84 -0
- platformforge/resources.py +36 -0
- platformforge/risk/__init__.py +5 -0
- platformforge/risk/engine.py +95 -0
- platformforge/risk/signals.py +49 -0
- platformforge/routing/__init__.py +13 -0
- platformforge/routing/decision.py +191 -0
- platformforge/routing/router.py +328 -0
- platformforge/rtk/__init__.py +9 -0
- platformforge/rtk/compact.py +127 -0
- platformforge/rtk/parsers.py +199 -0
- platformforge/rules/__init__.py +5 -0
- platformforge/rules/engine.py +345 -0
- platformforge/sandbox/__init__.py +7 -0
- platformforge/sandbox/env.py +151 -0
- platformforge/sandbox/review.py +92 -0
- platformforge/sdd/__init__.py +8 -0
- platformforge/sdd/lifecycle.py +239 -0
- platformforge/security/__init__.py +19 -0
- platformforge/security/cosign.py +67 -0
- platformforge/security/iam.py +112 -0
- platformforge/security/identity.py +84 -0
- platformforge/security/kyverno.py +106 -0
- platformforge/security/sbom.py +59 -0
- platformforge/security/scan.py +63 -0
- platformforge/security/slsa.py +86 -0
- platformforge/security/supply.py +62 -0
- platformforge/tokensave/__init__.py +20 -0
- platformforge/tokensave/budget.py +62 -0
- platformforge/tokensave/estimate.py +21 -0
- platformforge/tokensave/index.py +172 -0
- platformforge/tokensave/ledger.py +121 -0
- platformforge/tokensave/packs.py +285 -0
- platformforge/ui/__init__.py +28 -0
- platformforge/ui/app.py +465 -0
- platformforge/ui/home.py +113 -0
- platformforge/ui/i18n.py +143 -0
- platformforge/ui/kit.py +689 -0
- platformforge/ui/screen.py +628 -0
- platformforge/ui/tui.py +94 -0
- platformforge/ui/wizard.py +309 -0
- platformforge/workspace/__init__.py +13 -0
- platformforge/workspace/models.py +35 -0
- platformforge/workspace/service.py +69 -0
- platformforge-0.1.0.dist-info/METADATA +212 -0
- platformforge-0.1.0.dist-info/RECORD +659 -0
- platformforge-0.1.0.dist-info/WHEEL +4 -0
- platformforge-0.1.0.dist-info/entry_points.txt +3 -0
- platformforge-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"""Cycle 4 — operation state machine + append-only ledger + locks
|
|
2
|
+
(§87–106, ADR-0026).
|
|
3
|
+
|
|
4
|
+
An Operation is an explicit FSM — `draft → planned → … → converged |
|
|
5
|
+
failed | rolled-back | …` — never a mutable status string. Invalid
|
|
6
|
+
transitions refuse. Every transition appends a hash-chained receipt to
|
|
7
|
+
the OperationLedger (`seq` + `prev_hash`, git-style — tamper-evident,
|
|
8
|
+
replayable). Locks are per canonical resource; conflicting operations
|
|
9
|
+
block, queue or arbitrate — never run side by side. Idempotency keys
|
|
10
|
+
dedup retries; resume replays the ledger instead of re-executing.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from platformforge.live.models import canonical_hash, now_iso
|
|
19
|
+
|
|
20
|
+
STATES = ("draft", "planned", "simulated", "policy-reviewed",
|
|
21
|
+
"awaiting-approval", "approved", "scheduled", "executing",
|
|
22
|
+
"verifying", "converged", "partially-converged", "failed",
|
|
23
|
+
"rollback-planned", "rolling-back", "rolled-back",
|
|
24
|
+
"regressed", "cancelled", "expired", "unresolved")
|
|
25
|
+
|
|
26
|
+
TERMINAL = {"converged", "cancelled", "expired", "rolled-back"}
|
|
27
|
+
|
|
28
|
+
# §90 — the only legal edges. Anything else → PF-OPS-BAD-TRANSITION.
|
|
29
|
+
TRANSITIONS: dict[str, set[str]] = {
|
|
30
|
+
"draft": {"planned", "cancelled"},
|
|
31
|
+
"planned": {"simulated", "policy-reviewed", "cancelled"},
|
|
32
|
+
"simulated": {"policy-reviewed", "planned", "cancelled"},
|
|
33
|
+
"policy-reviewed": {"awaiting-approval", "approved", "cancelled",
|
|
34
|
+
"planned"},
|
|
35
|
+
"awaiting-approval": {"approved", "cancelled", "expired"},
|
|
36
|
+
"approved": {"scheduled", "executing", "expired", "cancelled"},
|
|
37
|
+
"scheduled": {"executing", "expired", "cancelled"},
|
|
38
|
+
"executing": {"verifying", "failed", "partially-converged",
|
|
39
|
+
"rolling-back"},
|
|
40
|
+
"verifying": {"converged", "partially-converged", "regressed",
|
|
41
|
+
"failed", "rollback-planned"},
|
|
42
|
+
"converged": set(),
|
|
43
|
+
"partially-converged": {"verifying", "rollback-planned",
|
|
44
|
+
"converged", "unresolved"},
|
|
45
|
+
"failed": {"rollback-planned", "unresolved"},
|
|
46
|
+
"rollback-planned": {"rolling-back", "failed", "unresolved"},
|
|
47
|
+
"rolling-back": {"rolled-back", "failed", "unresolved"},
|
|
48
|
+
"rolled-back": set(),
|
|
49
|
+
"regressed": {"rollback-planned", "unresolved"},
|
|
50
|
+
"cancelled": set(),
|
|
51
|
+
"expired": set(),
|
|
52
|
+
"unresolved": {"planned", "cancelled"},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
ACTORS = ("human", "agent", "host", "system")
|
|
56
|
+
|
|
57
|
+
LEDGER_EVENTS = ("intent.created", "plan.created", "simulation.completed",
|
|
58
|
+
"policy.evaluated", "approval.granted", "approval.denied",
|
|
59
|
+
"execution.started", "step.started", "step.completed",
|
|
60
|
+
"step.failed", "verification.started", "verified",
|
|
61
|
+
"converged", "rollback.started", "rollback.completed",
|
|
62
|
+
"compensation.started", "compensation.completed",
|
|
63
|
+
"operation.failed", "operation.cancelled",
|
|
64
|
+
"operation.expired", "lock.acquired", "lock.released",
|
|
65
|
+
"resume.attempted", "precondition.failed")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class LedgerEntry:
|
|
70
|
+
seq: int
|
|
71
|
+
event: str
|
|
72
|
+
operation_id: str
|
|
73
|
+
actor: str = "system" # human|agent|host|system
|
|
74
|
+
at: str = ""
|
|
75
|
+
data: dict[str, Any] = field(default_factory=dict)
|
|
76
|
+
prev_hash: str = "genesis"
|
|
77
|
+
entry_hash: str = ""
|
|
78
|
+
|
|
79
|
+
def __post_init__(self):
|
|
80
|
+
if not self.at:
|
|
81
|
+
self.at = now_iso()
|
|
82
|
+
if not self.entry_hash:
|
|
83
|
+
body = {"seq": self.seq, "event": self.event,
|
|
84
|
+
"operation_id": self.operation_id,
|
|
85
|
+
"actor": self.actor, "at": self.at,
|
|
86
|
+
"data": self.data, "prev_hash": self.prev_hash}
|
|
87
|
+
self.entry_hash = "sha256:" + canonical_hash(body)
|
|
88
|
+
|
|
89
|
+
def to_dict(self) -> dict[str, Any]:
|
|
90
|
+
return {"seq": self.seq, "event": self.event,
|
|
91
|
+
"operation_id": self.operation_id, "actor": self.actor,
|
|
92
|
+
"at": self.at, "data": self.data,
|
|
93
|
+
"prev_hash": self.prev_hash,
|
|
94
|
+
"entry_hash": self.entry_hash}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class OperationLedger:
|
|
98
|
+
"""Append-only, hash-chained (§92–95, §235). Tip is the anchor."""
|
|
99
|
+
|
|
100
|
+
def __init__(self):
|
|
101
|
+
self.entries: list[LedgerEntry] = []
|
|
102
|
+
self._tip = "genesis"
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def tip(self) -> str:
|
|
106
|
+
return self._tip
|
|
107
|
+
|
|
108
|
+
def append(self, event: str, operation_id: str, *,
|
|
109
|
+
actor: str = "system", data: dict[str, Any] | None = None
|
|
110
|
+
) -> LedgerEntry:
|
|
111
|
+
if actor not in ACTORS:
|
|
112
|
+
actor = "system"
|
|
113
|
+
from platformforge.core.redaction import redact_obj
|
|
114
|
+
e = LedgerEntry(seq=len(self.entries) + 1, event=event,
|
|
115
|
+
operation_id=operation_id, actor=actor,
|
|
116
|
+
data=redact_obj(dict(data or {})),
|
|
117
|
+
prev_hash=self._tip)
|
|
118
|
+
self.entries.append(e)
|
|
119
|
+
self._tip = e.entry_hash
|
|
120
|
+
return e
|
|
121
|
+
|
|
122
|
+
def for_operation(self, op_id: str) -> list[LedgerEntry]:
|
|
123
|
+
return [e for e in self.entries if e.operation_id == op_id]
|
|
124
|
+
|
|
125
|
+
def verify_chain(self) -> bool:
|
|
126
|
+
prev = "genesis"
|
|
127
|
+
for i, e in enumerate(self.entries):
|
|
128
|
+
if e.seq != i + 1 or e.prev_hash != prev:
|
|
129
|
+
return False
|
|
130
|
+
body = {"seq": e.seq, "event": e.event,
|
|
131
|
+
"operation_id": e.operation_id, "actor": e.actor,
|
|
132
|
+
"at": e.at, "data": e.data, "prev_hash": e.prev_hash}
|
|
133
|
+
if e.entry_hash != "sha256:" + canonical_hash(body):
|
|
134
|
+
return False
|
|
135
|
+
prev = e.entry_hash
|
|
136
|
+
return True
|
|
137
|
+
|
|
138
|
+
def to_dict(self) -> dict[str, Any]:
|
|
139
|
+
return {"entries": [e.to_dict() for e in self.entries],
|
|
140
|
+
"tip": self._tip, "count": len(self.entries)}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@dataclass
|
|
144
|
+
class ExecutionStep:
|
|
145
|
+
"""§96 — step record with hashes + receipts (saga-aware)."""
|
|
146
|
+
step_id: str = ""
|
|
147
|
+
action: str = ""
|
|
148
|
+
params: dict[str, Any] = field(default_factory=dict)
|
|
149
|
+
depends_on: list[str] = field(default_factory=list)
|
|
150
|
+
status: str = "pending" # pending|running|completed|failed|skipped
|
|
151
|
+
attempt: int = 0
|
|
152
|
+
compensation: str = "" # action that compensates this step
|
|
153
|
+
saga_class: str = "" # compensable|pivot|retryable
|
|
154
|
+
inputs_hash: str = ""
|
|
155
|
+
outputs_hash: str = ""
|
|
156
|
+
started_at: str = ""
|
|
157
|
+
finished_at: str = ""
|
|
158
|
+
receipt: dict[str, Any] = field(default_factory=dict)
|
|
159
|
+
idempotency_key: str = ""
|
|
160
|
+
|
|
161
|
+
def key(self, plan_hash: str) -> str:
|
|
162
|
+
"""Stripe-style: deterministic key from plan+step content."""
|
|
163
|
+
return "sha256:" + canonical_hash(
|
|
164
|
+
{"plan": plan_hash, "step": self.step_id,
|
|
165
|
+
"action": self.action, "params": self.params})
|
|
166
|
+
|
|
167
|
+
def effect_key(self, plan_hash: str) -> str:
|
|
168
|
+
"""Side-effect identity — action+params only. Two steps with the
|
|
169
|
+
same effect dedupe to one execution (idempotency gate)."""
|
|
170
|
+
return "sha256:" + canonical_hash(
|
|
171
|
+
{"plan": plan_hash, "action": self.action,
|
|
172
|
+
"params": self.params})
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@dataclass
|
|
176
|
+
class Operation:
|
|
177
|
+
"""§89 — the governed unit of work."""
|
|
178
|
+
operation_id: str = ""
|
|
179
|
+
intent_id: str = ""
|
|
180
|
+
plan_hash: str = ""
|
|
181
|
+
envelope_hash: str = ""
|
|
182
|
+
state: str = "draft"
|
|
183
|
+
actor: str = "system"
|
|
184
|
+
steps: list[ExecutionStep] = field(default_factory=list)
|
|
185
|
+
resources: list[str] = field(default_factory=list) # lock scope
|
|
186
|
+
history: list[dict[str, Any]] = field(default_factory=list)
|
|
187
|
+
idempotency_key: str = ""
|
|
188
|
+
created_at: str = ""
|
|
189
|
+
meta: dict[str, Any] = field(default_factory=dict)
|
|
190
|
+
|
|
191
|
+
def __post_init__(self):
|
|
192
|
+
if not self.created_at:
|
|
193
|
+
self.created_at = now_iso()
|
|
194
|
+
|
|
195
|
+
def transition(self, to: str, ledger: OperationLedger,
|
|
196
|
+
actor: str | None = None, data: dict[str, Any] | None = None
|
|
197
|
+
) -> dict[str, Any]:
|
|
198
|
+
"""§90–91 — valid transitions only; anything else refuses."""
|
|
199
|
+
if to not in STATES:
|
|
200
|
+
return {"refusal": "PF-OPS-UNKNOWN-STATE", "unlock":
|
|
201
|
+
f"state must be one of {STATES}"}
|
|
202
|
+
if to not in TRANSITIONS.get(self.state, set()):
|
|
203
|
+
return {"refusal": "PF-OPS-BAD-TRANSITION",
|
|
204
|
+
"unlock": f"{self.state} → {to} is not a valid "
|
|
205
|
+
f"transition; valid: "
|
|
206
|
+
f"{sorted(TRANSITIONS.get(self.state, []))}"}
|
|
207
|
+
frm = self.state
|
|
208
|
+
self.state = to
|
|
209
|
+
rec = {"from": frm, "to": to, "at": now_iso()}
|
|
210
|
+
self.history.append(rec)
|
|
211
|
+
ledger.append(f"op.{to}", self.operation_id,
|
|
212
|
+
actor=actor or self.actor,
|
|
213
|
+
data={"from": frm, "to": to, **(data or {})})
|
|
214
|
+
return {"ok": True, "from": frm, "to": to, "state": to}
|
|
215
|
+
|
|
216
|
+
def to_dict(self) -> dict[str, Any]:
|
|
217
|
+
return {"operation_id": self.operation_id,
|
|
218
|
+
"intent_id": self.intent_id, "plan_hash": self.plan_hash,
|
|
219
|
+
"envelope_hash": self.envelope_hash, "state": self.state,
|
|
220
|
+
"actor": self.actor, "resources": self.resources,
|
|
221
|
+
"idempotency_key": self.idempotency_key,
|
|
222
|
+
"history": self.history,
|
|
223
|
+
"created_at": self.created_at,
|
|
224
|
+
"steps": [{"step_id": s.step_id, "action": s.action,
|
|
225
|
+
"status": s.status, "attempt": s.attempt}
|
|
226
|
+
for s in self.steps]}
|
|
227
|
+
|
|
228
|
+
@staticmethod
|
|
229
|
+
def from_dict(d: dict[str, Any]) -> Operation:
|
|
230
|
+
op = Operation(operation_id=d.get("operation_id", ""),
|
|
231
|
+
intent_id=d.get("intent_id", ""),
|
|
232
|
+
plan_hash=d.get("plan_hash", ""),
|
|
233
|
+
envelope_hash=d.get("envelope_hash", ""),
|
|
234
|
+
state=d.get("state", "draft"),
|
|
235
|
+
actor=d.get("actor", "system"),
|
|
236
|
+
resources=list(d.get("resources", [])),
|
|
237
|
+
history=list(d.get("history", [])),
|
|
238
|
+
idempotency_key=d.get("idempotency_key", ""),
|
|
239
|
+
created_at=d.get("created_at", ""),
|
|
240
|
+
meta=dict(d.get("meta", {})))
|
|
241
|
+
for s in d.get("steps", []):
|
|
242
|
+
op.steps.append(ExecutionStep(
|
|
243
|
+
step_id=s.get("step_id", ""), action=s.get("action", ""),
|
|
244
|
+
status=s.get("status", "pending"),
|
|
245
|
+
attempt=s.get("attempt", 0)))
|
|
246
|
+
return op
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@dataclass
|
|
250
|
+
class OperationLock:
|
|
251
|
+
"""§99–100 — per-resource mutual exclusion."""
|
|
252
|
+
resource: str = ""
|
|
253
|
+
operation_id: str = ""
|
|
254
|
+
acquired_at: str = ""
|
|
255
|
+
|
|
256
|
+
def to_dict(self) -> dict[str, Any]:
|
|
257
|
+
return {"resource": self.resource,
|
|
258
|
+
"operation_id": self.operation_id,
|
|
259
|
+
"acquired_at": self.acquired_at}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class LockTable:
|
|
263
|
+
"""Conflicting operations block/queue/arbitrate — never race."""
|
|
264
|
+
|
|
265
|
+
def __init__(self):
|
|
266
|
+
self._locks: dict[str, OperationLock] = {}
|
|
267
|
+
|
|
268
|
+
def acquire(self, resources: list[str], op_id: str,
|
|
269
|
+
ledger: OperationLedger | None = None
|
|
270
|
+
) -> dict[str, Any]:
|
|
271
|
+
held = [r for r in resources
|
|
272
|
+
if r in self._locks
|
|
273
|
+
and self._locks[r].operation_id != op_id]
|
|
274
|
+
if held:
|
|
275
|
+
return {"refusal": "PF-OPS-LOCK-CONFLICT",
|
|
276
|
+
"unlock": "resources locked by operations "
|
|
277
|
+
f"{sorted({self._locks[r].operation_id for r in held})} "
|
|
278
|
+
"— wait, queue, or arbitrate",
|
|
279
|
+
"held": held}
|
|
280
|
+
for r in resources:
|
|
281
|
+
self._locks[r] = OperationLock(resource=r, operation_id=op_id,
|
|
282
|
+
acquired_at=now_iso())
|
|
283
|
+
if ledger:
|
|
284
|
+
ledger.append("lock.acquired", op_id,
|
|
285
|
+
data={"resources": resources})
|
|
286
|
+
return {"ok": True, "locked": resources}
|
|
287
|
+
|
|
288
|
+
def release(self, op_id: str,
|
|
289
|
+
ledger: OperationLedger | None = None) -> list[str]:
|
|
290
|
+
freed = [r for r, l in self._locks.items()
|
|
291
|
+
if l.operation_id == op_id]
|
|
292
|
+
for r in freed:
|
|
293
|
+
del self._locks[r]
|
|
294
|
+
if ledger and freed:
|
|
295
|
+
ledger.append("lock.released", op_id, data={"resources": freed})
|
|
296
|
+
return sorted(freed)
|
|
297
|
+
|
|
298
|
+
def held_by(self, resource: str) -> str | None:
|
|
299
|
+
l = self._locks.get(resource)
|
|
300
|
+
return l.operation_id if l else None
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def resume(operation: Operation, ledger: OperationLedger
|
|
304
|
+
) -> dict[str, Any]:
|
|
305
|
+
"""§102 — resume after interruption: replay the ledger, revalidate,
|
|
306
|
+
never blind-reexecute. Steps with a stored completed receipt are
|
|
307
|
+
skipped; running/failed steps must be re-planned."""
|
|
308
|
+
entries = ledger.for_operation(operation.operation_id)
|
|
309
|
+
done_steps = {e.data.get("step_id") for e in entries
|
|
310
|
+
if e.event == "step.completed"}
|
|
311
|
+
plan_steps = {s.step_id for s in operation.steps}
|
|
312
|
+
dangling = [e.data.get("step_id") for e in entries
|
|
313
|
+
if e.event == "step.completed"
|
|
314
|
+
and e.data.get("step_id") not in plan_steps]
|
|
315
|
+
ledger.append("resume.attempted", operation.operation_id,
|
|
316
|
+
data={"completed_steps": sorted(done_steps)})
|
|
317
|
+
return {
|
|
318
|
+
"operation_id": operation.operation_id,
|
|
319
|
+
"state": operation.state,
|
|
320
|
+
"completed_steps": sorted(done_steps),
|
|
321
|
+
"pending_steps": sorted(plan_steps - done_steps),
|
|
322
|
+
"dangling_completed": sorted(dangling),
|
|
323
|
+
"requires": (["revalidate-approval", "revalidate-environment",
|
|
324
|
+
"revalidate-observation-freshness"]
|
|
325
|
+
if operation.state in ("executing", "verifying",
|
|
326
|
+
"rolling-back") else []),
|
|
327
|
+
"note": "no step is re-executed without fresh precondition checks"}
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def dedup_check(seen_keys: set[str], key: str) -> bool:
|
|
331
|
+
"""§219 — same idempotency key + same params → no duplicate effect."""
|
|
332
|
+
if key in seen_keys:
|
|
333
|
+
return False
|
|
334
|
+
seen_keys.add(key)
|
|
335
|
+
return True
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Cycle 4 phase R — operational Graphfy projection.
|
|
2
|
+
|
|
3
|
+
Projects an Operation + its ExecutionEnvelope + approvals into graph
|
|
4
|
+
nodes/edges. Operational edges carry provenance=observed, a
|
|
5
|
+
`layer="operation"` evidence record, and cite the ledger receipt/event
|
|
6
|
+
hashes — execution history stays queryable but separable from the
|
|
7
|
+
platform dependency graph (kind-prefix `operation:` on the layer).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from platformforge.graph.model import Edge, Graph, Node
|
|
15
|
+
|
|
16
|
+
LAYER = "operation"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _node(kind: str, label: str, attrs: dict[str, Any] | None = None,
|
|
20
|
+
fact_ids=()) -> Node:
|
|
21
|
+
return Node.make(kind, label, attrs, fact_ids)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _entry_index(ledger) -> dict[str, list]:
|
|
25
|
+
"""event-name → [entry] so every edge cites the ledger receipt(s)
|
|
26
|
+
that prove it — not a vague tail slice (cycle 4.1 §I)."""
|
|
27
|
+
idx: dict[str, list] = {}
|
|
28
|
+
for e in (ledger.entries if ledger else []):
|
|
29
|
+
idx.setdefault(e.event, []).append(e)
|
|
30
|
+
return idx
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def project_operation(op, envelope=None,
|
|
34
|
+
approvals: list | None = None,
|
|
35
|
+
ledger=None,
|
|
36
|
+
materials: dict | None = None,
|
|
37
|
+
rollback_receipt: dict | None = None
|
|
38
|
+
) -> dict[str, list]:
|
|
39
|
+
"""Build nodes+edges for one operation. `op` is an Operation;
|
|
40
|
+
`envelope` an ExecutionEnvelope or None; `approvals` Approval
|
|
41
|
+
objects; `ledger` an OperationLedger; `materials` captured
|
|
42
|
+
RollbackMaterial per step_id; `rollback_receipt` the §40 receipt.
|
|
43
|
+
|
|
44
|
+
Completeness contract (cycle 4.1 §I): every node has ≥1 edge
|
|
45
|
+
(no orphans), every edge cites its evidence — receipt entry
|
|
46
|
+
hashes for observed edges, envelope hash for planned ones,
|
|
47
|
+
approval subject_hash for approvals, material_hash for mutated
|
|
48
|
+
resources."""
|
|
49
|
+
nodes: list[Node] = []
|
|
50
|
+
edges: list[Edge] = []
|
|
51
|
+
idx = _entry_index(ledger)
|
|
52
|
+
|
|
53
|
+
def receipts_of(*events: str, tail: int = 5) -> list[str]:
|
|
54
|
+
hs = [e.entry_hash for ev in events
|
|
55
|
+
for e in idx.get(ev, [])]
|
|
56
|
+
return hs[-tail:] or [e.entry_hash for e in
|
|
57
|
+
(ledger.entries if ledger else
|
|
58
|
+
[])][-tail:]
|
|
59
|
+
|
|
60
|
+
op_node = _node("operation", op.operation_id,
|
|
61
|
+
{"state": op.state, "actor": op.actor})
|
|
62
|
+
nodes.append(op_node)
|
|
63
|
+
|
|
64
|
+
intent_node = _node("change_intent", getattr(op, "intent_id", "")
|
|
65
|
+
or (envelope.intent_id if envelope else "unknown"))
|
|
66
|
+
nodes.append(intent_node)
|
|
67
|
+
edges.append(Edge(op_node.node_id, intent_node.node_id, "intends",
|
|
68
|
+
provenance="observed",
|
|
69
|
+
evidence=({"provenance": "observed",
|
|
70
|
+
"layer": LAYER,
|
|
71
|
+
"receipt_ids": receipts_of(
|
|
72
|
+
"planned", "intent.received",
|
|
73
|
+
"policy.decided")},)))
|
|
74
|
+
|
|
75
|
+
if envelope is not None:
|
|
76
|
+
env_node = _node("execution_envelope", envelope.execution_id,
|
|
77
|
+
{"executor": envelope.executor,
|
|
78
|
+
"hash": envelope.hash()})
|
|
79
|
+
nodes.append(env_node)
|
|
80
|
+
edges.append(Edge(op_node.node_id, env_node.node_id,
|
|
81
|
+
"executed_by", provenance="observed",
|
|
82
|
+
evidence=({"provenance": "observed",
|
|
83
|
+
"layer": LAYER,
|
|
84
|
+
"receipt_ids": receipts_of(
|
|
85
|
+
"step.completed",
|
|
86
|
+
"executed")},)))
|
|
87
|
+
mats = materials or {}
|
|
88
|
+
# per-step mutation edges — cite step receipt + material so the
|
|
89
|
+
# `mutates` claim is auditable to the captured pre-state
|
|
90
|
+
step_rcpt = {e.data.get("step_id"): e.data.get("receipt")
|
|
91
|
+
for e in idx.get("step.completed", [])
|
|
92
|
+
if not e.data.get("rollback")}
|
|
93
|
+
scope_set = set(envelope.scope or ())
|
|
94
|
+
covered: set[str] = set()
|
|
95
|
+
for a in envelope.actions:
|
|
96
|
+
sid = a.get("step_id", "")
|
|
97
|
+
res = (a.get("params") or {}).get(
|
|
98
|
+
"resource") or next(iter(scope_set), "unknown")
|
|
99
|
+
covered.add(res)
|
|
100
|
+
res_node = _node("workload", res)
|
|
101
|
+
nodes.append(res_node)
|
|
102
|
+
m = mats.get(sid)
|
|
103
|
+
mh = (m.hash() if hasattr(m, "hash") else
|
|
104
|
+
(m or {}).get("hash"))
|
|
105
|
+
edges.append(Edge(env_node.node_id, res_node.node_id,
|
|
106
|
+
"mutates", provenance="planned",
|
|
107
|
+
confidence=0.9,
|
|
108
|
+
evidence=({"provenance": "planned",
|
|
109
|
+
"layer": LAYER,
|
|
110
|
+
"step_id": sid,
|
|
111
|
+
"action": a.get("action"),
|
|
112
|
+
"envelope_hash":
|
|
113
|
+
envelope.hash(),
|
|
114
|
+
"step_receipt":
|
|
115
|
+
step_rcpt.get(sid),
|
|
116
|
+
"material_hash": mh},)))
|
|
117
|
+
# scope-only resources still get an edge (no orphan nodes)
|
|
118
|
+
for res in scope_set - covered:
|
|
119
|
+
res_node = _node("workload", res)
|
|
120
|
+
if res_node.node_id not in {n.node_id for n in nodes}:
|
|
121
|
+
nodes.append(res_node)
|
|
122
|
+
edges.append(Edge(env_node.node_id, res_node.node_id,
|
|
123
|
+
"mutates", provenance="planned",
|
|
124
|
+
confidence=0.9,
|
|
125
|
+
evidence=({"provenance": "planned",
|
|
126
|
+
"layer": LAYER,
|
|
127
|
+
"resource": res,
|
|
128
|
+
"envelope_hash":
|
|
129
|
+
envelope.hash()},)))
|
|
130
|
+
|
|
131
|
+
for ap in approvals or []:
|
|
132
|
+
ap_node = _node("approval", ap.approval_id,
|
|
133
|
+
{"actor": ap.actor, "role": ap.role,
|
|
134
|
+
"type": ap.type})
|
|
135
|
+
nodes.append(ap_node)
|
|
136
|
+
edges.append(Edge(op_node.node_id, ap_node.node_id,
|
|
137
|
+
"approved_by", provenance="observed",
|
|
138
|
+
evidence=({"provenance": "observed",
|
|
139
|
+
"layer": LAYER,
|
|
140
|
+
"subject_hash": ap.subject_hash,
|
|
141
|
+
"receipt_ids": receipts_of(
|
|
142
|
+
"approval.recorded",
|
|
143
|
+
"approval.denied")},)))
|
|
144
|
+
|
|
145
|
+
# rollback — rolled_back_by edge cites the rollback receipt +
|
|
146
|
+
# material hashes so the reversal claim traces to captured state
|
|
147
|
+
rb_entries = idx.get("rollback.completed", [])
|
|
148
|
+
if rb_entries or rollback_receipt:
|
|
149
|
+
rec = rollback_receipt or {}
|
|
150
|
+
rb_node = _node("rollback", f"{op.operation_id}-rollback",
|
|
151
|
+
{"strategy": rec.get("strategy") or
|
|
152
|
+
(rb_entries[-1].data.get("strategy")
|
|
153
|
+
if rb_entries else ""),
|
|
154
|
+
"result": rec.get("result") or
|
|
155
|
+
(rb_entries[-1].data.get(
|
|
156
|
+
"rollback_verification")
|
|
157
|
+
if rb_entries else "")})
|
|
158
|
+
nodes.append(rb_node)
|
|
159
|
+
edges.append(Edge(op_node.node_id, rb_node.node_id,
|
|
160
|
+
"rolled_back_by", provenance="observed",
|
|
161
|
+
evidence=({"provenance": "observed",
|
|
162
|
+
"layer": LAYER,
|
|
163
|
+
"trigger": rec.get("trigger"),
|
|
164
|
+
"material_hashes":
|
|
165
|
+
rec.get("material_hashes", []),
|
|
166
|
+
"receipt_ids": [
|
|
167
|
+
e.entry_hash for e in
|
|
168
|
+
rb_entries[-3:]]},)))
|
|
169
|
+
|
|
170
|
+
return {"nodes": nodes, "edges": edges}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def validate_projection(projection: dict[str, list]) -> list[str]:
|
|
174
|
+
"""§115–118 — completeness invariants over a projection:
|
|
175
|
+
- every observed edge cites ≥1 receipt/event hash
|
|
176
|
+
- no orphan nodes (every node touched by ≥1 edge)
|
|
177
|
+
- approved_by edges cite an approval subject_hash
|
|
178
|
+
- mutates edges cite the affected resource + envelope
|
|
179
|
+
Returns a list of gap strings; empty == complete."""
|
|
180
|
+
gaps: list[str] = []
|
|
181
|
+
nodes = projection["nodes"]
|
|
182
|
+
edges = projection["edges"]
|
|
183
|
+
touched = {e.src for e in edges} | {e.dst for e in edges}
|
|
184
|
+
for n in nodes:
|
|
185
|
+
if n.node_id not in touched:
|
|
186
|
+
gaps.append(f"orphan-node:{n.kind}:{n.label}")
|
|
187
|
+
for e in edges:
|
|
188
|
+
ev = e.layers()[0] if e.layers() else {}
|
|
189
|
+
prov = ev.get("provenance", "")
|
|
190
|
+
if prov == "observed" and not (
|
|
191
|
+
ev.get("receipt_ids") or ev.get("subject_hash")):
|
|
192
|
+
gaps.append(f"edge-no-receipt:{e.kind}:{e.src}")
|
|
193
|
+
if e.kind == "approved_by" and not ev.get("subject_hash"):
|
|
194
|
+
gaps.append(f"approved_by-missing-approval:{e.dst}")
|
|
195
|
+
if e.kind == "mutates" and not (
|
|
196
|
+
ev.get("envelope_hash") and
|
|
197
|
+
(ev.get("step_id") or ev.get("resource"))):
|
|
198
|
+
gaps.append(f"mutates-missing-evidence:{e.dst}")
|
|
199
|
+
return gaps
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def apply_operation_projection(graph: Graph, projection: dict[str, list]
|
|
203
|
+
) -> dict[str, int]:
|
|
204
|
+
"""Fold an operation projection into a graph — merges like any
|
|
205
|
+
other layer via evidence records."""
|
|
206
|
+
for n in projection["nodes"]:
|
|
207
|
+
graph.add_node(n)
|
|
208
|
+
for e in projection["edges"]:
|
|
209
|
+
graph.add_edge(e)
|
|
210
|
+
return {"nodes_added": len(projection["nodes"]),
|
|
211
|
+
"edges_added": len(projection["edges"])}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def operation_only(graph: Graph) -> Graph:
|
|
215
|
+
"""Query helper — subgraph of the operational layer only.
|
|
216
|
+
Non-operational endpoint nodes (e.g. a mutated workload) are
|
|
217
|
+
carried in so edges stay resolvable."""
|
|
218
|
+
sub = Graph()
|
|
219
|
+
for n in graph.nodes.values():
|
|
220
|
+
if n.kind in ("operation", "change_intent", "change_plan",
|
|
221
|
+
"execution_envelope", "approval", "runbook",
|
|
222
|
+
"rollback", "platform_request",
|
|
223
|
+
"policy_decision"):
|
|
224
|
+
sub.add_node(n)
|
|
225
|
+
for e in graph.edges.values():
|
|
226
|
+
if e.kind in ("intends", "planned_by", "mutates", "approved_by",
|
|
227
|
+
"executed_by", "verifies", "rolled_back_by",
|
|
228
|
+
"requested_by", "produced_receipt"):
|
|
229
|
+
for nid in (e.src, e.dst):
|
|
230
|
+
if nid not in sub.nodes and nid in graph.nodes:
|
|
231
|
+
sub.add_node(graph.nodes[nid])
|
|
232
|
+
sub.add_edge(e)
|
|
233
|
+
return sub
|