opencode-wait-a-minute 0.0.0-stage → 1.1.0-rc.8
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,463 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evidence Freshness / Closure
|
|
3
|
+
*
|
|
4
|
+
* `refreshEvidenceValidity(taskId, root)` is the orchestrator that detects
|
|
5
|
+
* environment and artifact drift, invalidates the affected evidence records,
|
|
6
|
+
* recomputes requirement satisfaction, and returns a structured delta report.
|
|
7
|
+
*
|
|
8
|
+
* Drift sources:
|
|
9
|
+
* 1. Environment fingerprint drift (os / node version / executable / version /
|
|
10
|
+
* git revision). This is the same check used by
|
|
11
|
+
* `detectInvalidatedEvidence` in evidence-lineage.js, extended with a
|
|
12
|
+
* git-revision probe and file-hash tracking.
|
|
13
|
+
* 2. Artifact drift — any file whose hash (or mtime, as a cheap proxy)
|
|
14
|
+
* changed since the evidence was captured. The set of relevant files is
|
|
15
|
+
* declared by the task state under `state.relevantFiles`; if absent, we
|
|
16
|
+
* conservatively consider every file in the task's lineage directory.
|
|
17
|
+
*
|
|
18
|
+
* Evidence metadata:
|
|
19
|
+
* `refreshEvidenceValidity` also enriches evidence records with:
|
|
20
|
+
* - `environment` (EnvironmentFingerprint at capture time)
|
|
21
|
+
* - `artifactHashes` (map of relevant path -> sha256)
|
|
22
|
+
* - `artifactMtimes` (map of relevant path -> mtime ms)
|
|
23
|
+
* These are stored alongside the evidence so future freshness checks can
|
|
24
|
+
* diff against them.
|
|
25
|
+
*
|
|
26
|
+
* Returned delta report shape:
|
|
27
|
+
* {
|
|
28
|
+
* taskId,
|
|
29
|
+
* root,
|
|
30
|
+
* drifted: boolean,
|
|
31
|
+
* reasons: string[], // human-readable drift reasons
|
|
32
|
+
* previousEnvironment: object|null, // env fingerprint last recorded
|
|
33
|
+
* currentEnvironment: object, // env fingerprint observed now
|
|
34
|
+
* artifactChanges: [ // per-file drift entries
|
|
35
|
+
* { path, previousHash, currentHash, previousMtime, currentMtime, status }
|
|
36
|
+
* ],
|
|
37
|
+
* invalidatedEvidence: [ // evidence records marked invalidated
|
|
38
|
+
* { id, requirementId, status, invalidationReason }
|
|
39
|
+
* ],
|
|
40
|
+
* satisfiedBefore: string[], // requirement IDs satisfied pre-refresh
|
|
41
|
+
* satisfiedAfter: string[], // requirement IDs satisfied post-refresh
|
|
42
|
+
* unsatisfiedDelta: { // recomputed gap
|
|
43
|
+
* gained: string[], // requirements that LOST satisfaction
|
|
44
|
+
* lost: string[], // requirements that LOST satisfaction
|
|
45
|
+
* },
|
|
46
|
+
* recomputedAt: number,
|
|
47
|
+
* }
|
|
48
|
+
*
|
|
49
|
+
* The function is read-mostly: it writes only to evidence records it
|
|
50
|
+
* invalidates and to a single `.wam/tasks/<taskId>/freshness.json` snapshot.
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
import fs from "node:fs";
|
|
54
|
+
import path from "node:path";
|
|
55
|
+
import crypto from "node:crypto";
|
|
56
|
+
import { execFileSync } from "node:child_process";
|
|
57
|
+
|
|
58
|
+
import {
|
|
59
|
+
getAllEvidence,
|
|
60
|
+
getEvidence,
|
|
61
|
+
invalidateEvidence,
|
|
62
|
+
isRequirementSatisfied,
|
|
63
|
+
getSatisfiedRequirements,
|
|
64
|
+
getUnsatisfiedRequirements,
|
|
65
|
+
} from "./evidence-lineage.js";
|
|
66
|
+
import { getTaskState } from "../skills/engine.js";
|
|
67
|
+
|
|
68
|
+
function saveTaskState(taskId, state, root) {
|
|
69
|
+
const dir = path.join(root, ".wam", "tasks", taskId);
|
|
70
|
+
ensureDir(dir);
|
|
71
|
+
fs.writeFileSync(path.join(dir, "state.yaml"), JSON.stringify(state, null, 2));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function ensureDir(dir) {
|
|
75
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function safeReadFile(p) {
|
|
79
|
+
try {
|
|
80
|
+
return fs.readFileSync(p);
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function sha256(buf) {
|
|
87
|
+
return crypto.createHash("sha256").update(buf).digest("hex");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Compute the current environment fingerprint.
|
|
92
|
+
*
|
|
93
|
+
* @param {string} root - project root
|
|
94
|
+
* @returns {{
|
|
95
|
+
* os: string,
|
|
96
|
+
* nodeVersion: string,
|
|
97
|
+
* executable: string,
|
|
98
|
+
* version: string,
|
|
99
|
+
* repositoryRevision: string,
|
|
100
|
+
* capturedAt: number,
|
|
101
|
+
* }}
|
|
102
|
+
*/
|
|
103
|
+
export function captureEnvironment(root) {
|
|
104
|
+
let revision = "";
|
|
105
|
+
try {
|
|
106
|
+
revision = execFileSync(
|
|
107
|
+
"git",
|
|
108
|
+
["rev-parse", "HEAD"],
|
|
109
|
+
{ cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
|
|
110
|
+
).trim();
|
|
111
|
+
} catch {
|
|
112
|
+
revision = "";
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
os: `${process.platform}-${process.arch}`,
|
|
116
|
+
nodeVersion: process.version,
|
|
117
|
+
executable: process.execPath,
|
|
118
|
+
version: process.env.npm_package_version || "",
|
|
119
|
+
repositoryRevision: revision,
|
|
120
|
+
capturedAt: Date.now(),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Compute a sha256 + mtime for a single file. Returns null for missing files.
|
|
126
|
+
*/
|
|
127
|
+
function fingerprintFile(absPath) {
|
|
128
|
+
const buf = safeReadFile(absPath);
|
|
129
|
+
if (buf === null) return null;
|
|
130
|
+
let mtime = 0;
|
|
131
|
+
try {
|
|
132
|
+
mtime = fs.statSync(absPath).mtimeMs;
|
|
133
|
+
} catch {
|
|
134
|
+
mtime = 0;
|
|
135
|
+
}
|
|
136
|
+
return { hash: sha256(buf), mtime };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Fingerprint a list of relative paths against root. Missing files map to null.
|
|
141
|
+
*/
|
|
142
|
+
export function fingerprintArtifacts(root, relativePaths) {
|
|
143
|
+
const out = {};
|
|
144
|
+
for (const rel of relativePaths || []) {
|
|
145
|
+
const abs = path.isAbsolute(rel) ? rel : path.join(root, rel);
|
|
146
|
+
out[rel] = fingerprintFile(abs);
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Resolve the list of relevant artifact paths for a task.
|
|
153
|
+
*
|
|
154
|
+
* Order of precedence:
|
|
155
|
+
* 1. state.relevantFiles (authoritative)
|
|
156
|
+
* 2. evidence.artifactHashes union (re-derived from existing evidence)
|
|
157
|
+
* 3. empty list (no relevant files known)
|
|
158
|
+
*/
|
|
159
|
+
function resolveRelevantFiles(state, evidence) {
|
|
160
|
+
if (Array.isArray(state.relevantFiles) && state.relevantFiles.length > 0) {
|
|
161
|
+
return state.relevantFiles;
|
|
162
|
+
}
|
|
163
|
+
const collected = new Set();
|
|
164
|
+
for (const ev of evidence) {
|
|
165
|
+
if (ev && ev.artifactHashes) {
|
|
166
|
+
for (const p of Object.keys(ev.artifactHashes)) collected.add(p);
|
|
167
|
+
}
|
|
168
|
+
if (ev && ev.artifactMtimes) {
|
|
169
|
+
for (const p of Object.keys(ev.artifactMtimes)) collected.add(p);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return [...collected];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Decide which evidence records are affected by a set of artifact changes.
|
|
177
|
+
*
|
|
178
|
+
* An evidence record is affected if any of its declared artifactHashes /
|
|
179
|
+
* artifactMtimes correspond to a changed file.
|
|
180
|
+
*/
|
|
181
|
+
function evidenceAffectedByArtifactChanges(ev, artifactChanges) {
|
|
182
|
+
if (!ev) return false;
|
|
183
|
+
const changed = new Set(
|
|
184
|
+
artifactChanges
|
|
185
|
+
.filter((c) => c.status !== "unchanged")
|
|
186
|
+
.map((c) => c.path)
|
|
187
|
+
);
|
|
188
|
+
if (changed.size === 0) return false;
|
|
189
|
+
const declared = new Set();
|
|
190
|
+
if (ev.artifactHashes) for (const p of Object.keys(ev.artifactHashes)) declared.add(p);
|
|
191
|
+
if (ev.artifactMtimes) for (const p of Object.keys(ev.artifactMtimes)) declared.add(p);
|
|
192
|
+
for (const p of declared) {
|
|
193
|
+
if (changed.has(p)) return true;
|
|
194
|
+
}
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Recompute requirement satisfaction for a task from scratch (after
|
|
200
|
+
* invalidations have been applied). Returns { satisfied, unsatisfied }.
|
|
201
|
+
*/
|
|
202
|
+
function recomputeSatisfaction(taskId, root) {
|
|
203
|
+
return {
|
|
204
|
+
satisfied: getSatisfiedRequirements(taskId, root).map((r) => r.requirementId),
|
|
205
|
+
unsatisfied: getUnsatisfiedRequirements(taskId, root).map((r) => r.requirementId),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Persist the freshness snapshot.
|
|
211
|
+
*/
|
|
212
|
+
function writeFreshnessSnapshot(root, taskId, report) {
|
|
213
|
+
const dir = path.join(root, ".wam", "tasks", taskId);
|
|
214
|
+
ensureDir(dir);
|
|
215
|
+
const snap = path.join(dir, "freshness.json");
|
|
216
|
+
fs.writeFileSync(snap, JSON.stringify(report, null, 2));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* @typedef {Object} RefreshDeltaReport
|
|
221
|
+
* @property {string} taskId
|
|
222
|
+
* @property {string} root
|
|
223
|
+
* @property {boolean} drifted
|
|
224
|
+
* @property {string[]} reasons
|
|
225
|
+
* @property {object|null} previousEnvironment
|
|
226
|
+
* @property {object} currentEnvironment
|
|
227
|
+
* @property {Array<{path:string,previousHash:string|null,currentHash:string|null,previousMtime:number,currentMtime:number,status:string}>} artifactChanges
|
|
228
|
+
* @property {Array<{id:string,requirementId:string,status:string,invalidationReason:string}>} invalidatedEvidence
|
|
229
|
+
* @property {string[]} satisfiedBefore
|
|
230
|
+
* @property {string[]} satisfiedAfter
|
|
231
|
+
* @property {{gained:string[],lost:string[]}} unsatisfiedDelta
|
|
232
|
+
* @property {number} recomputedAt
|
|
233
|
+
*/
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Refresh evidence validity for a task. Detects drift, invalidates affected
|
|
237
|
+
* evidence, recomputes requirement satisfaction, and returns a delta report.
|
|
238
|
+
*
|
|
239
|
+
* The function is idempotent: running it twice in a row with no intervening
|
|
240
|
+
* changes should produce a `drifted: false` report with empty
|
|
241
|
+
* `invalidatedEvidence`.
|
|
242
|
+
*
|
|
243
|
+
* @param {string} taskId
|
|
244
|
+
* @param {string} [root] - project root (defaults to process.cwd())
|
|
245
|
+
* @returns {RefreshDeltaReport}
|
|
246
|
+
*/
|
|
247
|
+
export function refreshEvidenceValidity(taskId, root) {
|
|
248
|
+
const effectiveRoot = root || process.cwd();
|
|
249
|
+
const state = getTaskState(taskId, effectiveRoot);
|
|
250
|
+
if (!state) {
|
|
251
|
+
return {
|
|
252
|
+
taskId,
|
|
253
|
+
root: effectiveRoot,
|
|
254
|
+
drifted: false,
|
|
255
|
+
reasons: [`task state not found for ${taskId}`],
|
|
256
|
+
previousEnvironment: null,
|
|
257
|
+
currentEnvironment: captureEnvironment(effectiveRoot),
|
|
258
|
+
artifactChanges: [],
|
|
259
|
+
invalidatedEvidence: [],
|
|
260
|
+
satisfiedBefore: [],
|
|
261
|
+
satisfiedAfter: [],
|
|
262
|
+
unsatisfiedDelta: { gained: [], lost: [] },
|
|
263
|
+
recomputedAt: Date.now(),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const previousEnvironment = state.currentEnvironment || null;
|
|
268
|
+
const currentEnvironment = captureEnvironment(effectiveRoot);
|
|
269
|
+
const evidence = getAllEvidence(taskId, effectiveRoot);
|
|
270
|
+
|
|
271
|
+
// 1) Environment drift detection.
|
|
272
|
+
const envDrifted = isEnvironmentDrifted(previousEnvironment, currentEnvironment);
|
|
273
|
+
|
|
274
|
+
// 2) Artifact drift detection.
|
|
275
|
+
const relevantFiles = resolveRelevantFiles(state, evidence);
|
|
276
|
+
const previousArtifacts = {};
|
|
277
|
+
for (const ev of evidence) {
|
|
278
|
+
if (!ev) continue;
|
|
279
|
+
if (ev.artifactHashes) {
|
|
280
|
+
for (const [p, h] of Object.entries(ev.artifactHashes)) {
|
|
281
|
+
const existing = previousArtifacts[p] || {};
|
|
282
|
+
existing.hash = h;
|
|
283
|
+
previousArtifacts[p] = existing;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (ev.artifactMtimes) {
|
|
287
|
+
for (const [p, m] of Object.entries(ev.artifactMtimes)) {
|
|
288
|
+
const existing = previousArtifacts[p] || {};
|
|
289
|
+
existing.mtime = m;
|
|
290
|
+
previousArtifacts[p] = existing;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const currentArtifacts = fingerprintArtifacts(effectiveRoot, relevantFiles);
|
|
295
|
+
const artifactChanges = diffArtifacts(previousArtifacts, currentArtifacts);
|
|
296
|
+
|
|
297
|
+
const reasons = [];
|
|
298
|
+
if (envDrifted) reasons.push("environment fingerprint changed");
|
|
299
|
+
if (artifactChanges.some((c) => c.status !== "unchanged")) {
|
|
300
|
+
reasons.push("artifact hash or mtime changed");
|
|
301
|
+
}
|
|
302
|
+
const drifted = reasons.length > 0;
|
|
303
|
+
|
|
304
|
+
// Capture pre-drift satisfaction BEFORE we invalidate anything, so the delta
|
|
305
|
+
// report can show which requirements we just lost.
|
|
306
|
+
const satisfiedBefore = getSatisfiedRequirements(taskId, effectiveRoot);
|
|
307
|
+
|
|
308
|
+
// 3) Decide which evidence to invalidate.
|
|
309
|
+
const invalidatedEvidence = [];
|
|
310
|
+
if (drifted) {
|
|
311
|
+
for (const ev of evidence) {
|
|
312
|
+
if (!ev) continue;
|
|
313
|
+
if (ev.status === "invalidated") continue;
|
|
314
|
+
const envInvalidates = envDrifted;
|
|
315
|
+
const artifactInvalidates = evidenceAffectedByArtifactChanges(ev, artifactChanges);
|
|
316
|
+
if (envInvalidates || artifactInvalidates) {
|
|
317
|
+
const reasonParts = [];
|
|
318
|
+
if (envInvalidates) reasonParts.push("environment drift");
|
|
319
|
+
if (artifactInvalidates) reasonParts.push("artifact drift");
|
|
320
|
+
const reason = reasonParts.join(" + ");
|
|
321
|
+
const ok = invalidateEvidence(taskId, ev.id, reason, effectiveRoot);
|
|
322
|
+
if (ok) {
|
|
323
|
+
invalidatedEvidence.push({
|
|
324
|
+
id: ev.id,
|
|
325
|
+
requirementId: ev.requirementId,
|
|
326
|
+
status: "invalidated",
|
|
327
|
+
invalidationReason: reason,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// 4) Stamp current environment + artifacts onto surviving evidence.
|
|
335
|
+
stampEvidenceMetadata(taskId, evidence, currentEnvironment, currentArtifacts, effectiveRoot);
|
|
336
|
+
|
|
337
|
+
// 5) Recompute requirement satisfaction. satisfiedBefore was captured before
|
|
338
|
+
// invalidation; now read the post-invalidation set.
|
|
339
|
+
const after = recomputeSatisfaction(taskId, effectiveRoot);
|
|
340
|
+
const satisfiedAfter = after.satisfied;
|
|
341
|
+
|
|
342
|
+
const lost = satisfiedBefore.filter((id) => !satisfiedAfter.includes(id));
|
|
343
|
+
const gained = satisfiedAfter.filter((id) => !satisfiedBefore.includes(id));
|
|
344
|
+
|
|
345
|
+
// 6) Persist current environment on the task state.
|
|
346
|
+
state.currentEnvironment = currentEnvironment;
|
|
347
|
+
if (!Array.isArray(state.relevantFiles)) {
|
|
348
|
+
state.relevantFiles = relevantFiles;
|
|
349
|
+
}
|
|
350
|
+
saveTaskState(taskId, state, effectiveRoot);
|
|
351
|
+
|
|
352
|
+
const report = {
|
|
353
|
+
taskId,
|
|
354
|
+
root: effectiveRoot,
|
|
355
|
+
drifted,
|
|
356
|
+
reasons,
|
|
357
|
+
previousEnvironment,
|
|
358
|
+
currentEnvironment,
|
|
359
|
+
artifactChanges,
|
|
360
|
+
invalidatedEvidence,
|
|
361
|
+
satisfiedBefore,
|
|
362
|
+
satisfiedAfter,
|
|
363
|
+
unsatisfiedDelta: { gained, lost },
|
|
364
|
+
recomputedAt: Date.now(),
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
writeFreshnessSnapshot(effectiveRoot, taskId, report);
|
|
368
|
+
return report;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function isEnvironmentDrifted(prev, curr) {
|
|
372
|
+
if (!prev) return false;
|
|
373
|
+
return (
|
|
374
|
+
prev.os !== curr.os ||
|
|
375
|
+
prev.nodeVersion !== curr.nodeVersion ||
|
|
376
|
+
prev.executable !== curr.executable ||
|
|
377
|
+
prev.version !== curr.version ||
|
|
378
|
+
(prev.repositoryRevision || "") !== (curr.repositoryRevision || "")
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function diffArtifacts(previousArtifacts, currentArtifacts) {
|
|
383
|
+
const out = [];
|
|
384
|
+
const allKeys = new Set([
|
|
385
|
+
...Object.keys(previousArtifacts || {}),
|
|
386
|
+
...Object.keys(currentArtifacts || {}),
|
|
387
|
+
]);
|
|
388
|
+
for (const p of allKeys) {
|
|
389
|
+
const prev = previousArtifacts[p] || null;
|
|
390
|
+
const curr = currentArtifacts[p] || null;
|
|
391
|
+
if (!prev && !curr) {
|
|
392
|
+
out.push({ path: p, previousHash: null, currentHash: null, previousMtime: 0, currentMtime: 0, status: "missing" });
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (!prev && curr) {
|
|
396
|
+
out.push({ path: p, previousHash: null, currentHash: curr.hash, previousMtime: 0, currentMtime: curr.mtime, status: "added" });
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (prev && !curr) {
|
|
400
|
+
out.push({ path: p, previousHash: prev.hash, currentHash: null, previousMtime: prev.mtime, currentMtime: 0, status: "removed" });
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
const status = prev.hash === curr.hash ? "unchanged" : "modified";
|
|
404
|
+
out.push({
|
|
405
|
+
path: p,
|
|
406
|
+
previousHash: prev.hash,
|
|
407
|
+
currentHash: curr.hash,
|
|
408
|
+
previousMtime: prev.mtime,
|
|
409
|
+
currentMtime: curr.mtime,
|
|
410
|
+
status,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
return out;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function stampEvidenceMetadata(taskId, evidence, environment, artifacts, root) {
|
|
417
|
+
for (const ev of evidence) {
|
|
418
|
+
const live = getEvidence(taskId, ev.id, root);
|
|
419
|
+
if (!live) continue;
|
|
420
|
+
if (live.status === "invalidated") continue;
|
|
421
|
+
live.environment = environment;
|
|
422
|
+
live.artifactHashes = {};
|
|
423
|
+
live.artifactMtimes = {};
|
|
424
|
+
for (const [p, fp] of Object.entries(artifacts)) {
|
|
425
|
+
if (!fp) continue;
|
|
426
|
+
live.artifactHashes[p] = fp.hash;
|
|
427
|
+
live.artifactMtimes[p] = fp.mtime;
|
|
428
|
+
}
|
|
429
|
+
const file = path.join(root, ".wam", "tasks", taskId, "lineage", `${live.id}.json`);
|
|
430
|
+
try {
|
|
431
|
+
fs.writeFileSync(file, JSON.stringify(live, null, 2));
|
|
432
|
+
} catch {
|
|
433
|
+
// best-effort stamp
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Check whether the current evidence for a task allows the DONE state.
|
|
440
|
+
* Returns { allowed: boolean, reason: string, report: RefreshDeltaReport }.
|
|
441
|
+
*
|
|
442
|
+
* This is the gate `refreshEvidenceValidity` was built for: a DONE transition
|
|
443
|
+
* is blocked whenever refreshEvidenceValidity reports drifted=true and at
|
|
444
|
+
* least one requirement that was satisfied before is no longer satisfied.
|
|
445
|
+
*/
|
|
446
|
+
export function isDoneAllowed(taskId, root) {
|
|
447
|
+
const report = refreshEvidenceValidity(taskId, root);
|
|
448
|
+
if (!report.drifted) {
|
|
449
|
+
return { allowed: true, reason: "no drift detected", report };
|
|
450
|
+
}
|
|
451
|
+
if (report.unsatisfiedDelta.lost.length > 0) {
|
|
452
|
+
return {
|
|
453
|
+
allowed: false,
|
|
454
|
+
reason: `drift invalidated evidence for ${report.unsatisfiedDelta.lost.length} requirement(s)`,
|
|
455
|
+
report,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
allowed: false,
|
|
460
|
+
reason: `drift detected (${report.reasons.join(", ")}) but no satisfied requirement lost`,
|
|
461
|
+
report,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { EVIDENCE_TYPES, EVIDENCE_STRENGTH } from "./evidence.js";
|
|
2
|
+
|
|
3
|
+
export function detectEvidenceGaps(requirements, evidence) {
|
|
4
|
+
const gaps = [];
|
|
5
|
+
|
|
6
|
+
for (const req of requirements) {
|
|
7
|
+
const reqEvidence = evidence.filter(e => e.requirementId === req.id);
|
|
8
|
+
|
|
9
|
+
// No evidence at all
|
|
10
|
+
if (reqEvidence.length === 0) {
|
|
11
|
+
gaps.push({
|
|
12
|
+
requirementId: req.id,
|
|
13
|
+
gapType: "NO_EVIDENCE",
|
|
14
|
+
missingEvidence: ["Any evidence"],
|
|
15
|
+
suggestedVerification: getSuggestedVerification(req)
|
|
16
|
+
});
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Check strength
|
|
21
|
+
const maxStrength = getMaxStrength(reqEvidence);
|
|
22
|
+
const requiredStrength = req.critical ? EVIDENCE_STRENGTH.L3_DIRECT : EVIDENCE_STRENGTH.L2_OBSERVATION;
|
|
23
|
+
|
|
24
|
+
if (maxStrength < requiredStrength) {
|
|
25
|
+
gaps.push({
|
|
26
|
+
requirementId: req.id,
|
|
27
|
+
gapType: "INSUFFICIENT_STRENGTH",
|
|
28
|
+
missingEvidence: [`Need at least ${requiredStrength} strength`],
|
|
29
|
+
currentStrength: maxStrength,
|
|
30
|
+
suggestedVerification: getSuggestedVerification(req)
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check for contradictions
|
|
35
|
+
const hasSupport = reqEvidence.some(e => e.supports !== false);
|
|
36
|
+
const hasContradiction = reqEvidence.some(e => e.supports === false);
|
|
37
|
+
|
|
38
|
+
if (hasSupport && hasContradiction) {
|
|
39
|
+
gaps.push({
|
|
40
|
+
requirementId: req.id,
|
|
41
|
+
gapType: "CONTRADICTION",
|
|
42
|
+
missingEvidence: ["Resolve conflicting evidence"],
|
|
43
|
+
suggestedVerification: "Re-evaluate with fresh evidence"
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return gaps;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getMaxStrength(evidence) {
|
|
52
|
+
const strengthOrder = [
|
|
53
|
+
EVIDENCE_STRENGTH.L0_CLAIM,
|
|
54
|
+
EVIDENCE_STRENGTH.L1_INFERENCE,
|
|
55
|
+
EVIDENCE_STRENGTH.L2_OBSERVATION,
|
|
56
|
+
EVIDENCE_STRENGTH.L3_DIRECT,
|
|
57
|
+
EVIDENCE_STRENGTH.L4_VERIFICATION,
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
let maxIdx = -1;
|
|
61
|
+
for (const ev of evidence) {
|
|
62
|
+
const idx = strengthOrder.indexOf(ev.strength);
|
|
63
|
+
if (idx > maxIdx) maxIdx = idx;
|
|
64
|
+
}
|
|
65
|
+
return strengthOrder[maxIdx] || EVIDENCE_STRENGTH.L0_CLAIM;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getSuggestedVerification(req) {
|
|
69
|
+
if (req.claim?.includes("test") || req.claim?.includes("verify")) {
|
|
70
|
+
return "Execute test or verification command";
|
|
71
|
+
}
|
|
72
|
+
if (req.claim?.includes("launch") || req.claim?.includes("run")) {
|
|
73
|
+
return "Execute and observe actual behavior";
|
|
74
|
+
}
|
|
75
|
+
return "Gather direct evidence";
|
|
76
|
+
}
|