@guyghost/swarm-dao-core 0.3.0 → 0.5.0
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.
- package/dist/adapters/attention/fs-attention.store.d.ts +17 -0
- package/dist/adapters/attention/fs-attention.store.d.ts.map +1 -0
- package/dist/adapters/attention/fs-attention.store.js +59 -0
- package/dist/adapters/attention/fs-attention.store.js.map +1 -0
- package/dist/adapters/git-workspace.d.ts +26 -0
- package/dist/adapters/git-workspace.d.ts.map +1 -0
- package/dist/adapters/git-workspace.js +84 -0
- package/dist/adapters/git-workspace.js.map +1 -0
- package/dist/adapters/index.d.ts +6 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +6 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/persistence/file-dao-state.repository.d.ts +15 -0
- package/dist/adapters/persistence/file-dao-state.repository.d.ts.map +1 -0
- package/dist/adapters/persistence/file-dao-state.repository.js +85 -0
- package/dist/adapters/persistence/file-dao-state.repository.js.map +1 -0
- package/dist/adapters/persistence/in-memory-dao-state.repository.d.ts +10 -0
- package/dist/adapters/persistence/in-memory-dao-state.repository.d.ts.map +1 -0
- package/dist/adapters/persistence/in-memory-dao-state.repository.js +14 -0
- package/dist/adapters/persistence/in-memory-dao-state.repository.js.map +1 -0
- package/dist/adapters/persistence/legacy-dao-state.repository.d.ts +11 -0
- package/dist/adapters/persistence/legacy-dao-state.repository.d.ts.map +1 -0
- package/dist/adapters/persistence/legacy-dao-state.repository.js +14 -0
- package/dist/adapters/persistence/legacy-dao-state.repository.js.map +1 -0
- package/dist/application/index.d.ts +16 -0
- package/dist/application/index.d.ts.map +1 -0
- package/dist/application/index.js +16 -0
- package/dist/application/index.js.map +1 -0
- package/dist/application/initialize-dao.use-case.d.ts +20 -0
- package/dist/application/initialize-dao.use-case.d.ts.map +1 -0
- package/dist/application/initialize-dao.use-case.js +17 -0
- package/dist/application/initialize-dao.use-case.js.map +1 -0
- package/dist/application/proposals/control-proposal.use-case.d.ts +23 -0
- package/dist/application/proposals/control-proposal.use-case.d.ts.map +1 -0
- package/dist/application/proposals/control-proposal.use-case.js +53 -0
- package/dist/application/proposals/control-proposal.use-case.js.map +1 -0
- package/dist/application/proposals/create-amendment-proposal.use-case.d.ts +25 -0
- package/dist/application/proposals/create-amendment-proposal.use-case.d.ts.map +1 -0
- package/dist/application/proposals/create-amendment-proposal.use-case.js +39 -0
- package/dist/application/proposals/create-amendment-proposal.use-case.js.map +1 -0
- package/dist/application/proposals/create-proposal.use-case.d.ts +34 -0
- package/dist/application/proposals/create-proposal.use-case.d.ts.map +1 -0
- package/dist/application/proposals/create-proposal.use-case.js +49 -0
- package/dist/application/proposals/create-proposal.use-case.js.map +1 -0
- package/dist/application/proposals/deliberate-proposal.use-case.d.ts +35 -0
- package/dist/application/proposals/deliberate-proposal.use-case.d.ts.map +1 -0
- package/dist/application/proposals/deliberate-proposal.use-case.js +67 -0
- package/dist/application/proposals/deliberate-proposal.use-case.js.map +1 -0
- package/dist/application/proposals/dry-run-proposal.use-case.d.ts +21 -0
- package/dist/application/proposals/dry-run-proposal.use-case.d.ts.map +1 -0
- package/dist/application/proposals/dry-run-proposal.use-case.js +20 -0
- package/dist/application/proposals/dry-run-proposal.use-case.js.map +1 -0
- package/dist/application/proposals/execute-proposal.use-case.d.ts +29 -0
- package/dist/application/proposals/execute-proposal.use-case.d.ts.map +1 -0
- package/dist/application/proposals/execute-proposal.use-case.js +66 -0
- package/dist/application/proposals/execute-proposal.use-case.js.map +1 -0
- package/dist/application/proposals/rate-proposal.use-case.d.ts +24 -0
- package/dist/application/proposals/rate-proposal.use-case.d.ts.map +1 -0
- package/dist/application/proposals/rate-proposal.use-case.js +33 -0
- package/dist/application/proposals/rate-proposal.use-case.js.map +1 -0
- package/dist/application/proposals/record-deliberation-outputs.use-case.d.ts +21 -0
- package/dist/application/proposals/record-deliberation-outputs.use-case.d.ts.map +1 -0
- package/dist/application/proposals/record-deliberation-outputs.use-case.js +65 -0
- package/dist/application/proposals/record-deliberation-outputs.use-case.js.map +1 -0
- package/dist/application/proposals/rollback-proposal.use-case.d.ts +20 -0
- package/dist/application/proposals/rollback-proposal.use-case.d.ts.map +1 -0
- package/dist/application/proposals/rollback-proposal.use-case.js +17 -0
- package/dist/application/proposals/rollback-proposal.use-case.js.map +1 -0
- package/dist/application/proposals/round-table.use-case.d.ts +27 -0
- package/dist/application/proposals/round-table.use-case.d.ts.map +1 -0
- package/dist/application/proposals/round-table.use-case.js +55 -0
- package/dist/application/proposals/round-table.use-case.js.map +1 -0
- package/dist/application/proposals/ship-proposal.use-case.d.ts +26 -0
- package/dist/application/proposals/ship-proposal.use-case.d.ts.map +1 -0
- package/dist/application/proposals/ship-proposal.use-case.js +60 -0
- package/dist/application/proposals/ship-proposal.use-case.js.map +1 -0
- package/dist/application/proposals/start-deliberation.use-case.d.ts +21 -0
- package/dist/application/proposals/start-deliberation.use-case.d.ts.map +1 -0
- package/dist/application/proposals/start-deliberation.use-case.js +30 -0
- package/dist/application/proposals/start-deliberation.use-case.js.map +1 -0
- package/dist/application/proposals/transition-proposal.use-case.d.ts +20 -0
- package/dist/application/proposals/transition-proposal.use-case.d.ts.map +1 -0
- package/dist/application/proposals/transition-proposal.use-case.js +19 -0
- package/dist/application/proposals/transition-proposal.use-case.js.map +1 -0
- package/dist/application/proposals/update-proposal.use-case.d.ts +26 -0
- package/dist/application/proposals/update-proposal.use-case.d.ts.map +1 -0
- package/dist/application/proposals/update-proposal.use-case.js +19 -0
- package/dist/application/proposals/update-proposal.use-case.js.map +1 -0
- package/dist/commands/index.d.ts +2 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +2 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/registry.d.ts +46 -0
- package/dist/commands/registry.d.ts.map +1 -0
- package/dist/commands/registry.js +367 -0
- package/dist/commands/registry.js.map +1 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +9 -1
- package/dist/config.js.map +1 -1
- package/dist/control/gates.d.ts +4 -1
- package/dist/control/gates.d.ts.map +1 -1
- package/dist/control/gates.js +22 -10
- package/dist/control/gates.js.map +1 -1
- package/dist/delivery/dependencies.d.ts.map +1 -1
- package/dist/delivery/dependencies.js +15 -7
- package/dist/delivery/dependencies.js.map +1 -1
- package/dist/delivery/dry-run.d.ts.map +1 -1
- package/dist/delivery/dry-run.js +3 -44
- package/dist/delivery/dry-run.js.map +1 -1
- package/dist/delivery/execution-isolation.d.ts +34 -0
- package/dist/delivery/execution-isolation.d.ts.map +1 -0
- package/dist/delivery/execution-isolation.js +97 -0
- package/dist/delivery/execution-isolation.js.map +1 -0
- package/dist/delivery/execution.d.ts.map +1 -1
- package/dist/delivery/execution.js +7 -5
- package/dist/delivery/execution.js.map +1 -1
- package/dist/delivery/index.d.ts +1 -0
- package/dist/delivery/index.d.ts.map +1 -1
- package/dist/delivery/index.js +1 -0
- package/dist/delivery/index.js.map +1 -1
- package/dist/delivery/plans.d.ts +3 -1
- package/dist/delivery/plans.d.ts.map +1 -1
- package/dist/delivery/plans.js +2 -2
- package/dist/delivery/plans.js.map +1 -1
- package/dist/domain/dry-run.d.ts +3 -0
- package/dist/domain/dry-run.d.ts.map +1 -0
- package/dist/domain/dry-run.js +28 -0
- package/dist/domain/dry-run.js.map +1 -0
- package/dist/domain/index.d.ts +6 -0
- package/dist/domain/index.d.ts.map +1 -0
- package/dist/domain/index.js +7 -0
- package/dist/domain/index.js.map +1 -0
- package/dist/governance/agents.d.ts +2 -0
- package/dist/governance/agents.d.ts.map +1 -1
- package/dist/governance/agents.js +148 -173
- package/dist/governance/agents.js.map +1 -1
- package/dist/governance/charter.d.ts +14 -0
- package/dist/governance/charter.d.ts.map +1 -0
- package/dist/governance/charter.js +65 -0
- package/dist/governance/charter.js.map +1 -0
- package/dist/governance/delegation.machine.d.ts +316 -0
- package/dist/governance/delegation.machine.d.ts.map +1 -0
- package/dist/governance/delegation.machine.js +387 -0
- package/dist/governance/delegation.machine.js.map +1 -0
- package/dist/governance/delegation.utils.d.ts +55 -0
- package/dist/governance/delegation.utils.d.ts.map +1 -0
- package/dist/governance/delegation.utils.js +157 -0
- package/dist/governance/delegation.utils.js.map +1 -0
- package/dist/governance/edit-gate.d.ts +47 -0
- package/dist/governance/edit-gate.d.ts.map +1 -0
- package/dist/governance/edit-gate.js +156 -0
- package/dist/governance/edit-gate.js.map +1 -0
- package/dist/governance/index.d.ts +3 -0
- package/dist/governance/index.d.ts.map +1 -1
- package/dist/governance/index.js +3 -0
- package/dist/governance/index.js.map +1 -1
- package/dist/governance/lifecycle.d.ts +0 -6
- package/dist/governance/lifecycle.d.ts.map +1 -1
- package/dist/governance/lifecycle.js +7 -31
- package/dist/governance/lifecycle.js.map +1 -1
- package/dist/governance/proposal.machine.d.ts +2 -130
- package/dist/governance/proposal.machine.d.ts.map +1 -1
- package/dist/governance/proposal.machine.js +2 -377
- package/dist/governance/proposal.machine.js.map +1 -1
- package/dist/governance/proposal.utils.d.ts +22 -165
- package/dist/governance/proposal.utils.d.ts.map +1 -1
- package/dist/governance/proposal.utils.js +52 -119
- package/dist/governance/proposal.utils.js.map +1 -1
- package/dist/governance/scoring.d.ts.map +1 -1
- package/dist/governance/scoring.js +36 -16
- package/dist/governance/scoring.js.map +1 -1
- package/dist/governance/voting.d.ts.map +1 -1
- package/dist/governance/voting.js +24 -14
- package/dist/governance/voting.js.map +1 -1
- package/dist/host-tools/github-config.d.ts.map +1 -1
- package/dist/host-tools/github-config.js +17 -5
- package/dist/host-tools/github-config.js.map +1 -1
- package/dist/host-tools/handlers.d.ts +14 -11
- package/dist/host-tools/handlers.d.ts.map +1 -1
- package/dist/host-tools/handlers.js +204 -335
- package/dist/host-tools/handlers.js.map +1 -1
- package/dist/host-tools/messages.d.ts +10 -4
- package/dist/host-tools/messages.d.ts.map +1 -1
- package/dist/host-tools/messages.js +14 -17
- package/dist/host-tools/messages.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/dist/integrations/github.d.ts.map +1 -1
- package/dist/integrations/github.js +5 -2
- package/dist/integrations/github.js.map +1 -1
- package/dist/integrations/http.d.ts +12 -1
- package/dist/integrations/http.d.ts.map +1 -1
- package/dist/integrations/http.js +114 -17
- package/dist/integrations/http.js.map +1 -1
- package/dist/intelligence/delegation.d.ts +41 -0
- package/dist/intelligence/delegation.d.ts.map +1 -0
- package/dist/intelligence/delegation.js +162 -0
- package/dist/intelligence/delegation.js.map +1 -0
- package/dist/intelligence/index.d.ts +2 -0
- package/dist/intelligence/index.d.ts.map +1 -1
- package/dist/intelligence/index.js +2 -0
- package/dist/intelligence/index.js.map +1 -1
- package/dist/intelligence/model.d.ts +28 -2
- package/dist/intelligence/model.d.ts.map +1 -1
- package/dist/intelligence/model.js +36 -3
- package/dist/intelligence/model.js.map +1 -1
- package/dist/intelligence/roundtable.d.ts +4 -2
- package/dist/intelligence/roundtable.d.ts.map +1 -1
- package/dist/intelligence/roundtable.js +2 -2
- package/dist/intelligence/roundtable.js.map +1 -1
- package/dist/intelligence/sequential.d.ts +36 -0
- package/dist/intelligence/sequential.d.ts.map +1 -0
- package/dist/intelligence/sequential.js +91 -0
- package/dist/intelligence/sequential.js.map +1 -0
- package/dist/intelligence/swarm.d.ts +19 -4
- package/dist/intelligence/swarm.d.ts.map +1 -1
- package/dist/intelligence/swarm.js +107 -53
- package/dist/intelligence/swarm.js.map +1 -1
- package/dist/intelligence/synthesis.d.ts.map +1 -1
- package/dist/intelligence/synthesis.js +16 -4
- package/dist/intelligence/synthesis.js.map +1 -1
- package/dist/models/graph-engineering.machine.d.ts +219 -0
- package/dist/models/graph-engineering.machine.d.ts.map +1 -0
- package/dist/models/graph-engineering.machine.js +214 -0
- package/dist/models/graph-engineering.machine.js.map +1 -0
- package/dist/models/improvement-loop.machine.d.ts +239 -0
- package/dist/models/improvement-loop.machine.d.ts.map +1 -0
- package/dist/models/improvement-loop.machine.js +254 -0
- package/dist/models/improvement-loop.machine.js.map +1 -0
- package/dist/models/index.d.ts +5 -0
- package/dist/models/index.d.ts.map +1 -0
- package/dist/models/index.js +5 -0
- package/dist/models/index.js.map +1 -0
- package/dist/models/product-loop.machine.d.ts +531 -0
- package/dist/models/product-loop.machine.d.ts.map +1 -0
- package/dist/models/product-loop.machine.js +699 -0
- package/dist/models/product-loop.machine.js.map +1 -0
- package/dist/models/proposal.machine.d.ts +82 -0
- package/dist/models/proposal.machine.d.ts.map +1 -0
- package/dist/models/proposal.machine.js +68 -0
- package/dist/models/proposal.machine.js.map +1 -0
- package/dist/observability/attention.d.ts +40 -0
- package/dist/observability/attention.d.ts.map +1 -0
- package/dist/observability/attention.js +119 -0
- package/dist/observability/attention.js.map +1 -0
- package/dist/observability/index.d.ts +1 -0
- package/dist/observability/index.d.ts.map +1 -1
- package/dist/observability/index.js +1 -0
- package/dist/observability/index.js.map +1 -1
- package/dist/observability/logging.d.ts +6 -0
- package/dist/observability/logging.d.ts.map +1 -1
- package/dist/observability/logging.js +36 -6
- package/dist/observability/logging.js.map +1 -1
- package/dist/observability/metrics.d.ts +7 -0
- package/dist/observability/metrics.d.ts.map +1 -1
- package/dist/observability/metrics.js +179 -43
- package/dist/observability/metrics.js.map +1 -1
- package/dist/persistence.d.ts +5 -6
- package/dist/persistence.d.ts.map +1 -1
- package/dist/persistence.js +197 -140
- package/dist/persistence.js.map +1 -1
- package/dist/ports/clock.d.ts +7 -0
- package/dist/ports/clock.d.ts.map +1 -0
- package/dist/ports/clock.js +5 -0
- package/dist/ports/clock.js.map +1 -0
- package/dist/ports/host.d.ts +8 -0
- package/dist/ports/host.d.ts.map +1 -0
- package/dist/ports/host.js +2 -0
- package/dist/ports/host.js.map +1 -0
- package/dist/ports/index.d.ts +5 -0
- package/dist/ports/index.d.ts.map +1 -0
- package/dist/ports/index.js +5 -0
- package/dist/ports/index.js.map +1 -0
- package/dist/ports/repository.d.ts +7 -0
- package/dist/ports/repository.d.ts.map +1 -0
- package/dist/ports/repository.js +2 -0
- package/dist/ports/repository.js.map +1 -0
- package/dist/ports/workspace.d.ts +14 -0
- package/dist/ports/workspace.d.ts.map +1 -0
- package/dist/ports/workspace.js +14 -0
- package/dist/ports/workspace.js.map +1 -0
- package/dist/presenters/index.d.ts +2 -0
- package/dist/presenters/index.d.ts.map +1 -0
- package/dist/presenters/index.js +2 -0
- package/dist/presenters/index.js.map +1 -0
- package/dist/presenters/proposal.presenter.d.ts +30 -0
- package/dist/presenters/proposal.presenter.d.ts.map +1 -0
- package/dist/presenters/proposal.presenter.js +47 -0
- package/dist/presenters/proposal.presenter.js.map +1 -0
- package/dist/types/index.d.ts +70 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +12 -0
- package/dist/types/index.js.map +1 -1
- package/dist/utils/host.d.ts.map +1 -1
- package/dist/utils/host.js +77 -6
- package/dist/utils/host.js.map +1 -1
- package/dist/utils/security.d.ts +7 -1
- package/dist/utils/security.d.ts.map +1 -1
- package/dist/utils/security.js +18 -2
- package/dist/utils/security.js.map +1 -1
- package/package.json +13 -1
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
import { assign, createActor, setup } from "xstate";
|
|
2
|
+
export const REQUIRED_PRODUCT_ANCHORS = [
|
|
3
|
+
"qualification-passed",
|
|
4
|
+
"vote-quorum",
|
|
5
|
+
"budget-envelope",
|
|
6
|
+
"controls-passed",
|
|
7
|
+
"auto-ship-policy",
|
|
8
|
+
"observation-window",
|
|
9
|
+
"rollback-path-exists",
|
|
10
|
+
"frozen-set-intact",
|
|
11
|
+
"regression",
|
|
12
|
+
];
|
|
13
|
+
export const PRODUCT_TERMINAL_STATES = ["validated", "rejected"];
|
|
14
|
+
export const PRODUCT_VOTE_EXPIRY_HOURS = { standard: 72, criticalSecurity: 12 };
|
|
15
|
+
export const PRODUCT_OBSERVATION_CONSECUTIVE_MEASUREMENTS = 3;
|
|
16
|
+
export const PRODUCT_MEMBER_QUOTA_RENEWAL_MINUTES = 300;
|
|
17
|
+
export const PRODUCT_ALLOWED_CATEGORIES = ["performance", "refactor", "tooling", "docs", "security"];
|
|
18
|
+
// Anchors auto-sealed by machine actions (the tool never records these).
|
|
19
|
+
// `rollback-path-exists` is NOT auto-sealed: it is an external, tool-recorded
|
|
20
|
+
// anchor so the ship gate trusts a verified artifact rather than a bare draft
|
|
21
|
+
// string (the verifier records it via ANCHOR_RECORDED only after the rollback
|
|
22
|
+
// command confirms the artifact is genuine).
|
|
23
|
+
export const PRODUCT_AUTO_SEALED_ANCHORS = [
|
|
24
|
+
"qualification-passed",
|
|
25
|
+
"vote-quorum",
|
|
26
|
+
"budget-envelope",
|
|
27
|
+
"controls-passed",
|
|
28
|
+
"auto-ship-policy",
|
|
29
|
+
"observation-window",
|
|
30
|
+
];
|
|
31
|
+
// Anchors the tool records via ANCHOR_RECORDED (the machine never auto-seals these).
|
|
32
|
+
export const PRODUCT_EXTERNAL_ANCHORS = ["frozen-set-intact", "regression", "rollback-path-exists"];
|
|
33
|
+
/**
|
|
34
|
+
* Subset of required anchors that must ALREADY be passed at the VERIFY_EVALUATE
|
|
35
|
+
* ship gate. It deliberately EXCLUDES `observation-window` (sealed only when the
|
|
36
|
+
* observation window completes) and the anchors `sealVerificationAnchors` itself
|
|
37
|
+
* seals when it transitions to `ship` (`controls-passed`, `auto-ship-policy`) —
|
|
38
|
+
* those decisions are re-derived from draft/controls/budget by `canAutoShip`,
|
|
39
|
+
* never read back as their own gate. `rollback-path-exists` IS included because
|
|
40
|
+
* it is now an external, tool-recorded anchor (not sealed by the ship action),
|
|
41
|
+
* so there is no chicken-and-egg: the verifier must have already recorded it.
|
|
42
|
+
*/
|
|
43
|
+
export const PRODUCT_SHIP_GATE_ANCHORS = [
|
|
44
|
+
"qualification-passed",
|
|
45
|
+
"vote-quorum",
|
|
46
|
+
"budget-envelope",
|
|
47
|
+
"frozen-set-intact",
|
|
48
|
+
"regression",
|
|
49
|
+
"rollback-path-exists",
|
|
50
|
+
];
|
|
51
|
+
const isNonEmpty = (value) => typeof value === "string" && value.trim().length > 0;
|
|
52
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
53
|
+
const isProposalDraft = (value) => {
|
|
54
|
+
if (!isRecord(value))
|
|
55
|
+
return false;
|
|
56
|
+
const deps = value.dependencies;
|
|
57
|
+
return (isNonEmpty(value.scope) &&
|
|
58
|
+
PRODUCT_ALLOWED_CATEGORIES.includes(value.category) &&
|
|
59
|
+
typeof value.touchesSensitive === "boolean" &&
|
|
60
|
+
Array.isArray(deps) &&
|
|
61
|
+
deps.every((dep) => isNonEmpty(dep)) &&
|
|
62
|
+
typeof value.budgetAllocation === "number" &&
|
|
63
|
+
value.budgetAllocation >= 0 &&
|
|
64
|
+
isNonEmpty(value.rollbackArtifact) &&
|
|
65
|
+
isNonEmpty(value.evidence));
|
|
66
|
+
};
|
|
67
|
+
export const isRequiredProductAnchor = (value) => typeof value === "string" && REQUIRED_PRODUCT_ANCHORS.includes(value);
|
|
68
|
+
// --- Deterministic policies. Pure. The model — not the tool adapter — decides. ---
|
|
69
|
+
/**
|
|
70
|
+
* Qualification: scope, dependencies, allowed category, affirmative permission
|
|
71
|
+
* clearance, budget presence, and rollback artifact. Permission clearance is an
|
|
72
|
+
* explicit, evidence-backed boolean carried by QUALIFICATION_RUN — qualification
|
|
73
|
+
* never passes on the mere absence of a denial. Sensitive security work may pass
|
|
74
|
+
* (it is auto-votable) but is flagged, which later forbids auto-ship.
|
|
75
|
+
*/
|
|
76
|
+
export const qualifyProposal = (draft, permissionsCleared) => {
|
|
77
|
+
if (!draft)
|
|
78
|
+
return { qualified: false, reason: "missing draft" };
|
|
79
|
+
if (!isNonEmpty(draft.scope))
|
|
80
|
+
return { qualified: false, reason: "empty scope" };
|
|
81
|
+
if (!PRODUCT_ALLOWED_CATEGORIES.includes(draft.category)) {
|
|
82
|
+
return { qualified: false, reason: "category not allowed" };
|
|
83
|
+
}
|
|
84
|
+
if (!Array.isArray(draft.dependencies) || draft.dependencies.some((dep) => !isNonEmpty(dep))) {
|
|
85
|
+
return { qualified: false, reason: "unresolvable dependencies" };
|
|
86
|
+
}
|
|
87
|
+
if (!permissionsCleared)
|
|
88
|
+
return { qualified: false, reason: "permissions not cleared" };
|
|
89
|
+
if (draft.budgetAllocation <= 0)
|
|
90
|
+
return { qualified: false, reason: "no budget allocated" };
|
|
91
|
+
if (!isNonEmpty(draft.rollbackArtifact))
|
|
92
|
+
return { qualified: false, reason: "no rollback artifact" };
|
|
93
|
+
return { qualified: true, reason: "scope, dependencies, category, permissions, and budget validated" };
|
|
94
|
+
};
|
|
95
|
+
/** Vote tally: quorum is a minimum favorable-vote threshold, not participation. */
|
|
96
|
+
export const tallyProductVotes = (favorable, config) => {
|
|
97
|
+
if (!config)
|
|
98
|
+
return { quorumReached: false };
|
|
99
|
+
return { quorumReached: config.quorum > 0 && favorable >= config.quorum };
|
|
100
|
+
};
|
|
101
|
+
/** Budget ledger: remaining = initial - consumed; never negative. */
|
|
102
|
+
export const applyBudgetCharge = (envelope, action) => {
|
|
103
|
+
if (!envelope)
|
|
104
|
+
return { initial: 0, consumed: 0, history: [] };
|
|
105
|
+
const consumed = envelope.consumed + Math.max(0, action.amount);
|
|
106
|
+
return {
|
|
107
|
+
initial: envelope.initial,
|
|
108
|
+
consumed,
|
|
109
|
+
history: [...envelope.history, action],
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
export const budgetRemaining = (envelope) => {
|
|
113
|
+
if (!envelope)
|
|
114
|
+
return 0;
|
|
115
|
+
return Math.max(0, envelope.initial - envelope.consumed);
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* Auto-ship gate. Decides deterministically from the draft + controls + budget.
|
|
119
|
+
* Does NOT depend on an anchor it would itself set (no chicken-and-egg).
|
|
120
|
+
* Sensitive security work is forbidden from auto-ship even if reversible.
|
|
121
|
+
*/
|
|
122
|
+
export const canAutoShip = (context) => {
|
|
123
|
+
const { draft, controls, budget } = context;
|
|
124
|
+
if (!draft)
|
|
125
|
+
return { allowed: false, reason: "missing draft" };
|
|
126
|
+
const controlValues = Object.values(controls);
|
|
127
|
+
if (controlValues.length === 0)
|
|
128
|
+
return { allowed: false, reason: "no controls recorded" };
|
|
129
|
+
if (controlValues.some((c) => c.status !== "passed"))
|
|
130
|
+
return { allowed: false, reason: "a control failed" };
|
|
131
|
+
if (!PRODUCT_ALLOWED_CATEGORIES.includes(draft.category)) {
|
|
132
|
+
return { allowed: false, reason: "category not allowed for auto-ship" };
|
|
133
|
+
}
|
|
134
|
+
if (draft.category === "security" || draft.touchesSensitive) {
|
|
135
|
+
return { allowed: false, reason: "sensitive change requires human review before deploy" };
|
|
136
|
+
}
|
|
137
|
+
if (!isNonEmpty(draft.rollbackArtifact))
|
|
138
|
+
return { allowed: false, reason: "no rollback artifact" };
|
|
139
|
+
if (budgetRemaining(budget) <= 0)
|
|
140
|
+
return { allowed: false, reason: "budget exhausted" };
|
|
141
|
+
return { allowed: true, reason: "reversible, allowed, rollback exists, budget remains, not sensitive" };
|
|
142
|
+
};
|
|
143
|
+
const isSensitiveDraft = (draft) => draft !== null && (draft.category === "security" || draft.touchesSensitive === true);
|
|
144
|
+
/**
|
|
145
|
+
* Human deploy-authorization gate for SENSITIVE changes only. Auto-ship forbids
|
|
146
|
+
* sensitive work; such a change that otherwise passes verification routes to
|
|
147
|
+
* Review, where the human owner may authorize deployment. This gate requires:
|
|
148
|
+
* the draft is sensitive, every control passed, budget remains, and the
|
|
149
|
+
* ship-gate anchors (including the externally verified rollback path) hold.
|
|
150
|
+
* It can never be exercised by an AI source — REVIEW_RESOLVED is human-only.
|
|
151
|
+
*/
|
|
152
|
+
export const canHumanAuthorizeSensitiveDeploy = (context) => {
|
|
153
|
+
const { draft, controls, budget, anchors } = context;
|
|
154
|
+
if (!draft)
|
|
155
|
+
return { allowed: false, reason: "missing draft" };
|
|
156
|
+
if (!isSensitiveDraft(draft))
|
|
157
|
+
return { allowed: false, reason: "deploy-authorized is only for sensitive changes" };
|
|
158
|
+
const controlValues = Object.values(controls);
|
|
159
|
+
if (controlValues.length === 0 || controlValues.some((c) => c.status !== "passed")) {
|
|
160
|
+
return { allowed: false, reason: "every control must pass before deploy" };
|
|
161
|
+
}
|
|
162
|
+
if (budgetRemaining(budget) <= 0)
|
|
163
|
+
return { allowed: false, reason: "budget exhausted" };
|
|
164
|
+
if (!shipGateAnchorsPassed(anchors))
|
|
165
|
+
return { allowed: false, reason: "ship-gate anchors not passed" };
|
|
166
|
+
return { allowed: true, reason: "human-authorized deploy of sensitive, verified, reversible change" };
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* Observation gate. Priorities: errors > aiCost > latency. Satisfaction is a
|
|
170
|
+
* signal only and can NEVER confirm degradation. A single measurement never
|
|
171
|
+
* confirms; require N consecutive threshold-crossing measurements on the
|
|
172
|
+
* highest-priority breached metric.
|
|
173
|
+
*/
|
|
174
|
+
export const evaluateObservation = (samples, _windowElapsed) => {
|
|
175
|
+
const priorities = ["errors", "aiCost", "latency"];
|
|
176
|
+
for (const metric of priorities) {
|
|
177
|
+
const forMetric = samples.filter((s) => s.metric === metric);
|
|
178
|
+
if (forMetric.length < PRODUCT_OBSERVATION_CONSECUTIVE_MEASUREMENTS)
|
|
179
|
+
continue;
|
|
180
|
+
const tail = forMetric.slice(-PRODUCT_OBSERVATION_CONSECUTIVE_MEASUREMENTS);
|
|
181
|
+
if (tail.every((s) => s.exceeded))
|
|
182
|
+
return { confirmed: true, metric };
|
|
183
|
+
}
|
|
184
|
+
return { confirmed: false, metric: null };
|
|
185
|
+
};
|
|
186
|
+
/** True only when every required anchor is passed. Terminal invariant at `validated`. */
|
|
187
|
+
export const allRequiredAnchorsPassed = (anchors) => REQUIRED_PRODUCT_ANCHORS.every((anchor) => anchors[anchor]?.status === "passed" && isNonEmpty(anchors[anchor]?.evidence));
|
|
188
|
+
/** True only when every ship-gate anchor is passed. Used at the VERIFY_EVALUATE ship gate. */
|
|
189
|
+
export const shipGateAnchorsPassed = (anchors) => PRODUCT_SHIP_GATE_ANCHORS.every((anchor) => anchors[anchor]?.status === "passed" && isNonEmpty(anchors[anchor]?.evidence));
|
|
190
|
+
const initialContext = (input) => ({
|
|
191
|
+
runId: input.runId,
|
|
192
|
+
proposalId: input.proposalId ?? null,
|
|
193
|
+
improvementCycleId: input.improvementCycleId ?? null,
|
|
194
|
+
draft: null,
|
|
195
|
+
voteConfig: null,
|
|
196
|
+
favorableVotes: 0,
|
|
197
|
+
budget: null,
|
|
198
|
+
controls: {},
|
|
199
|
+
observationSamples: [],
|
|
200
|
+
contactVoteOpen: false,
|
|
201
|
+
contactVoteQuorumReached: false,
|
|
202
|
+
contactRelayAuthorized: false,
|
|
203
|
+
reviewReason: null,
|
|
204
|
+
permissionsCleared: false,
|
|
205
|
+
permissionEvidence: null,
|
|
206
|
+
signalLog: [],
|
|
207
|
+
anchors: {},
|
|
208
|
+
terminalReason: null,
|
|
209
|
+
});
|
|
210
|
+
const productSetup = setup({
|
|
211
|
+
types: {
|
|
212
|
+
context: {},
|
|
213
|
+
events: {},
|
|
214
|
+
input: {},
|
|
215
|
+
},
|
|
216
|
+
guards: {
|
|
217
|
+
isAiAgentSignal: ({ event }) => event.type === "AGENT_SIGNAL" && event.source === "ai" && isNonEmpty(event.note),
|
|
218
|
+
isAiFeedbackAggregated: ({ event }) => event.type === "FEEDBACK_AGGREGATED" && event.source === "ai" && isNonEmpty(event.summary),
|
|
219
|
+
isAiProposalDrafted: ({ event }) => event.type === "PROPOSAL_DRAFTED" && event.source === "ai" && isProposalDraft(event.draft),
|
|
220
|
+
isToolOpenProposition: ({ context, event }) => event.type === "OPEN_PROPOSITION" && event.source === "tool" && context.draft !== null,
|
|
221
|
+
isToolQualificationRun: ({ event }) => event.type === "QUALIFICATION_RUN" &&
|
|
222
|
+
event.source === "tool" &&
|
|
223
|
+
event.permissionCleared === true &&
|
|
224
|
+
isNonEmpty(event.permissionEvidence),
|
|
225
|
+
isToolVoteOpenedFirstTime: ({ context }) => context.voteConfig === null,
|
|
226
|
+
isToolVoteOpened: ({ event }) => event.type === "VOTE_OPENED" &&
|
|
227
|
+
event.source === "tool" &&
|
|
228
|
+
event.config.quorum > 0 &&
|
|
229
|
+
(event.config.kind === "standard" || event.config.kind === "criticalSecurity") &&
|
|
230
|
+
event.config.expiryHours === PRODUCT_VOTE_EXPIRY_HOURS[event.config.kind],
|
|
231
|
+
isToolVoteCast: ({ event }) => event.type === "VOTE_CAST" && event.source === "tool" && event.favorable > 0,
|
|
232
|
+
isSystemVoteEvaluateWithQuorum: ({ context, event }) => event.type === "VOTE_EVALUATE" &&
|
|
233
|
+
event.source === "system" &&
|
|
234
|
+
tallyProductVotes(context.favorableVotes, context.voteConfig).quorumReached,
|
|
235
|
+
isToolVoteExpiredWithQuorum: ({ context, event }) => event.type === "VOTE_EXPIRED" &&
|
|
236
|
+
event.source === "tool" &&
|
|
237
|
+
tallyProductVotes(context.favorableVotes, context.voteConfig).quorumReached,
|
|
238
|
+
isToolVoteExpiredWithoutQuorum: ({ context, event }) => event.type === "VOTE_EXPIRED" &&
|
|
239
|
+
event.source === "tool" &&
|
|
240
|
+
!tallyProductVotes(context.favorableVotes, context.voteConfig).quorumReached,
|
|
241
|
+
isToolBudgetCharge: ({ event }) => event.type === "BUDGET_CHARGE" &&
|
|
242
|
+
event.source === "tool" &&
|
|
243
|
+
isNonEmpty(event.action.description) &&
|
|
244
|
+
isNonEmpty(event.action.evidence) &&
|
|
245
|
+
event.action.amount >= 0,
|
|
246
|
+
chargeExhaustsBudget: ({ context, event }) => event.type === "BUDGET_CHARGE" &&
|
|
247
|
+
event.source === "tool" &&
|
|
248
|
+
budgetRemaining(applyBudgetCharge(context.budget, event.action)) <= 0,
|
|
249
|
+
isToolExecutionDone: ({ context, event }) => event.type === "EXECUTION_DONE" && event.source === "tool" && budgetRemaining(context.budget) > 0,
|
|
250
|
+
isToolVerifyRun: ({ event }) => event.type === "VERIFY_RUN" &&
|
|
251
|
+
event.source === "tool" &&
|
|
252
|
+
isNonEmpty(event.control.name) &&
|
|
253
|
+
(event.control.status === "passed" || event.control.status === "failed") &&
|
|
254
|
+
isNonEmpty(event.control.evidence),
|
|
255
|
+
canShip: ({ context }) => canAutoShip({ draft: context.draft, controls: context.controls, budget: context.budget }).allowed &&
|
|
256
|
+
shipGateAnchorsPassed(context.anchors),
|
|
257
|
+
isSystemVerifyEvaluate: ({ event }) => event.type === "VERIFY_EVALUATE" && event.source === "system",
|
|
258
|
+
isToolObservationSample: ({ event }) => event.type === "OBSERVATION_SAMPLE" &&
|
|
259
|
+
event.source === "tool" &&
|
|
260
|
+
["errors", "aiCost", "latency", "satisfaction"].includes(event.sample.metric) &&
|
|
261
|
+
typeof event.sample.value === "number" &&
|
|
262
|
+
typeof event.sample.threshold === "number" &&
|
|
263
|
+
typeof event.sample.exceeded === "boolean" &&
|
|
264
|
+
isNonEmpty(event.sample.evidence),
|
|
265
|
+
observationDegraded: ({ context, event }) => event.type === "OBSERVATION_EVALUATE" &&
|
|
266
|
+
evaluateObservation(context.observationSamples, event.windowElapsed).confirmed,
|
|
267
|
+
observationClean: ({ context, event }) => event.type === "OBSERVATION_EVALUATE" &&
|
|
268
|
+
context.observationSamples.length >= PRODUCT_OBSERVATION_CONSECUTIVE_MEASUREMENTS &&
|
|
269
|
+
!evaluateObservation(context.observationSamples, event.windowElapsed).confirmed &&
|
|
270
|
+
event.windowElapsed,
|
|
271
|
+
isValidExternalAnchor: ({ event }) => event.type === "ANCHOR_RECORDED" &&
|
|
272
|
+
event.source === "tool" &&
|
|
273
|
+
PRODUCT_EXTERNAL_ANCHORS.includes(event.anchor) &&
|
|
274
|
+
(event.status === "passed" || event.status === "failed") &&
|
|
275
|
+
isNonEmpty(event.evidence),
|
|
276
|
+
isToolCorrectivePropositionOpened: ({ event }) => event.type === "CORRECTIVE_PROPOSITION_OPENED" && event.source === "tool",
|
|
277
|
+
isHumanReviewResolved: ({ event }) => event.type === "REVIEW_RESOLVED" &&
|
|
278
|
+
event.source === "human" &&
|
|
279
|
+
(event.resolution === "scope-reduced" ||
|
|
280
|
+
event.resolution === "budget-expanded" ||
|
|
281
|
+
event.resolution === "abandoned"),
|
|
282
|
+
isHumanDeployAuthorized: ({ context, event }) => event.type === "REVIEW_RESOLVED" &&
|
|
283
|
+
event.source === "human" &&
|
|
284
|
+
event.resolution === "deploy-authorized" &&
|
|
285
|
+
canHumanAuthorizeSensitiveDeploy({
|
|
286
|
+
draft: context.draft,
|
|
287
|
+
controls: context.controls,
|
|
288
|
+
budget: context.budget,
|
|
289
|
+
anchors: context.anchors,
|
|
290
|
+
}).allowed,
|
|
291
|
+
isHumanRetryVerification: ({ event }) => event.type === "RETRY_VERIFICATION_AUTHORIZED" && event.source === "human",
|
|
292
|
+
isToolContactVoteOpened: ({ event }) => event.type === "CONTACT_VOTE_OPENED" && event.source === "tool",
|
|
293
|
+
isToolContactVoteQuorum: ({ event }) => event.type === "CONTACT_VOTE_QUORUM" && event.source === "tool" && typeof event.reached === "boolean",
|
|
294
|
+
isHumanContactRelayAuthorized: ({ context, event }) => event.type === "CONTACT_RELAY_AUTHORIZED" &&
|
|
295
|
+
event.source === "human" &&
|
|
296
|
+
context.contactVoteQuorumReached === true,
|
|
297
|
+
isToolPermissionDenial: ({ event }) => event.type === "PERMISSION_DENIED" && event.source === "tool" && isNonEmpty(event.reason),
|
|
298
|
+
isHumanCancellation: ({ event }) => event.type === "CANCEL" && event.source === "human" && isNonEmpty(event.reason),
|
|
299
|
+
},
|
|
300
|
+
actions: {
|
|
301
|
+
recordDraft: assign(({ context, event }) => event.type === "PROPOSAL_DRAFTED" ? { ...context, draft: event.draft } : context),
|
|
302
|
+
recordSignal: assign(({ context, event }) => {
|
|
303
|
+
// Signal-only events (AGENT_SIGNAL, FEEDBACK_AGGREGATED) carry no state
|
|
304
|
+
// target; they are recorded as traceable product signal evidence so the
|
|
305
|
+
// persisted snapshot reflects that the machine accepted them. This keeps
|
|
306
|
+
// feedback/signal history queryable and makes acceptance observable to the
|
|
307
|
+
// journal-based runner without coupling it to guard internals.
|
|
308
|
+
if (event.type !== "AGENT_SIGNAL" && event.type !== "FEEDBACK_AGGREGATED")
|
|
309
|
+
return context;
|
|
310
|
+
const entry = event.type === "AGENT_SIGNAL" ? `agent-signal: ${event.note}` : `feedback-aggregated: ${event.summary}`;
|
|
311
|
+
return { ...context, signalLog: [...context.signalLog, entry] };
|
|
312
|
+
}),
|
|
313
|
+
recordPermission: assign(({ context, event }) => event.type === "QUALIFICATION_RUN"
|
|
314
|
+
? {
|
|
315
|
+
...context,
|
|
316
|
+
permissionsCleared: event.permissionCleared,
|
|
317
|
+
permissionEvidence: event.permissionEvidence,
|
|
318
|
+
}
|
|
319
|
+
: context),
|
|
320
|
+
sealQualificationPassed: assign(({ context }) => ({
|
|
321
|
+
...context,
|
|
322
|
+
anchors: {
|
|
323
|
+
...context.anchors,
|
|
324
|
+
"qualification-passed": {
|
|
325
|
+
status: "passed",
|
|
326
|
+
evidence: `qualifier: scope, dependencies, category, permissions, budget validated (${context.permissionEvidence ?? "none"})`,
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
})),
|
|
330
|
+
recordVoteConfig: assign(({ context, event }) => event.type === "VOTE_OPENED" ? { ...context, voteConfig: event.config, favorableVotes: 0 } : context),
|
|
331
|
+
recordVote: assign(({ context, event }) => event.type === "VOTE_CAST" ? { ...context, favorableVotes: context.favorableVotes + event.favorable } : context),
|
|
332
|
+
sealVoteQuorum: assign(({ context }) => ({
|
|
333
|
+
...context,
|
|
334
|
+
anchors: {
|
|
335
|
+
...context.anchors,
|
|
336
|
+
"vote-quorum": {
|
|
337
|
+
status: "passed",
|
|
338
|
+
evidence: `quorum ${context.voteConfig?.quorum} reached with ${context.favorableVotes} favorable`,
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
})),
|
|
342
|
+
openBudget: assign(({ context }) => {
|
|
343
|
+
const allocation = context.draft?.budgetAllocation ?? 0;
|
|
344
|
+
return {
|
|
345
|
+
...context,
|
|
346
|
+
budget: { initial: allocation, consumed: 0, history: [] },
|
|
347
|
+
anchors: {
|
|
348
|
+
...context.anchors,
|
|
349
|
+
"budget-envelope": { status: "passed", evidence: `envelope opened at ${allocation}` },
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}),
|
|
353
|
+
chargeBudget: assign(({ context, event }) => {
|
|
354
|
+
if (event.type !== "BUDGET_CHARGE")
|
|
355
|
+
return context;
|
|
356
|
+
return { ...context, budget: applyBudgetCharge(context.budget, event.action) };
|
|
357
|
+
}),
|
|
358
|
+
recordControl: assign(({ context, event }) => {
|
|
359
|
+
if (event.type !== "VERIFY_RUN")
|
|
360
|
+
return context;
|
|
361
|
+
return { ...context, controls: { ...context.controls, [event.control.name]: event.control } };
|
|
362
|
+
}),
|
|
363
|
+
sealVerificationAnchors: assign(({ context }) => {
|
|
364
|
+
const decision = canAutoShip({ draft: context.draft, controls: context.controls, budget: context.budget });
|
|
365
|
+
return {
|
|
366
|
+
...context,
|
|
367
|
+
anchors: {
|
|
368
|
+
...context.anchors,
|
|
369
|
+
"controls-passed": {
|
|
370
|
+
status: "passed",
|
|
371
|
+
evidence: `${Object.keys(context.controls).length} controls passed`,
|
|
372
|
+
},
|
|
373
|
+
"auto-ship-policy": {
|
|
374
|
+
status: decision.allowed ? "passed" : "failed",
|
|
375
|
+
evidence: decision.reason,
|
|
376
|
+
},
|
|
377
|
+
// NOTE: `rollback-path-exists` is NOT sealed here. It is an external
|
|
378
|
+
// anchor recorded by the verifier tool (product:anchors) via
|
|
379
|
+
// ANCHOR_RECORDED after the rollback artifact is confirmed genuine.
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
}),
|
|
383
|
+
sealHumanAuthorizedDeploy: assign(({ context }) => ({
|
|
384
|
+
...context,
|
|
385
|
+
anchors: {
|
|
386
|
+
...context.anchors,
|
|
387
|
+
"controls-passed": {
|
|
388
|
+
status: "passed",
|
|
389
|
+
evidence: `${Object.keys(context.controls).length} controls passed (human deploy review)`,
|
|
390
|
+
},
|
|
391
|
+
"auto-ship-policy": {
|
|
392
|
+
status: "passed",
|
|
393
|
+
evidence: "sensitive change deploy authorized by human owner review; auto-ship does not apply",
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
})),
|
|
397
|
+
recordObservationSample: assign(({ context, event }) => {
|
|
398
|
+
if (event.type !== "OBSERVATION_SAMPLE")
|
|
399
|
+
return context;
|
|
400
|
+
// Satisfaction is recorded as signal only; it can never confirm degradation.
|
|
401
|
+
return { ...context, observationSamples: [...context.observationSamples, event.sample] };
|
|
402
|
+
}),
|
|
403
|
+
sealObservationWindow: assign(({ context }) => ({
|
|
404
|
+
...context,
|
|
405
|
+
anchors: {
|
|
406
|
+
...context.anchors,
|
|
407
|
+
"observation-window": {
|
|
408
|
+
status: "passed",
|
|
409
|
+
evidence: `${context.observationSamples.length} samples recorded`,
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
})),
|
|
413
|
+
recordExternalAnchorOnce: assign(({ context, event }) => {
|
|
414
|
+
if (event.type !== "ANCHOR_RECORDED")
|
|
415
|
+
return context;
|
|
416
|
+
if (context.anchors[event.anchor] !== undefined)
|
|
417
|
+
return context; // failed anchors are immutable per run
|
|
418
|
+
return {
|
|
419
|
+
...context,
|
|
420
|
+
anchors: {
|
|
421
|
+
...context.anchors,
|
|
422
|
+
[event.anchor]: { status: event.status, evidence: event.evidence },
|
|
423
|
+
},
|
|
424
|
+
};
|
|
425
|
+
}),
|
|
426
|
+
prepareCorrectiveProposition: assign(({ context }) => ({
|
|
427
|
+
...context,
|
|
428
|
+
// A corrective/re-scope proposal starts fresh in proposition; prior
|
|
429
|
+
// attempt-scoped evidence is cleared. Draft must be re-submitted and
|
|
430
|
+
// re-qualified; no anchor carries over.
|
|
431
|
+
draft: null,
|
|
432
|
+
voteConfig: null,
|
|
433
|
+
favorableVotes: 0,
|
|
434
|
+
controls: {},
|
|
435
|
+
observationSamples: [],
|
|
436
|
+
anchors: {},
|
|
437
|
+
})),
|
|
438
|
+
recordReviewResolution: assign(({ context }) => ({ ...context })),
|
|
439
|
+
recordBudgetBlockedEntry: assign(({ context }) => ({
|
|
440
|
+
...context,
|
|
441
|
+
reviewReason: "budget-exhausted",
|
|
442
|
+
})),
|
|
443
|
+
expandBudget: assign(({ context, event }) => {
|
|
444
|
+
if (event.type !== "REVIEW_RESOLVED" || typeof event.expandedBudget !== "number")
|
|
445
|
+
return context;
|
|
446
|
+
const consumed = context.budget?.consumed ?? 0;
|
|
447
|
+
const initial = Math.max(event.expandedBudget, consumed);
|
|
448
|
+
return {
|
|
449
|
+
...context,
|
|
450
|
+
budget: context.budget ? { ...context.budget, initial } : { initial, consumed: 0, history: [] },
|
|
451
|
+
};
|
|
452
|
+
}),
|
|
453
|
+
resetControlsForRetry: assign(({ context }) => ({
|
|
454
|
+
...context,
|
|
455
|
+
controls: {},
|
|
456
|
+
anchors: { ...context.anchors },
|
|
457
|
+
})),
|
|
458
|
+
recordContactVoteOpened: assign(({ context }) => ({ ...context, contactVoteOpen: true })),
|
|
459
|
+
recordContactVoteQuorum: assign(({ context, event }) => event.type === "CONTACT_VOTE_QUORUM" ? { ...context, contactVoteQuorumReached: event.reached } : context),
|
|
460
|
+
recordContactRelay: assign(({ context }) => ({ ...context, contactRelayAuthorized: true })),
|
|
461
|
+
recordTerminalRejected: assign(({ context }) => ({
|
|
462
|
+
...context,
|
|
463
|
+
terminalReason: "vote expired without quorum",
|
|
464
|
+
})),
|
|
465
|
+
recordTerminalCancelled: assign(({ context }) => ({
|
|
466
|
+
...context,
|
|
467
|
+
terminalReason: "run cancelled by owner",
|
|
468
|
+
})),
|
|
469
|
+
recordTerminalValidated: assign(({ context }) => ({
|
|
470
|
+
...context,
|
|
471
|
+
terminalReason: "observation completed without confirmed degradation",
|
|
472
|
+
})),
|
|
473
|
+
},
|
|
474
|
+
});
|
|
475
|
+
export const productMachine = productSetup.createMachine({
|
|
476
|
+
id: "swarm-dao-product-loop",
|
|
477
|
+
initial: "exploration",
|
|
478
|
+
context: ({ input }) => initialContext(input),
|
|
479
|
+
on: {
|
|
480
|
+
PERMISSION_DENIED: { guard: "isToolPermissionDenial", target: ".review" },
|
|
481
|
+
CANCEL: { guard: "isHumanCancellation", target: ".rejected", actions: "recordTerminalCancelled" },
|
|
482
|
+
CONTACT_VOTE_OPENED: { guard: "isToolContactVoteOpened", actions: "recordContactVoteOpened" },
|
|
483
|
+
CONTACT_VOTE_QUORUM: { guard: "isToolContactVoteQuorum", actions: "recordContactVoteQuorum" },
|
|
484
|
+
CONTACT_RELAY_AUTHORIZED: {
|
|
485
|
+
guard: "isHumanContactRelayAuthorized",
|
|
486
|
+
actions: "recordContactRelay",
|
|
487
|
+
},
|
|
488
|
+
// NOTE: ANCHOR_RECORDED is intentionally NOT global. External anchors
|
|
489
|
+
// (frozen-set-intact, regression, rollback-path-exists) are only meaningful
|
|
490
|
+
// and only accepted while the machine is in the `verification` state, where
|
|
491
|
+
// the verifier tool records them. Accepting them globally would let a
|
|
492
|
+
// verifier seal regression/frozen-set before any implementation exists.
|
|
493
|
+
},
|
|
494
|
+
states: {
|
|
495
|
+
exploration: {
|
|
496
|
+
on: {
|
|
497
|
+
AGENT_SIGNAL: { guard: "isAiAgentSignal", actions: "recordSignal" },
|
|
498
|
+
FEEDBACK_AGGREGATED: { guard: "isAiFeedbackAggregated", actions: "recordSignal" },
|
|
499
|
+
PROPOSAL_DRAFTED: { guard: "isAiProposalDrafted", actions: "recordDraft" },
|
|
500
|
+
OPEN_PROPOSITION: { guard: "isToolOpenProposition", target: "proposition" },
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
proposition: {
|
|
504
|
+
on: {
|
|
505
|
+
// A corrective/re-scope flow clears the draft on entry to proposition
|
|
506
|
+
// (prepareCorrectiveProposition). The replacement draft must be recorded
|
|
507
|
+
// here before qualification can run, exactly as in exploration.
|
|
508
|
+
PROPOSAL_DRAFTED: { guard: "isAiProposalDrafted", actions: "recordDraft" },
|
|
509
|
+
QUALIFICATION_RUN: {
|
|
510
|
+
guard: "isToolQualificationRun",
|
|
511
|
+
target: "qualification",
|
|
512
|
+
actions: "recordPermission",
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
qualification: {
|
|
517
|
+
always: [
|
|
518
|
+
{
|
|
519
|
+
guard: ({ context }) => qualifyProposal(context.draft, context.permissionsCleared).qualified,
|
|
520
|
+
target: "vote",
|
|
521
|
+
actions: "sealQualificationPassed",
|
|
522
|
+
},
|
|
523
|
+
{ target: "review" },
|
|
524
|
+
],
|
|
525
|
+
},
|
|
526
|
+
vote: {
|
|
527
|
+
on: {
|
|
528
|
+
VOTE_OPENED: {
|
|
529
|
+
guard: ({ context, event }) => context.voteConfig === null &&
|
|
530
|
+
event.type === "VOTE_OPENED" &&
|
|
531
|
+
event.source === "tool" &&
|
|
532
|
+
event.config.quorum > 0 &&
|
|
533
|
+
(event.config.kind === "standard" || event.config.kind === "criticalSecurity") &&
|
|
534
|
+
event.config.expiryHours === PRODUCT_VOTE_EXPIRY_HOURS[event.config.kind],
|
|
535
|
+
actions: "recordVoteConfig",
|
|
536
|
+
},
|
|
537
|
+
VOTE_CAST: { guard: "isToolVoteCast", actions: "recordVote" },
|
|
538
|
+
VOTE_EVALUATE: {
|
|
539
|
+
guard: "isSystemVoteEvaluateWithQuorum",
|
|
540
|
+
target: "adopted",
|
|
541
|
+
actions: "sealVoteQuorum",
|
|
542
|
+
},
|
|
543
|
+
// Expiry is deterministic: if quorum was already reached when the expiry
|
|
544
|
+
// event arrives, the task is adopted (and the quorum anchor sealed); only
|
|
545
|
+
// expiry WITHOUT quorum rejects. This prevents a late expiry from killing
|
|
546
|
+
// an already-quorate task whose EVALUATE had not yet been emitted.
|
|
547
|
+
VOTE_EXPIRED: [
|
|
548
|
+
{
|
|
549
|
+
guard: "isToolVoteExpiredWithQuorum",
|
|
550
|
+
target: "adopted",
|
|
551
|
+
actions: "sealVoteQuorum",
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
guard: "isToolVoteExpiredWithoutQuorum",
|
|
555
|
+
target: "rejected",
|
|
556
|
+
actions: "recordTerminalRejected",
|
|
557
|
+
},
|
|
558
|
+
],
|
|
559
|
+
},
|
|
560
|
+
},
|
|
561
|
+
adopted: {
|
|
562
|
+
always: [{ target: "execution", actions: "openBudget" }],
|
|
563
|
+
},
|
|
564
|
+
execution: {
|
|
565
|
+
on: {
|
|
566
|
+
BUDGET_CHARGE: [
|
|
567
|
+
{
|
|
568
|
+
guard: "chargeExhaustsBudget",
|
|
569
|
+
target: "budgetBlocked",
|
|
570
|
+
actions: "chargeBudget",
|
|
571
|
+
},
|
|
572
|
+
{ guard: "isToolBudgetCharge", actions: "chargeBudget" },
|
|
573
|
+
],
|
|
574
|
+
EXECUTION_DONE: { guard: "isToolExecutionDone", target: "verification" },
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
budgetBlocked: {
|
|
578
|
+
// Budget exhaustion has only one deterministic escape: escalate to human
|
|
579
|
+
// review. This is a model-mandated transition (not an agent decision): no
|
|
580
|
+
// contributor swap, retry, or bypass may skip review. The human owner then
|
|
581
|
+
// resolves via REVIEW_RESOLVED (expand budget / reduce scope / abandon) in
|
|
582
|
+
// the review state. The `always` makes the escalation immediate so the
|
|
583
|
+
// persisted snapshot records the budget block as the cause entering review.
|
|
584
|
+
always: [{ target: "review", actions: "recordBudgetBlockedEntry" }],
|
|
585
|
+
},
|
|
586
|
+
verification: {
|
|
587
|
+
on: {
|
|
588
|
+
VERIFY_RUN: { guard: "isToolVerifyRun", actions: "recordControl" },
|
|
589
|
+
// External anchors (frozen-set-intact, regression, rollback-path-exists)
|
|
590
|
+
// are recorded ONLY in verification by the verifier tool. `rollback-path-
|
|
591
|
+
// exists` is a ship-gate prerequisite, so it must be recorded before
|
|
592
|
+
// VERIFY_EVALUATE selects ship.
|
|
593
|
+
ANCHOR_RECORDED: { guard: "isValidExternalAnchor", actions: "recordExternalAnchorOnce" },
|
|
594
|
+
VERIFY_EVALUATE: [
|
|
595
|
+
{
|
|
596
|
+
guard: ({ context, event }) => event.type === "VERIFY_EVALUATE" &&
|
|
597
|
+
event.source === "system" &&
|
|
598
|
+
canAutoShip({ draft: context.draft, controls: context.controls, budget: context.budget }).allowed &&
|
|
599
|
+
shipGateAnchorsPassed(context.anchors),
|
|
600
|
+
target: "ship",
|
|
601
|
+
actions: "sealVerificationAnchors",
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
guard: "isSystemVerifyEvaluate",
|
|
605
|
+
target: "review",
|
|
606
|
+
actions: "sealVerificationAnchors",
|
|
607
|
+
},
|
|
608
|
+
],
|
|
609
|
+
},
|
|
610
|
+
},
|
|
611
|
+
ship: {
|
|
612
|
+
always: [{ target: "observation" }],
|
|
613
|
+
},
|
|
614
|
+
observation: {
|
|
615
|
+
on: {
|
|
616
|
+
OBSERVATION_SAMPLE: { guard: "isToolObservationSample", actions: "recordObservationSample" },
|
|
617
|
+
OBSERVATION_EVALUATE: [
|
|
618
|
+
{
|
|
619
|
+
guard: "observationDegraded",
|
|
620
|
+
target: "rollback",
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
guard: "observationClean",
|
|
624
|
+
target: "validated",
|
|
625
|
+
actions: ["sealObservationWindow", "recordTerminalValidated"],
|
|
626
|
+
},
|
|
627
|
+
],
|
|
628
|
+
},
|
|
629
|
+
},
|
|
630
|
+
rollback: {
|
|
631
|
+
on: {
|
|
632
|
+
CORRECTIVE_PROPOSITION_OPENED: {
|
|
633
|
+
guard: "isToolCorrectivePropositionOpened",
|
|
634
|
+
target: "proposition",
|
|
635
|
+
actions: "prepareCorrectiveProposition",
|
|
636
|
+
},
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
review: {
|
|
640
|
+
on: {
|
|
641
|
+
REVIEW_RESOLVED: [
|
|
642
|
+
{
|
|
643
|
+
guard: ({ event }) => event.type === "REVIEW_RESOLVED" && event.source === "human" && event.resolution === "scope-reduced",
|
|
644
|
+
target: "proposition",
|
|
645
|
+
actions: "prepareCorrectiveProposition",
|
|
646
|
+
},
|
|
647
|
+
{
|
|
648
|
+
// Budget expansion is the ONLY escape from BudgetBlocked, and only
|
|
649
|
+
// when the review was actually entered because of budget exhaustion.
|
|
650
|
+
// This prevents any other review cause (qualification failure,
|
|
651
|
+
// permission denial, verification failure) from using budget-expanded
|
|
652
|
+
// to jump straight to execution, bypassing qualification and vote. The
|
|
653
|
+
// human review must declare a strictly larger envelope than what has
|
|
654
|
+
// already been consumed; otherwise the task would immediately
|
|
655
|
+
// re-block. Consumed spend and history are preserved.
|
|
656
|
+
guard: ({ context, event }) => event.type === "REVIEW_RESOLVED" &&
|
|
657
|
+
event.source === "human" &&
|
|
658
|
+
event.resolution === "budget-expanded" &&
|
|
659
|
+
context.reviewReason === "budget-exhausted" &&
|
|
660
|
+
typeof event.expandedBudget === "number" &&
|
|
661
|
+
Number.isFinite(event.expandedBudget) &&
|
|
662
|
+
event.expandedBudget > (context.budget?.consumed ?? 0),
|
|
663
|
+
target: "execution",
|
|
664
|
+
actions: "expandBudget",
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
// Human deploy-authorization: the ONLY path to ship for sensitive
|
|
668
|
+
// changes (security/permissions/secrets/payments/sensitive-data) that
|
|
669
|
+
// passed verification but are forbidden from auto-ship. The
|
|
670
|
+
// isHumanDeployAuthorized guard re-checks controls, budget, and the
|
|
671
|
+
// ship-gate anchors (including the externally verified rollback
|
|
672
|
+
// path). No AI source may emit this.
|
|
673
|
+
guard: "isHumanDeployAuthorized",
|
|
674
|
+
target: "ship",
|
|
675
|
+
actions: "sealHumanAuthorizedDeploy",
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
guard: ({ event }) => event.type === "REVIEW_RESOLVED" && event.source === "human" && event.resolution === "abandoned",
|
|
679
|
+
target: "rejected",
|
|
680
|
+
actions: "recordReviewResolution",
|
|
681
|
+
},
|
|
682
|
+
],
|
|
683
|
+
RETRY_VERIFICATION_AUTHORIZED: {
|
|
684
|
+
guard: "isHumanRetryVerification",
|
|
685
|
+
target: "verification",
|
|
686
|
+
actions: "resetControlsForRetry",
|
|
687
|
+
},
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
validated: { type: "final" },
|
|
691
|
+
rejected: { type: "final" },
|
|
692
|
+
},
|
|
693
|
+
});
|
|
694
|
+
export const createProductActor = (input) => {
|
|
695
|
+
const actor = createActor(productMachine, { input });
|
|
696
|
+
actor.start();
|
|
697
|
+
return actor;
|
|
698
|
+
};
|
|
699
|
+
//# sourceMappingURL=product-loop.machine.js.map
|