project-tiny-context-harness 0.2.82 → 0.2.83

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.
Files changed (46) hide show
  1. package/README.md +9 -3
  2. package/assets/README.md +9 -3
  3. package/assets/README.zh-CN.md +7 -1
  4. package/assets/protected-harness-baseline.json +18 -0
  5. package/assets/skills/composite-long-task-workflow/SKILL.md +19 -1
  6. package/assets/skills/composite-long-task-workflow/assets/execution-binding.template.md +10 -0
  7. package/assets/skills/composite-long-task-workflow/assets/goal-objective.template.md +6 -15
  8. package/assets/skills/composite-long-task-workflow/references/composite-long-task-workflow-protocol.md +26 -14
  9. package/dist/commands/composite-long-task.js +45 -2
  10. package/dist/lib/superpowers-task-ac010.d.ts +6 -0
  11. package/dist/lib/superpowers-task-ac010.js +26 -0
  12. package/dist/lib/superpowers-task-assertion-normalizers.js +4 -0
  13. package/dist/lib/superpowers-task-assertions.js +18 -4
  14. package/dist/lib/superpowers-task-attempt.d.ts +4 -0
  15. package/dist/lib/superpowers-task-attempt.js +102 -0
  16. package/dist/lib/superpowers-task-command-specs.d.ts +3 -0
  17. package/dist/lib/superpowers-task-command-specs.js +52 -0
  18. package/dist/lib/superpowers-task-compile.d.ts +4 -1
  19. package/dist/lib/superpowers-task-compile.js +7 -1
  20. package/dist/lib/superpowers-task-contradictions.d.ts +6 -0
  21. package/dist/lib/superpowers-task-contradictions.js +126 -0
  22. package/dist/lib/superpowers-task-current-evidence.d.ts +3 -0
  23. package/dist/lib/superpowers-task-current-evidence.js +154 -0
  24. package/dist/lib/superpowers-task-derive.js +14 -0
  25. package/dist/lib/superpowers-task-evidence-kernel.d.ts +12 -0
  26. package/dist/lib/superpowers-task-evidence-kernel.js +351 -0
  27. package/dist/lib/superpowers-task-evidence-records.d.ts +2 -0
  28. package/dist/lib/superpowers-task-evidence-records.js +55 -0
  29. package/dist/lib/superpowers-task-evidence.d.ts +10 -0
  30. package/dist/lib/superpowers-task-evidence.js +141 -0
  31. package/dist/lib/superpowers-task-gates.js +36 -24
  32. package/dist/lib/superpowers-task-harness-drift.d.ts +11 -0
  33. package/dist/lib/superpowers-task-harness-drift.js +86 -0
  34. package/dist/lib/superpowers-task-protected-baseline.d.ts +10 -0
  35. package/dist/lib/superpowers-task-protected-baseline.js +47 -0
  36. package/dist/lib/superpowers-task-state-schema.d.ts +99 -3
  37. package/dist/lib/superpowers-task-state-schema.js +23 -1
  38. package/dist/lib/superpowers-task-state-shape.d.ts +3 -0
  39. package/dist/lib/superpowers-task-state-shape.js +50 -0
  40. package/dist/lib/superpowers-task-state.js +7 -36
  41. package/dist/lib/superpowers-task-status.js +11 -1
  42. package/dist/lib/superpowers-task-under-specified.d.ts +7 -0
  43. package/dist/lib/superpowers-task-under-specified.js +61 -0
  44. package/dist/lib/superpowers-task-validator.js +5 -27
  45. package/package.json +1 -1
  46. package/source-mappings.yaml +3 -0
@@ -0,0 +1,351 @@
1
+ import { deriveRequiredCommandSpecs, requiredCommandSpecsHash } from "./superpowers-task-command-specs.js";
2
+ import { evaluateAc010Bootstrap } from "./superpowers-task-ac010.js";
3
+ import { evaluateCurrentAttemptArtifact } from "./superpowers-task-current-evidence.js";
4
+ import { scanSuperpowersContradictions } from "./superpowers-task-contradictions.js";
5
+ import { detectHarnessDrift } from "./superpowers-task-harness-drift.js";
6
+ import { evaluateProtectedBaseline } from "./superpowers-task-protected-baseline.js";
7
+ import { evaluateProofLayerAssertions, isMachineVerifiableLayer } from "./superpowers-task-assertions.js";
8
+ import { findUnderSpecifiedAcs } from "./superpowers-task-under-specified.js";
9
+ import { loadSuperpowersState, sourceRecords } from "./superpowers-task-state.js";
10
+ export async function evaluateTrustedEvidenceKernel(workdir, providedState) {
11
+ const state = providedState ?? (await loadSuperpowersState(workdir));
12
+ const errors = [];
13
+ const acStatuses = {};
14
+ const staleEvidenceIds = new Set();
15
+ const attempt = currentAttempt(state);
16
+ const currentSources = await sourceRecords(workdir);
17
+ if (!attempt) {
18
+ errors.push("missing current attempt: final-gate requires current_attempt_id with ExecutionAttempt");
19
+ }
20
+ else {
21
+ validateAttemptAgainstSources(state, attempt, currentSources, errors);
22
+ }
23
+ const expectedSpecs = deriveRequiredCommandSpecs(state);
24
+ validateRequiredSpecs(state, attempt, expectedSpecs, errors);
25
+ const underSpecified = new Map(findUnderSpecifiedAcs(state).map((item) => [item.ac_id, item.reasons]));
26
+ for (const reasons of underSpecified.values()) {
27
+ errors.push(...reasons);
28
+ }
29
+ const contradictionScan = await scanSuperpowersContradictions(workdir, state);
30
+ errors.push(...contradictionScan.errors);
31
+ const drift = detectHarnessDrift(state);
32
+ errors.push(...drift.errors);
33
+ const baseline = evaluateProtectedBaseline(state);
34
+ errors.push(...baseline.errors);
35
+ const evidenceById = new Map((state.evidence ?? []).map((evidence) => [evidence.evidence_id, evidence]));
36
+ for (const [acId, ac] of Object.entries(state.graph?.acceptance_criteria ?? {})) {
37
+ const acErrors = [];
38
+ if (underSpecified.has(acId)) {
39
+ acStatuses[acId] = "under_specified";
40
+ continue;
41
+ }
42
+ const requiredLayers = ac.required_proof_layers ?? [];
43
+ const spec = specForAc(state, expectedSpecs, acId);
44
+ if ((ac.machine_blocking === true || ac.assertion_result_required === true) && !spec) {
45
+ acErrors.push(`${acId} missing required_command_spec`);
46
+ }
47
+ if (spec) {
48
+ acErrors.push(...validateCommandRunsForSpec(state, attempt, spec));
49
+ }
50
+ for (const layerName of requiredLayers) {
51
+ const layerId = `${acId}.${layerName}`;
52
+ const layer = state.graph.proof_layers?.[layerId];
53
+ if (!layer || layer.status !== "satisfied") {
54
+ acErrors.push(`${layerId} missing current satisfied proof layer`);
55
+ continue;
56
+ }
57
+ if (isMachineVerifiableLayer(layerId)) {
58
+ const evaluation = evaluateProofLayerAssertions(state, layerId);
59
+ acErrors.push(...evaluation.blocking_assertion_failures, ...evaluation.negative_evidence_findings);
60
+ for (const evidenceId of layer.evidence_ids ?? []) {
61
+ const evidence = evidenceById.get(evidenceId);
62
+ if (!evidence) {
63
+ acErrors.push(`${layerId} references unregistered evidence ${evidenceId}`);
64
+ continue;
65
+ }
66
+ acErrors.push(...validateEvidenceAgainstSpec(state, evidence, spec, layerId));
67
+ acErrors.push(...(await evaluateCurrentAttemptArtifact(workdir, evidence, layerId)));
68
+ if (isStaleEvidenceError(acErrors)) {
69
+ staleEvidenceIds.add(evidence.evidence_id);
70
+ }
71
+ }
72
+ }
73
+ }
74
+ errors.push(...acErrors);
75
+ acStatuses[acId] = statusForAcErrors(acErrors, requiredLayers.length);
76
+ }
77
+ const ac010 = evaluateAc010Bootstrap(state, acStatuses);
78
+ for (const acId of ac010.invalidated_ac_ids) {
79
+ acStatuses[acId] = "invalidated";
80
+ }
81
+ errors.push(...ac010.errors);
82
+ const piStatuses = recomputePlanStatuses(state, acStatuses);
83
+ const allAcsComplete = Object.keys(state.graph?.acceptance_criteria ?? {}).length > 0 &&
84
+ Object.values(acStatuses).every((status) => status === "complete" || status === "out_of_scope_NA");
85
+ const allPisComplete = Object.keys(state.graph?.plan_items ?? {}).length > 0 &&
86
+ Object.values(piStatuses).every((status) => status === "complete" || status === "out_of_scope_NA");
87
+ const uniqueErrors = unique(errors);
88
+ const productComplete = uniqueErrors.length === 0 && allAcsComplete && allPisComplete && drift.product_goal_complete !== false && baseline.product_goal_complete !== false;
89
+ return {
90
+ product_goal_complete: productComplete,
91
+ acceptance_target_status: productComplete ? "complete" : statusForGlobalErrors(uniqueErrors, acStatuses),
92
+ errors: uniqueErrors,
93
+ ac_statuses: acStatuses,
94
+ pi_statuses: piStatuses,
95
+ stale_evidence_ids: [...staleEvidenceIds],
96
+ harness_task_final_verdict: drift.harness_task_final_verdict
97
+ };
98
+ }
99
+ export function applyTrustedEvidenceKernelResult(state, result) {
100
+ for (const [acId, status] of Object.entries(result.ac_statuses)) {
101
+ const ac = state.graph.acceptance_criteria[acId];
102
+ if (ac) {
103
+ ac.status = status;
104
+ }
105
+ }
106
+ for (const [piId, status] of Object.entries(result.pi_statuses)) {
107
+ const pi = state.graph.plan_items[piId];
108
+ if (pi) {
109
+ pi.status = status;
110
+ }
111
+ }
112
+ state.final.product_goal_complete = result.product_goal_complete;
113
+ state.meta.product_goal_complete = result.product_goal_complete;
114
+ state.final.acceptance_target_status = result.acceptance_target_status;
115
+ state.meta.acceptance_target_status = result.acceptance_target_status;
116
+ state.final.audit_task_complete = true;
117
+ state.meta.audit_task_complete = true;
118
+ state.final.completion_basis = result.product_goal_complete
119
+ ? ["trusted_evidence_kernel", "current_attempt_evidence", "negative_evidence_scan_passed", "harness_drift_lock_passed"]
120
+ : [];
121
+ state.final.next_required_actions = result.product_goal_complete ? [] : result.errors.slice(0, 12);
122
+ state.gates.final_gate = {
123
+ status: result.product_goal_complete ? "pass" : result.acceptance_target_status,
124
+ kernel: "trusted_evidence_kernel",
125
+ order: [
126
+ "load_three_inputs",
127
+ "recompute_source_hashes",
128
+ "load_task_state",
129
+ "load_current_attempt",
130
+ "load_command_run_records",
131
+ "load_registered_evidence_records",
132
+ "discard_stale_evidence",
133
+ "contradiction_scan",
134
+ "recompute_every_ac",
135
+ "recompute_every_pi",
136
+ "recompute_acceptance_target_status",
137
+ "recompute_product_goal_complete",
138
+ "regenerate_derived",
139
+ "append_event"
140
+ ],
141
+ errors: result.errors,
142
+ stale_evidence_ids: result.stale_evidence_ids,
143
+ harness_task_final_verdict: result.harness_task_final_verdict,
144
+ next_required_actions: state.final.next_required_actions
145
+ };
146
+ }
147
+ function validateAttemptAgainstSources(state, attempt, currentSources, errors) {
148
+ const productHash = currentSources.product_architecture_source?.sha256 ?? "";
149
+ const planHash = currentSources.technical_realization_plan?.sha256 ?? "";
150
+ const checklistHash = currentSources.acceptance_checklist?.sha256 ?? "";
151
+ for (const [key, source] of Object.entries(currentSources)) {
152
+ const expected = state.sources[key]?.sha256;
153
+ if (expected && source.sha256 !== expected) {
154
+ errors.push(`source hash mismatch for ${key}: expected ${expected}, actual ${source.sha256}; recompile graph before final-gate`);
155
+ }
156
+ }
157
+ if (attempt.product_source_hash !== productHash) {
158
+ errors.push("source hash mismatch: current attempt product_source_hash does not match product-architecture-source.md");
159
+ }
160
+ if (attempt.technical_plan_hash !== planHash) {
161
+ errors.push("source hash mismatch: current attempt technical_plan_hash does not match technical-realization-plan.md");
162
+ }
163
+ if (attempt.acceptance_checklist_hash !== checklistHash) {
164
+ errors.push("source hash mismatch: current attempt acceptance_checklist_hash does not match acceptance-checklist.md");
165
+ }
166
+ for (const field of [
167
+ "task_attempt_id",
168
+ "source_bundle_hash",
169
+ "product_source_hash",
170
+ "technical_plan_hash",
171
+ "acceptance_checklist_hash",
172
+ "git_head",
173
+ "git_status_short",
174
+ "tracked_diff_hash",
175
+ "relevant_untracked_hash",
176
+ "worktree_fingerprint",
177
+ "started_at",
178
+ "required_command_specs_hash",
179
+ "mode"
180
+ ]) {
181
+ if (!attempt[field]) {
182
+ errors.push(`current attempt missing required field ${field}`);
183
+ }
184
+ }
185
+ }
186
+ function validateRequiredSpecs(state, attempt, expectedSpecs, errors) {
187
+ const expectedByAc = new Map(expectedSpecs.map((spec) => [spec.ac_id, spec]));
188
+ for (const [acId, ac] of Object.entries(state.graph?.acceptance_criteria ?? {})) {
189
+ if (ac.machine_blocking !== true && ac.assertion_result_required !== true) {
190
+ continue;
191
+ }
192
+ const expected = expectedByAc.get(acId);
193
+ const actual = (state.required_command_specs ?? []).find((spec) => spec.ac_id === acId);
194
+ if (!expected || !actual) {
195
+ errors.push(`${acId} missing required_command_spec`);
196
+ continue;
197
+ }
198
+ if (actual.command_spec_id !== expected.command_spec_id) {
199
+ errors.push(`${acId} command_spec_id mismatch; required command specs must be recompiled from Acceptance Checklist`);
200
+ }
201
+ }
202
+ if (attempt) {
203
+ const specsHash = requiredCommandSpecsHash(state.required_command_specs ?? []);
204
+ if (attempt.required_command_specs_hash !== specsHash) {
205
+ errors.push("required_command_specs_hash mismatch for current attempt");
206
+ }
207
+ }
208
+ }
209
+ function validateCommandRunsForSpec(state, attempt, spec) {
210
+ const errors = [];
211
+ for (const proofLayer of spec.proof_layers.filter((layer) => isMachineVerifiableLayer(`${spec.ac_id}.${layer}`))) {
212
+ const run = (state.command_runs ?? []).find((item) => item.task_attempt_id === state.current_attempt_id &&
213
+ item.command_spec_id === spec.command_spec_id &&
214
+ item.ac_id === spec.ac_id &&
215
+ item.proof_layer === proofLayer);
216
+ if (!run) {
217
+ errors.push(`${spec.ac_id}.${proofLayer} missing current attempt command-run record for command_spec_id ${spec.command_spec_id}`);
218
+ continue;
219
+ }
220
+ errors.push(...validateCommandRun(run, attempt));
221
+ }
222
+ return errors;
223
+ }
224
+ function validateCommandRun(run, attempt) {
225
+ const errors = [];
226
+ if (attempt && run.task_attempt_id !== attempt.task_attempt_id) {
227
+ errors.push(`${run.command_run_id} stale command run from ${run.task_attempt_id}; expected ${attempt.task_attempt_id}`);
228
+ }
229
+ if (run.exit_code !== 0) {
230
+ errors.push(`${run.command_run_id} command_exit_code=${run.exit_code}; expected 0`);
231
+ }
232
+ if (!run.command_line.trim()) {
233
+ errors.push(`${run.command_run_id} missing command_line`);
234
+ }
235
+ return errors;
236
+ }
237
+ function validateEvidenceAgainstSpec(state, evidence, spec, layerId) {
238
+ if (!spec) {
239
+ return [];
240
+ }
241
+ const errors = [];
242
+ const assertion = evidence.assertion_result;
243
+ const label = `proof layer ${layerId} evidence ${evidence.evidence_id}`;
244
+ const expectedPiIds = state.graph.acceptance_criteria[spec.ac_id]?.related_plan_items ?? [];
245
+ const targetPiIds = evidence.target_pi_ids ?? assertion?.target_pi_ids ?? [];
246
+ for (const piId of expectedPiIds) {
247
+ if (!targetPiIds.includes(piId)) {
248
+ errors.push(`${label} target_pi_ids ${targetPiIds.join(", ") || "(none)"} do not include ${piId}`);
249
+ }
250
+ }
251
+ if (assertion) {
252
+ const assertionIds = new Set(assertion.positive_assertions.map((item) => item.id));
253
+ for (const id of spec.positive_assertions ?? []) {
254
+ if (!assertionIds.has(id)) {
255
+ errors.push(`${label} missing positive assertion ${id}`);
256
+ }
257
+ }
258
+ const negativeIds = new Set(assertion.negative_assertions.map((item) => item.id));
259
+ for (const id of spec.negative_assertions ?? []) {
260
+ if (!negativeIds.has(id)) {
261
+ errors.push(`${label} missing negative assertion ${id}`);
262
+ }
263
+ }
264
+ const requiredTests = new Set(assertion.required_test_ids ?? []);
265
+ for (const testId of spec.required_test_ids ?? []) {
266
+ if (!requiredTests.has(testId)) {
267
+ errors.push(`${label} assertion_result.required_test_ids missing ${testId}`);
268
+ }
269
+ }
270
+ const invalidChecks = [
271
+ ...((assertion.invalid_completion_signals ?? []).map((item) => `${item.id} ${item.forbidden_text ?? ""}`)),
272
+ ...((assertion.negative_evidence_scan?.invalid_completion_signals_checked ?? []).map((item) => String(item)))
273
+ ].join("\n");
274
+ for (const signal of spec.invalid_completion_signals ?? []) {
275
+ if (!invalidChecks.includes(signal)) {
276
+ errors.push(`${label} invalid_completion_signals did not check ${signal}`);
277
+ }
278
+ }
279
+ }
280
+ return errors;
281
+ }
282
+ function specForAc(state, expectedSpecs, acId) {
283
+ const actual = (state.required_command_specs ?? []).find((spec) => spec.ac_id === acId);
284
+ if (actual) {
285
+ return actual;
286
+ }
287
+ return expectedSpecs.find((spec) => spec.ac_id === acId);
288
+ }
289
+ function statusForAcErrors(errors, requiredLayerCount) {
290
+ if (requiredLayerCount === 0) {
291
+ return "not_run";
292
+ }
293
+ const text = errors.join("\n");
294
+ if (!text) {
295
+ return "complete";
296
+ }
297
+ if (/under_specified/i.test(text)) {
298
+ return "under_specified";
299
+ }
300
+ if (/stale|failed|invalid|contradiction|negative evidence|forbidden|bootstrap/i.test(text)) {
301
+ return "invalidated";
302
+ }
303
+ if (/blocked|harness_drift|protected_baseline|source hash mismatch/i.test(text)) {
304
+ return "blocked";
305
+ }
306
+ return "partial";
307
+ }
308
+ function recomputePlanStatuses(state, acStatuses) {
309
+ const statuses = {};
310
+ for (const [planId, item] of Object.entries(state.graph?.plan_items ?? {})) {
311
+ const relatedStatuses = (item.related_acs ?? []).map((acId) => acStatuses[acId] ?? state.graph.acceptance_criteria[acId]?.status ?? "not_run");
312
+ if (relatedStatuses.length > 0 && relatedStatuses.every((status) => status === "complete" || status === "out_of_scope_NA")) {
313
+ statuses[planId] = "complete";
314
+ }
315
+ else if (relatedStatuses.some((status) => status === "under_specified" || status === "blocked")) {
316
+ statuses[planId] = "blocked";
317
+ }
318
+ else if (relatedStatuses.some((status) => status === "invalidated")) {
319
+ statuses[planId] = "invalidated";
320
+ }
321
+ else if (relatedStatuses.some((status) => status === "partial")) {
322
+ statuses[planId] = "partial";
323
+ }
324
+ else {
325
+ statuses[planId] = "not_started";
326
+ }
327
+ }
328
+ return statuses;
329
+ }
330
+ function statusForGlobalErrors(errors, acStatuses) {
331
+ const text = errors.join("\n");
332
+ if (Object.values(acStatuses).includes("under_specified") || /under_specified/i.test(text)) {
333
+ return "under_specified";
334
+ }
335
+ if (/harness_drift|protected_baseline|source hash mismatch|missing current attempt|required_command_specs_hash|harness_task_missing/i.test(text)) {
336
+ return "blocked";
337
+ }
338
+ if (/stale|failed|invalid|contradiction|negative evidence|forbidden|bootstrap/i.test(text)) {
339
+ return "invalidated";
340
+ }
341
+ return "partial";
342
+ }
343
+ function currentAttempt(state) {
344
+ return (state.attempts ?? []).find((item) => item.task_attempt_id === state.current_attempt_id) ?? (state.attempts ?? []).at(-1);
345
+ }
346
+ function isStaleEvidenceError(errors) {
347
+ return errors.some((error) => /stale evidence|source_bundle_hash mismatch|artifact_sha256 mismatch|artifact_mtime/i.test(error));
348
+ }
349
+ function unique(values) {
350
+ return [...new Set(values.filter(Boolean))];
351
+ }
@@ -0,0 +1,2 @@
1
+ import { type SuperpowersEvidenceRecord } from "./superpowers-task-state-schema.js";
2
+ export declare function readEvidenceRecords(value: unknown): SuperpowersEvidenceRecord[];
@@ -0,0 +1,55 @@
1
+ import { normalizeAssertionResult, normalizeNegativeEvidenceScan } from "./superpowers-task-assertion-normalizers.js";
2
+ import { normalizeProofLayerId } from "./superpowers-task-fields.js";
3
+ import { asStringArray, isRecord } from "./superpowers-task-state-schema.js";
4
+ export function readEvidenceRecords(value) {
5
+ if (!Array.isArray(value)) {
6
+ return [];
7
+ }
8
+ return value.filter(isRecord).map((item) => ({
9
+ evidence_id: String(item.evidence_id ?? item.evidenceId ?? ""),
10
+ schema_version: item.schema_version === undefined ? undefined : String(item.schema_version),
11
+ task_attempt_id: item.task_attempt_id === undefined ? undefined : String(item.task_attempt_id),
12
+ source_bundle_hash: item.source_bundle_hash === undefined ? undefined : String(item.source_bundle_hash),
13
+ product_source_hash: item.product_source_hash === undefined ? undefined : String(item.product_source_hash),
14
+ technical_plan_hash: item.technical_plan_hash === undefined ? undefined : String(item.technical_plan_hash),
15
+ acceptance_checklist_hash: item.acceptance_checklist_hash === undefined ? undefined : String(item.acceptance_checklist_hash),
16
+ git_head: item.git_head === undefined ? undefined : String(item.git_head),
17
+ worktree_fingerprint: item.worktree_fingerprint === undefined ? undefined : String(item.worktree_fingerprint),
18
+ command_spec_id: item.command_spec_id === undefined ? undefined : String(item.command_spec_id),
19
+ command_run_id: item.command_run_id === undefined ? undefined : String(item.command_run_id),
20
+ command_line: item.command_line === undefined ? undefined : String(item.command_line),
21
+ artifact_path: item.artifact_path === undefined ? undefined : String(item.artifact_path),
22
+ artifact_sha256: item.artifact_sha256 === undefined ? undefined : String(item.artifact_sha256),
23
+ artifact_mtime: item.artifact_mtime === undefined ? undefined : String(item.artifact_mtime),
24
+ target_ac_ids: asStringArray(item.target_ac_ids),
25
+ target_pi_ids: asStringArray(item.target_pi_ids),
26
+ target_proof_layers: asStringArray(item.target_proof_layers).map(normalizeProofLayerId),
27
+ slice_id: String(item.slice_id ?? item.sliceId ?? ""),
28
+ type: String(item.type ?? ""),
29
+ freshness: isRecord(item.freshness)
30
+ ? {
31
+ created_at: String(item.freshness.created_at ?? ""),
32
+ valid_for: String(item.freshness.valid_for ?? ""),
33
+ stale_after: item.freshness.stale_after === null ? null : item.freshness.stale_after === undefined ? null : String(item.freshness.stale_after)
34
+ }
35
+ : { created_at: "", valid_for: "", stale_after: null },
36
+ command: item.command === undefined ? undefined : String(item.command),
37
+ command_exit_code: item.command_exit_code === undefined ? undefined : Number(item.command_exit_code),
38
+ artifact_paths: asStringArray(item.artifact_paths),
39
+ proves: asStringArray(item.proves).map(normalizeProofLayerId),
40
+ does_not_prove: asStringArray(item.does_not_prove).map((claim) => (claim.includes(".") ? normalizeProofLayerId(claim) : claim)),
41
+ redaction: isRecord(item.redaction)
42
+ ? { checked: item.redaction.checked === true, contains_secret: item.redaction.contains_secret === true }
43
+ : { checked: false, contains_secret: false },
44
+ reviewability: isRecord(item.reviewability)
45
+ ? {
46
+ external_reviewer_can_reproduce: item.reviewability.external_reviewer_can_reproduce === true,
47
+ reproduction_steps: String(item.reviewability.reproduction_steps ?? "")
48
+ }
49
+ : { external_reviewer_can_reproduce: false, reproduction_steps: "" },
50
+ assertion_result: normalizeAssertionResult(item.assertion_result),
51
+ negative_evidence_scan: normalizeNegativeEvidenceScan(item.negative_evidence_scan),
52
+ sibling_substitution_used: item.sibling_substitution_used === true,
53
+ sibling_substitution_approval_source: item.sibling_substitution_approval_source === undefined ? undefined : String(item.sibling_substitution_approval_source)
54
+ }));
55
+ }
@@ -0,0 +1,10 @@
1
+ import { type CommandRunRecord, type SuperpowersEvidenceRecord } from "./superpowers-task-state-schema.js";
2
+ export declare function runSuperpowersAssertion(workdir: string, options: {
3
+ acId: string;
4
+ proofLayer: string;
5
+ commandArgs: string[];
6
+ }): Promise<CommandRunRecord>;
7
+ export declare function recordSuperpowersEvidence(workdir: string, options: {
8
+ artifactPath: string;
9
+ commandRunId: string;
10
+ }): Promise<SuperpowersEvidenceRecord>;
@@ -0,0 +1,141 @@
1
+ import { createHash } from "node:crypto";
2
+ import { spawn } from "node:child_process";
3
+ import { promises as fs } from "node:fs";
4
+ import path from "node:path";
5
+ import { pathExists, readText } from "./fs.js";
6
+ import { appendSuperpowersEvent } from "./superpowers-task-events.js";
7
+ import { computeSourceBundleHash } from "./superpowers-task-attempt.js";
8
+ import { normalizeAssertionResult, normalizeNegativeEvidenceScan } from "./superpowers-task-assertions.js";
9
+ import { normalizeProofLayerId, normalizeProofLayerName } from "./superpowers-task-fields.js";
10
+ import { loadSuperpowersState, saveSuperpowersState, sha256 } from "./superpowers-task-state.js";
11
+ import { asStringArray, isRecord } from "./superpowers-task-state-schema.js";
12
+ export async function runSuperpowersAssertion(workdir, options) {
13
+ if (!options.acId || !options.proofLayer) {
14
+ throw new Error("run-assertion requires --ac <AC-ID> and --proof-layer <layer>");
15
+ }
16
+ if (options.commandArgs.length === 0) {
17
+ throw new Error("run-assertion requires a command after --");
18
+ }
19
+ const state = await loadSuperpowersState(workdir);
20
+ const proofLayer = normalizeProofLayerName(options.proofLayer);
21
+ const spec = (state.required_command_specs ?? []).find((item) => item.ac_id === options.acId && item.proof_layers.map(normalizeProofLayerName).includes(proofLayer));
22
+ if (!spec) {
23
+ throw new Error(`no required command spec for ${options.acId}.${proofLayer}`);
24
+ }
25
+ const startedAt = new Date().toISOString();
26
+ const exitCode = await runCommand(options.commandArgs);
27
+ const endedAt = new Date().toISOString();
28
+ const commandLine = options.commandArgs.join(" ");
29
+ const commandRun = {
30
+ command_run_id: `CR-${compactDate(startedAt)}-${sha256(commandLine).slice(0, 8)}`,
31
+ task_attempt_id: state.current_attempt_id,
32
+ command_spec_id: spec.command_spec_id,
33
+ ac_id: options.acId,
34
+ proof_layer: proofLayer,
35
+ command_line: commandLine,
36
+ exit_code: exitCode,
37
+ started_at: startedAt,
38
+ ended_at: endedAt,
39
+ artifact_paths: []
40
+ };
41
+ state.command_runs = [...(state.command_runs ?? []), commandRun];
42
+ await saveSuperpowersState(workdir, state);
43
+ await appendSuperpowersEvent(workdir, "assertion_command_run", {
44
+ command_run_id: commandRun.command_run_id,
45
+ command_spec_id: commandRun.command_spec_id,
46
+ exit_code: commandRun.exit_code
47
+ });
48
+ return commandRun;
49
+ }
50
+ export async function recordSuperpowersEvidence(workdir, options) {
51
+ const state = await loadSuperpowersState(workdir);
52
+ const commandRun = (state.command_runs ?? []).find((item) => item.command_run_id === options.commandRunId);
53
+ if (!commandRun) {
54
+ throw new Error(`command run not found: ${options.commandRunId}`);
55
+ }
56
+ const artifactPath = path.resolve(options.artifactPath);
57
+ if (!(await pathExists(artifactPath))) {
58
+ throw new Error(`evidence artifact not found: ${options.artifactPath}`);
59
+ }
60
+ const artifactText = await readText(artifactPath);
61
+ const artifact = JSON.parse(artifactText);
62
+ const artifactRecord = isRecord(artifact) ? artifact : {};
63
+ const assertion = normalizeAssertionResult(artifactRecord.assertion_result ?? artifactRecord);
64
+ if (!assertion) {
65
+ throw new Error(`evidence artifact is missing assertion_result: ${options.artifactPath}`);
66
+ }
67
+ const stats = await fs.stat(artifactPath);
68
+ const attempt = (state.attempts ?? []).find((item) => item.task_attempt_id === commandRun.task_attempt_id);
69
+ const commandSpec = (state.required_command_specs ?? []).find((item) => item.command_spec_id === commandRun.command_spec_id);
70
+ const layerId = normalizeProofLayerId(`${commandRun.ac_id}.${commandRun.proof_layer}`);
71
+ const relativeArtifactPath = slash(path.relative(workdir, artifactPath));
72
+ const targetPiIds = commandSpec ? (state.graph.acceptance_criteria[commandSpec.ac_id]?.related_plan_items ?? []) : [];
73
+ const negativeScan = normalizeNegativeEvidenceScan(artifactRecord.negative_evidence_scan);
74
+ if (assertion.schema_version === "assertion-result-v2") {
75
+ assertion.target_pi_ids = assertion.target_pi_ids ?? targetPiIds;
76
+ assertion.invalid_completion_signals = assertion.invalid_completion_signals ?? [];
77
+ assertion.required_test_ids = assertion.required_test_ids ?? commandSpec?.required_test_ids ?? [];
78
+ assertion.negative_evidence_scan = assertion.negative_evidence_scan ?? negativeScan;
79
+ }
80
+ const evidence = {
81
+ schema_version: "evidence-record-v2",
82
+ evidence_id: `EV2-${compactDate(new Date().toISOString())}-${sha256(options.commandRunId + artifactText).slice(0, 8)}`,
83
+ task_attempt_id: commandRun.task_attempt_id,
84
+ source_bundle_hash: attempt?.source_bundle_hash ?? computeSourceBundleHash(state),
85
+ product_source_hash: attempt?.product_source_hash ?? state.sources.product_architecture_source?.sha256 ?? "",
86
+ technical_plan_hash: attempt?.technical_plan_hash ?? state.sources.technical_realization_plan?.sha256 ?? "",
87
+ acceptance_checklist_hash: attempt?.acceptance_checklist_hash ?? state.sources.acceptance_checklist?.sha256 ?? "",
88
+ git_head: attempt?.git_head ?? "",
89
+ worktree_fingerprint: attempt?.worktree_fingerprint ?? "",
90
+ command_spec_id: commandRun.command_spec_id,
91
+ command_run_id: commandRun.command_run_id,
92
+ command_line: commandRun.command_line,
93
+ command_exit_code: commandRun.exit_code,
94
+ artifact_path: relativeArtifactPath,
95
+ artifact_sha256: createHash("sha256").update(artifactText).digest("hex"),
96
+ artifact_mtime: stats.mtime.toISOString(),
97
+ target_ac_ids: [commandRun.ac_id],
98
+ target_pi_ids: targetPiIds,
99
+ target_proof_layers: [layerId],
100
+ slice_id: String(artifactRecord.slice_id ?? "attempt-evidence"),
101
+ type: String(artifactRecord.type ?? `${commandRun.proof_layer}_assertion`),
102
+ freshness: { created_at: commandRun.ended_at, valid_for: "current_attempt", stale_after: null },
103
+ command: commandRun.command_line,
104
+ artifact_paths: [relativeArtifactPath],
105
+ proves: [layerId],
106
+ does_not_prove: asStringArray(artifactRecord.does_not_prove).length > 0 ? asStringArray(artifactRecord.does_not_prove) : ["unrelated proof layer"],
107
+ redaction: isRecord(artifactRecord.redaction)
108
+ ? { checked: artifactRecord.redaction.checked === true, contains_secret: artifactRecord.redaction.contains_secret === true }
109
+ : { checked: true, contains_secret: false },
110
+ reviewability: isRecord(artifactRecord.reviewability)
111
+ ? {
112
+ external_reviewer_can_reproduce: artifactRecord.reviewability.external_reviewer_can_reproduce === true,
113
+ reproduction_steps: String(artifactRecord.reviewability.reproduction_steps ?? commandRun.command_line)
114
+ }
115
+ : { external_reviewer_can_reproduce: true, reproduction_steps: commandRun.command_line },
116
+ assertion_result: assertion,
117
+ negative_evidence_scan: negativeScan
118
+ };
119
+ state.evidence = [...(state.evidence ?? []), evidence];
120
+ const proofLayer = state.graph.proof_layers[layerId];
121
+ if (proofLayer && commandRun.exit_code === 0 && assertion.status === "passed") {
122
+ proofLayer.status = "satisfied";
123
+ proofLayer.evidence_ids = [...new Set([...(proofLayer.evidence_ids ?? []), evidence.evidence_id])];
124
+ }
125
+ await saveSuperpowersState(workdir, state);
126
+ await appendSuperpowersEvent(workdir, "evidence_recorded", { evidence_id: evidence.evidence_id, command_run_id: commandRun.command_run_id });
127
+ return evidence;
128
+ }
129
+ function runCommand(args) {
130
+ return new Promise((resolve) => {
131
+ const child = spawn(args[0], args.slice(1), { cwd: process.cwd(), stdio: "ignore", windowsHide: true });
132
+ child.on("error", () => resolve(1));
133
+ child.on("exit", (code) => resolve(code ?? 1));
134
+ });
135
+ }
136
+ function compactDate(value) {
137
+ return value.replace(/[-:.TZ]/g, "").slice(0, 17);
138
+ }
139
+ function slash(value) {
140
+ return value.split(path.sep).join("/");
141
+ }