opencode-wait-a-minute 0.0.0-stage → 1.1.0-rc.10
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 +21 -0
- package/README.md +318 -2
- package/index.js +1836 -0
- package/package.json +70 -4
- package/skills/codebase-design/SKILL.md +114 -0
- package/skills/design-contract/SKILL.md +27 -0
- package/skills/design-critique/SKILL.md +19 -0
- package/skills/design-system/SKILL.md +17 -0
- package/skills/design-system/references/image-analysis.md +14 -0
- package/skills/design-system/references/tokens.md +32 -0
- package/skills/design-system/references/ui-rules.md +14 -0
- package/skills/diagnosing-bugs/SKILL.md +138 -0
- package/skills/frontend-design/SKILL.md +36 -0
- package/skills/handoff/SKILL.md +16 -0
- package/skills/improve-codebase-architecture/SKILL.md +71 -0
- package/skills/linter-negative/SKILL.md +74 -0
- package/skills/no-slop-ui/SKILL.md +26 -0
- package/skills/prototype/SKILL.md +26 -0
- package/skills/registry.json +44087 -0
- package/skills/tastemaker/SKILL.md +8 -0
- package/skills/tastemaker/references/tastemaker.md +14 -0
- package/skills/tdd/SKILL.md +38 -0
- package/skills/visual-review/SKILL.md +103 -0
- package/skills/visual-review/references/review-gates.md +42 -0
- package/skills/visual-review/references/surface-archetypes.md +37 -0
- package/skills/wizard/SKILL.md +44 -0
- package/skills/writing-for-agents/SKILL.md +81 -0
- package/src/cognition/cognition-store.js +251 -0
- package/src/cognition/cognitive-state.js +126 -0
- package/src/cognition/hypothesis-manager.js +113 -0
- package/src/cognition/observation-engine.js +112 -0
- package/src/cognition/observation-provenance.js +268 -0
- package/src/context/active-context-boundary.js +68 -0
- package/src/context/assembly.js +494 -0
- package/src/context/assessment-rules.js +55 -0
- package/src/context/capsule-staleness.js +267 -0
- package/src/context/capsules.js +2 -0
- package/src/context/compare-array.js +98 -0
- package/src/context/compare-object.js +89 -0
- package/src/context/compare-values.js +42 -0
- package/src/context/context-assembly-contract.js +71 -0
- package/src/context/context-budget-manager.js +232 -0
- package/src/context/context-builder.js +49 -0
- package/src/context/context-cache.js +55 -0
- package/src/context/context-capture.js +243 -0
- package/src/context/context-classification.js +199 -0
- package/src/context/context-compaction.js +179 -0
- package/src/context/context-decision-audit.js +267 -0
- package/src/context/context-demotion.js +173 -0
- package/src/context/context-dictionary.js +106 -0
- package/src/context/context-drift-detection.js +225 -0
- package/src/context/context-evaluation.js +374 -0
- package/src/context/context-event-ingress.js +100 -0
- package/src/context/context-event-normalization.js +224 -0
- package/src/context/context-feedback-loop.js +216 -0
- package/src/context/context-freshness.js +21 -0
- package/src/context/context-graph-builder.js +71 -0
- package/src/context/context-graph.js +451 -0
- package/src/context/context-history-relevance.js +321 -0
- package/src/context/context-interception.js +145 -0
- package/src/context/context-invalidation-events.js +154 -0
- package/src/context/context-lifecycle.js +170 -0
- package/src/context/context-loop-prevention.js +347 -0
- package/src/context/context-manager.js +340 -0
- package/src/context/context-memory-layers.js +265 -0
- package/src/context/context-minimality.js +166 -0
- package/src/context/context-normalization.js +130 -0
- package/src/context/context-optimization-metrics.js +434 -0
- package/src/context/context-output-policy.js +135 -0
- package/src/context/context-pack-authority.js +157 -0
- package/src/context/context-promotion.js +172 -0
- package/src/context/context-query-contract.js +101 -0
- package/src/context/context-ranking.js +164 -0
- package/src/context/context-rate-distortion.js +261 -0
- package/src/context/context-recovery.js +209 -0
- package/src/context/context-retrieval-integration.js +60 -0
- package/src/context/context-retrieval.js +93 -0
- package/src/context/context-router.js +457 -0
- package/src/context/context-routing-evaluation.js +331 -0
- package/src/context/context-routing-observability.js +296 -0
- package/src/context/context-routing-shadow.js +237 -0
- package/src/context/context-scope.js +181 -0
- package/src/context/context-snapshot.js +226 -0
- package/src/context/context-source-registry.js +95 -0
- package/src/context/context-sufficiency-gate.js +120 -0
- package/src/context/context-sufficiency-oracle.js +167 -0
- package/src/context/context-telemetry.js +176 -0
- package/src/context/context.js +633 -0
- package/src/context/dependency-closure.js +246 -0
- package/src/context/page-fault-tracker.js +275 -0
- package/src/context/prompt-engine.js +74 -0
- package/src/context/runtime-context-graph.js +260 -0
- package/src/context/selection.js +2 -0
- package/src/context/session.js +2 -0
- package/src/context/sufficiency-contract.js +300 -0
- package/src/context/tokenizer.js +17 -0
- package/src/context-benchmark-router.mjs +125 -0
- package/src/evidence/evidence-engine.js +161 -0
- package/src/evidence/evidence-factory.js +3 -0
- package/src/evidence/evidence-freshness.js +463 -0
- package/src/evidence/evidence-gap.js +76 -0
- package/src/evidence/evidence-lineage.js +525 -0
- package/src/evidence/evidence.js +136 -0
- package/src/evidence/freshness-validation.js +187 -0
- package/src/evidence/lineage.js +4 -0
- package/src/execution/action-evaluation.js +179 -0
- package/src/execution/circuit-breaker.js +70 -0
- package/src/execution/claim-interception.js +63 -0
- package/src/execution/execution-engine.js +258 -0
- package/src/execution/execution-intent.js +69 -0
- package/src/execution/execution-state.js +89 -0
- package/src/execution/formatting.js +60 -0
- package/src/execution/l1-sanitize.js +16 -0
- package/src/execution/l2-resource-guards.js +19 -0
- package/src/execution/l3-invariant-checker.js +5 -0
- package/src/execution/l4-containment.js +6 -0
- package/src/execution/runtime-guards.js +106 -0
- package/src/execution/strategy-identity.js +162 -0
- package/src/execution/tool-interceptor.js +7 -0
- package/src/execution/tool-result-classifier.js +63 -0
- package/src/integration/canary-deploy.js +143 -0
- package/src/integration/collector.js +59 -0
- package/src/integration/error-recovery.js +6 -0
- package/src/integration/event-bus.js +66 -0
- package/src/integration/health-checker.js +57 -0
- package/src/integration/logger.js +39 -0
- package/src/integration/message-handler.js +611 -0
- package/src/integration/opencode-db.js +153 -0
- package/src/integration/orchestration.js +419 -0
- package/src/integration/part-provenance.js +65 -0
- package/src/integration/rate-limiter.js +55 -0
- package/src/integration/release-gate.js +336 -0
- package/src/integration/rollback-manager.js +164 -0
- package/src/integration/router-adapter.js +269 -0
- package/src/integration/telemetry.js +45 -0
- package/src/integration/update-checker.js +10 -0
- package/src/persistence/event-sourcing.js +75 -0
- package/src/persistence/memory.js +501 -0
- package/src/persistence/migration-runner.js +16 -0
- package/src/persistence/persistence-manager.js +39 -0
- package/src/persistence/transaction-log.js +157 -0
- package/src/policy/action-capabilities.js +116 -0
- package/src/policy/action-policy.js +4 -0
- package/src/policy/assessment-engine.js +104 -0
- package/src/policy/assumption-tracking.js +66 -0
- package/src/policy/budget-enforcer.js +7 -0
- package/src/policy/config-validator.js +102 -0
- package/src/policy/execution-assessment.js +142 -0
- package/src/policy/failure-classification.js +5 -0
- package/src/policy/feature-flags.js +65 -0
- package/src/policy/governance-enforcement.js +118 -0
- package/src/policy/locale.js +15 -0
- package/src/policy/multi-signer.js +151 -0
- package/src/policy/policy-state-machine.js +97 -0
- package/src/policy/ponytail-constraint.js +38 -0
- package/src/policy/project-inspector.js +93 -0
- package/src/policy/query-builder.js +14 -0
- package/src/policy/request-classifier.js +79 -0
- package/src/policy/risk-engine.js +266 -0
- package/src/policy/scope-enforcement.js +140 -0
- package/src/policy/secret-scanner.js +68 -0
- package/src/policy/skill-routing.js +104 -0
- package/src/policy/strategy-capabilities.js +211 -0
- package/src/policy/strategy.js +6 -0
- package/src/policy/uncertainty.js +117 -0
- package/src/shared/container.js +44 -0
- package/src/shared/decision-types.js +24 -0
- package/src/shared/simple-test.js +160 -0
- package/src/shared/wam-events.js +52 -0
- package/src/skills/engine.js +1705 -0
- package/src/skills/skill-registry.js +19 -0
- package/src/skills/skill-routing.js +340 -0
- package/src/state/lifecycle-manager.js +113 -0
- package/src/state/operational-state.js +198 -0
- package/src/state/projections.js +47 -0
- package/src/state/replay-engine.js +5 -0
- package/src/state/requirement-ownership.js +20 -0
- package/src/state/requirement-state.js +152 -0
- package/src/state/run-event-identity.js +172 -0
- package/src/state/state-engine.js +19 -0
- package/src/state/state-machine.js +119 -0
- package/src/state/state-schema.js +318 -0
- package/src/state/state-store.js +335 -0
- package/src/state/state-transition.js +84 -0
- package/src/state/task-dependencies.js +287 -0
- package/src/state/task-execution.js +217 -0
- package/src/state/task-lifecycle.js +59 -0
- package/src/state/task-runs.js +361 -0
- package/src/state/task-state-file.js +86 -0
- package/src/state/task-state.js +41 -0
- package/src/state/task-status.js +54 -0
- package/src/state/task-store.js +48 -0
- package/src/state/wam-state.js +148 -0
- package/src/verification/completion-gate-simple.js +8 -0
- package/src/verification/completion-gate.js +183 -0
- package/src/verification/contract.js +9 -0
- package/src/verification/contradiction-resolution.js +255 -0
- package/src/verification/diagnostic-engine.js +659 -0
- package/src/verification/false-completion-prevention.js +100 -0
- package/src/verification/hook-validator.js +6 -0
- package/src/verification/requirements.js +6 -0
- package/src/verification/verification-context.js +162 -0
- package/src/verification/verification-lifecycle.js +66 -0
- package/src/verification/verification-model.js +89 -0
- package/src/verification/verification-persistence.js +129 -0
- package/src/verification/verification-policy.js +127 -0
- package/src/verification/verification.js +415 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { detectEvidenceGaps } from "../evidence/evidence-gap.js";
|
|
2
|
+
|
|
3
|
+
export const COMPLETION_KEYWORDS = [
|
|
4
|
+
"done", "fixed", "working", "solved", "completed",
|
|
5
|
+
"implemented", "resolved", "ready", "complete"
|
|
6
|
+
];
|
|
7
|
+
|
|
8
|
+
export function interceptCompletionClaim(agentMessage) {
|
|
9
|
+
const claims = [];
|
|
10
|
+
const lower = agentMessage.toLowerCase();
|
|
11
|
+
|
|
12
|
+
for (const keyword of COMPLETION_KEYWORDS) {
|
|
13
|
+
const regex = new RegExp(`\\b${keyword}\\b`, "gi");
|
|
14
|
+
let match;
|
|
15
|
+
while ((match = regex.exec(agentMessage)) !== null) {
|
|
16
|
+
claims.push({
|
|
17
|
+
keyword,
|
|
18
|
+
position: match.index,
|
|
19
|
+
context: agentMessage.slice(Math.max(0, match.index - 30), match.index + 30)
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return claims;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function classifyClaim(claim, context) {
|
|
28
|
+
const patterns = {
|
|
29
|
+
FACT: /\b(exists|present|available|installed|configured)\b/i,
|
|
30
|
+
HYPOTHESIS: /\b(think|believe|probably|might|could be)\b/i,
|
|
31
|
+
OBSERVATION: /\b(see|observe|notice|found|detected)\b/i,
|
|
32
|
+
INTENT: /\b(will|going to|plan|should|need to)\b/i,
|
|
33
|
+
CONCLUSION: /\b(therefore|so|thus|conclude)\b/i,
|
|
34
|
+
COMPLETION_CLAIM: /\b(done|fixed|working|solved|completed)\b/i
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
for (const [type, pattern] of Object.entries(patterns)) {
|
|
38
|
+
if (pattern.test(claim)) return type;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return "UNKNOWN";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function validateCompletionClaim(claim, taskState) {
|
|
45
|
+
if (!taskState.requirements || taskState.requirements.length === 0) {
|
|
46
|
+
return { valid: false, reason: "No requirements defined" };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const gaps = detectEvidenceGaps(taskState.requirements, taskState.evidence || []);
|
|
50
|
+
const mandatoryIncomplete = taskState.requirements.filter(
|
|
51
|
+
r => !r.optional && r.status !== "VERIFIED"
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
valid: mandatoryIncomplete.length === 0 && gaps.length === 0,
|
|
56
|
+
reason: gaps.length > 0 ? "Evidence gaps remain" :
|
|
57
|
+
mandatoryIncomplete.length > 0 ? "Requirements not verified" : null,
|
|
58
|
+
incompleteRequirements: mandatoryIncomplete.map(r => r.id),
|
|
59
|
+
evidenceGaps: gaps.map(g => g.requirementId)
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function validateContractNotReduced(originalRequirements, currentRequirements) {
|
|
64
|
+
const violations = [];
|
|
65
|
+
|
|
66
|
+
for (const orig of originalRequirements) {
|
|
67
|
+
const current = currentRequirements.find(r => r.id === orig.id);
|
|
68
|
+
if (!current) {
|
|
69
|
+
violations.push({ type: "REMOVED", requirementId: orig.id });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (orig.acceptanceCriteria && current.acceptanceCriteria) {
|
|
74
|
+
const origLen = orig.acceptanceCriteria.length;
|
|
75
|
+
const currLen = current.acceptanceCriteria.length;
|
|
76
|
+
if (currLen < origLen) {
|
|
77
|
+
violations.push({
|
|
78
|
+
type: "WEAKENED",
|
|
79
|
+
requirementId: orig.id,
|
|
80
|
+
originalCount: origLen,
|
|
81
|
+
currentCount: currLen
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (orig.claim && current.claim) {
|
|
87
|
+
if (current.claim.length < orig.claim.length * 0.8) {
|
|
88
|
+
violations.push({
|
|
89
|
+
type: "WEAKENED_CLAIM",
|
|
90
|
+
requirementId: orig.id
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
valid: violations.length === 0,
|
|
98
|
+
violations
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// Change 12 — context-verification
|
|
2
|
+
// Change 13 — verification-context-budget
|
|
3
|
+
// Change 15 — verification-invalidation
|
|
4
|
+
|
|
5
|
+
export const CONTEXT_LEVELS = {
|
|
6
|
+
N0: "N0", // Task / requirements
|
|
7
|
+
N1: "N1", // Domain-specific verification knowledge
|
|
8
|
+
N2: "N2", // Current evidence / observations
|
|
9
|
+
N3: "N3" // Opportunistic context
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function loadVerificationContext(level, taskData) {
|
|
13
|
+
switch (level) {
|
|
14
|
+
case CONTEXT_LEVELS.N0:
|
|
15
|
+
return { requirements: taskData.requirements };
|
|
16
|
+
case CONTEXT_LEVELS.N1:
|
|
17
|
+
return {
|
|
18
|
+
requirements: taskData.requirements,
|
|
19
|
+
verificationMethods: taskData.verificationMethods || []
|
|
20
|
+
};
|
|
21
|
+
case CONTEXT_LEVELS.N2:
|
|
22
|
+
return {
|
|
23
|
+
requirements: taskData.requirements,
|
|
24
|
+
evidence: taskData.evidence || [],
|
|
25
|
+
observations: taskData.observations || []
|
|
26
|
+
};
|
|
27
|
+
case CONTEXT_LEVELS.N3:
|
|
28
|
+
return {
|
|
29
|
+
opportunistic: true,
|
|
30
|
+
context: taskData.opportunisticContext
|
|
31
|
+
};
|
|
32
|
+
default:
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Rule: Never load N3 to substitute missing N2 evidence
|
|
38
|
+
export function validateContextBudget(loadedContext, requiredLevels) {
|
|
39
|
+
const missing = requiredLevels.filter(l => !loadedContext[l]);
|
|
40
|
+
return {
|
|
41
|
+
valid: missing.length === 0,
|
|
42
|
+
missingLevels: missing,
|
|
43
|
+
warning: loadedContext[CONTEXT_LEVELS.N3] && !loadedContext[CONTEXT_LEVELS.N2]
|
|
44
|
+
? "N3 context used to substitute missing N2"
|
|
45
|
+
: null
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const DEFAULT_VERIFICATION_BUDGET = {
|
|
50
|
+
maxActions: 10,
|
|
51
|
+
maxTokens: 50000,
|
|
52
|
+
maxLatencyMs: 120000,
|
|
53
|
+
maxRetries: 2
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export function createVerificationBudget(overrides = {}) {
|
|
57
|
+
return { ...DEFAULT_VERIFICATION_BUDGET, ...overrides };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Change 32 — verification-invalidation
|
|
61
|
+
export function createVerificationInvalidation(requirement, reason, relatedEvidence = []) {
|
|
62
|
+
return {
|
|
63
|
+
requirementId: requirement.id,
|
|
64
|
+
previousStatus: requirement.status,
|
|
65
|
+
newStatus: "UNKNOWN",
|
|
66
|
+
reason,
|
|
67
|
+
relatedEvidence: relatedEvidence.map(e => e.id),
|
|
68
|
+
invalidatedAt: Date.now(),
|
|
69
|
+
invalidatedBy: "verificationEngine"
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function checkVerificationDependencies(requirement, currentContext) {
|
|
74
|
+
const dependencies = requirement.dependencies || [];
|
|
75
|
+
const changes = [];
|
|
76
|
+
|
|
77
|
+
for (const dep of dependencies) {
|
|
78
|
+
const currentValue = currentContext[dep.key];
|
|
79
|
+
const previousValue = dep.value;
|
|
80
|
+
|
|
81
|
+
if (currentValue !== previousValue) {
|
|
82
|
+
changes.push({
|
|
83
|
+
dependency: dep.key,
|
|
84
|
+
previous: previousValue,
|
|
85
|
+
current: currentValue
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
stable: changes.length === 0,
|
|
92
|
+
changes
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function shouldInvalidate(requirement, currentContext, stalenessThreshold = 3600000) {
|
|
97
|
+
if (!requirement.verifiedAt) return false;
|
|
98
|
+
|
|
99
|
+
const age = Date.now() - requirement.verifiedAt;
|
|
100
|
+
if (age > stalenessThreshold) return true;
|
|
101
|
+
|
|
102
|
+
const deps = checkVerificationDependencies(requirement, currentContext);
|
|
103
|
+
return !deps.stable;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function isBudgetExhausted(budget, usage) {
|
|
107
|
+
return (
|
|
108
|
+
usage.actions >= budget.maxActions ||
|
|
109
|
+
usage.tokens >= budget.maxTokens ||
|
|
110
|
+
usage.latencyMs >= budget.maxLatencyMs ||
|
|
111
|
+
usage.retries >= budget.maxRetries
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function invalidateVerification(requirement, invalidatingObservation) {
|
|
116
|
+
return {
|
|
117
|
+
...requirement,
|
|
118
|
+
status: "UNKNOWN",
|
|
119
|
+
invalidatedAt: Date.now(),
|
|
120
|
+
invalidationReason: invalidatingObservation,
|
|
121
|
+
previousStatus: requirement.status
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Before verifying a requirement, reject STALE evidence.
|
|
127
|
+
* If any linked evidence is STALE the requirement returns to PENDING
|
|
128
|
+
* and verification is skipped (caller must replan).
|
|
129
|
+
*
|
|
130
|
+
* @param {object} requirement
|
|
131
|
+
* @param {Array<{id?: string, status?: string}>} evidence
|
|
132
|
+
* @returns {{ canVerify: boolean, requirement: object, staleEvidence: Array, reason: string|null }}
|
|
133
|
+
*/
|
|
134
|
+
export function checkEvidenceFreshness(requirement, evidence = []) {
|
|
135
|
+
const staleEvidence = (evidence || []).filter(
|
|
136
|
+
(e) => e && (e.status === "stale" || e.status === "STALE")
|
|
137
|
+
);
|
|
138
|
+
if (staleEvidence.length === 0) {
|
|
139
|
+
return { canVerify: true, requirement, staleEvidence: [], reason: null };
|
|
140
|
+
}
|
|
141
|
+
const reverted = {
|
|
142
|
+
...requirement,
|
|
143
|
+
status: "pending",
|
|
144
|
+
previousStatus: requirement.status,
|
|
145
|
+
replan: true,
|
|
146
|
+
staleEvidenceIds: staleEvidence.map((e) => e.id).filter(Boolean),
|
|
147
|
+
};
|
|
148
|
+
return {
|
|
149
|
+
canVerify: false,
|
|
150
|
+
requirement: reverted,
|
|
151
|
+
staleEvidence,
|
|
152
|
+
reason: "STALE evidence — requirement returned to PENDING, replan required",
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Gate used immediately before verifying a requirement.
|
|
158
|
+
* Returns the requirement (possibly reset to PENDING) plus a verify flag.
|
|
159
|
+
*/
|
|
160
|
+
export function prepareRequirementVerification(requirement, evidence = []) {
|
|
161
|
+
return checkEvidenceFreshness(requirement, evidence);
|
|
162
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verification Lifecycle — requirement verification state machine.
|
|
3
|
+
* Implements spec: verification-lifecycle.
|
|
4
|
+
*
|
|
5
|
+
* States: UNVERIFIED → VERIFYING → VERIFIED (or → FAILED → new hypothesis)
|
|
6
|
+
* Evidence: method (test/lint/build/other), result (pass/fail), details
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const VERIFICATION_STATES = new Set(["UNVERIFIED", "VERIFYING", "VERIFIED", "FAILED", "INVALIDATED"]);
|
|
10
|
+
|
|
11
|
+
const VALID_TRANSITIONS = {
|
|
12
|
+
UNVERIFIED: new Set(["VERIFYING"]),
|
|
13
|
+
VERIFYING: new Set(["VERIFIED", "FAILED"]),
|
|
14
|
+
FAILED: new Set(["VERIFYING"]),
|
|
15
|
+
VERIFIED: new Set(["INVALIDATED"]),
|
|
16
|
+
INVALIDATED: new Set(["VERIFYING"]),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function isValidVerificationState(state) {
|
|
20
|
+
return VERIFICATION_STATES.has(state);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function transitionVerification(from, to) {
|
|
24
|
+
if (!isValidVerificationState(from)) throw new Error(`Invalid verification state: ${from}`);
|
|
25
|
+
if (!VALID_TRANSITIONS[from].has(to)) throw new Error(`Invalid transition: ${from} → ${to}. Allowed: ${[...VALID_TRANSITIONS[from]].join(", ") || "(none)"}`);
|
|
26
|
+
return to;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createEvidence(method, result, details) {
|
|
30
|
+
if (!method || !result) throw new Error("Evidence requires method and result");
|
|
31
|
+
return { method, result, details: details || "", at: Date.now() };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isVerified(requirement) {
|
|
35
|
+
return requirement?.verificationStatus === "VERIFIED" && Array.isArray(requirement.evidence) && requirement.evidence.length > 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function canComplete(taskState) {
|
|
39
|
+
const reqs = taskState?.requirements || [];
|
|
40
|
+
const unverified = reqs.filter((r) => !isVerified(r));
|
|
41
|
+
return { canComplete: unverified.length === 0, unverifiedCount: unverified.length, unverifiedIds: unverified.map((r) => r.id) };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function hasOutstandingWork(taskState) {
|
|
45
|
+
const reqs = taskState?.requirements || [];
|
|
46
|
+
const unverified = reqs.filter((r) => !isVerified(r));
|
|
47
|
+
const backlogPending = (taskState?.backlog || []).filter((b) => b.status === "pending");
|
|
48
|
+
return unverified.length > 0 || backlogPending.length > 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function verifyRequirement(requirement, evidenceItems = []) {
|
|
52
|
+
if (evidenceItems.length === 0) throw new Error(`Cannot verify ${requirement.id}: no evidence provided`);
|
|
53
|
+
return {
|
|
54
|
+
...requirement,
|
|
55
|
+
verificationStatus: "VERIFIED",
|
|
56
|
+
evidence: evidenceItems,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function failRequirement(requirement, reason = "") {
|
|
61
|
+
return {
|
|
62
|
+
...requirement,
|
|
63
|
+
verificationStatus: "FAILED",
|
|
64
|
+
failureReason: reason,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Requirement states
|
|
2
|
+
export const REQUIREMENT_STATES = {
|
|
3
|
+
OPEN: "OPEN",
|
|
4
|
+
IN_PROGRESS: "IN_PROGRESS",
|
|
5
|
+
OBSERVED: "OBSERVED",
|
|
6
|
+
SUPPORTED: "SUPPORTED",
|
|
7
|
+
VERIFIED: "VERIFIED",
|
|
8
|
+
FAILED: "FAILED",
|
|
9
|
+
UNKNOWN: "UNKNOWN"
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Validation rules:
|
|
13
|
+
// - OBSERVED never implies VERIFIED
|
|
14
|
+
// - SUPPORTED requires relevant evidence
|
|
15
|
+
// - VERIFIED requires satisfying acceptanceCriteria
|
|
16
|
+
// - FAILED requires contradictory evidence
|
|
17
|
+
// - UNKNOWN means insufficient evidence
|
|
18
|
+
// - No VERIFIED allowed without verificationMethod
|
|
19
|
+
// - No task completion while any mandatory requirement is in OPEN, UNKNOWN, OBSERVED or SUPPORTED
|
|
20
|
+
|
|
21
|
+
export function createRequirement({ id, claim, intent, expectedOutcome, acceptanceCriteria = [], verificationMethod = null, optional = false }) {
|
|
22
|
+
if (!id || !claim) throw new Error("id and claim are required");
|
|
23
|
+
return {
|
|
24
|
+
id,
|
|
25
|
+
claim,
|
|
26
|
+
intent: intent || "",
|
|
27
|
+
expectedOutcome: expectedOutcome || "",
|
|
28
|
+
acceptanceCriteria,
|
|
29
|
+
verificationMethod,
|
|
30
|
+
status: REQUIREMENT_STATES.OPEN,
|
|
31
|
+
evidence: [],
|
|
32
|
+
optional,
|
|
33
|
+
createdAt: Date.now(),
|
|
34
|
+
updatedAt: Date.now()
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isRequirementCompletable(requirement) {
|
|
39
|
+
const terminalStates = [REQUIREMENT_STATES.VERIFIED, REQUIREMENT_STATES.FAILED];
|
|
40
|
+
return terminalStates.includes(requirement.status);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function canCompleteTask(requirements) {
|
|
44
|
+
const mandatory = requirements.filter(r => !r.optional);
|
|
45
|
+
const incomplete = mandatory.filter(r => !isRequirementCompletable(r));
|
|
46
|
+
return {
|
|
47
|
+
canComplete: incomplete.length === 0,
|
|
48
|
+
incompleteRequirements: incomplete.map(r => ({ id: r.id, status: r.status }))
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// State transitions (per spec)
|
|
53
|
+
export const VALID_TRANSITIONS = {
|
|
54
|
+
[REQUIREMENT_STATES.OPEN]: [REQUIREMENT_STATES.IN_PROGRESS],
|
|
55
|
+
[REQUIREMENT_STATES.IN_PROGRESS]: [REQUIREMENT_STATES.OBSERVED, REQUIREMENT_STATES.FAILED, REQUIREMENT_STATES.UNKNOWN],
|
|
56
|
+
[REQUIREMENT_STATES.OBSERVED]: [REQUIREMENT_STATES.SUPPORTED, REQUIREMENT_STATES.FAILED, REQUIREMENT_STATES.UNKNOWN],
|
|
57
|
+
[REQUIREMENT_STATES.SUPPORTED]: [REQUIREMENT_STATES.VERIFIED, REQUIREMENT_STATES.FAILED, REQUIREMENT_STATES.UNKNOWN],
|
|
58
|
+
[REQUIREMENT_STATES.UNKNOWN]: [REQUIREMENT_STATES.IN_PROGRESS],
|
|
59
|
+
[REQUIREMENT_STATES.VERIFIED]: [REQUIREMENT_STATES.OPEN], // only if new evidence invalidates
|
|
60
|
+
[REQUIREMENT_STATES.FAILED]: [], // terminal
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export function canTransition(fromState, toState) {
|
|
64
|
+
const allowed = VALID_TRANSITIONS[fromState];
|
|
65
|
+
return allowed && allowed.includes(toState);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function transitionRequirement(requirement, toState, evidence = null) {
|
|
69
|
+
if (!canTransition(requirement.status, toState)) {
|
|
70
|
+
throw new Error(`Invalid transition: ${requirement.status} → ${toState}. Allowed: ${VALID_TRANSITIONS[requirement.status]?.join(", ") || "none"}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const updated = {
|
|
74
|
+
...requirement,
|
|
75
|
+
status: toState,
|
|
76
|
+
updatedAt: Date.now()
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
if (evidence) {
|
|
80
|
+
updated.evidence = [...requirement.evidence, evidence];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// If moving back to OPEN from VERIFIED, mark as stale
|
|
84
|
+
if (requirement.status === REQUIREMENT_STATES.VERIFIED && toState === REQUIREMENT_STATES.OPEN) {
|
|
85
|
+
updated.invalidatedAt = Date.now();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return updated;
|
|
89
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verification Persistence + Stale Evidence — WAM 1.1
|
|
3
|
+
*
|
|
4
|
+
* Change 16 — session-persistence:
|
|
5
|
+
* persistVerificationState, loadVerificationState, createVerificationSnapshot
|
|
6
|
+
*
|
|
7
|
+
* Change 17 — stale-evidence:
|
|
8
|
+
* EVIDENCE_TTL, getEvidenceTTL, isEvidenceStale
|
|
9
|
+
*
|
|
10
|
+
* Persistence root: .wam/verifications/ (mirrors PersistenceManager convention).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
|
|
16
|
+
let snapshotCounter = 0;
|
|
17
|
+
|
|
18
|
+
// ── Stale Evidence Constants ────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
/** Default TTL for evidence freshness: 24 hours in milliseconds. */
|
|
21
|
+
export const EVIDENCE_TTL = 24 * 60 * 60 * 1000; // 86_400_000 ms
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Returns the configured TTL for evidence staleness checks (ms).
|
|
25
|
+
* Override via WAM_EVIDENCE_TTL env var for testing flexibility.
|
|
26
|
+
*/
|
|
27
|
+
export function getEvidenceTTL() {
|
|
28
|
+
const env = process.env.WAM_EVIDENCE_TTL;
|
|
29
|
+
if (env) {
|
|
30
|
+
const parsed = parseInt(env, 10);
|
|
31
|
+
if (!isNaN(parsed) && parsed > 0) return parsed;
|
|
32
|
+
}
|
|
33
|
+
return EVIDENCE_TTL;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Determines whether evidence is stale relative to the configured TTL.
|
|
38
|
+
* Evidence is stale when (now - evidence.timestamp) > getEvidenceTTL().
|
|
39
|
+
* Accepts evidence objects with a `timestamp` (number, ms) or `at` (ISO string) field.
|
|
40
|
+
*/
|
|
41
|
+
export function isEvidenceStale(evidence) {
|
|
42
|
+
if (!evidence) return true;
|
|
43
|
+
const ttl = getEvidenceTTL();
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
|
|
46
|
+
let evidenceTime;
|
|
47
|
+
if (typeof evidence.timestamp === "number") {
|
|
48
|
+
evidenceTime = evidence.timestamp;
|
|
49
|
+
} else if (typeof evidence.at === "string") {
|
|
50
|
+
evidenceTime = new Date(evidence.at).getTime();
|
|
51
|
+
} else {
|
|
52
|
+
return true; // no recognizable timestamp → stale
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (isNaN(evidenceTime)) return true;
|
|
56
|
+
return now - evidenceTime > ttl;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── Session Persistence ───────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
const VERIFICATIONS_DIR = path.join(process.cwd(), ".wam", "verifications");
|
|
62
|
+
|
|
63
|
+
function ensureDir(dir) {
|
|
64
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getPath(taskId) {
|
|
68
|
+
return path.join(VERIFICATIONS_DIR, `${taskId}.json`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Persists the verification state for a task to disk as JSON.
|
|
73
|
+
* @param {string} taskId
|
|
74
|
+
* @param {object} state — verification state object to serialize
|
|
75
|
+
* @returns {{ taskId, persistedAt, path }}
|
|
76
|
+
*/
|
|
77
|
+
export function persistVerificationState(taskId, state) {
|
|
78
|
+
if (!taskId) throw new Error("taskId is required");
|
|
79
|
+
ensureDir(VERIFICATIONS_DIR);
|
|
80
|
+
const filePath = getPath(taskId);
|
|
81
|
+
const record = {
|
|
82
|
+
taskId,
|
|
83
|
+
state,
|
|
84
|
+
persistedAt: new Date().toISOString(),
|
|
85
|
+
};
|
|
86
|
+
fs.writeFileSync(filePath, JSON.stringify(record, null, 2), "utf-8");
|
|
87
|
+
return { taskId, persistedAt: record.persistedAt, path: filePath };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Loads the verification state for a task from disk.
|
|
92
|
+
* Returns null if no file exists for the task.
|
|
93
|
+
* @param {string} taskId
|
|
94
|
+
* @returns {object|null} — { taskId, state, persistedAt } or null
|
|
95
|
+
*/
|
|
96
|
+
export function loadVerificationState(taskId) {
|
|
97
|
+
if (!taskId) throw new Error("taskId is required");
|
|
98
|
+
const filePath = getPath(taskId);
|
|
99
|
+
if (!fs.existsSync(filePath)) return null;
|
|
100
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
101
|
+
return JSON.parse(raw);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Creates a timestamped snapshot of the current verification state.
|
|
106
|
+
* Appends a snapshot entry to the task's existing record (or creates a new one).
|
|
107
|
+
* Snapshots are stored in `.wam/verifications/{taskId}.snapshots/` as individual files.
|
|
108
|
+
* @param {string} taskId
|
|
109
|
+
* @param {object} state — current verification state to snapshot
|
|
110
|
+
* @returns {{ taskId, snapshotId, snapshotAt, path }}
|
|
111
|
+
*/
|
|
112
|
+
export function createVerificationSnapshot(taskId, state) {
|
|
113
|
+
if (!taskId) throw new Error("taskId is required");
|
|
114
|
+
const snapshotsDir = path.join(VERIFICATIONS_DIR, taskId, "snapshots");
|
|
115
|
+
ensureDir(snapshotsDir);
|
|
116
|
+
|
|
117
|
+
const snapshotId = Date.now().toString(36) + "-" + (snapshotCounter++);
|
|
118
|
+
const snapshotAt = new Date().toISOString();
|
|
119
|
+
const snapshot = {
|
|
120
|
+
snapshotId,
|
|
121
|
+
taskId,
|
|
122
|
+
state,
|
|
123
|
+
snapshotAt,
|
|
124
|
+
};
|
|
125
|
+
const filePath = path.join(snapshotsDir, `${snapshotId}.json`);
|
|
126
|
+
fs.writeFileSync(filePath, JSON.stringify(snapshot, null, 2), "utf-8");
|
|
127
|
+
|
|
128
|
+
return { taskId, snapshotId, snapshotAt, path: filePath };
|
|
129
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { detectEvidenceGaps } from "../evidence/evidence-gap.js";
|
|
2
|
+
import { EVIDENCE_STRENGTH } from "../evidence/evidence.js";
|
|
3
|
+
|
|
4
|
+
export const VERIFICATION_STRATEGY = {
|
|
5
|
+
EXISTING_TEST: "existing_test",
|
|
6
|
+
TARGETED_COMMAND: "targeted_command",
|
|
7
|
+
TARGETED_INSPECTION: "targeted_inspection",
|
|
8
|
+
MINIMAL_REPRODUCTION: "minimal_reproduction",
|
|
9
|
+
BROADER_TEST: "broader_test",
|
|
10
|
+
MANUAL_VALIDATION: "manual_validation"
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function selectMinimalVerification(gap, availableChecks = []) {
|
|
14
|
+
// Prefer existing tests
|
|
15
|
+
const existingTest = availableChecks.find(c => c.type === "test");
|
|
16
|
+
if (existingTest) return { strategy: VERIFICATION_STRATEGY.EXISTING_TEST, check: existingTest };
|
|
17
|
+
|
|
18
|
+
// Then targeted command
|
|
19
|
+
const targetedCmd = availableChecks.find(c => c.type === "command");
|
|
20
|
+
if (targetedCmd) return { strategy: VERIFICATION_STRATEGY.TARGETED_COMMAND, check: targetedCmd };
|
|
21
|
+
|
|
22
|
+
// Then targeted inspection
|
|
23
|
+
const inspection = availableChecks.find(c => c.type === "inspection");
|
|
24
|
+
if (inspection) return { strategy: VERIFICATION_STRATEGY.TARGETED_INSPECTION, check: inspection };
|
|
25
|
+
|
|
26
|
+
// Fallback to broader test
|
|
27
|
+
const broader = availableChecks.find(c => c.type === "broader_test");
|
|
28
|
+
if (broader) return { strategy: VERIFICATION_STRATEGY.BROADER_TEST, check: broader };
|
|
29
|
+
|
|
30
|
+
return { strategy: VERIFICATION_STRATEGY.MANUAL_VALIDATION, check: null };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Change 28 — Verification-strategy: ordered strategy with cost estimation
|
|
34
|
+
export const VERIFICATION_STRATEGY_ORDER = [
|
|
35
|
+
VERIFICATION_STRATEGY.EXISTING_TEST,
|
|
36
|
+
VERIFICATION_STRATEGY.TARGETED_COMMAND,
|
|
37
|
+
VERIFICATION_STRATEGY.TARGETED_INSPECTION,
|
|
38
|
+
VERIFICATION_STRATEGY.MINIMAL_REPRODUCTION,
|
|
39
|
+
VERIFICATION_STRATEGY.BROADER_TEST,
|
|
40
|
+
VERIFICATION_STRATEGY.MANUAL_VALIDATION
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
export function rankVerificationStrategies(strategies, context) {
|
|
44
|
+
// Always prefer cheaper strategies first
|
|
45
|
+
return strategies
|
|
46
|
+
.filter(s => VERIFICATION_STRATEGY_ORDER.includes(s))
|
|
47
|
+
.sort((a, b) => VERIFICATION_STRATEGY_ORDER.indexOf(a) - VERIFICATION_STRATEGY_ORDER.indexOf(b));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function estimateStrategyCost(strategy) {
|
|
51
|
+
const costs = {
|
|
52
|
+
[VERIFICATION_STRATEGY.EXISTING_TEST]: 1,
|
|
53
|
+
[VERIFICATION_STRATEGY.TARGETED_COMMAND]: 2,
|
|
54
|
+
[VERIFICATION_STRATEGY.TARGETED_INSPECTION]: 3,
|
|
55
|
+
[VERIFICATION_STRATEGY.MINIMAL_REPRODUCTION]: 4,
|
|
56
|
+
[VERIFICATION_STRATEGY.BROADER_TEST]: 5,
|
|
57
|
+
[VERIFICATION_STRATEGY.MANUAL_VALIDATION]: 10
|
|
58
|
+
};
|
|
59
|
+
return costs[strategy] || 10;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Verification-strategy coordination: validate that strategy transitions are legal.
|
|
63
|
+
// NOTE: operates on VERIFICATION_STRATEGY, not policy states. Distinct from
|
|
64
|
+
// policy-state-machine.js#validatePolicyFlow despite the shared name.
|
|
65
|
+
export function validatePolicyFlow(currentPolicy, nextPolicy) {
|
|
66
|
+
const validTransitions = {
|
|
67
|
+
[VERIFICATION_STRATEGY.EXISTING_TEST]: [VERIFICATION_STRATEGY.TARGETED_COMMAND],
|
|
68
|
+
[VERIFICATION_STRATEGY.TARGETED_COMMAND]: [VERIFICATION_STRATEGY.TARGETED_INSPECTION],
|
|
69
|
+
[VERIFICATION_STRATEGY.TARGETED_INSPECTION]: [VERIFICATION_STRATEGY.MINIMAL_REPRODUCTION],
|
|
70
|
+
[VERIFICATION_STRATEGY.MINIMAL_REPRODUCTION]: [VERIFICATION_STRATEGY.BROADER_TEST],
|
|
71
|
+
[VERIFICATION_STRATEGY.BROADER_TEST]: [VERIFICATION_STRATEGY.MANUAL_VALIDATION],
|
|
72
|
+
};
|
|
73
|
+
if (currentPolicy === nextPolicy) return { valid: false, reason: "same-policy" };
|
|
74
|
+
const allowed = validTransitions[currentPolicy] || [];
|
|
75
|
+
if (!allowed.includes(nextPolicy)) return { valid: false, reason: "invalid-transition" };
|
|
76
|
+
return { valid: true };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Verification-strategy coordination: get next strategy in escalation chain.
|
|
80
|
+
// Distinct from policy-state-machine.js#getNextPolicy.
|
|
81
|
+
export function getNextPolicy(currentPolicy) {
|
|
82
|
+
const chain = [
|
|
83
|
+
VERIFICATION_STRATEGY.EXISTING_TEST,
|
|
84
|
+
VERIFICATION_STRATEGY.TARGETED_COMMAND,
|
|
85
|
+
VERIFICATION_STRATEGY.TARGETED_INSPECTION,
|
|
86
|
+
VERIFICATION_STRATEGY.MINIMAL_REPRODUCTION,
|
|
87
|
+
VERIFICATION_STRATEGY.BROADER_TEST,
|
|
88
|
+
VERIFICATION_STRATEGY.MANUAL_VALIDATION,
|
|
89
|
+
];
|
|
90
|
+
const idx = chain.indexOf(currentPolicy);
|
|
91
|
+
if (idx === -1 || idx === chain.length - 1) return null;
|
|
92
|
+
return chain[idx + 1];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Fail-closed completion gate over a task's requirements and evidence.
|
|
97
|
+
*
|
|
98
|
+
* Invariants:
|
|
99
|
+
* - A task is `blocked` unless EVERY mandatory (non-optional) requirement is
|
|
100
|
+
* `VERIFIED` AND there are no evidence gaps.
|
|
101
|
+
* - Missing or malformed evidence never passes: absent requirements/evidence
|
|
102
|
+
* default to empty arrays, which blocks completion.
|
|
103
|
+
* - Optional requirements do not affect `blocked`.
|
|
104
|
+
*
|
|
105
|
+
* @param {{ requirements?: Array<{id:string, optional?:boolean, status?:string}>,
|
|
106
|
+
* evidence?: Array<object> }} task
|
|
107
|
+
* @returns {{ blocked: boolean, completedRequirements: number,
|
|
108
|
+
* totalRequirements: number, evidenceGaps: Array<object>,
|
|
109
|
+
* unresolvedRequirements: string[] }}
|
|
110
|
+
*/
|
|
111
|
+
export function evaluateCompletionGate(task) {
|
|
112
|
+
const { requirements = [], evidence = [] } = task;
|
|
113
|
+
|
|
114
|
+
const gaps = detectEvidenceGaps(requirements, evidence);
|
|
115
|
+
const mandatory = requirements.filter(r => !r.optional);
|
|
116
|
+
const verified = mandatory.filter(r => r.status === "VERIFIED");
|
|
117
|
+
|
|
118
|
+
const blocked = verified.length !== mandatory.length || gaps.length > 0;
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
blocked,
|
|
122
|
+
completedRequirements: verified.length,
|
|
123
|
+
totalRequirements: mandatory.length,
|
|
124
|
+
evidenceGaps: gaps,
|
|
125
|
+
unresolvedRequirements: gaps.map(g => g.requirementId)
|
|
126
|
+
};
|
|
127
|
+
}
|