@agilno-tech/rivet 0.1.0-alpha.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/LICENSE +202 -0
- package/NOTICE +6 -0
- package/README.md +59 -0
- package/bin/cli.js +42 -0
- package/dist/agents/backend-django-agent/SKILL.md +35 -0
- package/dist/agents/backend-nestjs-agent/SKILL.md +36 -0
- package/dist/agents/boss-agent/SKILL.md +28 -0
- package/dist/agents/bounded-worker/SKILL.md +28 -0
- package/dist/agents/data-performance-agent/SKILL.md +32 -0
- package/dist/agents/delivery-ticketing-agent/SKILL.md +36 -0
- package/dist/agents/devops-agent/SKILL.md +34 -0
- package/dist/agents/engineering-manager/SKILL.md +28 -0
- package/dist/agents/frontend-nextjs-agent/SKILL.md +34 -0
- package/dist/agents/frontend-web-agent/SKILL.md +31 -0
- package/dist/agents/incident-response-agent/SKILL.md +42 -0
- package/dist/agents/jest-agent/SKILL.md +77 -0
- package/dist/agents/mobile-agent/SKILL.md +32 -0
- package/dist/agents/playwright-agent/SKILL.md +54 -0
- package/dist/agents/product-design-manager/SKILL.md +27 -0
- package/dist/agents/qa-agent/SKILL.md +29 -0
- package/dist/agents/quality-manager/SKILL.md +28 -0
- package/dist/agents/vitest-agent/SKILL.md +77 -0
- package/dist/governance/pre-push-rules.md +75 -0
- package/dist/governance/prompt-hygiene.md +22 -0
- package/dist/governance/review-checklist.md +24 -0
- package/dist/governance/safety-and-data.md +27 -0
- package/dist/governance/usage-rules.md +21 -0
- package/dist/mandatory/address-pr-feedback/SKILL.md +196 -0
- package/dist/mandatory/agentic-goal/SKILL.md +42 -0
- package/dist/mandatory/agentic-status/SKILL.md +74 -0
- package/dist/mandatory/apply-design/SKILL.md +228 -0
- package/dist/mandatory/check-ac/SKILL.md +124 -0
- package/dist/mandatory/create-pr-and-commit/SKILL.md +304 -0
- package/dist/mandatory/design/SKILL.md +312 -0
- package/dist/mandatory/feature-workflow/SKILL.md +87 -0
- package/dist/mandatory/hotfix/SKILL.md +195 -0
- package/dist/mandatory/pre-push/SKILL.md +272 -0
- package/dist/mandatory/project-context/SKILL.md +180 -0
- package/dist/mandatory/release-docs/SKILL.md +191 -0
- package/dist/mandatory/review-pr/SKILL.md +323 -0
- package/dist/mandatory/security-review/SKILL.md +157 -0
- package/dist/skills/api-contract/SKILL.md +77 -0
- package/dist/skills/backend-django/SKILL.md +27 -0
- package/dist/skills/backend-nestjs/SKILL.md +65 -0
- package/dist/skills/bug-ticket-creation/SKILL.md +43 -0
- package/dist/skills/cloudwatch-troubleshooting/SKILL.md +73 -0
- package/dist/skills/database-migration/SKILL.md +97 -0
- package/dist/skills/debugging/SKILL.md +32 -0
- package/dist/skills/devops-infra/SKILL.md +26 -0
- package/dist/skills/documentation/SKILL.md +24 -0
- package/dist/skills/frontend-nextjs/SKILL.md +27 -0
- package/dist/skills/incident-postmortem/SKILL.md +38 -0
- package/dist/skills/kubernetes-troubleshooting/SKILL.md +71 -0
- package/dist/skills/mobile-react-native/SKILL.md +37 -0
- package/dist/skills/postgres-analytics/SKILL.md +29 -0
- package/dist/skills/product-jira-ticketing/SKILL.md +28 -0
- package/dist/skills/qa-bug-analysis/SKILL.md +27 -0
- package/dist/skills/refactoring/SKILL.md +32 -0
- package/dist/skills/sprint-planning/SKILL.md +32 -0
- package/dist/skills/testing-quality/SKILL.md +62 -0
- package/dist/v2/protocols/agent-orchestration.md +49 -0
- package/dist/v2/protocols/delivery.md +45 -0
- package/dist/v2/protocols/design-authority.md +29 -0
- package/dist/v2/protocols/goal-graph.md +44 -0
- package/dist/v2/protocols/pattern-first-development.md +29 -0
- package/dist/v2/protocols/qa-evidence.md +29 -0
- package/dist/v2/protocols/security.md +29 -0
- package/dist/v2/schemas/event.schema.json +86 -0
- package/dist/v2/schemas/evidence.schema.json +94 -0
- package/dist/v2/schemas/feature-decomposition.schema.json +48 -0
- package/dist/v2/schemas/feature-plan.schema.json +95 -0
- package/dist/v2/schemas/goal-graph.schema.json +61 -0
- package/dist/v2/schemas/integration.schema.json +119 -0
- package/dist/v2/schemas/orchestration.schema.json +69 -0
- package/dist/v2/schemas/project.schema.json +209 -0
- package/dist/v2/schemas/providers.schema.json +142 -0
- package/dist/v2/schemas/quality.schema.json +50 -0
- package/dist/v2/schemas/work-action.schema.json +44 -0
- package/dist/v2/schemas/work-request.schema.json +253 -0
- package/dist/v2/templates/evidence/qa-bundle.json +194 -0
- package/dist/v2/templates/github-actions/rivet-deploy.yml +98 -0
- package/dist/v2/templates/harness/SKILL.md +90 -0
- package/dist/v2/templates/project/.rivet/orchestration.yaml +42 -0
- package/dist/v2/templates/project/.rivet/project.yaml +14 -0
- package/dist/v2/templates/project/.rivet/providers.yaml +8 -0
- package/dist/v2/templates/project/.rivet/quality.yaml +16 -0
- package/package.json +58 -0
- package/protocols/agent-orchestration.md +49 -0
- package/protocols/delivery.md +45 -0
- package/protocols/design-authority.md +29 -0
- package/protocols/goal-graph.md +44 -0
- package/protocols/pattern-first-development.md +29 -0
- package/protocols/qa-evidence.md +29 -0
- package/protocols/security.md +29 -0
- package/schemas/event.schema.json +86 -0
- package/schemas/evidence.schema.json +94 -0
- package/schemas/feature-decomposition.schema.json +48 -0
- package/schemas/feature-plan.schema.json +95 -0
- package/schemas/goal-graph.schema.json +61 -0
- package/schemas/integration.schema.json +119 -0
- package/schemas/orchestration.schema.json +69 -0
- package/schemas/project.schema.json +209 -0
- package/schemas/providers.schema.json +142 -0
- package/schemas/quality.schema.json +50 -0
- package/schemas/work-action.schema.json +44 -0
- package/schemas/work-request.schema.json +253 -0
- package/src/adapters/confluence.js +138 -0
- package/src/adapters/contract.js +657 -0
- package/src/adapters/factory.js +122 -0
- package/src/adapters/figma.js +178 -0
- package/src/adapters/fixtures.js +183 -0
- package/src/adapters/github.js +314 -0
- package/src/adapters/http.js +704 -0
- package/src/adapters/jira.js +195 -0
- package/src/adapters/linear.js +171 -0
- package/src/adapters/node-transport.js +59 -0
- package/src/cli/integration-setup-prompt.js +32 -0
- package/src/cli/interrupt.js +25 -0
- package/src/cli/main.js +596 -0
- package/src/cli/output.js +174 -0
- package/src/cli/parse-args.js +246 -0
- package/src/cli/project-discovery.js +70 -0
- package/src/cli/task-confirmation.js +38 -0
- package/src/cli/task-presentation.js +22 -0
- package/src/clients/claude.js +214 -0
- package/src/clients/codex.js +180 -0
- package/src/clients/compatibility.js +23 -0
- package/src/clients/contract.js +224 -0
- package/src/clients/fake.js +170 -0
- package/src/clients/process-runner.js +657 -0
- package/src/clients/result-contract.js +78 -0
- package/src/commands/delivery-publish.js +80 -0
- package/src/commands/delivery-remote.js +230 -0
- package/src/commands/delivery-review-update.js +39 -0
- package/src/commands/delivery-tracker.js +94 -0
- package/src/commands/delivery-transition.js +79 -0
- package/src/commands/delivery.js +223 -0
- package/src/commands/dependency-approval.js +21 -0
- package/src/commands/doctor.js +191 -0
- package/src/commands/evidence.js +44 -0
- package/src/commands/feature.js +212 -0
- package/src/commands/goals.js +231 -0
- package/src/commands/human-run.js +208 -0
- package/src/commands/human-task.js +278 -0
- package/src/commands/init.js +848 -0
- package/src/commands/install.js +814 -0
- package/src/commands/integration-setup.js +65 -0
- package/src/commands/integrations.js +54 -0
- package/src/commands/models.js +158 -0
- package/src/commands/orchestrate.js +61 -0
- package/src/commands/preflight.js +125 -0
- package/src/commands/protocols.js +228 -0
- package/src/commands/repositories.js +77 -0
- package/src/commands/setup-remote.js +41 -0
- package/src/commands/setup.js +214 -0
- package/src/commands/status.js +224 -0
- package/src/commands/support.js +44 -0
- package/src/commands/task-completion.js +104 -0
- package/src/commands/uninstall.js +234 -0
- package/src/commands/verify.js +171 -0
- package/src/commands/work.js +220 -0
- package/src/commands/worktrees.js +52 -0
- package/src/config/command-readiness.js +218 -0
- package/src/config/commands.js +206 -0
- package/src/config/defaults.js +55 -0
- package/src/config/load.js +230 -0
- package/src/config/validate.js +560 -0
- package/src/delivery/bitbucket-review.js +123 -0
- package/src/delivery/branch-publication.js +23 -0
- package/src/delivery/contract.js +379 -0
- package/src/delivery/github-deployment.js +132 -0
- package/src/delivery/github-review.js +5 -0
- package/src/delivery/github.js +382 -0
- package/src/delivery/gitlab-review.js +5 -0
- package/src/delivery/gitlab.js +395 -0
- package/src/delivery/prepare.js +90 -0
- package/src/delivery/publication-process.js +33 -0
- package/src/delivery/publication-transport.js +112 -0
- package/src/delivery/review-request.js +184 -0
- package/src/delivery/review-update.js +74 -0
- package/src/delivery/service.js +400 -0
- package/src/delivery/store.js +76 -0
- package/src/delivery/tracker-target.js +51 -0
- package/src/delivery/tracker-transition.js +158 -0
- package/src/delivery/tracker.js +164 -0
- package/src/discovery/git.js +85 -0
- package/src/discovery/portable.js +108 -0
- package/src/discovery/project.js +345 -0
- package/src/discovery/tools.js +145 -0
- package/src/evaluations/approval.js +20 -0
- package/src/evaluations/cost-policy.js +20 -0
- package/src/evaluations/harness-attempt.js +113 -0
- package/src/evaluations/live-runner.js +55 -0
- package/src/evaluations/profile.js +28 -0
- package/src/evaluations/report.js +11 -0
- package/src/evaluations/text-attempt.js +18 -0
- package/src/evidence/checksum.js +87 -0
- package/src/evidence/collect.js +727 -0
- package/src/evidence/validate.js +354 -0
- package/src/feature/accepted-integration.js +46 -0
- package/src/feature/actions.js +37 -0
- package/src/feature/client-profile.js +56 -0
- package/src/feature/decomposition-contract.js +58 -0
- package/src/feature/host-execution.js +633 -0
- package/src/feature/host-lock-recovery.js +96 -0
- package/src/feature/host-run-lock.js +32 -0
- package/src/feature/local-approval.js +183 -0
- package/src/feature/plan-contract.js +215 -0
- package/src/feature/planner.js +253 -0
- package/src/feature/run-store.js +241 -0
- package/src/feature/runtime-bridge.js +840 -0
- package/src/feature/verification-report.js +168 -0
- package/src/feature/workflow.js +375 -0
- package/src/git/client.js +509 -0
- package/src/git/integration-worktree.js +153 -0
- package/src/git/reconcile.js +222 -0
- package/src/git/reservations.js +450 -0
- package/src/git/worktrees.js +478 -0
- package/src/graph/completion.js +189 -0
- package/src/graph/fixtures.js +116 -0
- package/src/graph/reducer.js +124 -0
- package/src/graph/scheduler.js +252 -0
- package/src/graph/validate.js +106 -0
- package/src/install/managed.js +579 -0
- package/src/install/project-reference.js +33 -0
- package/src/install/project-runtime.js +142 -0
- package/src/install/runtime-integrity.cjs +141 -0
- package/src/integrations/capabilities.js +69 -0
- package/src/integrations/host-observation.js +26 -0
- package/src/integrations/registry.js +56 -0
- package/src/models/delegate.js +102 -0
- package/src/models/profiles.js +53 -0
- package/src/models/protocols.js +208 -0
- package/src/models/registry.js +83 -0
- package/src/models/transport.js +59 -0
- package/src/policy/approvals.js +251 -0
- package/src/policy/authority.js +282 -0
- package/src/policy/budget.js +149 -0
- package/src/policy/command-bootstrap.js +219 -0
- package/src/policy/commands.js +803 -0
- package/src/prompts/launch-contract.js +43 -0
- package/src/prompts/planning-contract.js +92 -0
- package/src/protocols/presentation.js +53 -0
- package/src/protocols/project.js +289 -0
- package/src/quality/runner.js +497 -0
- package/src/quality/traceability.js +215 -0
- package/src/repositories/identity.js +59 -0
- package/src/repositories/index.js +2 -0
- package/src/repositories/provider.js +210 -0
- package/src/runtime/application.js +353 -0
- package/src/runtime/dependency-directory.js +33 -0
- package/src/runtime/harness-discovery.js +121 -0
- package/src/runtime/heartbeat.js +47 -0
- package/src/runtime/instance-store.js +112 -0
- package/src/runtime/orchestrator.js +1133 -0
- package/src/runtime/portable-dependencies.js +94 -0
- package/src/runtime/recovery.js +190 -0
- package/src/runtime/retry.js +54 -0
- package/src/runtime/supervisor.js +166 -0
- package/src/runtime/worktree-bootstrap.js +169 -0
- package/src/state/event-store.js +333 -0
- package/src/state/lock.js +266 -0
- package/src/state/paths.js +274 -0
- package/src/state/redact.js +106 -0
- package/src/state/snapshot-store.js +310 -0
- package/src/status/public/app.js +96 -0
- package/src/status/public/index.html +37 -0
- package/src/status/public/styles.css +50 -0
- package/src/status/server.js +323 -0
- package/src/status/view-model.js +356 -0
- package/src/support/bundle.js +195 -0
- package/src/support/failure-report.js +96 -0
- package/src/work-request/contract.js +250 -0
- package/src/work-request/host.js +78 -0
- package/src/work-request/local.js +163 -0
- package/src/work-request/tracker.js +105 -0
- package/templates/evidence/qa-bundle.json +194 -0
- package/templates/github-actions/rivet-deploy.yml +98 -0
- package/templates/harness/SKILL.md +90 -0
- package/templates/project/.rivet/orchestration.yaml +42 -0
- package/templates/project/.rivet/project.yaml +14 -0
- package/templates/project/.rivet/providers.yaml +8 -0
- package/templates/project/.rivet/quality.yaml +16 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { graphFailure, sanitizeGraphOperation } from './validate.js';
|
|
2
|
+
|
|
3
|
+
const budget = (timeMinutes, tokenLimit, costUsd, taskLimit) => ({ timeMinutes, tokenLimit, costUsd, taskLimit });
|
|
4
|
+
|
|
5
|
+
function node({
|
|
6
|
+
id, parentId, objective, role, ownerId, dependencies, authorityScopes,
|
|
7
|
+
nodeBudget, completionProfile, requiredEvidenceTypes, evidenceRefs, status, approvalGate,
|
|
8
|
+
}) {
|
|
9
|
+
return {
|
|
10
|
+
id,
|
|
11
|
+
...(parentId ? { parentId } : {}),
|
|
12
|
+
objective,
|
|
13
|
+
owner: { role, id: ownerId },
|
|
14
|
+
dependencies,
|
|
15
|
+
authorityScopes,
|
|
16
|
+
budget: nodeBudget,
|
|
17
|
+
completionProfile,
|
|
18
|
+
requiredEvidenceTypes,
|
|
19
|
+
evidenceRefs,
|
|
20
|
+
status,
|
|
21
|
+
...(approvalGate ? { approvalGate } : {}),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function commonNodes(goalPrefix = '') {
|
|
26
|
+
return [
|
|
27
|
+
node({
|
|
28
|
+
id: 'boss-plan', objective: `${goalPrefix}Own the approved delivery goal`, role: 'boss', ownerId: 'portfolio-boss',
|
|
29
|
+
dependencies: [], authorityScopes: ['plan', 'implement', 'delegate', 'verify'],
|
|
30
|
+
nodeBudget: budget(480, 500000, 100, 100), completionProfile: 'delivery',
|
|
31
|
+
requiredEvidenceTypes: ['commit', 'test', 'review', 'human-approval'],
|
|
32
|
+
evidenceRefs: ['boss-commit', 'boss-test', 'boss-review', 'boss-approval'], status: 'completed',
|
|
33
|
+
}),
|
|
34
|
+
node({
|
|
35
|
+
id: 'manager-plan', parentId: 'boss-plan', objective: `${goalPrefix}Coordinate implementation`, role: 'manager', ownerId: 'engineering-manager',
|
|
36
|
+
dependencies: ['boss-plan'], authorityScopes: ['implement', 'delegate', 'verify'],
|
|
37
|
+
nodeBudget: budget(240, 250000, 50, 40), completionProfile: 'engineering',
|
|
38
|
+
requiredEvidenceTypes: ['commit', 'test'], evidenceRefs: ['manager-commit', 'manager-test'], status: 'completed',
|
|
39
|
+
}),
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parallelFanIn() {
|
|
44
|
+
const nodes = commonNodes();
|
|
45
|
+
nodes.push(
|
|
46
|
+
node({
|
|
47
|
+
id: 'api-contract', parentId: 'manager-plan', objective: 'Define the completed API contract', role: 'worker', ownerId: 'api-contract-worker',
|
|
48
|
+
dependencies: ['manager-plan'], authorityScopes: ['implement', 'verify'], nodeBudget: budget(60, 30000, 6, 5),
|
|
49
|
+
completionProfile: 'engineering', requiredEvidenceTypes: ['commit', 'test'], evidenceRefs: ['contract-commit', 'contract-test'], status: 'completed',
|
|
50
|
+
}),
|
|
51
|
+
node({
|
|
52
|
+
id: 'design', parentId: 'manager-plan', objective: 'Implement approved component states', role: 'worker', ownerId: 'design-worker',
|
|
53
|
+
dependencies: ['manager-plan'], authorityScopes: ['implement', 'verify'], nodeBudget: budget(90, 50000, 10, 8),
|
|
54
|
+
completionProfile: 'engineering', requiredEvidenceTypes: ['commit', 'test'], evidenceRefs: ['design-commit', 'design-test'], status: 'ready',
|
|
55
|
+
}),
|
|
56
|
+
node({
|
|
57
|
+
id: 'api', parentId: 'manager-plan', objective: 'Implement the agenda API', role: 'worker', ownerId: 'api-worker',
|
|
58
|
+
dependencies: ['api-contract'], authorityScopes: ['implement', 'verify'], nodeBudget: budget(120, 60000, 12, 10),
|
|
59
|
+
completionProfile: 'engineering', requiredEvidenceTypes: ['commit', 'test'], evidenceRefs: ['api-commit', 'api-test'], status: 'ready',
|
|
60
|
+
}),
|
|
61
|
+
node({
|
|
62
|
+
id: 'integration', parentId: 'manager-plan', objective: 'Integrate UI and API journeys', role: 'worker', ownerId: 'integration-worker',
|
|
63
|
+
dependencies: ['design', 'api'], authorityScopes: ['implement', 'verify'], nodeBudget: budget(120, 70000, 14, 12),
|
|
64
|
+
completionProfile: 'engineering', requiredEvidenceTypes: ['commit', 'test', 'journey'],
|
|
65
|
+
evidenceRefs: ['integration-commit', 'integration-test', 'integration-journey'], status: 'ready',
|
|
66
|
+
}),
|
|
67
|
+
node({
|
|
68
|
+
id: 'human-final', parentId: 'boss-plan', objective: 'Approve final delivery', role: 'boss', ownerId: 'portfolio-boss',
|
|
69
|
+
dependencies: ['integration'], authorityScopes: ['verify'], nodeBudget: budget(30, 1000, 1, 1), completionProfile: 'delivery',
|
|
70
|
+
requiredEvidenceTypes: ['commit', 'test', 'review', 'human-approval'],
|
|
71
|
+
evidenceRefs: ['final-commit', 'final-test', 'final-review', 'final-approval'], status: 'ready', approvalGate: 'final-delivery',
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
return { schemaVersion: 1, id: 'parallel-fan-in', goal: 'Deliver a dependency-aware conference demo', providerRefs: ['git-ci-main'], maxDelegationDepth: 3, status: 'approved', nodes };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function corrective() {
|
|
78
|
+
const nodes = commonNodes();
|
|
79
|
+
nodes[0].objective = 'Own the corrective delivery goal';
|
|
80
|
+
nodes[1].objective = 'Coordinate the corrective task';
|
|
81
|
+
nodes[0].budget = budget(240, 200000, 40, 40);
|
|
82
|
+
nodes[1].budget = budget(180, 150000, 30, 30);
|
|
83
|
+
nodes.push(
|
|
84
|
+
node({
|
|
85
|
+
id: 'fix-journey', parentId: 'manager-plan', objective: 'Correct the journey regression', role: 'worker', ownerId: 'quality-worker',
|
|
86
|
+
dependencies: ['manager-plan'], authorityScopes: ['implement', 'verify'], nodeBudget: budget(90, 50000, 10, 8),
|
|
87
|
+
completionProfile: 'engineering', requiredEvidenceTypes: ['commit', 'test', 'journey'],
|
|
88
|
+
evidenceRefs: ['fix-commit', 'fix-test', 'fix-journey-result'], status: 'corrective',
|
|
89
|
+
}),
|
|
90
|
+
node({
|
|
91
|
+
id: 'human-final', parentId: 'boss-plan', objective: 'Approve corrected delivery', role: 'boss', ownerId: 'portfolio-boss',
|
|
92
|
+
dependencies: ['fix-journey'], authorityScopes: ['verify'], nodeBudget: budget(30, 1000, 1, 1), completionProfile: 'delivery',
|
|
93
|
+
requiredEvidenceTypes: ['commit', 'test', 'review', 'human-approval'],
|
|
94
|
+
evidenceRefs: ['final-commit', 'final-test', 'final-review', 'final-approval'], status: 'ready', approvalGate: 'final-delivery',
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
return { schemaVersion: 1, id: 'corrective-loop', goal: 'Correct a failed journey before delivery', providerRefs: ['git-ci-main'], maxDelegationDepth: 2, status: 'running', nodes };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const BUILDERS = Object.freeze({
|
|
101
|
+
'parallel-fan-in': parallelFanIn,
|
|
102
|
+
corrective,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
function graphFixtureUnsafe(name) {
|
|
106
|
+
const builder = BUILDERS[name];
|
|
107
|
+
if (!builder) graphFailure('unknown-graph-fixture');
|
|
108
|
+
return structuredClone(builder());
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function graphFixture(name) {
|
|
112
|
+
return sanitizeGraphOperation(
|
|
113
|
+
() => graphFixtureUnsafe(name),
|
|
114
|
+
'invalid-graph-fixture',
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { validateEvent } from '../config/validate.js';
|
|
2
|
+
import { graphFailure, sanitizeGraphOperation, validatedGraphSnapshot } from './validate.js';
|
|
3
|
+
|
|
4
|
+
const EVENT_KEYS = new Set([
|
|
5
|
+
'schemaVersion', 'eventId', 'graphId', 'nodeId', 'sequence', 'timestamp', 'actor', 'type',
|
|
6
|
+
'priorState', 'newState', 'retryReason', 'authorityDelta', 'budgetDelta', 'evidenceRefs',
|
|
7
|
+
'instanceId', 'leaseId', 'heartbeatSequence', 'heartbeatIntervalMs', 'approvalReceiptId',
|
|
8
|
+
]);
|
|
9
|
+
const ACTOR_KEYS = new Set(['role', 'id']);
|
|
10
|
+
const AUTHORITY_DELTA_KEYS = new Set(['added', 'removed']);
|
|
11
|
+
const BUDGET_DELTA_KEYS = new Set(['timeMinutes', 'tokenLimit', 'costUsd', 'taskLimit']);
|
|
12
|
+
|
|
13
|
+
const TRANSITIONS = Object.freeze({
|
|
14
|
+
proposed: new Set(['approved', 'blocked', 'cancelled']),
|
|
15
|
+
approved: new Set(['ready', 'blocked', 'cancelled']),
|
|
16
|
+
ready: new Set(['reserved', 'blocked', 'cancelled']),
|
|
17
|
+
reserved: new Set(['ready', 'running', 'blocked', 'cancelled']),
|
|
18
|
+
running: new Set(['verifying', 'corrective', 'blocked', 'failed', 'cancelled']),
|
|
19
|
+
verifying: new Set(['completed', 'corrective', 'blocked', 'failed', 'cancelled']),
|
|
20
|
+
corrective: new Set(['ready', 'reserved', 'running', 'verifying', 'blocked', 'failed', 'cancelled']),
|
|
21
|
+
blocked: new Set(['ready', 'corrective', 'failed', 'cancelled']),
|
|
22
|
+
failed: new Set(['corrective', 'archived']),
|
|
23
|
+
completed: new Set(['archived']),
|
|
24
|
+
archived: new Set(),
|
|
25
|
+
cancelled: new Set(['archived']),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function transitionAllowedUnsafe(priorState, newState) {
|
|
29
|
+
return TRANSITIONS[priorState]?.has(newState) ?? false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function transitionAllowed(priorState, newState) {
|
|
33
|
+
return sanitizeGraphOperation(
|
|
34
|
+
() => transitionAllowedUnsafe(priorState, newState),
|
|
35
|
+
'invalid-transition-arguments',
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function checkedEvent(event) {
|
|
40
|
+
validateEvent(event);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function capturedObject(value, allowedKeys) {
|
|
44
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) graphFailure('invalid-event');
|
|
45
|
+
const keys = Object.keys(value);
|
|
46
|
+
if (keys.length > allowedKeys.size || keys.some(key => !allowedKeys.has(key))) graphFailure('invalid-event');
|
|
47
|
+
const captured = {};
|
|
48
|
+
for (const key of keys) captured[key] = value[key];
|
|
49
|
+
return captured;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function capturedArray(value, maximum) {
|
|
53
|
+
if (!Array.isArray(value)) graphFailure('invalid-event');
|
|
54
|
+
const length = value.length;
|
|
55
|
+
if (!Number.isSafeInteger(length) || length > maximum) graphFailure('invalid-event');
|
|
56
|
+
const captured = [];
|
|
57
|
+
for (let index = 0; index < length; index += 1) captured.push(value[index]);
|
|
58
|
+
return captured;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function capturedEvent(event) {
|
|
62
|
+
const captured = capturedObject(event, EVENT_KEYS);
|
|
63
|
+
if ('actor' in captured) captured.actor = capturedObject(captured.actor, ACTOR_KEYS);
|
|
64
|
+
if ('authorityDelta' in captured) {
|
|
65
|
+
captured.authorityDelta = capturedObject(captured.authorityDelta, AUTHORITY_DELTA_KEYS);
|
|
66
|
+
if ('added' in captured.authorityDelta) captured.authorityDelta.added = capturedArray(captured.authorityDelta.added, 32);
|
|
67
|
+
if ('removed' in captured.authorityDelta) captured.authorityDelta.removed = capturedArray(captured.authorityDelta.removed, 32);
|
|
68
|
+
}
|
|
69
|
+
if ('budgetDelta' in captured) captured.budgetDelta = capturedObject(captured.budgetDelta, BUDGET_DELTA_KEYS);
|
|
70
|
+
if ('evidenceRefs' in captured) captured.evidenceRefs = capturedArray(captured.evidenceRefs, 64);
|
|
71
|
+
return captured;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function targetFor(graph, event) {
|
|
75
|
+
if (!event.nodeId) return graph;
|
|
76
|
+
const node = graph.nodes.find(candidate => candidate.id === event.nodeId);
|
|
77
|
+
if (!node) graphFailure('unknown-node');
|
|
78
|
+
return node;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function appendEvidence(target, evidenceRefs) {
|
|
82
|
+
if (!Array.isArray(target.evidenceRefs)) graphFailure('graph-evidence-target');
|
|
83
|
+
const known = new Set(target.evidenceRefs);
|
|
84
|
+
if (evidenceRefs.some(reference => known.has(reference))) graphFailure('duplicate-evidence');
|
|
85
|
+
target.evidenceRefs.push(...evidenceRefs);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function exactReferences(left, right) {
|
|
89
|
+
if (left.length !== right.length) return false;
|
|
90
|
+
const actual = [...left].sort(); const expected = [...right].sort();
|
|
91
|
+
return actual.every((value, index) => value === expected[index]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function reduceGraphUnsafe(graph, event) {
|
|
95
|
+
const stableGraph = validatedGraphSnapshot(graph);
|
|
96
|
+
const stableEvent = capturedEvent(event);
|
|
97
|
+
checkedEvent(stableEvent);
|
|
98
|
+
if (stableEvent.graphId !== stableGraph.id) graphFailure('graph-event-mismatch');
|
|
99
|
+
|
|
100
|
+
const next = stableGraph;
|
|
101
|
+
const target = targetFor(next, stableEvent);
|
|
102
|
+
if (stableEvent.type === 'state-transition') {
|
|
103
|
+
if (target.status !== stableEvent.priorState) graphFailure('stale-transition');
|
|
104
|
+
if (!transitionAllowedUnsafe(stableEvent.priorState, stableEvent.newState)) graphFailure('invalid-transition');
|
|
105
|
+
target.status = stableEvent.newState;
|
|
106
|
+
} else if (stableEvent.type === 'cancelled') {
|
|
107
|
+
if (!transitionAllowedUnsafe(target.status, 'cancelled')) graphFailure('invalid-transition');
|
|
108
|
+
target.status = 'cancelled';
|
|
109
|
+
} else if (stableEvent.type === 'evidence-recorded') {
|
|
110
|
+
appendEvidence(target, stableEvent.evidenceRefs);
|
|
111
|
+
} else if (stableEvent.type === 'approval-recorded' && stableEvent.nodeId) {
|
|
112
|
+
if (target.owner.role !== 'boss' || !target.approvalGate
|
|
113
|
+
|| !exactReferences(stableEvent.evidenceRefs, target.evidenceRefs)) graphFailure('invalid-approval-fact');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return validatedGraphSnapshot(next);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function reduceGraph(graph, event) {
|
|
120
|
+
return sanitizeGraphOperation(
|
|
121
|
+
() => reduceGraphUnsafe(graph, event),
|
|
122
|
+
'invalid-event',
|
|
123
|
+
);
|
|
124
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { graphFailure, sanitizeGraphOperation, validatedGraphSnapshot } from './validate.js';
|
|
2
|
+
|
|
3
|
+
const CANDIDATE_STATES = new Set(['ready', 'corrective']);
|
|
4
|
+
const ACTIVE_STATES = new Set(['reserved', 'running', 'verifying']);
|
|
5
|
+
const BUDGET_KEYS = ['timeMinutes', 'tokenLimit', 'costUsd', 'taskLimit'];
|
|
6
|
+
const OPTION_KEYS = new Set([
|
|
7
|
+
'maxActiveNodes',
|
|
8
|
+
'maxDelegationDepth',
|
|
9
|
+
'priorities',
|
|
10
|
+
'capacityByOwner',
|
|
11
|
+
'childCapacityByParent',
|
|
12
|
+
'budget',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
function codePointCompare(left, right) {
|
|
16
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function nonNegativeSafeInteger(value, reason) {
|
|
20
|
+
if (!Number.isSafeInteger(value) || value < 0) graphFailure(reason);
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function decimalFromNumber(value) {
|
|
25
|
+
const match = /^(\d+)(?:\.(\d+))?(?:e([+-]?\d+))?$/i.exec(String(value));
|
|
26
|
+
if (!match) graphFailure('invalid-budget');
|
|
27
|
+
const fraction = match[2] ?? '';
|
|
28
|
+
const exponent = Number(match[3] ?? 0);
|
|
29
|
+
let coefficient = BigInt(`${match[1]}${fraction}`);
|
|
30
|
+
let scale = fraction.length - exponent;
|
|
31
|
+
if (scale < 0) {
|
|
32
|
+
coefficient *= 10n ** BigInt(-scale);
|
|
33
|
+
scale = 0;
|
|
34
|
+
}
|
|
35
|
+
while (scale > 0 && coefficient % 10n === 0n) {
|
|
36
|
+
coefficient /= 10n;
|
|
37
|
+
scale -= 1;
|
|
38
|
+
}
|
|
39
|
+
return { coefficient, scale };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function decimalCoefficientAt(value, scale) {
|
|
43
|
+
return value.coefficient * (10n ** BigInt(scale - value.scale));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function compareDecimals(left, right) {
|
|
47
|
+
const scale = Math.max(left.scale, right.scale);
|
|
48
|
+
const leftCoefficient = decimalCoefficientAt(left, scale);
|
|
49
|
+
const rightCoefficient = decimalCoefficientAt(right, scale);
|
|
50
|
+
return leftCoefficient < rightCoefficient ? -1 : leftCoefficient > rightCoefficient ? 1 : 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function subtractDecimals(left, right) {
|
|
54
|
+
const scale = Math.max(left.scale, right.scale);
|
|
55
|
+
let coefficient = decimalCoefficientAt(left, scale) - decimalCoefficientAt(right, scale);
|
|
56
|
+
if (coefficient < 0n) graphFailure('budget-underflow');
|
|
57
|
+
let normalizedScale = scale;
|
|
58
|
+
while (normalizedScale > 0 && coefficient % 10n === 0n) {
|
|
59
|
+
coefficient /= 10n;
|
|
60
|
+
normalizedScale -= 1;
|
|
61
|
+
}
|
|
62
|
+
return { coefficient, scale: normalizedScale };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function capturedObject(value, allowedKeys, reason) {
|
|
66
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) graphFailure(reason);
|
|
67
|
+
const keys = Object.keys(value);
|
|
68
|
+
if (keys.some(key => !allowedKeys.has(key))) graphFailure(reason);
|
|
69
|
+
const captured = {};
|
|
70
|
+
for (const key of keys) captured[key] = value[key];
|
|
71
|
+
return captured;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function validateOptions(rawOptions, graph) {
|
|
75
|
+
const options = capturedObject(rawOptions, OPTION_KEYS, 'invalid-scheduler-options');
|
|
76
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) graphFailure('invalid-scheduler-options');
|
|
77
|
+
const maxActiveNodes = options.maxActiveNodes === undefined
|
|
78
|
+
? graph.nodes.length
|
|
79
|
+
: nonNegativeSafeInteger(options.maxActiveNodes, 'invalid-active-capacity');
|
|
80
|
+
const maxDelegationDepth = options.maxDelegationDepth === undefined
|
|
81
|
+
? graph.maxDelegationDepth
|
|
82
|
+
: nonNegativeSafeInteger(options.maxDelegationDepth, 'invalid-delegation-depth');
|
|
83
|
+
|
|
84
|
+
const nodeIds = new Set(graph.nodes.map(node => node.id));
|
|
85
|
+
const ownerIds = new Set(graph.nodes.map(node => node.owner.id));
|
|
86
|
+
const parentIds = new Set(graph.nodes.flatMap(node => (node.parentId ? [node.parentId] : [])));
|
|
87
|
+
const normalizedMaps = {};
|
|
88
|
+
for (const [name, values, allowedKeys] of [
|
|
89
|
+
['priorities', options.priorities ?? {}, nodeIds],
|
|
90
|
+
['capacity', options.capacityByOwner ?? {}, ownerIds],
|
|
91
|
+
['child-capacity', options.childCapacityByParent ?? {}, parentIds],
|
|
92
|
+
]) {
|
|
93
|
+
const captured = capturedObject(values, allowedKeys, `unknown-${name}-key`);
|
|
94
|
+
for (const [id, value] of Object.entries(captured)) {
|
|
95
|
+
nonNegativeSafeInteger(value, `invalid-${name}`);
|
|
96
|
+
}
|
|
97
|
+
normalizedMaps[name] = captured;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let budget;
|
|
101
|
+
if (options.budget !== undefined) {
|
|
102
|
+
const capturedBudget = capturedObject(options.budget, new Set(BUDGET_KEYS), 'invalid-budget');
|
|
103
|
+
if (Object.keys(capturedBudget).length !== BUDGET_KEYS.length) graphFailure('invalid-budget');
|
|
104
|
+
budget = {};
|
|
105
|
+
for (const key of BUDGET_KEYS) {
|
|
106
|
+
const value = capturedBudget[key];
|
|
107
|
+
if (key === 'costUsd') {
|
|
108
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) graphFailure('invalid-budget');
|
|
109
|
+
budget[key] = decimalFromNumber(value);
|
|
110
|
+
} else {
|
|
111
|
+
nonNegativeSafeInteger(value, 'invalid-budget');
|
|
112
|
+
budget[key] = value;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
maxActiveNodes,
|
|
119
|
+
maxDelegationDepth,
|
|
120
|
+
priorities: normalizedMaps.priorities,
|
|
121
|
+
capacityByOwner: normalizedMaps.capacity,
|
|
122
|
+
childCapacityByParent: normalizedMaps['child-capacity'],
|
|
123
|
+
budget,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function dependencyDepths(graph) {
|
|
128
|
+
const nodes = new Map(graph.nodes.map(node => [node.id, node]));
|
|
129
|
+
const depths = new Map(graph.nodes.map(node => [node.id, 0]));
|
|
130
|
+
const remaining = new Map(graph.nodes.map(node => [node.id, node.dependencies.length]));
|
|
131
|
+
const dependents = new Map(graph.nodes.map(node => [node.id, []]));
|
|
132
|
+
for (const node of graph.nodes) {
|
|
133
|
+
for (const dependency of node.dependencies) dependents.get(dependency).push(node.id);
|
|
134
|
+
}
|
|
135
|
+
const queue = graph.nodes.filter(node => node.dependencies.length === 0).map(node => node.id).sort(codePointCompare);
|
|
136
|
+
for (let index = 0; index < queue.length; index += 1) {
|
|
137
|
+
const id = queue[index];
|
|
138
|
+
for (const dependentId of dependents.get(id).sort(codePointCompare)) {
|
|
139
|
+
depths.set(dependentId, Math.max(depths.get(dependentId), depths.get(id) + 1));
|
|
140
|
+
const next = remaining.get(dependentId) - 1;
|
|
141
|
+
remaining.set(dependentId, next);
|
|
142
|
+
if (next === 0) queue.push(dependentId);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if ([...remaining.values()].some(value => value !== 0)) graphFailure('dependency-cycle');
|
|
146
|
+
return depths;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function delegationDepth(node, nodes) {
|
|
150
|
+
let depth = 0;
|
|
151
|
+
let current = node;
|
|
152
|
+
while (current.parentId) {
|
|
153
|
+
depth += 1;
|
|
154
|
+
current = nodes.get(current.parentId);
|
|
155
|
+
}
|
|
156
|
+
return depth;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function dependenciesComplete(node, nodes) {
|
|
160
|
+
return node.dependencies.every(id => nodes.get(id).status === 'completed');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function candidates(graph, options) {
|
|
164
|
+
const nodes = new Map(graph.nodes.map(node => [node.id, node]));
|
|
165
|
+
const depths = dependencyDepths(graph);
|
|
166
|
+
return graph.nodes
|
|
167
|
+
.filter(node => (
|
|
168
|
+
CANDIDATE_STATES.has(node.status)
|
|
169
|
+
&& dependenciesComplete(node, nodes)
|
|
170
|
+
&& delegationDepth(node, nodes) <= options.maxDelegationDepth
|
|
171
|
+
))
|
|
172
|
+
.sort((left, right) => (
|
|
173
|
+
((options.priorities[left.id] ?? 0) < (options.priorities[right.id] ?? 0) ? 1
|
|
174
|
+
: (options.priorities[left.id] ?? 0) > (options.priorities[right.id] ?? 0) ? -1 : 0)
|
|
175
|
+
|| depths.get(left.id) - depths.get(right.id)
|
|
176
|
+
|| codePointCompare(left.id, right.id)
|
|
177
|
+
));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function budgetFits(node, remaining) {
|
|
181
|
+
return !remaining || BUDGET_KEYS.every(key => (
|
|
182
|
+
key === 'costUsd'
|
|
183
|
+
? compareDecimals(decimalFromNumber(node.budget[key]), remaining[key]) <= 0
|
|
184
|
+
: node.budget[key] <= remaining[key]
|
|
185
|
+
));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function consumeBudget(node, remaining) {
|
|
189
|
+
if (!remaining) return;
|
|
190
|
+
for (const key of BUDGET_KEYS) {
|
|
191
|
+
remaining[key] = key === 'costUsd'
|
|
192
|
+
? subtractDecimals(remaining[key], decimalFromNumber(node.budget[key]))
|
|
193
|
+
: remaining[key] - node.budget[key];
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function scheduleReadyNodesUnsafe(graph, rawOptions) {
|
|
198
|
+
const stableGraph = validatedGraphSnapshot(graph);
|
|
199
|
+
return scheduleValidatedGraph(stableGraph, rawOptions);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function scheduleValidatedGraph(graph, rawOptions) {
|
|
203
|
+
const options = validateOptions(rawOptions, graph);
|
|
204
|
+
const active = graph.nodes.filter(node => ACTIVE_STATES.has(node.status));
|
|
205
|
+
let slots = Math.max(0, options.maxActiveNodes - active.length);
|
|
206
|
+
const ownerCounts = new Map();
|
|
207
|
+
const childCounts = new Map();
|
|
208
|
+
for (const node of active) {
|
|
209
|
+
ownerCounts.set(node.owner.id, (ownerCounts.get(node.owner.id) ?? 0) + 1);
|
|
210
|
+
if (node.parentId) childCounts.set(node.parentId, (childCounts.get(node.parentId) ?? 0) + 1);
|
|
211
|
+
}
|
|
212
|
+
const remaining = options.budget ? { ...options.budget } : undefined;
|
|
213
|
+
const selected = [];
|
|
214
|
+
for (const node of candidates(graph, options)) {
|
|
215
|
+
if (slots === 0) break;
|
|
216
|
+
const ownerCapacity = options.capacityByOwner[node.owner.id] ?? graph.nodes.length;
|
|
217
|
+
const childCapacity = node.parentId
|
|
218
|
+
? options.childCapacityByParent[node.parentId] ?? graph.nodes.length
|
|
219
|
+
: graph.nodes.length;
|
|
220
|
+
if ((ownerCounts.get(node.owner.id) ?? 0) >= ownerCapacity) continue;
|
|
221
|
+
if (node.parentId && (childCounts.get(node.parentId) ?? 0) >= childCapacity) continue;
|
|
222
|
+
if (!budgetFits(node, remaining)) continue;
|
|
223
|
+
selected.push(node.id);
|
|
224
|
+
slots -= 1;
|
|
225
|
+
ownerCounts.set(node.owner.id, (ownerCounts.get(node.owner.id) ?? 0) + 1);
|
|
226
|
+
if (node.parentId) childCounts.set(node.parentId, (childCounts.get(node.parentId) ?? 0) + 1);
|
|
227
|
+
consumeBudget(node, remaining);
|
|
228
|
+
}
|
|
229
|
+
return selected;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function scheduleReadyNodes(graph, rawOptions = {}) {
|
|
233
|
+
return sanitizeGraphOperation(
|
|
234
|
+
() => scheduleReadyNodesUnsafe(graph, rawOptions),
|
|
235
|
+
'invalid-scheduler-options',
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function readyNodeIds(graph, options = {}) {
|
|
240
|
+
return sanitizeGraphOperation(
|
|
241
|
+
() => scheduleReadyNodesUnsafe(graph, options),
|
|
242
|
+
'invalid-scheduler-options',
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function canStart(graph, nodeId, options = {}) {
|
|
247
|
+
return sanitizeGraphOperation(() => {
|
|
248
|
+
const stableGraph = validatedGraphSnapshot(graph);
|
|
249
|
+
if (!stableGraph.nodes.some(node => node.id === nodeId)) graphFailure('unknown-node');
|
|
250
|
+
return scheduleValidatedGraph(stableGraph, options).includes(nodeId);
|
|
251
|
+
}, 'invalid-scheduler-options');
|
|
252
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { validateGoalGraph } from '../config/validate.js';
|
|
2
|
+
|
|
3
|
+
const GRAPH_KEYS = new Set(['schemaVersion', 'id', 'goal', 'providerRefs', 'maxDelegationDepth', 'status', 'nodes']);
|
|
4
|
+
const NODE_KEYS = new Set([
|
|
5
|
+
'id', 'parentId', 'objective', 'owner', 'dependencies', 'authorityScopes', 'budget',
|
|
6
|
+
'completionProfile', 'requiredEvidenceTypes', 'evidenceRefs', 'status', 'approvalGate',
|
|
7
|
+
]);
|
|
8
|
+
const OWNER_KEYS = new Set(['role', 'id']);
|
|
9
|
+
const BUDGET_KEYS = new Set(['timeMinutes', 'tokenLimit', 'costUsd', 'taskLimit']);
|
|
10
|
+
|
|
11
|
+
export class GraphValidationError extends Error {
|
|
12
|
+
constructor(reason = 'invalid-graph') {
|
|
13
|
+
super('Goal graph operation is invalid.');
|
|
14
|
+
this.name = 'GraphValidationError';
|
|
15
|
+
this.code = 'ERR_INVALID_GOAL_GRAPH';
|
|
16
|
+
this.safeMessage = this.message;
|
|
17
|
+
this.details = Object.freeze({ reason });
|
|
18
|
+
Object.freeze(this);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function graphFailure(reason) {
|
|
23
|
+
throw new GraphValidationError(reason);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function sanitizeGraphOperation(operation, reason) {
|
|
27
|
+
try {
|
|
28
|
+
return operation();
|
|
29
|
+
} catch {
|
|
30
|
+
throw new GraphValidationError(reason);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function capturedObject(value, allowedKeys) {
|
|
35
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) graphFailure('invalid-graph');
|
|
36
|
+
const keys = Object.keys(value);
|
|
37
|
+
if (keys.length > allowedKeys.size || keys.some(key => !allowedKeys.has(key))) graphFailure('invalid-graph');
|
|
38
|
+
const captured = {};
|
|
39
|
+
for (const key of keys) captured[key] = value[key];
|
|
40
|
+
return captured;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function capturedArray(value, maximum, transform = item => item) {
|
|
44
|
+
if (!Array.isArray(value)) graphFailure('invalid-graph');
|
|
45
|
+
const length = value.length;
|
|
46
|
+
if (!Number.isSafeInteger(length) || length > maximum) graphFailure('invalid-graph');
|
|
47
|
+
const captured = [];
|
|
48
|
+
for (let index = 0; index < length; index += 1) captured.push(transform(value[index], index));
|
|
49
|
+
return captured;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function capturedOwner(value) {
|
|
53
|
+
return capturedObject(value, OWNER_KEYS);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function capturedBudget(value) {
|
|
57
|
+
return capturedObject(value, BUDGET_KEYS);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function capturedNode(value) {
|
|
61
|
+
const node = capturedObject(value, NODE_KEYS);
|
|
62
|
+
if ('owner' in node) node.owner = capturedOwner(node.owner);
|
|
63
|
+
if ('dependencies' in node) node.dependencies = capturedArray(node.dependencies, 100);
|
|
64
|
+
if ('authorityScopes' in node) node.authorityScopes = capturedArray(node.authorityScopes, 32);
|
|
65
|
+
if ('budget' in node) node.budget = capturedBudget(node.budget);
|
|
66
|
+
if ('requiredEvidenceTypes' in node) node.requiredEvidenceTypes = capturedArray(node.requiredEvidenceTypes, 8);
|
|
67
|
+
if ('evidenceRefs' in node) node.evidenceRefs = capturedArray(node.evidenceRefs, 64);
|
|
68
|
+
return node;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function capturedGraph(graph) {
|
|
72
|
+
const captured = capturedObject(graph, GRAPH_KEYS);
|
|
73
|
+
if ('providerRefs' in captured) captured.providerRefs = capturedArray(captured.providerRefs, 64);
|
|
74
|
+
if ('nodes' in captured) captured.nodes = capturedArray(captured.nodes, 1000, capturedNode);
|
|
75
|
+
return captured;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function validatedGraphSnapshotUnsafe(graph, config) {
|
|
79
|
+
const snapshot = capturedGraph(graph);
|
|
80
|
+
validateGoalGraph(snapshot, config);
|
|
81
|
+
|
|
82
|
+
const rolesByOwner = new Map();
|
|
83
|
+
for (const node of snapshot.nodes) {
|
|
84
|
+
const priorRole = rolesByOwner.get(node.owner.id);
|
|
85
|
+
if (priorRole && priorRole !== node.owner.role) graphFailure('owner-role-conflict');
|
|
86
|
+
rolesByOwner.set(node.owner.id, node.owner.role);
|
|
87
|
+
}
|
|
88
|
+
return snapshot;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function validatedGraphSnapshot(graph, config) {
|
|
92
|
+
return sanitizeGraphOperation(
|
|
93
|
+
() => validatedGraphSnapshotUnsafe(graph, config),
|
|
94
|
+
'invalid-graph',
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function validateExecutionGraph(graph, config) {
|
|
99
|
+
return sanitizeGraphOperation(
|
|
100
|
+
() => {
|
|
101
|
+
validatedGraphSnapshotUnsafe(graph, config);
|
|
102
|
+
return true;
|
|
103
|
+
},
|
|
104
|
+
'invalid-graph',
|
|
105
|
+
);
|
|
106
|
+
}
|