project-tiny-context-harness 0.2.80 → 0.2.81

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 (30) hide show
  1. package/README.md +8 -8
  2. package/assets/README.md +7 -7
  3. package/assets/README.zh-CN.md +8 -0
  4. package/assets/github/harness.yml +1 -1
  5. package/assets/skills/composite-long-task-workflow/SKILL.md +8 -2
  6. package/assets/skills/composite-long-task-workflow/assets/execution-binding.template.md +2 -0
  7. package/assets/skills/composite-long-task-workflow/assets/goal-objective.template.md +6 -2
  8. package/assets/skills/composite-long-task-workflow/references/composite-long-task-workflow-protocol.md +22 -11
  9. package/dist/lib/composite-long-task-renderer.js +18 -62
  10. package/dist/lib/superpowers-task-assertion-normalizers.d.ts +3 -0
  11. package/dist/lib/superpowers-task-assertion-normalizers.js +67 -0
  12. package/dist/lib/superpowers-task-assertions.d.ts +20 -0
  13. package/dist/lib/superpowers-task-assertions.js +242 -0
  14. package/dist/lib/superpowers-task-compile-diagnostics.d.ts +5 -0
  15. package/dist/lib/superpowers-task-compile-diagnostics.js +20 -0
  16. package/dist/lib/superpowers-task-compile-guards.d.ts +2 -0
  17. package/dist/lib/superpowers-task-compile-guards.js +66 -0
  18. package/dist/lib/superpowers-task-compile.js +25 -5
  19. package/dist/lib/superpowers-task-conformance.d.ts +2 -0
  20. package/dist/lib/superpowers-task-conformance.js +24 -0
  21. package/dist/lib/superpowers-task-derive.js +60 -4
  22. package/dist/lib/superpowers-task-gates.js +57 -3
  23. package/dist/lib/superpowers-task-source-compile.js +49 -11
  24. package/dist/lib/superpowers-task-source-parser.js +12 -16
  25. package/dist/lib/superpowers-task-state-schema.d.ts +61 -1
  26. package/dist/lib/superpowers-task-state.js +19 -3
  27. package/dist/lib/superpowers-task-status.d.ts +2 -0
  28. package/dist/lib/superpowers-task-status.js +14 -0
  29. package/dist/lib/superpowers-task-validator.js +14 -1
  30. package/package.json +5 -5
@@ -0,0 +1,20 @@
1
+ import { type SuperpowersEvidenceRecord, type SuperpowersTaskState } from "./superpowers-task-state-schema.js";
2
+ export { normalizeAssertionResult, normalizeNegativeEvidenceScan } from "./superpowers-task-assertion-normalizers.js";
3
+ export type AssertionStatus = "passed" | "failed" | "missing" | "stale" | "not_applicable";
4
+ export interface ProofLayerAssertionEvaluation {
5
+ assertion_status: AssertionStatus;
6
+ blocking_assertion_failures: string[];
7
+ negative_evidence_findings: string[];
8
+ }
9
+ export declare const MACHINE_VERIFIABLE_PROOF_LAYERS: Set<string>;
10
+ export declare const UI_BROWSER_ASSERTION_TYPES: Set<string>;
11
+ export declare function proofLayerName(layerId: string): string;
12
+ export declare function proofLayerAcId(layerId: string): string;
13
+ export declare function isMachineVerifiableLayer(layerId: string): boolean;
14
+ export declare function isUiBrowserLayer(layerId: string): boolean;
15
+ export declare function evaluateAcEvidence(state: SuperpowersTaskState, acId: string): ProofLayerAssertionEvaluation;
16
+ export declare function evaluateProofLayer(state: SuperpowersTaskState, layerId: string): ProofLayerAssertionEvaluation;
17
+ export declare function evaluateProofLayerAssertions(state: SuperpowersTaskState, layerId: string): ProofLayerAssertionEvaluation;
18
+ export declare function assertionFailuresForState(state: SuperpowersTaskState): string[];
19
+ export declare function evaluateAssertionEvidence(evidence: SuperpowersEvidenceRecord, layerId: string): string[];
20
+ export declare function evaluateNegativeEvidence(evidence: SuperpowersEvidenceRecord, layerId: string): string[];
@@ -0,0 +1,242 @@
1
+ export { normalizeAssertionResult, normalizeNegativeEvidenceScan } from "./superpowers-task-assertion-normalizers.js";
2
+ export const MACHINE_VERIFIABLE_PROOF_LAYERS = new Set([
3
+ "ui_browser",
4
+ "api_schema",
5
+ "runtime",
6
+ "worker_runtime",
7
+ "data_artifact",
8
+ "integration",
9
+ "security_redaction",
10
+ "test",
11
+ "all_provider_all_runner",
12
+ "cleanup_stale_scan"
13
+ ]);
14
+ export const UI_BROWSER_ASSERTION_TYPES = new Set(["browser_assertion", "playwright_assertion", "ui_browser_assertion"]);
15
+ const DEFAULT_UI_FORBIDDEN_FINAL_STATES = ["未验证", "不可用", "暂不可用", "页面无明显变化"];
16
+ const GENERIC_INVALID_EVIDENCE_TYPE_PATTERNS = [
17
+ /\bfinal[-_ ]?(?:result[-_ ]?)?card\b/i,
18
+ /\b(?:plan[-_ ]?conformance[-_ ]?)?matrix\b/i,
19
+ /\b(?:final[-_ ]?acceptance[-_ ]?)?verdict\b/i,
20
+ /\bvalidator[-_ ]?pass\b/i,
21
+ /\bauditor[-_ ]?pass\b/i,
22
+ /\bsubagent[-_ ]?summary\b/i,
23
+ /\bagent[-_ ]?summary\b/i,
24
+ /\bprose[-_ ]?summary\b/i,
25
+ /\bfile[-_ ]?exists\b/i,
26
+ /\bartifact[-_ ]?exists\b/i,
27
+ /\btest[-_ ]?name[-_ ]?only\b/i,
28
+ /\bdiagnostic[-_ ]?(?:surface|page)?\b/i,
29
+ /\blocal[-_ ]?audit\b/i,
30
+ /\bcomponent[-_ ]?screenshot\b/i,
31
+ /\bstorybook\b/i,
32
+ /\bdom[-_ ]?snippet\b/i,
33
+ /\bsample[-_ ]?for[-_ ]?full[-_ ]?population\b/i
34
+ ];
35
+ export function proofLayerName(layerId) {
36
+ return layerId.includes(".") ? layerId.slice(layerId.lastIndexOf(".") + 1) : layerId;
37
+ }
38
+ export function proofLayerAcId(layerId) {
39
+ return layerId.includes(".") ? layerId.slice(0, layerId.lastIndexOf(".")) : "";
40
+ }
41
+ export function isMachineVerifiableLayer(layerId) {
42
+ return MACHINE_VERIFIABLE_PROOF_LAYERS.has(proofLayerName(layerId));
43
+ }
44
+ export function isUiBrowserLayer(layerId) {
45
+ return proofLayerName(layerId) === "ui_browser";
46
+ }
47
+ export function evaluateAcEvidence(state, acId) {
48
+ const ac = state.graph.acceptance_criteria[acId];
49
+ if (!ac) {
50
+ return {
51
+ assertion_status: "missing",
52
+ blocking_assertion_failures: [`AC ${acId} missing from task graph`],
53
+ negative_evidence_findings: []
54
+ };
55
+ }
56
+ const evaluations = ac.required_proof_layers.map((layer) => evaluateProofLayer(state, `${acId}.${layer}`));
57
+ const applicable = evaluations.filter((item) => item.assertion_status !== "not_applicable");
58
+ const blocking_assertion_failures = applicable.flatMap((item) => item.blocking_assertion_failures);
59
+ const negative_evidence_findings = applicable.flatMap((item) => item.negative_evidence_findings);
60
+ const statuses = applicable.map((item) => item.assertion_status);
61
+ const assertion_status = applicable.length === 0
62
+ ? "not_applicable"
63
+ : statuses.includes("failed")
64
+ ? "failed"
65
+ : statuses.includes("stale")
66
+ ? "stale"
67
+ : statuses.includes("missing")
68
+ ? "missing"
69
+ : "passed";
70
+ return { assertion_status, blocking_assertion_failures, negative_evidence_findings };
71
+ }
72
+ export function evaluateProofLayer(state, layerId) {
73
+ return evaluateProofLayerAssertions(state, layerId);
74
+ }
75
+ export function evaluateProofLayerAssertions(state, layerId) {
76
+ if (!isMachineVerifiableLayer(layerId)) {
77
+ return { assertion_status: "not_applicable", blocking_assertion_failures: [], negative_evidence_findings: [] };
78
+ }
79
+ const layer = state.graph.proof_layers[layerId];
80
+ const evidenceById = new Map((state.evidence ?? []).map((item) => [item.evidence_id, item]));
81
+ const evidenceRecords = (layer?.evidence_ids ?? []).map((id) => evidenceById.get(id)).filter((item) => Boolean(item));
82
+ if (!layer || evidenceRecords.length === 0) {
83
+ return {
84
+ assertion_status: "missing",
85
+ blocking_assertion_failures: [`proof layer ${layerId} missing assertion result`],
86
+ negative_evidence_findings: []
87
+ };
88
+ }
89
+ const blocking = [];
90
+ const negative = [];
91
+ for (const evidence of evidenceRecords) {
92
+ blocking.push(...evaluateAssertionEvidence(evidence, layerId));
93
+ negative.push(...evaluateNegativeEvidence(evidence, layerId));
94
+ }
95
+ const combined = [...blocking, ...negative];
96
+ if (combined.length === 0) {
97
+ return { assertion_status: "passed", blocking_assertion_failures: [], negative_evidence_findings: [] };
98
+ }
99
+ const text = combined.join("\n");
100
+ const status = /missing assertion result/i.test(text)
101
+ ? "missing"
102
+ : /\bstale\b/i.test(text)
103
+ ? "stale"
104
+ : "failed";
105
+ return { assertion_status: status, blocking_assertion_failures: blocking, negative_evidence_findings: negative };
106
+ }
107
+ export function assertionFailuresForState(state) {
108
+ return Object.keys(state.graph?.proof_layers ?? {}).flatMap((layerId) => {
109
+ const evaluation = evaluateProofLayerAssertions(state, layerId);
110
+ return [...evaluation.blocking_assertion_failures, ...evaluation.negative_evidence_findings];
111
+ });
112
+ }
113
+ export function evaluateAssertionEvidence(evidence, layerId) {
114
+ const failures = [];
115
+ const acId = proofLayerAcId(layerId);
116
+ const layerName = proofLayerName(layerId);
117
+ const assertion = evidence.assertion_result;
118
+ const label = `proof layer ${layerId} evidence ${evidence.evidence_id}`;
119
+ const invalidEvidence = invalidEvidenceReason(evidence, layerName);
120
+ if (invalidEvidence) {
121
+ failures.push(`${label} invalid evidence forbidden shortcut ${evidence.type || "(missing)"} cannot satisfy ${layerName}: ${invalidEvidence}`);
122
+ }
123
+ if (!assertion) {
124
+ failures.push(`${label} missing assertion result; ${layerName} proof not machine-backed`);
125
+ return failures;
126
+ }
127
+ if (assertion.schema_version !== "assertion-result-v1") {
128
+ failures.push(`${label} assertion_result.schema_version must be assertion-result-v1`);
129
+ }
130
+ if (assertion.status !== "passed") {
131
+ failures.push(`${label} assertion_result.status=${assertion.status}; expected passed`);
132
+ }
133
+ if (assertion.exit_code !== 0) {
134
+ failures.push(`${label} assertion exit_code=${assertion.exit_code}; expected 0`);
135
+ }
136
+ if (evidence.command_exit_code !== undefined && evidence.command_exit_code !== 0) {
137
+ failures.push(`${label} command_exit_code=${evidence.command_exit_code}; expected 0`);
138
+ }
139
+ if (!assertion.target_ac_ids.includes(acId)) {
140
+ failures.push(`${label} assertion target ACs ${assertion.target_ac_ids.join(", ") || "(none)"} do not include ${acId}`);
141
+ }
142
+ if (!assertion.target_proof_layers.includes(layerId) && !assertion.target_proof_layers.includes(layerName)) {
143
+ failures.push(`${label} assertion target_proof_layers ${assertion.target_proof_layers.join(", ") || "(none)"} do not include ${layerId} or ${layerName}`);
144
+ }
145
+ failures.push(...checkAssertions(`${label} positive assertion`, assertion.positive_assertions));
146
+ failures.push(...checkAssertions(`${label} negative assertion`, assertion.negative_assertions));
147
+ if ((assertion.artifacts?.length ?? 0) === 0 && evidence.artifact_paths.length === 0) {
148
+ failures.push(`${label} assertion-backed evidence must include artifacts`);
149
+ }
150
+ if (isUiBrowserLayer(layerId)) {
151
+ failures.push(...validateUiBrowserAssertion(evidence, assertion, layerId));
152
+ }
153
+ return failures;
154
+ }
155
+ function validateUiBrowserAssertion(evidence, assertion, layerId) {
156
+ const failures = [];
157
+ const label = `proof layer ${layerId} evidence ${evidence.evidence_id}`;
158
+ if (!UI_BROWSER_ASSERTION_TYPES.has(evidence.type)) {
159
+ const apiOnly = /\bapi\b/i.test(evidence.type) ? "; API-only cannot satisfy UI Path AC" : "";
160
+ failures.push(`${label} ui_browser evidence type ${evidence.type || "(missing)"} is not allowed${apiOnly}`);
161
+ }
162
+ if (!assertion.owner_surface) {
163
+ failures.push(`${label} missing owner_surface`);
164
+ }
165
+ if (!assertion.route) {
166
+ failures.push(`${label} missing route`);
167
+ }
168
+ if (!assertion.action) {
169
+ failures.push(`${label} missing action-level UI proof`);
170
+ }
171
+ const artifacts = [...(assertion.artifacts ?? []), ...evidence.artifact_paths];
172
+ if (!artifacts.some((artifact) => /\.(png|jpe?g|zip|json)$/i.test(artifact) || /\b(trace|screenshot|report)\b/i.test(artifact))) {
173
+ failures.push(`${label} ui_browser assertion must include screenshot, trace or report artifact`);
174
+ }
175
+ const negativeCoverage = assertion.negative_assertions.map((item) => `${item.id} ${item.forbidden_text ?? ""}`).join("\n");
176
+ for (const forbidden of DEFAULT_UI_FORBIDDEN_FINAL_STATES) {
177
+ if (!negativeCoverage.includes(forbidden)) {
178
+ failures.push(`${label} missing negative assertion coverage for forbidden final state ${forbidden}`);
179
+ }
180
+ }
181
+ if (!evidence.negative_evidence_scan) {
182
+ failures.push(`${label} missing negative_evidence_scan for ui_browser layer`);
183
+ }
184
+ return failures;
185
+ }
186
+ function checkAssertions(prefix, checks) {
187
+ return checks
188
+ .filter((check) => check.status !== "passed")
189
+ .map((check) => `${prefix} ${check.id || "(unnamed)"} status=${check.status}; expected passed${check.forbidden_text ? ` forbidden_text=${check.forbidden_text}` : ""}`);
190
+ }
191
+ export function evaluateNegativeEvidence(evidence, layerId) {
192
+ const scan = evidence.negative_evidence_scan;
193
+ if (!scan) {
194
+ return [];
195
+ }
196
+ const acId = proofLayerAcId(layerId);
197
+ const label = `proof layer ${layerId} evidence ${evidence.evidence_id}`;
198
+ const findings = [];
199
+ if (scan.schema_version !== "negative-evidence-scan-v1") {
200
+ findings.push(`${label} negative_evidence_scan.schema_version must be negative-evidence-scan-v1`);
201
+ }
202
+ if (scan.status !== "passed") {
203
+ findings.push(`${label} negative evidence scan status=${scan.status}; expected passed`);
204
+ }
205
+ if (!scan.target_ac_ids.includes(acId)) {
206
+ findings.push(`${label} negative evidence scan target ACs ${scan.target_ac_ids.join(", ") || "(none)"} do not include ${acId}`);
207
+ }
208
+ for (const finding of scan.forbidden_findings ?? []) {
209
+ if (finding.status === "found") {
210
+ findings.push(`${label} negative evidence found forbidden text ${finding.forbidden_text ?? finding.id}: ${finding.actual ?? ""}`.trim());
211
+ }
212
+ }
213
+ for (const required of scan.required_findings ?? []) {
214
+ if (required.status !== "passed") {
215
+ findings.push(`${label} negative evidence required finding ${required.id} status=${required.status}; expected passed`);
216
+ }
217
+ }
218
+ if ((scan.artifacts ?? []).length === 0) {
219
+ findings.push(`${label} negative evidence scan must include artifacts`);
220
+ }
221
+ return findings;
222
+ }
223
+ function invalidEvidenceReason(evidence, layerName) {
224
+ const evidenceText = [evidence.type, evidence.command, ...evidence.artifact_paths, ...evidence.proves, ...evidence.does_not_prove].join("\n");
225
+ const genericShortcut = GENERIC_INVALID_EVIDENCE_TYPE_PATTERNS.find((pattern) => pattern.test(evidenceText));
226
+ if (genericShortcut) {
227
+ return `matches invalid completion evidence ${genericShortcut}`;
228
+ }
229
+ if ((layerName === "runtime" || layerName === "worker_runtime" || layerName === "integration" || layerName === "ui_browser") && /\b(unit|mock|viewmodel)\b/i.test(evidence.type)) {
230
+ return "unit, mock or viewmodel evidence is auxiliary only for runtime, worker, integration and UI layers";
231
+ }
232
+ if (layerName === "ui_browser" && /\b(api|schema)\b/i.test(evidence.type)) {
233
+ return "API-only evidence cannot satisfy UI Path AC";
234
+ }
235
+ if (layerName === "ui_browser" && /^screenshot$/i.test(evidence.type)) {
236
+ return "screenshot-only evidence cannot satisfy UI Path AC";
237
+ }
238
+ if ((layerName === "runtime" || layerName === "worker_runtime" || layerName === "data_artifact" || layerName === "security_redaction") && /\b(ui|browser|playwright|screenshot)\b/i.test(evidence.type)) {
239
+ return "UI-only evidence cannot satisfy runtime, data or security proof layers";
240
+ }
241
+ return undefined;
242
+ }
@@ -0,0 +1,5 @@
1
+ export type CompileReportCategory = "blocking_missing_source" | "blocking_missing_plan" | "blocking_missing_checklist" | "blocking_unparseable_object" | "blocking_scope_conflict" | "blocking_missing_assertion_spec" | "blocking_missing_owner_boundary" | "blocking_missing_primary_path" | "blocking_missing_observable_result" | "blocking_missing_invalid_evidence" | "warning_weak_acceptance_wording" | "hygiene_non_canonical_field_order";
2
+ export declare function compileError(message: string, category: CompileReportCategory, file: string, line: number, field: string, whyBlocking: string, requiredFix: string): string;
3
+ export declare function compileReportSuffix(category: CompileReportCategory, file: string, line: number, field: string, whyBlocking: string, requiredFix: string): string;
4
+ export declare function missingCategory(label: string): CompileReportCategory;
5
+ export declare function throwCompileErrors(errors: string[]): void;
@@ -0,0 +1,20 @@
1
+ export function compileError(message, category, file, line, field, whyBlocking, requiredFix) {
2
+ return `${message}${compileReportSuffix(category, file, line, field, whyBlocking, requiredFix)}`;
3
+ }
4
+ export function compileReportSuffix(category, file, line, field, whyBlocking, requiredFix) {
5
+ return ` [category=${category}; file=${file}; line=${line}; invalid_or_missing_field=${field}; why_blocking=${whyBlocking}; required_fix=${requiredFix}; rerun_compile_enough=true]`;
6
+ }
7
+ export function missingCategory(label) {
8
+ if (/^PI-\d+/i.test(label)) {
9
+ return "blocking_missing_plan";
10
+ }
11
+ if (/^AC-\d+/i.test(label)) {
12
+ return "blocking_missing_checklist";
13
+ }
14
+ return "blocking_missing_source";
15
+ }
16
+ export function throwCompileErrors(errors) {
17
+ if (errors.length > 0) {
18
+ throw new Error(`Superpowers source compile failed:\n- ${errors.join("\n- ")}`);
19
+ }
20
+ }
@@ -0,0 +1,2 @@
1
+ import { type SuperpowersAcceptanceCriterion, type SuperpowersPlanItem, type SuperpowersProductArchitectureScope } from "./superpowers-task-state-schema.js";
2
+ export declare function validateCompiledSources(product: SuperpowersProductArchitectureScope, planItems: Record<string, SuperpowersPlanItem>, acceptanceCriteria: Record<string, SuperpowersAcceptanceCriterion>): void;
@@ -0,0 +1,66 @@
1
+ import { MACHINE_VERIFIABLE_PROOF_LAYERS } from "./superpowers-task-assertions.js";
2
+ import { compileError, throwCompileErrors } from "./superpowers-task-compile-diagnostics.js";
3
+ const ALLOWED_PROOF_LAYERS = new Set(["code", ...MACHINE_VERIFIABLE_PROOF_LAYERS]);
4
+ export function validateCompiledSources(product, planItems, acceptanceCriteria) {
5
+ const errors = [];
6
+ const planIds = new Set(Object.keys(planItems));
7
+ const acIds = new Set(Object.keys(acceptanceCriteria));
8
+ for (const [planId, item] of Object.entries(planItems)) {
9
+ validatePlanItem(planId, item, acIds, acceptanceCriteria, errors);
10
+ }
11
+ for (const [acId, ac] of Object.entries(acceptanceCriteria)) {
12
+ validateAcceptanceCriterion(product, acId, ac, planIds, planItems, errors);
13
+ }
14
+ throwCompileErrors(errors);
15
+ }
16
+ function validatePlanItem(planId, item, acIds, acceptanceCriteria, errors) {
17
+ for (const acId of item.related_acs) {
18
+ if (!acIds.has(acId)) {
19
+ errors.push(compileError(`${planId} references unknown related_acs ${acId}`, "blocking_unparseable_object", item.source_file, item.source_start_line, "related_acs", "dangling plan-to-AC references make the graph ambiguous", "fix related_acs to existing AC ids"));
20
+ }
21
+ }
22
+ if (item.delivery_scope !== "out_of_scope_backlog" && item.implementation_paths.length === 0) {
23
+ errors.push(compileError(`${planId} missing implementation_paths`, "blocking_missing_primary_path", item.source_file, item.source_start_line, "implementation_paths", "plan conformance needs an implementation owner path", "add implementation_paths or mark the item out_of_scope_backlog"));
24
+ }
25
+ if (item.owner_surfaces.length > 0) {
26
+ const related = item.related_acs.length > 0 ? item.related_acs : Object.keys(acceptanceCriteria);
27
+ const hasUiLayer = related.some((acId) => acceptanceCriteria[acId]?.required_proof_layers.includes("ui_browser"));
28
+ if (!hasUiLayer) {
29
+ errors.push(compileError(`${planId} owner_surfaces requires a ui_browser proof layer`, "blocking_missing_owner_boundary", item.source_file, item.source_start_line, "owner_surfaces", "owner-surface work must have browser proof on the owner route", "add ui_browser to a related AC required_proof_layers"));
30
+ }
31
+ }
32
+ }
33
+ function validateAcceptanceCriterion(product, acId, ac, planIds, planItems, errors) {
34
+ for (const planId of ac.related_plan_items) {
35
+ if (!planIds.has(planId)) {
36
+ errors.push(compileError(`${acId} references unknown related_plan_items ${planId}`, "blocking_unparseable_object", ac.source_file, ac.source_start_line, "related_plan_items", "dangling AC-to-plan references make the graph ambiguous", "fix related_plan_items to existing PI ids"));
37
+ }
38
+ }
39
+ for (const layer of ac.required_proof_layers) {
40
+ if (!ALLOWED_PROOF_LAYERS.has(layer)) {
41
+ errors.push(compileError(`${acId} has invalid required_proof_layers ${layer}`, "blocking_unparseable_object", ac.source_file, ac.source_start_line, "required_proof_layers", "unknown proof layers cannot be evaluated", "use code or a supported machine-verifiable proof layer"));
42
+ }
43
+ }
44
+ if (ac.ac_validates.length === 0) {
45
+ errors.push(compileError(`${acId} missing observable acceptance result`, "blocking_missing_observable_result", ac.source_file, ac.source_start_line, "ac_validates", "ACs need an observable result before evidence can prove completion", "add concrete ac_validates outcomes"));
46
+ }
47
+ const machineRequirements = (ac.assertion_requirements ?? []).filter((item) => item.machine_blocking);
48
+ if (machineRequirements.some((item) => item.positive_assertions.length === 0)) {
49
+ errors.push(compileError(`${acId} machine-verifiable proof layer lacks positive assertion requirements`, "blocking_missing_assertion_spec", ac.source_file, ac.source_start_line, "assertion_requirements", "machine-verifiable ACs need observable positive assertions", "add ac_validates, required_test_ids, test_cases or final_evidence_expected"));
50
+ }
51
+ if (machineRequirements.some((item) => item.negative_assertions.length === 0)) {
52
+ errors.push(compileError(`${acId} machine-verifiable proof layer lacks negative assertion requirements`, "blocking_missing_invalid_evidence", ac.source_file, ac.source_start_line, "invalid_evidence", "negative assertions prevent forbidden completion shortcuts", "add ac_does_not_validate, fail_conditions or invalid_evidence"));
53
+ }
54
+ const relatedPlans = ac.related_plan_items.map((planId) => planItems[planId]).filter(Boolean);
55
+ if (ac.full_population_required === true && relatedPlans.length > 0 && relatedPlans.every((item) => item.delivery_scope === "representative_sample_validation")) {
56
+ errors.push(compileError(`${acId} full-population AC is backed only by representative sample plan items`, "blocking_scope_conflict", ac.source_file, ac.source_start_line, "full_population_required", "sample-only plan items cannot prove full-population acceptance", "add a full_population_operation plan item or change the AC boundary"));
57
+ }
58
+ const productNonCompleting = product.representative_samples_do_not_validate.join("\n");
59
+ if (productNonCompleting && !containsAny(ac.ac_does_not_validate, product.representative_samples_do_not_validate)) {
60
+ errors.push(compileError(`${acId} does not represent Product non-completing outcomes`, "blocking_missing_invalid_evidence", ac.source_file, ac.source_start_line, "ac_does_not_validate", "Product non-completing outcomes must remain visible in AC invalid evidence", "add matching ac_does_not_validate or invalid_evidence entries"));
61
+ }
62
+ }
63
+ function containsAny(values, needles) {
64
+ const text = values.join("\n").toLowerCase();
65
+ return needles.some((needle) => text.includes(needle.toLowerCase()));
66
+ }
@@ -1,11 +1,14 @@
1
1
  import path from "node:path";
2
- import { readText } from "./fs.js";
2
+ import { pathExists, readText } from "./fs.js";
3
3
  import { appendSuperpowersEvent } from "./superpowers-task-events.js";
4
+ import { compileError, compileReportSuffix, throwCompileErrors } from "./superpowers-task-compile-diagnostics.js";
5
+ import { validateCompiledSources } from "./superpowers-task-compile-guards.js";
4
6
  import { loadSuperpowersState, recomputeStatuses, saveSuperpowersState, refreshSourceHashes } from "./superpowers-task-state.js";
5
7
  import { DEFAULT_LAYERS, parseAcceptanceCriteria, parsePlanItems, parseProductArchitectureScope } from "./superpowers-task-source-compile.js";
6
8
  export async function compileSuperpowersTask(workdir) {
7
9
  const state = await loadSuperpowersState(workdir);
8
10
  await refreshSourceHashes(workdir, state);
11
+ await assertRequiredSourcesExist(workdir, state);
9
12
  const productSource = await readText(path.join(workdir, state.sources.product_architecture_source.path));
10
13
  const technicalPlan = await readText(path.join(workdir, state.sources.technical_realization_plan.path));
11
14
  const checklist = await readText(path.join(workdir, state.sources.acceptance_checklist.path));
@@ -15,6 +18,7 @@ export async function compileSuperpowersTask(workdir) {
15
18
  };
16
19
  const planItems = parsePlanItems(technicalPlan, state.sources.technical_realization_plan.path);
17
20
  const acceptanceCriteria = parseAcceptanceCriteria(checklist, state.sources.acceptance_checklist.path);
21
+ validateCompiledSources(state.delivery.product_architecture_scope, planItems, acceptanceCriteria);
18
22
  const acIds = Object.keys(acceptanceCriteria);
19
23
  for (const [planId, item] of Object.entries(planItems)) {
20
24
  if (item.related_acs.length === 0) {
@@ -46,6 +50,22 @@ export async function compileSuperpowersTask(workdir) {
46
50
  });
47
51
  return state;
48
52
  }
53
+ async function assertRequiredSourcesExist(workdir, state) {
54
+ const errors = [];
55
+ const checks = [
56
+ ["product_architecture_source", "blocking_missing_source", "Product / Architecture Source"],
57
+ ["technical_realization_plan", "blocking_missing_plan", "Technical Realization Plan"],
58
+ ["acceptance_checklist", "blocking_missing_checklist", "Acceptance Checklist"]
59
+ ];
60
+ for (const [key, category, label] of checks) {
61
+ const source = state.sources[key];
62
+ const file = source?.path ?? `${key}.md`;
63
+ if (!source || !(await pathExists(path.join(workdir, file)))) {
64
+ errors.push(compileError(`${label} input is missing: ${file}`, category, file, 1, key, "all three authority inputs are required", "restore the missing source file and rerun compile"));
65
+ }
66
+ }
67
+ throwCompileErrors(errors);
68
+ }
49
69
  export function computeScopeConflicts(state) {
50
70
  const conflicts = [];
51
71
  const product = state.delivery?.product_architecture_scope;
@@ -56,18 +76,18 @@ export function computeScopeConflicts(state) {
56
76
  product?.full_population_required === false;
57
77
  for (const [planId, item] of Object.entries(state.graph?.plan_items ?? {})) {
58
78
  if (productRequiresFullPopulation && item.delivery_scope !== "full_population_operation" && item.delivery_scope !== "out_of_scope_backlog") {
59
- conflicts.push(`scope_conflict_requires_decision: Product / Architecture Source requires full_population_operation but ${planId} delivery_scope=${item.delivery_scope || "missing"}`);
79
+ conflicts.push(`scope_conflict_requires_decision: Product / Architecture Source requires full_population_operation but ${planId} delivery_scope=${item.delivery_scope || "missing"}${compileReportSuffix("blocking_scope_conflict", item.source_file, item.source_start_line, "delivery_scope", "Product source and plan delivery scopes disagree", "align the source, plan and checklist delivery scope")}`);
60
80
  }
61
81
  if (productIsCapabilityOnly && item.delivery_scope === "full_population_operation" && productScope !== "mixed_scope_requires_boundary") {
62
- conflicts.push(`scope_conflict_requires_decision: Product / Architecture Source delivery_scope=${productScope || "missing"} but ${planId} delivery_scope=full_population_operation`);
82
+ conflicts.push(`scope_conflict_requires_decision: Product / Architecture Source delivery_scope=${productScope || "missing"} but ${planId} delivery_scope=full_population_operation${compileReportSuffix("blocking_scope_conflict", item.source_file, item.source_start_line, "delivery_scope", "Product source and plan delivery scopes disagree", "align the source, plan and checklist delivery scope")}`);
63
83
  }
64
84
  }
65
85
  for (const [acId, ac] of Object.entries(state.graph?.acceptance_criteria ?? {})) {
66
86
  if (productRequiresFullPopulation && (ac.acceptance_scope === "full_population_not_required" || ac.full_population_required === false)) {
67
- conflicts.push(`scope_conflict_requires_decision: Product / Architecture Source requires full_population_operation but ${acId} full_population_required=false`);
87
+ conflicts.push(`scope_conflict_requires_decision: Product / Architecture Source requires full_population_operation but ${acId} full_population_required=false${compileReportSuffix("blocking_scope_conflict", ac.source_file, ac.source_start_line, "full_population_required", "Product source and checklist full-population requirements disagree", "align the source, plan and checklist delivery scope")}`);
68
88
  }
69
89
  if (productIsCapabilityOnly && (ac.acceptance_scope === "full_population_operation" || ac.full_population_required === true) && productScope !== "mixed_scope_requires_boundary") {
70
- conflicts.push(`scope_conflict_requires_decision: Product / Architecture Source delivery_scope=${productScope || "missing"} but ${acId} acceptance_scope=full_population_operation`);
90
+ conflicts.push(`scope_conflict_requires_decision: Product / Architecture Source delivery_scope=${productScope || "missing"} but ${acId} acceptance_scope=full_population_operation${compileReportSuffix("blocking_scope_conflict", ac.source_file, ac.source_start_line, "acceptance_scope", "Product source and checklist delivery scopes disagree", "align the source, plan and checklist delivery scope")}`);
71
91
  }
72
92
  }
73
93
  return [...new Set(conflicts)];
@@ -0,0 +1,2 @@
1
+ import type { SuperpowersTaskState } from "./superpowers-task-state-schema.js";
2
+ export declare function validatePlanCompletionConformance(state: SuperpowersTaskState, errors: string[]): void;
@@ -0,0 +1,24 @@
1
+ import { primitiveText } from "./plan-validator-common.js";
2
+ export function validatePlanCompletionConformance(state, errors) {
3
+ for (const [planId, item] of Object.entries(state.graph?.plan_items ?? {})) {
4
+ if (item.status !== "complete") {
5
+ continue;
6
+ }
7
+ if (item.implementation_paths.length === 0 && item.delivery_scope !== "out_of_scope_backlog") {
8
+ errors.push(`plan item ${planId} is complete but has no implementation_paths`);
9
+ }
10
+ if ((item.owner_surfaces ?? []).length > 0 && !item.required_proof_layers.some((layer) => layer.endsWith(".ui_browser"))) {
11
+ errors.push(`plan item ${planId} is complete but owner_surfaces has no related ui_browser proof layer`);
12
+ }
13
+ if ((item.required_tests ?? []).length === 0 && item.explicit_no_test_scope !== true) {
14
+ errors.push(`plan item ${planId} is complete but required_tests is empty and no explicit_no_test_scope is recorded`);
15
+ }
16
+ const shortcuts = item.non_completing_shortcuts ?? [];
17
+ const evidenceText = primitiveText(state.evidence.filter((evidence) => evidence.proves.some((layer) => item.required_proof_layers.includes(layer))));
18
+ for (const shortcut of shortcuts) {
19
+ if (shortcut && evidenceText.toLowerCase().includes(shortcut.toLowerCase())) {
20
+ errors.push(`plan item ${planId} is complete but evidence uses forbidden shortcut: ${shortcut}`);
21
+ }
22
+ }
23
+ }
24
+ }
@@ -1,6 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { ensureDir, pathExists, readText, writeTextIfChanged } from "./fs.js";
3
3
  import { stableJson, loadSuperpowersState } from "./superpowers-task-state.js";
4
+ import { evaluateProofLayerAssertions } from "./superpowers-task-assertions.js";
4
5
  export async function deriveSuperpowersArtifacts(workdir) {
5
6
  const state = await loadSuperpowersState(workdir);
6
7
  const derived = deriveObjects(state);
@@ -23,8 +24,10 @@ export function deriveObjects(state) {
23
24
  const matrixRows = Object.entries(state.graph.plan_items).map(([planItemId, item]) => {
24
25
  const relatedAcs = item.related_acs;
25
26
  const requiredLayers = item.required_proof_layers;
26
- const missingLayers = requiredLayers.filter((layerId) => state.graph.proof_layers[layerId]?.status !== "satisfied");
27
+ const assertionSummary = assertionSummaryForLayers(state, requiredLayers);
28
+ const missingLayers = requiredLayers.filter((layerId) => state.graph.proof_layers[layerId]?.status !== "satisfied" || evaluateProofLayerAssertions(state, layerId).assertion_status === "failed" || evaluateProofLayerAssertions(state, layerId).assertion_status === "missing" || evaluateProofLayerAssertions(state, layerId).assertion_status === "stale");
27
29
  const evidenceIds = evidenceForLayers(state, requiredLayers);
30
+ const invalidEvidence = invalidEvidenceSignals(assertionSummary);
28
31
  return {
29
32
  plan_item_id: planItemId,
30
33
  plan_requirement: item.requirement,
@@ -51,16 +54,24 @@ export function deriveObjects(state) {
51
54
  required_proof_layers: requiredLayers,
52
55
  satisfied_proof_layers: requiredLayers.filter((layerId) => state.graph.proof_layers[layerId]?.status === "satisfied"),
53
56
  missing_required_layers: missingLayers,
57
+ assertion_status: assertionSummary.assertion_status,
58
+ blocking_assertion_failures: assertionSummary.blocking_assertion_failures,
59
+ negative_evidence_findings: assertionSummary.negative_evidence_findings,
60
+ invalid_evidence: invalidEvidence,
61
+ forbidden_shortcuts_hit: invalidEvidence.filter((item) => /forbidden shortcut|cannot satisfy/i.test(item)),
54
62
  evidence_ids: evidenceIds,
55
63
  scope_assessment: missingLayers.length === 0 ? "full" : "partial",
56
- drift: missingLayers.length === 0 ? "no drift detected" : "missing required proof layers"
64
+ drift: missingLayers.length === 0 ? "no drift detected" : "missing required proof layers",
65
+ decision: missingLayers.length === 0 ? "accept" : "continue"
57
66
  };
58
67
  });
59
68
  const verdictRows = Object.entries(state.graph.acceptance_criteria).map(([acId, ac]) => {
60
69
  const requiredLayers = ac.required_proof_layers.map((layer) => `${acId}.${layer}`);
61
- const missingLayers = requiredLayers.filter((layerId) => state.graph.proof_layers[layerId]?.status !== "satisfied");
70
+ const assertionSummary = assertionSummaryForLayers(state, requiredLayers);
71
+ const missingLayers = requiredLayers.filter((layerId) => state.graph.proof_layers[layerId]?.status !== "satisfied" || evaluateProofLayerAssertions(state, layerId).assertion_status === "failed" || evaluateProofLayerAssertions(state, layerId).assertion_status === "missing" || evaluateProofLayerAssertions(state, layerId).assertion_status === "stale");
62
72
  const evidenceIds = evidenceForLayers(state, requiredLayers);
63
73
  const status = missingLayers.length === 0 && requiredLayers.length > 0 ? "complete" : evidenceIds.length > 0 ? "partial" : ac.status;
74
+ const invalidCompletionSignals = invalidEvidenceSignals(assertionSummary);
64
75
  return {
65
76
  ac_id: acId,
66
77
  related_plan_item_ids: ac.related_plan_items,
@@ -76,6 +87,11 @@ export function deriveObjects(state) {
76
87
  fresh_evidence: evidenceText(state, evidenceIds),
77
88
  missing_evidence: [],
78
89
  missing_required_layers: missingLayers,
90
+ assertion_status: assertionSummary.assertion_status,
91
+ blocking_assertion_failures: assertionSummary.blocking_assertion_failures,
92
+ negative_evidence_findings: assertionSummary.negative_evidence_findings,
93
+ invalid_completion_signals: invalidCompletionSignals,
94
+ required_next_evidence: requiredNextEvidence(missingLayers, assertionSummary),
79
95
  contradictions: [],
80
96
  context_fact_refs: [],
81
97
  evidence_ids: evidenceIds,
@@ -88,6 +104,16 @@ export function deriveObjects(state) {
88
104
  const allComplete = verdictRows.length > 0 && verdictRows.every((row) => row.status === "complete");
89
105
  const progress = {
90
106
  ...state.progress,
107
+ acceptance_progress: {
108
+ status: allComplete ? "complete" : "partial",
109
+ complete_count: verdictRows.filter((row) => row.status === "complete").length,
110
+ required_count: verdictRows.filter((row) => row.status !== "out_of_scope_NA").length
111
+ },
112
+ engineering_implementation_progress: progressStatus(Object.values(state.graph.plan_items).map((item) => item.status)),
113
+ runtime_proof_progress: progressStatus(Object.values(state.graph.proof_layers).map((layer) => layer.status)),
114
+ proof_layer_milestones: Object.entries(state.graph.proof_layers).map(([layerId, layer]) => ({ layer_id: layerId, status: layer.status, evidence_ids: layer.evidence_ids })),
115
+ artifact_budget: { evidence_records: state.evidence.length, artifact_count: state.evidence.reduce((sum, item) => sum + item.artifact_paths.length, 0) },
116
+ workflow_overhead: { slices: state.slices.length, blockers: state.blockers.length },
91
117
  complete_count: verdictRows.filter((row) => row.status === "complete").length,
92
118
  partial_count: verdictRows.filter((row) => row.status === "partial").length,
93
119
  acceptance_required_count: verdictRows.filter((row) => row.status !== "out_of_scope_NA").length,
@@ -103,6 +129,16 @@ export function deriveObjects(state) {
103
129
  progress
104
130
  };
105
131
  }
132
+ function invalidEvidenceSignals(summary) {
133
+ return [...summary.blocking_assertion_failures, ...summary.negative_evidence_findings].filter((item) => /invalid evidence|forbidden shortcut|cannot satisfy|negative evidence|forbidden text/i.test(item));
134
+ }
135
+ function requiredNextEvidence(missingLayers, summary) {
136
+ return [...missingLayers.map((layerId) => `fresh assertion-backed evidence for ${layerId}`), ...summary.blocking_assertion_failures, ...summary.negative_evidence_findings];
137
+ }
138
+ function progressStatus(statuses) {
139
+ const complete = statuses.filter((status) => status === "complete" || status === "satisfied").length;
140
+ return { status: statuses.length > 0 && complete === statuses.length ? "complete" : complete > 0 ? "partial" : "not_started", complete, total: statuses.length };
141
+ }
106
142
  export async function derivedMatchesState(workdir, state) {
107
143
  const errors = [];
108
144
  const expected = deriveObjects(state);
@@ -123,15 +159,35 @@ async function assertDerivedJson(workdir, basename, expected, errors) {
123
159
  function evidenceForLayers(state, layerIds) {
124
160
  return [...new Set(layerIds.flatMap((layerId) => state.graph.proof_layers[layerId]?.evidence_ids ?? []))];
125
161
  }
162
+ function assertionSummaryForLayers(state, layerIds) {
163
+ const evaluations = layerIds.map((layerId) => evaluateProofLayerAssertions(state, layerId));
164
+ const applicable = evaluations.filter((item) => item.assertion_status !== "not_applicable");
165
+ const blocking_assertion_failures = applicable.flatMap((item) => item.blocking_assertion_failures);
166
+ const negative_evidence_findings = applicable.flatMap((item) => item.negative_evidence_findings);
167
+ const statuses = applicable.map((item) => item.assertion_status);
168
+ const assertion_status = applicable.length === 0
169
+ ? "not_applicable"
170
+ : statuses.includes("failed")
171
+ ? "failed"
172
+ : statuses.includes("stale")
173
+ ? "stale"
174
+ : statuses.includes("missing")
175
+ ? "missing"
176
+ : "passed";
177
+ return { assertion_status, blocking_assertion_failures, negative_evidence_findings };
178
+ }
126
179
  function evidenceText(state, evidenceIds, type) {
127
180
  return evidenceIds
128
181
  .map((evidenceId) => state.evidence.find((item) => item.evidence_id === evidenceId))
129
- .filter((item) => item && (!type || item.type === type || (type === "artifact" && item.artifact_paths.length > 0)))
182
+ .filter((item) => item && (!type || evidenceTypeMatches(item.type, type) || (type === "artifact" && item.artifact_paths.length > 0)))
130
183
  .map((item) => {
131
184
  const artifacts = item?.artifact_paths.join(", ");
132
185
  return `${item?.type} ${item?.command ?? ""} ${artifacts}`.trim();
133
186
  });
134
187
  }
188
+ function evidenceTypeMatches(actual, expected) {
189
+ return actual === expected || actual.includes(expected) || (expected === "browser" && /\b(playwright|ui_browser|browser)_assertion\b/.test(actual));
190
+ }
135
191
  function auditorStatus(state) {
136
192
  const auditor = state.gates.auditor;
137
193
  if (auditor && typeof auditor === "object" && !Array.isArray(auditor) && typeof auditor.auditor_status === "string") {