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.
- package/README.md +8 -8
- package/assets/README.md +7 -7
- package/assets/README.zh-CN.md +8 -0
- package/assets/github/harness.yml +1 -1
- package/assets/skills/composite-long-task-workflow/SKILL.md +8 -2
- package/assets/skills/composite-long-task-workflow/assets/execution-binding.template.md +2 -0
- package/assets/skills/composite-long-task-workflow/assets/goal-objective.template.md +6 -2
- package/assets/skills/composite-long-task-workflow/references/composite-long-task-workflow-protocol.md +22 -11
- package/dist/lib/composite-long-task-renderer.js +18 -62
- package/dist/lib/superpowers-task-assertion-normalizers.d.ts +3 -0
- package/dist/lib/superpowers-task-assertion-normalizers.js +67 -0
- package/dist/lib/superpowers-task-assertions.d.ts +20 -0
- package/dist/lib/superpowers-task-assertions.js +242 -0
- package/dist/lib/superpowers-task-compile-diagnostics.d.ts +5 -0
- package/dist/lib/superpowers-task-compile-diagnostics.js +20 -0
- package/dist/lib/superpowers-task-compile-guards.d.ts +2 -0
- package/dist/lib/superpowers-task-compile-guards.js +66 -0
- package/dist/lib/superpowers-task-compile.js +25 -5
- package/dist/lib/superpowers-task-conformance.d.ts +2 -0
- package/dist/lib/superpowers-task-conformance.js +24 -0
- package/dist/lib/superpowers-task-derive.js +60 -4
- package/dist/lib/superpowers-task-gates.js +57 -3
- package/dist/lib/superpowers-task-source-compile.js +49 -11
- package/dist/lib/superpowers-task-source-parser.js +12 -16
- package/dist/lib/superpowers-task-state-schema.d.ts +61 -1
- package/dist/lib/superpowers-task-state.js +19 -3
- package/dist/lib/superpowers-task-status.d.ts +2 -0
- package/dist/lib/superpowers-task-status.js +14 -0
- package/dist/lib/superpowers-task-validator.js +14 -1
- package/package.json +5 -5
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { appendSuperpowersEvent } from "./superpowers-task-events.js";
|
|
2
2
|
import { deriveSuperpowersArtifacts } from "./superpowers-task-derive.js";
|
|
3
|
+
import { validatePlanAcceptance } from "./plan-acceptance-validator.js";
|
|
3
4
|
import { loadSuperpowersState, recomputeStatuses, saveSuperpowersState } from "./superpowers-task-state.js";
|
|
4
5
|
import { completionConditionErrors, validateSuperpowersState } from "./superpowers-task-validator.js";
|
|
5
6
|
export async function runSliceGate(workdir, sliceId) {
|
|
@@ -30,20 +31,73 @@ export async function runFinalGate(workdir) {
|
|
|
30
31
|
await saveSuperpowersState(workdir, state);
|
|
31
32
|
await deriveSuperpowersArtifacts(workdir);
|
|
32
33
|
const report = await validateSuperpowersState(workdir, [workdir]);
|
|
34
|
+
const acceptanceReport = await validatePlanAcceptance(workdir, [workdir]);
|
|
33
35
|
const latest = await loadSuperpowersState(workdir);
|
|
34
36
|
const completionErrors = completionConditionErrors(latest);
|
|
35
|
-
const errors = [...new Set([...report.errors, ...completionErrors])];
|
|
37
|
+
const errors = [...new Set([...report.errors, ...acceptanceReport.errors, ...completionErrors])];
|
|
36
38
|
const complete = errors.length === 0;
|
|
39
|
+
const acceptanceStatus = complete ? "complete" : acceptanceStatusForErrors(errors);
|
|
40
|
+
const nextRequiredActions = complete ? [] : nextActionsForErrors(errors);
|
|
37
41
|
latest.final.product_goal_complete = complete;
|
|
38
42
|
latest.meta.product_goal_complete = complete;
|
|
39
|
-
latest.final.acceptance_target_status =
|
|
43
|
+
latest.final.acceptance_target_status = acceptanceStatus;
|
|
40
44
|
latest.meta.acceptance_target_status = latest.final.acceptance_target_status;
|
|
41
45
|
latest.final.audit_task_complete = true;
|
|
42
46
|
latest.meta.audit_task_complete = true;
|
|
43
|
-
latest.final.completion_basis = complete
|
|
47
|
+
latest.final.completion_basis = complete
|
|
48
|
+
? ["all_required_acs_complete", "validator_passed", "assertion_evidence_passed", "negative_evidence_scan_passed", "auditor_no_blocker"]
|
|
49
|
+
: [];
|
|
50
|
+
latest.final.next_required_actions = nextRequiredActions;
|
|
44
51
|
latest.gates.validator = { status: errors.length === 0 ? "pass" : "blocked", errors };
|
|
52
|
+
latest.gates.final_gate = {
|
|
53
|
+
status: complete ? "pass" : acceptanceStatus,
|
|
54
|
+
order: [
|
|
55
|
+
"derive",
|
|
56
|
+
"verification_before_completion_expected",
|
|
57
|
+
"validate_state",
|
|
58
|
+
"validate_derived",
|
|
59
|
+
"validate_plan_acceptance",
|
|
60
|
+
"auditor_blocker_scan",
|
|
61
|
+
"stale_overclaim_scope_scan",
|
|
62
|
+
"ac_evidence_assertion_gate",
|
|
63
|
+
"negative_evidence_scan_gate",
|
|
64
|
+
"compute_completion"
|
|
65
|
+
],
|
|
66
|
+
errors,
|
|
67
|
+
next_required_actions: nextRequiredActions
|
|
68
|
+
};
|
|
45
69
|
await saveSuperpowersState(workdir, latest);
|
|
46
70
|
await deriveSuperpowersArtifacts(workdir);
|
|
47
71
|
await appendSuperpowersEvent(workdir, "final_gate", { product_goal_complete: complete });
|
|
48
72
|
return { product_goal_complete: complete, errors };
|
|
49
73
|
}
|
|
74
|
+
function acceptanceStatusForErrors(errors) {
|
|
75
|
+
const text = errors.join("\n");
|
|
76
|
+
if (/source hash mismatch|source_changed_requires_recompile|scope_conflict_requires_decision|auditor blocker|Context Delta coverage is unresolved/i.test(text)) {
|
|
77
|
+
return "blocked";
|
|
78
|
+
}
|
|
79
|
+
if (/invalid evidence|forbidden shortcut|negative evidence|stale evidence|raw secret|contains_secret|sibling substitution|assertion_result\.status=failed|assertion exit_code=|command_exit_code=|forbidden text/i.test(text)) {
|
|
80
|
+
return "invalidated";
|
|
81
|
+
}
|
|
82
|
+
return "partial";
|
|
83
|
+
}
|
|
84
|
+
function nextActionsForErrors(errors) {
|
|
85
|
+
return errors.slice(0, 12).map((error) => {
|
|
86
|
+
if (/source hash mismatch/i.test(error)) {
|
|
87
|
+
return `${error}; rerun ty-context composite-long-task compile <workdir> after approving source changes`;
|
|
88
|
+
}
|
|
89
|
+
if (/scope_conflict_requires_decision/i.test(error)) {
|
|
90
|
+
return `${error}; clarify Product / Plan / Checklist delivery scope before more implementation`;
|
|
91
|
+
}
|
|
92
|
+
if (/missing assertion result|not machine-backed/i.test(error)) {
|
|
93
|
+
return `${error}; add assertion_result evidence for the target AC/proof layer`;
|
|
94
|
+
}
|
|
95
|
+
if (/negative evidence|forbidden/i.test(error)) {
|
|
96
|
+
return `${error}; rerun the negative evidence scan and fix the contradictory owner-surface state`;
|
|
97
|
+
}
|
|
98
|
+
if (/derived\/.*does not match/i.test(error)) {
|
|
99
|
+
return `${error}; rerun ty-context composite-long-task derive <workdir>`;
|
|
100
|
+
}
|
|
101
|
+
return error;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { MACHINE_VERIFIABLE_PROOF_LAYERS, proofLayerName } from "./superpowers-task-assertions.js";
|
|
2
|
+
import { compileReportSuffix, missingCategory, throwCompileErrors } from "./superpowers-task-compile-diagnostics.js";
|
|
1
3
|
import { fieldArray, fieldBoolean, fieldLine, fieldText, parseDocumentFields, parseHeadingDefinitions } from "./superpowers-task-source-parser.js";
|
|
2
4
|
export const DEFAULT_LAYERS = ["code", "test"];
|
|
3
5
|
const PRODUCT_DELIVERY_SCOPES = new Set(["system_capability_build", "representative_sample_validation", "full_population_operation", "mixed_scope_requires_boundary"]);
|
|
@@ -105,6 +107,11 @@ export function parsePlanItems(content, sourceFile) {
|
|
|
105
107
|
forbidden_surfaces: optionalArray(fields, "forbidden_surfaces"),
|
|
106
108
|
implementation_paths: optionalArray(fields, "implementation_paths"),
|
|
107
109
|
required_tests: optionalArray(fields, "required_tests"),
|
|
110
|
+
proof_layer_ids: optionalArray(fields, "proof_layer_ids").map(normalizeLayer),
|
|
111
|
+
non_completing_shortcuts: optionalArray(fields, "non_completing_shortcuts"),
|
|
112
|
+
substitution_policy: optionalArray(fields, "substitution_policy"),
|
|
113
|
+
explicit_no_test_scope: fieldBoolean(fields, "explicit_no_test_scope") === true,
|
|
114
|
+
context_fact_refs: optionalArray(fields, "context_fact_refs"),
|
|
108
115
|
status: "not_started",
|
|
109
116
|
related_acs: optionalArray(fields, "related_acs").map((item) => item.toUpperCase()),
|
|
110
117
|
required_proof_layers: []
|
|
@@ -119,6 +126,7 @@ export function parseAcceptanceCriteria(content, sourceFile) {
|
|
|
119
126
|
for (const definition of parseHeadingDefinitions(content, { kind: "AC", sourceFile, allowedFields: ACCEPTANCE_FIELDS })) {
|
|
120
127
|
const fields = definition.fields;
|
|
121
128
|
const layers = optionalArray(fields, "required_proof_layers").map(normalizeLayer).filter(Boolean);
|
|
129
|
+
const requiredProofLayers = layers.length > 0 ? layers : DEFAULT_LAYERS;
|
|
122
130
|
items[definition.id] = {
|
|
123
131
|
scope: definition.title,
|
|
124
132
|
source_file: definition.source_file,
|
|
@@ -130,7 +138,13 @@ export function parseAcceptanceCriteria(content, sourceFile) {
|
|
|
130
138
|
sample_boundary: requireText(errors, definition.id, "sample_boundary", fields, sourceFile, definition.source_start_line),
|
|
131
139
|
full_population_required: requireBoolean(errors, definition.id, "full_population_required", fields, sourceFile, definition.source_start_line),
|
|
132
140
|
related_plan_items: optionalArray(fields, "related_plan_items").map((item) => item.toUpperCase()),
|
|
133
|
-
required_proof_layers:
|
|
141
|
+
required_proof_layers: requiredProofLayers,
|
|
142
|
+
assertion_requirements: assertionRequirements(fields, requiredProofLayers),
|
|
143
|
+
required_test_ids: optionalArray(fields, "required_test_ids"),
|
|
144
|
+
fail_conditions: optionalArray(fields, "fail_conditions"),
|
|
145
|
+
invalid_evidence: optionalArray(fields, "invalid_evidence"),
|
|
146
|
+
final_evidence_expected: optionalArray(fields, "final_evidence_expected"),
|
|
147
|
+
explicit_no_test_scope: fieldBoolean(fields, "explicit_no_test_scope") === true,
|
|
134
148
|
status: "not_run"
|
|
135
149
|
};
|
|
136
150
|
}
|
|
@@ -140,15 +154,41 @@ export function parseAcceptanceCriteria(content, sourceFile) {
|
|
|
140
154
|
function normalizeLayer(value) {
|
|
141
155
|
return value.trim().toLowerCase().replace(/[- ]+/g, "_");
|
|
142
156
|
}
|
|
157
|
+
function assertionRequirements(fields, layers) {
|
|
158
|
+
const requiredTestIds = optionalArray(fields, "required_test_ids");
|
|
159
|
+
const positiveAssertions = unique([
|
|
160
|
+
...fieldArray(fields, "ac_validates"),
|
|
161
|
+
...requiredTestIds,
|
|
162
|
+
...optionalArray(fields, "test_cases"),
|
|
163
|
+
...optionalArray(fields, "verification_method"),
|
|
164
|
+
...optionalArray(fields, "final_evidence_expected")
|
|
165
|
+
]);
|
|
166
|
+
const negativeAssertions = unique([
|
|
167
|
+
...fieldArray(fields, "ac_does_not_validate"),
|
|
168
|
+
...optionalArray(fields, "fail_conditions"),
|
|
169
|
+
...optionalArray(fields, "invalid_evidence")
|
|
170
|
+
]);
|
|
171
|
+
return layers.map((layer) => {
|
|
172
|
+
const normalized = proofLayerName(normalizeLayer(layer));
|
|
173
|
+
return {
|
|
174
|
+
proof_layer: normalized,
|
|
175
|
+
required: true,
|
|
176
|
+
machine_blocking: MACHINE_VERIFIABLE_PROOF_LAYERS.has(normalized),
|
|
177
|
+
required_test_ids: requiredTestIds,
|
|
178
|
+
positive_assertions: positiveAssertions,
|
|
179
|
+
negative_assertions: negativeAssertions
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
}
|
|
143
183
|
function requireEnum(errors, label, name, fields, allowed, sourceFile, fallbackLine) {
|
|
144
184
|
const value = fieldText(fields, name);
|
|
145
185
|
const line = fieldLine(fields, name) ?? fallbackLine;
|
|
146
186
|
if (!value) {
|
|
147
|
-
errors.push(`${label} missing ${name} at ${sourceFile}:${line}`);
|
|
187
|
+
errors.push(`${label} missing ${name} at ${sourceFile}:${line}${compileReportSuffix(missingCategory(label), sourceFile, line, name, "field is required", "add the required field and rerun compile")}`);
|
|
148
188
|
return "";
|
|
149
189
|
}
|
|
150
190
|
if (!allowed.has(value)) {
|
|
151
|
-
errors.push(`${label} invalid ${name}: ${value} at ${sourceFile}:${line}; allowed: ${[...allowed].join(", ")}`);
|
|
191
|
+
errors.push(`${label} invalid ${name}: ${value} at ${sourceFile}:${line}; allowed: ${[...allowed].join(", ")}${compileReportSuffix("blocking_unparseable_object", sourceFile, line, name, `value must be one of ${[...allowed].join(", ")}`, "fix the field value and rerun compile")}`);
|
|
152
192
|
}
|
|
153
193
|
return value;
|
|
154
194
|
}
|
|
@@ -156,14 +196,14 @@ function requireText(errors, label, name, fields, sourceFile, fallbackLine) {
|
|
|
156
196
|
const value = fieldText(fields, name);
|
|
157
197
|
const line = fieldLine(fields, name) ?? fallbackLine;
|
|
158
198
|
if (!value) {
|
|
159
|
-
errors.push(`${label} missing ${name} at ${sourceFile}:${line}`);
|
|
199
|
+
errors.push(`${label} missing ${name} at ${sourceFile}:${line}${compileReportSuffix(missingCategory(label), sourceFile, line, name, "field is required", "add non-empty text and rerun compile")}`);
|
|
160
200
|
}
|
|
161
201
|
return value;
|
|
162
202
|
}
|
|
163
203
|
function requireArray(errors, label, name, fields, sourceFile, fallbackLine) {
|
|
164
204
|
const line = fieldLine(fields, name) ?? fallbackLine;
|
|
165
205
|
if (!fields[name]) {
|
|
166
|
-
errors.push(`${label} missing ${name} at ${sourceFile}:${line}`);
|
|
206
|
+
errors.push(`${label} missing ${name} at ${sourceFile}:${line}${compileReportSuffix(missingCategory(label), sourceFile, line, name, "field is required", "add the list field and rerun compile")}`);
|
|
167
207
|
return [];
|
|
168
208
|
}
|
|
169
209
|
return fieldArray(fields, name);
|
|
@@ -171,20 +211,18 @@ function requireArray(errors, label, name, fields, sourceFile, fallbackLine) {
|
|
|
171
211
|
function requireBoolean(errors, label, name, fields, sourceFile, fallbackLine) {
|
|
172
212
|
const line = fieldLine(fields, name) ?? fallbackLine;
|
|
173
213
|
if (!fields[name]) {
|
|
174
|
-
errors.push(`${label} missing ${name} at ${sourceFile}:${line}`);
|
|
214
|
+
errors.push(`${label} missing ${name} at ${sourceFile}:${line}${compileReportSuffix(missingCategory(label), sourceFile, line, name, "field is required", "add true or false and rerun compile")}`);
|
|
175
215
|
return null;
|
|
176
216
|
}
|
|
177
217
|
const value = fieldBoolean(fields, name);
|
|
178
218
|
if (value === null) {
|
|
179
|
-
errors.push(`${label} invalid ${name}: ${fieldText(fields, name)} at ${sourceFile}:${line}; must be true or false`);
|
|
219
|
+
errors.push(`${label} invalid ${name}: ${fieldText(fields, name)} at ${sourceFile}:${line}; must be true or false${compileReportSuffix("blocking_unparseable_object", sourceFile, line, name, "value must be true or false", "fix the boolean and rerun compile")}`);
|
|
180
220
|
}
|
|
181
221
|
return value;
|
|
182
222
|
}
|
|
183
223
|
function optionalArray(fields, name) {
|
|
184
224
|
return fields[name] ? fieldArray(fields, name) : [];
|
|
185
225
|
}
|
|
186
|
-
function
|
|
187
|
-
|
|
188
|
-
throw new Error(`Superpowers source compile failed:\n- ${errors.join("\n- ")}`);
|
|
189
|
-
}
|
|
226
|
+
function unique(values) {
|
|
227
|
+
return [...new Set(values.filter(Boolean))];
|
|
190
228
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { compileError, throwCompileErrors } from "./superpowers-task-compile-diagnostics.js";
|
|
1
2
|
export function parseHeadingDefinitions(content, options) {
|
|
2
3
|
const lines = splitLines(content);
|
|
3
4
|
const errors = [];
|
|
@@ -8,7 +9,7 @@ export function parseHeadingDefinitions(content, options) {
|
|
|
8
9
|
for (const heading of headings) {
|
|
9
10
|
const firstLine = seen.get(heading.id);
|
|
10
11
|
if (firstLine !== undefined) {
|
|
11
|
-
errors.push(`${heading.id} duplicate definition at ${options.sourceFile}:${firstLine} and ${options.sourceFile}:${heading.line}
|
|
12
|
+
errors.push(compileError(`${heading.id} duplicate definition at ${options.sourceFile}:${firstLine} and ${options.sourceFile}:${heading.line}`, "blocking_unparseable_object", options.sourceFile, heading.line, heading.id, "duplicate object ids make PI/AC graph references ambiguous", "keep exactly one heading definition for this id"));
|
|
12
13
|
continue;
|
|
13
14
|
}
|
|
14
15
|
seen.set(heading.id, heading.line);
|
|
@@ -24,15 +25,15 @@ export function parseHeadingDefinitions(content, options) {
|
|
|
24
25
|
});
|
|
25
26
|
}
|
|
26
27
|
if (definitions.length === 0) {
|
|
27
|
-
errors.push(`${options.sourceFile} must define ${options.kind} items with Markdown headings like "## ${options.kind}-001: ..."`);
|
|
28
|
+
errors.push(compileError(`${options.sourceFile} must define ${options.kind} items with Markdown headings like "## ${options.kind}-001: ..."`, options.kind === "PI" ? "blocking_missing_plan" : "blocking_missing_checklist", options.sourceFile, 1, `${options.kind}_heading`, "the source cannot compile without at least one object heading", `add headings like ## ${options.kind}-001: ...`));
|
|
28
29
|
}
|
|
29
|
-
|
|
30
|
+
throwCompileErrors(errors);
|
|
30
31
|
return definitions;
|
|
31
32
|
}
|
|
32
33
|
export function parseDocumentFields(content, sourceFile, allowedFields) {
|
|
33
34
|
const errors = [];
|
|
34
35
|
const fields = parseFields(splitLines(content), sourceFile, 1, allowedFields, errors);
|
|
35
|
-
|
|
36
|
+
throwCompileErrors(errors);
|
|
36
37
|
return fields;
|
|
37
38
|
}
|
|
38
39
|
export function fieldText(fields, name) {
|
|
@@ -72,11 +73,11 @@ function parseFields(lines, sourceFile, startLine, allowedFields, errors) {
|
|
|
72
73
|
const lineNumber = startLine + index;
|
|
73
74
|
const heading = /^(#{1,6})\s+(.+?)\s*$/.exec(line);
|
|
74
75
|
if (heading && allowedFields.has(heading[2].trim())) {
|
|
75
|
-
errors.push(`${sourceFile}:${lineNumber} field headings are not supported; use "${heading[2].trim()}: ..."
|
|
76
|
+
errors.push(compileError(`${sourceFile}:${lineNumber} field headings are not supported; use "${heading[2].trim()}: ..."`, "blocking_unparseable_object", sourceFile, lineNumber, heading[2].trim(), "field heading syntax is ambiguous in compiled source", "use key: value fields"));
|
|
76
77
|
continue;
|
|
77
78
|
}
|
|
78
79
|
if (/^\s*\|/.test(line) && containsKnownField(line, allowedFields)) {
|
|
79
|
-
errors.push(`${sourceFile}:${lineNumber} table fields are not supported; use key: value fields
|
|
80
|
+
errors.push(compileError(`${sourceFile}:${lineNumber} table fields are not supported; use key: value fields`, "blocking_unparseable_object", sourceFile, lineNumber, "table_field", "table fields cannot be parsed into canonical state safely", "use key: value fields"));
|
|
80
81
|
continue;
|
|
81
82
|
}
|
|
82
83
|
const match = /^([a-z][a-z0-9_]*)\s*:\s*(.*)$/.exec(line);
|
|
@@ -86,12 +87,12 @@ function parseFields(lines, sourceFile, startLine, allowedFields, errors) {
|
|
|
86
87
|
const name = match[1];
|
|
87
88
|
if (!allowedFields.has(name)) {
|
|
88
89
|
if (name.includes("_")) {
|
|
89
|
-
errors.push(`${sourceFile}:${lineNumber} unknown field ${name}
|
|
90
|
+
errors.push(compileError(`${sourceFile}:${lineNumber} unknown field ${name}`, "blocking_unparseable_object", sourceFile, lineNumber, name, "unknown fields may hide required source semantics", "rename the field to a supported canonical key"));
|
|
90
91
|
}
|
|
91
92
|
continue;
|
|
92
93
|
}
|
|
93
94
|
if (fields[name]) {
|
|
94
|
-
errors.push(`${sourceFile}:${lineNumber} duplicate field ${name}
|
|
95
|
+
errors.push(compileError(`${sourceFile}:${lineNumber} duplicate field ${name}`, "blocking_unparseable_object", sourceFile, lineNumber, name, "duplicate fields make canonical source values ambiguous", "keep one field value"));
|
|
95
96
|
continue;
|
|
96
97
|
}
|
|
97
98
|
const parsed = parseFieldValue(lines, index, match[2], sourceFile, lineNumber, errors);
|
|
@@ -113,7 +114,7 @@ function parseFieldValue(lines, index, rest, sourceFile, lineNumber, errors) {
|
|
|
113
114
|
block.push(next.replace(/^\s{0,2}/, ""));
|
|
114
115
|
}
|
|
115
116
|
if (block.length === 0) {
|
|
116
|
-
errors.push(`${sourceFile}:${lineNumber} block field must include indented text
|
|
117
|
+
errors.push(compileError(`${sourceFile}:${lineNumber} block field must include indented text`, "blocking_unparseable_object", sourceFile, lineNumber, "block_field", "empty block field cannot prove source intent", "add indented text or remove the field"));
|
|
117
118
|
}
|
|
118
119
|
return { value: block.join("\n").trim(), endIndex: cursor - 1 };
|
|
119
120
|
}
|
|
@@ -132,7 +133,7 @@ function parseFieldValue(lines, index, rest, sourceFile, lineNumber, errors) {
|
|
|
132
133
|
}
|
|
133
134
|
const listItem = /^\s*[-*+]\s+(.+?)\s*$/.exec(next);
|
|
134
135
|
if (!listItem) {
|
|
135
|
-
errors.push(`${sourceFile}:${cursor + 1} field lists must use indented "- item" entries or key: | blocks
|
|
136
|
+
errors.push(compileError(`${sourceFile}:${cursor + 1} field lists must use indented "- item" entries or key: | blocks`, "blocking_unparseable_object", sourceFile, cursor + 1, "list_field", "list syntax cannot be parsed into canonical arrays", "use indented '- item' entries or key: | blocks"));
|
|
136
137
|
continue;
|
|
137
138
|
}
|
|
138
139
|
values.push(cleanValue(listItem[1]));
|
|
@@ -158,7 +159,7 @@ function rejectListStyleDefinitions(lines, options, errors) {
|
|
|
158
159
|
continue;
|
|
159
160
|
}
|
|
160
161
|
const id = match[1].toUpperCase();
|
|
161
|
-
errors.push(`${id} list-style definition is not allowed at ${options.sourceFile}:${index + 1}; use "## ${id}: ..."`);
|
|
162
|
+
errors.push(compileError(`${id} list-style definition is not allowed at ${options.sourceFile}:${index + 1}; use "## ${id}: ..."`, "blocking_unparseable_object", options.sourceFile, index + 1, id, "list-style object definitions are not canonical PI/AC headings", `use "## ${id}: ..."`));
|
|
162
163
|
}
|
|
163
164
|
}
|
|
164
165
|
function listItemBlock(lines, start) {
|
|
@@ -217,8 +218,3 @@ function splitLines(content) {
|
|
|
217
218
|
function cleanValue(value) {
|
|
218
219
|
return value.replace(/^[-#*\s]+/, "").trim();
|
|
219
220
|
}
|
|
220
|
-
function throwIfErrors(errors) {
|
|
221
|
-
if (errors.length > 0) {
|
|
222
|
-
throw new Error(`Superpowers source compile failed:\n- ${errors.join("\n- ")}`);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
@@ -54,7 +54,7 @@ export declare const SUPERPOWERS_TASK_STATE_JSON_SCHEMA: {
|
|
|
54
54
|
};
|
|
55
55
|
};
|
|
56
56
|
export type SuperpowersProofLayerStatus = "missing" | "satisfied" | "invalidated" | "blocked";
|
|
57
|
-
export type SuperpowersPlanItemStatus = "not_started" | "
|
|
57
|
+
export type SuperpowersPlanItemStatus = "not_started" | "in_progress" | "complete" | "partial" | "blocked" | "invalidated" | "out_of_scope_NA";
|
|
58
58
|
export type SuperpowersAcceptanceStatus = "not_run" | "complete" | "partial" | "blocked" | "invalidated" | "out_of_scope_NA";
|
|
59
59
|
export type SuperpowersProductDeliveryScope = "system_capability_build" | "representative_sample_validation" | "full_population_operation" | "mixed_scope_requires_boundary";
|
|
60
60
|
export type SuperpowersPlanDeliveryScope = "system_capability_build" | "representative_sample_validation" | "full_population_operation" | "out_of_scope_backlog";
|
|
@@ -95,6 +95,7 @@ export interface SuperpowersTaskState {
|
|
|
95
95
|
acceptance_target_status: string;
|
|
96
96
|
audit_task_complete: boolean;
|
|
97
97
|
completion_basis: string[];
|
|
98
|
+
next_required_actions?: string[];
|
|
98
99
|
};
|
|
99
100
|
}
|
|
100
101
|
export interface SuperpowersSourceRecord {
|
|
@@ -134,6 +135,11 @@ export interface SuperpowersPlanItem {
|
|
|
134
135
|
forbidden_surfaces: string[];
|
|
135
136
|
implementation_paths: string[];
|
|
136
137
|
required_tests: string[];
|
|
138
|
+
proof_layer_ids?: string[];
|
|
139
|
+
non_completing_shortcuts?: string[];
|
|
140
|
+
substitution_policy?: string[];
|
|
141
|
+
explicit_no_test_scope?: boolean;
|
|
142
|
+
context_fact_refs?: string[];
|
|
137
143
|
status: SuperpowersPlanItemStatus;
|
|
138
144
|
related_acs: string[];
|
|
139
145
|
required_proof_layers: string[];
|
|
@@ -150,8 +156,22 @@ export interface SuperpowersAcceptanceCriterion {
|
|
|
150
156
|
full_population_required: boolean | null;
|
|
151
157
|
related_plan_items: string[];
|
|
152
158
|
required_proof_layers: string[];
|
|
159
|
+
assertion_requirements?: AssertionRequirement[];
|
|
160
|
+
required_test_ids?: string[];
|
|
161
|
+
fail_conditions?: string[];
|
|
162
|
+
invalid_evidence?: string[];
|
|
163
|
+
final_evidence_expected?: string[];
|
|
164
|
+
explicit_no_test_scope?: boolean;
|
|
153
165
|
status: SuperpowersAcceptanceStatus;
|
|
154
166
|
}
|
|
167
|
+
export interface AssertionRequirement {
|
|
168
|
+
proof_layer: string;
|
|
169
|
+
required: boolean;
|
|
170
|
+
machine_blocking: boolean;
|
|
171
|
+
required_test_ids: string[];
|
|
172
|
+
positive_assertions: string[];
|
|
173
|
+
negative_assertions: string[];
|
|
174
|
+
}
|
|
155
175
|
export interface SuperpowersProofLayer {
|
|
156
176
|
required: boolean;
|
|
157
177
|
status: SuperpowersProofLayerStatus;
|
|
@@ -190,6 +210,7 @@ export interface SuperpowersEvidenceRecord {
|
|
|
190
210
|
stale_after: string | null;
|
|
191
211
|
};
|
|
192
212
|
command?: string;
|
|
213
|
+
command_exit_code?: number;
|
|
193
214
|
artifact_paths: string[];
|
|
194
215
|
proves: string[];
|
|
195
216
|
does_not_prove: string[];
|
|
@@ -201,8 +222,47 @@ export interface SuperpowersEvidenceRecord {
|
|
|
201
222
|
external_reviewer_can_reproduce: boolean;
|
|
202
223
|
reproduction_steps: string;
|
|
203
224
|
};
|
|
225
|
+
assertion_result?: AssertionResult;
|
|
226
|
+
negative_evidence_scan?: NegativeEvidenceScan;
|
|
204
227
|
sibling_substitution_used?: boolean;
|
|
205
228
|
sibling_substitution_approval_source?: string;
|
|
206
229
|
}
|
|
230
|
+
export interface AssertionResult {
|
|
231
|
+
schema_version: "assertion-result-v1";
|
|
232
|
+
status: "passed" | "failed" | "blocked" | "stale";
|
|
233
|
+
runner: string;
|
|
234
|
+
exit_code: number;
|
|
235
|
+
target_ac_ids: string[];
|
|
236
|
+
target_proof_layers: string[];
|
|
237
|
+
owner_surface?: string;
|
|
238
|
+
route?: string;
|
|
239
|
+
action?: string;
|
|
240
|
+
positive_assertions: AssertionCheck[];
|
|
241
|
+
negative_assertions: AssertionCheck[];
|
|
242
|
+
artifacts?: string[];
|
|
243
|
+
}
|
|
244
|
+
export interface AssertionCheck {
|
|
245
|
+
id: string;
|
|
246
|
+
status: "passed" | "failed" | "blocked" | "stale";
|
|
247
|
+
actual?: string;
|
|
248
|
+
expected?: string;
|
|
249
|
+
forbidden_text?: string;
|
|
250
|
+
}
|
|
251
|
+
export interface NegativeEvidenceScan {
|
|
252
|
+
schema_version: "negative-evidence-scan-v1";
|
|
253
|
+
status: "passed" | "failed" | "blocked" | "stale";
|
|
254
|
+
target_ac_ids: string[];
|
|
255
|
+
owner_surface?: string;
|
|
256
|
+
route?: string;
|
|
257
|
+
forbidden_findings: NegativeFinding[];
|
|
258
|
+
required_findings: AssertionCheck[];
|
|
259
|
+
artifacts: string[];
|
|
260
|
+
}
|
|
261
|
+
export interface NegativeFinding {
|
|
262
|
+
id: string;
|
|
263
|
+
status: "found" | "not_found";
|
|
264
|
+
forbidden_text?: string;
|
|
265
|
+
actual?: string;
|
|
266
|
+
}
|
|
207
267
|
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
208
268
|
export declare function asStringArray(value: unknown): string[];
|
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { ensureDir, pathExists, readText, writeTextIfChanged } from "./fs.js";
|
|
4
4
|
import { appendSuperpowersEvent } from "./superpowers-task-events.js";
|
|
5
|
+
import { evaluateProofLayerAssertions, isMachineVerifiableLayer, normalizeAssertionResult, normalizeNegativeEvidenceScan } from "./superpowers-task-assertions.js";
|
|
5
6
|
import { SUPERPOWERS_TASK_STATE_JSON_SCHEMA, SUPERPOWERS_TASK_STATE_SCHEMA_VERSION, asStringArray, isRecord } from "./superpowers-task-state-schema.js";
|
|
6
7
|
const SOURCE_FILES = {
|
|
7
8
|
product_architecture_source: {
|
|
@@ -17,6 +18,7 @@ const SOURCE_FILES = {
|
|
|
17
18
|
authority: "acs_completion_semantics_proof_layers"
|
|
18
19
|
}
|
|
19
20
|
};
|
|
21
|
+
const SLICE_PROGRESS_TYPES = new Set(["functional_gap_closed", "proof_gap_closed", "blocker_resolved", "invalid_evidence_removed"]);
|
|
20
22
|
export async function initializeSuperpowersTask(workdir, options = {}) {
|
|
21
23
|
await ensureDir(path.join(workdir, "derived"));
|
|
22
24
|
await writeTextIfChanged(path.join(workdir, "task-state.schema.json"), `${stableJson(SUPERPOWERS_TASK_STATE_JSON_SCHEMA)}\n`);
|
|
@@ -71,7 +73,8 @@ export async function initializeSuperpowersTask(workdir, options = {}) {
|
|
|
71
73
|
product_goal_complete: false,
|
|
72
74
|
acceptance_target_status: "not_run",
|
|
73
75
|
audit_task_complete: false,
|
|
74
|
-
completion_basis: []
|
|
76
|
+
completion_basis: [],
|
|
77
|
+
next_required_actions: []
|
|
75
78
|
}
|
|
76
79
|
};
|
|
77
80
|
await saveSuperpowersState(workdir, state);
|
|
@@ -96,6 +99,10 @@ export async function applySliceDelta(workdir, deltaFile) {
|
|
|
96
99
|
if (!progressValue || !String(progressValue.type ?? "").trim() || asStringArray(progressValue.closed_items).length === 0) {
|
|
97
100
|
throw new Error("slice_delta must include progress_value with type and closed_items");
|
|
98
101
|
}
|
|
102
|
+
const progressType = String(progressValue.type);
|
|
103
|
+
if (!SLICE_PROGRESS_TYPES.has(progressType)) {
|
|
104
|
+
throw new Error(`slice_delta progress_value.type must be one of ${[...SLICE_PROGRESS_TYPES].join(", ")}; got ${progressType || "(missing)"}`);
|
|
105
|
+
}
|
|
99
106
|
const evidenceRecords = readEvidenceRecords(delta.evidence_records);
|
|
100
107
|
for (const evidence of evidenceRecords) {
|
|
101
108
|
const existingIndex = state.evidence.findIndex((item) => item.evidence_id === evidence.evidence_id);
|
|
@@ -119,7 +126,7 @@ export async function applySliceDelta(workdir, deltaFile) {
|
|
|
119
126
|
blockers: Array.isArray(delta.blockers) ? delta.blockers : [],
|
|
120
127
|
cleanup_assertions: asStringArray(delta.cleanup_assertions),
|
|
121
128
|
progress_value: {
|
|
122
|
-
type:
|
|
129
|
+
type: progressType,
|
|
123
130
|
closed_items: asStringArray(progressValue.closed_items),
|
|
124
131
|
why_it_reduces_rework: String(progressValue.why_it_reduces_rework ?? "")
|
|
125
132
|
}
|
|
@@ -147,7 +154,7 @@ export function recomputeStatuses(state) {
|
|
|
147
154
|
if (layerIds.length === 0) {
|
|
148
155
|
ac.status = "not_run";
|
|
149
156
|
}
|
|
150
|
-
else if (layerIds.every((layerId) => state
|
|
157
|
+
else if (layerIds.every((layerId) => layerSatisfiedForCompletion(state, layerId))) {
|
|
151
158
|
ac.status = "complete";
|
|
152
159
|
}
|
|
153
160
|
else if (layerIds.some((layerId) => state.graph.proof_layers[layerId]?.status === "satisfied")) {
|
|
@@ -169,6 +176,12 @@ export function recomputeStatuses(state) {
|
|
|
169
176
|
}
|
|
170
177
|
}
|
|
171
178
|
}
|
|
179
|
+
function layerSatisfiedForCompletion(state, layerId) {
|
|
180
|
+
if (state.graph.proof_layers[layerId]?.status !== "satisfied") {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
return !isMachineVerifiableLayer(layerId) || evaluateProofLayerAssertions(state, layerId).assertion_status === "passed";
|
|
184
|
+
}
|
|
172
185
|
export function emptyProgressState() {
|
|
173
186
|
return {
|
|
174
187
|
system_capability_progress: { status: "not_started" },
|
|
@@ -208,6 +221,7 @@ function readEvidenceRecords(value) {
|
|
|
208
221
|
}
|
|
209
222
|
: { created_at: "", valid_for: "", stale_after: null },
|
|
210
223
|
command: item.command === undefined ? undefined : String(item.command),
|
|
224
|
+
command_exit_code: item.command_exit_code === undefined ? undefined : Number(item.command_exit_code),
|
|
211
225
|
artifact_paths: asStringArray(item.artifact_paths),
|
|
212
226
|
proves: asStringArray(item.proves),
|
|
213
227
|
does_not_prove: asStringArray(item.does_not_prove),
|
|
@@ -220,6 +234,8 @@ function readEvidenceRecords(value) {
|
|
|
220
234
|
reproduction_steps: String(item.reviewability.reproduction_steps ?? "")
|
|
221
235
|
}
|
|
222
236
|
: { external_reviewer_can_reproduce: false, reproduction_steps: "" },
|
|
237
|
+
assertion_result: normalizeAssertionResult(item.assertion_result),
|
|
238
|
+
negative_evidence_scan: normalizeNegativeEvidenceScan(item.negative_evidence_scan),
|
|
223
239
|
sibling_substitution_used: item.sibling_substitution_used === true,
|
|
224
240
|
sibling_substitution_approval_source: item.sibling_substitution_approval_source === undefined ? undefined : String(item.sibling_substitution_approval_source)
|
|
225
241
|
}));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const CANONICAL_PLAN_STATUSES = new Set(["not_started", "in_progress", "partial", "blocked", "invalidated", "complete", "out_of_scope_NA"]);
|
|
2
|
+
const CANONICAL_AC_STATUSES = new Set(["not_started", "not_run", "in_progress", "partial", "blocked", "invalidated", "complete", "out_of_scope_NA"]);
|
|
3
|
+
export function validateCanonicalStatuses(state, errors) {
|
|
4
|
+
for (const [planId, item] of Object.entries(state.graph?.plan_items ?? {})) {
|
|
5
|
+
if (!CANONICAL_PLAN_STATUSES.has(item.status)) {
|
|
6
|
+
errors.push(`plan item ${planId} has non-canonical status: ${item.status}`);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
for (const [acId, ac] of Object.entries(state.graph?.acceptance_criteria ?? {})) {
|
|
10
|
+
if (!CANONICAL_AC_STATUSES.has(ac.status)) {
|
|
11
|
+
errors.push(`AC ${acId} has non-canonical status: ${ac.status}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -3,9 +3,12 @@ import { pathExists, readText } from "./fs.js";
|
|
|
3
3
|
import { findSensitiveEvidence } from "./plan-acceptance-evidence.js";
|
|
4
4
|
import { primitiveText, repoRelative, resolveInputDir } from "./plan-validator-common.js";
|
|
5
5
|
import { derivedMatchesState } from "./superpowers-task-derive.js";
|
|
6
|
+
import { validatePlanCompletionConformance } from "./superpowers-task-conformance.js";
|
|
6
7
|
import { fullPopulationRequired, validateDeliveryContract, validateScopeConflicts } from "./superpowers-task-delivery.js";
|
|
7
8
|
import { loadSuperpowersState, sha256 } from "./superpowers-task-state.js";
|
|
9
|
+
import { validateCanonicalStatuses } from "./superpowers-task-status.js";
|
|
8
10
|
import { isRecord } from "./superpowers-task-state-schema.js";
|
|
11
|
+
import { evaluateProofLayerAssertions, isUiBrowserLayer } from "./superpowers-task-assertions.js";
|
|
9
12
|
export async function validateSuperpowersState(projectRoot, args = []) {
|
|
10
13
|
const info = [];
|
|
11
14
|
const warnings = [];
|
|
@@ -35,7 +38,9 @@ export async function validateSuperpowersState(projectRoot, args = []) {
|
|
|
35
38
|
}
|
|
36
39
|
await validateSourceHashes(targetDir, state, errors);
|
|
37
40
|
validateDeliveryContract(state, errors);
|
|
41
|
+
validateCanonicalStatuses(state, errors);
|
|
38
42
|
validateGraphReferences(state, errors);
|
|
43
|
+
validatePlanCompletionConformance(state, errors);
|
|
39
44
|
validateScopeConflicts(state, errors);
|
|
40
45
|
validateEvidenceRecords(state, errors);
|
|
41
46
|
validateProofLayers(state, errors);
|
|
@@ -163,7 +168,7 @@ function validateEvidenceRecords(state, errors) {
|
|
|
163
168
|
if (proofLayer.endsWith(".runtime") && /\b(mock|unit|viewmodel)\b/i.test(evidence.type)) {
|
|
164
169
|
errors.push(`${label} runtime proof cannot be mock/unit/viewmodel only`);
|
|
165
170
|
}
|
|
166
|
-
if (proofLayer.endsWith(".ui_browser") &&
|
|
171
|
+
if (proofLayer.endsWith(".ui_browser") && !/(browser|ui_browser|screenshot|playwright)/i.test(evidence.type)) {
|
|
167
172
|
errors.push(`${label} UI proof must use browser owner surface evidence`);
|
|
168
173
|
}
|
|
169
174
|
}
|
|
@@ -185,6 +190,13 @@ function validateProofLayers(state, errors) {
|
|
|
185
190
|
errors.push(`proof layer ${layerId} references ${evidenceId} but that evidence does not prove it`);
|
|
186
191
|
}
|
|
187
192
|
}
|
|
193
|
+
if (layer.status === "satisfied") {
|
|
194
|
+
const evaluation = evaluateProofLayerAssertions(state, layerId);
|
|
195
|
+
errors.push(...evaluation.blocking_assertion_failures, ...evaluation.negative_evidence_findings);
|
|
196
|
+
if (isUiBrowserLayer(layerId) && evaluation.assertion_status !== "passed") {
|
|
197
|
+
errors.push(`proof layer ${layerId} ui_browser proof not machine-backed`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
188
200
|
}
|
|
189
201
|
}
|
|
190
202
|
function validateAuditor(state, errors) {
|
|
@@ -242,6 +254,7 @@ export function completionConditionErrors(state) {
|
|
|
242
254
|
validateDeliveryContract(state, errors);
|
|
243
255
|
validateScopeConflicts(state, errors);
|
|
244
256
|
validateGraphReferences(state, errors);
|
|
257
|
+
validatePlanCompletionConformance(state, errors);
|
|
245
258
|
validateEvidenceRecords(state, errors);
|
|
246
259
|
validateProofLayers(state, errors);
|
|
247
260
|
validateAuditor(state, errors);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "project-tiny-context-harness",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.81",
|
|
4
4
|
"description": "Minimal project memory and validation harness for AI coding agents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Seven128",
|
|
@@ -55,15 +55,15 @@
|
|
|
55
55
|
"prepack": "npm run build"
|
|
56
56
|
},
|
|
57
57
|
"engines": {
|
|
58
|
-
"node": ">=
|
|
58
|
+
"node": ">=24"
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@google/design.md": "^0.
|
|
62
|
-
"impeccable": "^
|
|
61
|
+
"@google/design.md": "^0.3.0",
|
|
62
|
+
"impeccable": "^3.1.0",
|
|
63
63
|
"yaml": "^2.9.0"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
|
-
"@types/node": "^
|
|
66
|
+
"@types/node": "^26.0.0",
|
|
67
67
|
"typescript": "^5.5.0"
|
|
68
68
|
}
|
|
69
69
|
}
|