project-tiny-context-harness 0.2.79 → 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 +17 -15
- package/assets/README.md +17 -15
- package/assets/README.zh-CN.md +19 -9
- package/assets/agents/AGENTS_CORE.md +42 -42
- package/assets/github/harness.yml +1 -1
- package/assets/skills/composite-long-task-workflow/SKILL.md +218 -0
- package/assets/skills/composite-long-task-workflow/assets/execution-binding.template.md +33 -0
- package/assets/skills/composite-long-task-workflow/assets/goal-objective.template.md +26 -0
- package/assets/skills/composite-long-task-workflow/references/composite-long-task-workflow-protocol.md +644 -0
- package/assets/skills/normal-long-task/SKILL.md +12 -12
- package/dist/commands/composite-long-task.d.ts +6 -0
- package/dist/commands/composite-long-task.js +103 -0
- package/dist/commands/index.js +5 -3
- package/dist/commands/superpowers.js +6 -87
- package/dist/lib/composite-long-task-renderer.d.ts +12 -0
- package/dist/lib/composite-long-task-renderer.js +109 -0
- 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 +26 -6
- 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 +61 -5
- package/dist/lib/superpowers-task-gates.js +62 -6
- package/dist/lib/superpowers-task-source-compile.js +94 -14
- package/dist/lib/superpowers-task-source-parser.js +20 -18
- 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.d.ts +1 -0
- package/dist/lib/superpowers-task-validator.js +30 -4
- package/package.json +5 -5
- package/assets/skills/superpowers-long-task/SKILL.md +0 -612
|
@@ -1,7 +1,8 @@
|
|
|
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
|
-
import {
|
|
5
|
+
import { completionConditionErrors, validateSuperpowersState } from "./superpowers-task-validator.js";
|
|
5
6
|
export async function runSliceGate(workdir, sliceId) {
|
|
6
7
|
const state = await loadSuperpowersState(workdir);
|
|
7
8
|
const slice = state.slices.find((item) => item.slice_id === sliceId);
|
|
@@ -30,18 +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
|
-
const
|
|
36
|
+
const completionErrors = completionConditionErrors(latest);
|
|
37
|
+
const errors = [...new Set([...report.errors, ...acceptanceReport.errors, ...completionErrors])];
|
|
38
|
+
const complete = errors.length === 0;
|
|
39
|
+
const acceptanceStatus = complete ? "complete" : acceptanceStatusForErrors(errors);
|
|
40
|
+
const nextRequiredActions = complete ? [] : nextActionsForErrors(errors);
|
|
35
41
|
latest.final.product_goal_complete = complete;
|
|
36
42
|
latest.meta.product_goal_complete = complete;
|
|
37
|
-
latest.final.acceptance_target_status =
|
|
43
|
+
latest.final.acceptance_target_status = acceptanceStatus;
|
|
38
44
|
latest.meta.acceptance_target_status = latest.final.acceptance_target_status;
|
|
39
45
|
latest.final.audit_task_complete = true;
|
|
40
46
|
latest.meta.audit_task_complete = true;
|
|
41
|
-
latest.final.completion_basis = complete
|
|
42
|
-
|
|
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;
|
|
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
|
+
};
|
|
43
69
|
await saveSuperpowersState(workdir, latest);
|
|
44
70
|
await deriveSuperpowersArtifacts(workdir);
|
|
45
71
|
await appendSuperpowersEvent(workdir, "final_gate", { product_goal_complete: complete });
|
|
46
|
-
return { product_goal_complete: complete, errors
|
|
72
|
+
return { product_goal_complete: complete, errors };
|
|
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
|
+
});
|
|
47
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"]);
|
|
@@ -8,7 +10,15 @@ const PRODUCT_FIELDS = new Set([
|
|
|
8
10
|
"full_population_required",
|
|
9
11
|
"representative_samples_validate",
|
|
10
12
|
"representative_samples_do_not_validate",
|
|
11
|
-
"out_of_scope_backlog"
|
|
13
|
+
"out_of_scope_backlog",
|
|
14
|
+
"source_authority",
|
|
15
|
+
"product_goal",
|
|
16
|
+
"surface_ia_lock",
|
|
17
|
+
"decision_lock",
|
|
18
|
+
"context_delta",
|
|
19
|
+
"source_to_context_coverage",
|
|
20
|
+
"acceptance_semantics",
|
|
21
|
+
"impact"
|
|
12
22
|
]);
|
|
13
23
|
const PLAN_FIELDS = new Set([
|
|
14
24
|
"delivery_scope",
|
|
@@ -20,16 +30,50 @@ const PLAN_FIELDS = new Set([
|
|
|
20
30
|
"forbidden_surfaces",
|
|
21
31
|
"implementation_paths",
|
|
22
32
|
"required_tests",
|
|
23
|
-
"related_acs"
|
|
33
|
+
"related_acs",
|
|
34
|
+
"requirement_ref",
|
|
35
|
+
"decision_id",
|
|
36
|
+
"proof_layer_ids",
|
|
37
|
+
"api_schema_changes",
|
|
38
|
+
"state_machine",
|
|
39
|
+
"data_flow",
|
|
40
|
+
"worker_runtime_behavior",
|
|
41
|
+
"ui_ia_changes",
|
|
42
|
+
"migration_plan",
|
|
43
|
+
"evidence_artifacts",
|
|
44
|
+
"explicit_no_test_scope",
|
|
45
|
+
"non_completing_shortcuts",
|
|
46
|
+
"substitution_policy",
|
|
47
|
+
"drift_severity",
|
|
48
|
+
"partial_conditions",
|
|
49
|
+
"blockers",
|
|
50
|
+
"context_fact_refs"
|
|
24
51
|
]);
|
|
25
52
|
const ACCEPTANCE_FIELDS = new Set([
|
|
53
|
+
"checklist_source",
|
|
26
54
|
"acceptance_scope",
|
|
27
55
|
"ac_validates",
|
|
28
56
|
"ac_does_not_validate",
|
|
29
57
|
"sample_boundary",
|
|
30
58
|
"full_population_required",
|
|
31
59
|
"related_plan_items",
|
|
32
|
-
"required_proof_layers"
|
|
60
|
+
"required_proof_layers",
|
|
61
|
+
"ac_type",
|
|
62
|
+
"proof_chain",
|
|
63
|
+
"verification_method",
|
|
64
|
+
"fail_conditions",
|
|
65
|
+
"invalid_evidence",
|
|
66
|
+
"substitution_policy",
|
|
67
|
+
"missing_layer_downgrade",
|
|
68
|
+
"auditor_expectation",
|
|
69
|
+
"out_of_scope_na_approval_source",
|
|
70
|
+
"required_test_ids",
|
|
71
|
+
"explicit_no_test_scope",
|
|
72
|
+
"hard_blockers",
|
|
73
|
+
"validates_explanation",
|
|
74
|
+
"does_not_validate_explanation",
|
|
75
|
+
"final_evidence_expected",
|
|
76
|
+
"test_cases"
|
|
33
77
|
]);
|
|
34
78
|
export function parseProductArchitectureScope(content, sourceFile) {
|
|
35
79
|
const fields = parseDocumentFields(content, sourceFile, PRODUCT_FIELDS);
|
|
@@ -63,6 +107,11 @@ export function parsePlanItems(content, sourceFile) {
|
|
|
63
107
|
forbidden_surfaces: optionalArray(fields, "forbidden_surfaces"),
|
|
64
108
|
implementation_paths: optionalArray(fields, "implementation_paths"),
|
|
65
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"),
|
|
66
115
|
status: "not_started",
|
|
67
116
|
related_acs: optionalArray(fields, "related_acs").map((item) => item.toUpperCase()),
|
|
68
117
|
required_proof_layers: []
|
|
@@ -77,6 +126,7 @@ export function parseAcceptanceCriteria(content, sourceFile) {
|
|
|
77
126
|
for (const definition of parseHeadingDefinitions(content, { kind: "AC", sourceFile, allowedFields: ACCEPTANCE_FIELDS })) {
|
|
78
127
|
const fields = definition.fields;
|
|
79
128
|
const layers = optionalArray(fields, "required_proof_layers").map(normalizeLayer).filter(Boolean);
|
|
129
|
+
const requiredProofLayers = layers.length > 0 ? layers : DEFAULT_LAYERS;
|
|
80
130
|
items[definition.id] = {
|
|
81
131
|
scope: definition.title,
|
|
82
132
|
source_file: definition.source_file,
|
|
@@ -88,7 +138,13 @@ export function parseAcceptanceCriteria(content, sourceFile) {
|
|
|
88
138
|
sample_boundary: requireText(errors, definition.id, "sample_boundary", fields, sourceFile, definition.source_start_line),
|
|
89
139
|
full_population_required: requireBoolean(errors, definition.id, "full_population_required", fields, sourceFile, definition.source_start_line),
|
|
90
140
|
related_plan_items: optionalArray(fields, "related_plan_items").map((item) => item.toUpperCase()),
|
|
91
|
-
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,
|
|
92
148
|
status: "not_run"
|
|
93
149
|
};
|
|
94
150
|
}
|
|
@@ -98,15 +154,41 @@ export function parseAcceptanceCriteria(content, sourceFile) {
|
|
|
98
154
|
function normalizeLayer(value) {
|
|
99
155
|
return value.trim().toLowerCase().replace(/[- ]+/g, "_");
|
|
100
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
|
+
}
|
|
101
183
|
function requireEnum(errors, label, name, fields, allowed, sourceFile, fallbackLine) {
|
|
102
184
|
const value = fieldText(fields, name);
|
|
103
185
|
const line = fieldLine(fields, name) ?? fallbackLine;
|
|
104
186
|
if (!value) {
|
|
105
|
-
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")}`);
|
|
106
188
|
return "";
|
|
107
189
|
}
|
|
108
190
|
if (!allowed.has(value)) {
|
|
109
|
-
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")}`);
|
|
110
192
|
}
|
|
111
193
|
return value;
|
|
112
194
|
}
|
|
@@ -114,14 +196,14 @@ function requireText(errors, label, name, fields, sourceFile, fallbackLine) {
|
|
|
114
196
|
const value = fieldText(fields, name);
|
|
115
197
|
const line = fieldLine(fields, name) ?? fallbackLine;
|
|
116
198
|
if (!value) {
|
|
117
|
-
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")}`);
|
|
118
200
|
}
|
|
119
201
|
return value;
|
|
120
202
|
}
|
|
121
203
|
function requireArray(errors, label, name, fields, sourceFile, fallbackLine) {
|
|
122
204
|
const line = fieldLine(fields, name) ?? fallbackLine;
|
|
123
205
|
if (!fields[name]) {
|
|
124
|
-
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")}`);
|
|
125
207
|
return [];
|
|
126
208
|
}
|
|
127
209
|
return fieldArray(fields, name);
|
|
@@ -129,20 +211,18 @@ function requireArray(errors, label, name, fields, sourceFile, fallbackLine) {
|
|
|
129
211
|
function requireBoolean(errors, label, name, fields, sourceFile, fallbackLine) {
|
|
130
212
|
const line = fieldLine(fields, name) ?? fallbackLine;
|
|
131
213
|
if (!fields[name]) {
|
|
132
|
-
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")}`);
|
|
133
215
|
return null;
|
|
134
216
|
}
|
|
135
217
|
const value = fieldBoolean(fields, name);
|
|
136
218
|
if (value === null) {
|
|
137
|
-
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")}`);
|
|
138
220
|
}
|
|
139
221
|
return value;
|
|
140
222
|
}
|
|
141
223
|
function optionalArray(fields, name) {
|
|
142
224
|
return fields[name] ? fieldArray(fields, name) : [];
|
|
143
225
|
}
|
|
144
|
-
function
|
|
145
|
-
|
|
146
|
-
throw new Error(`Superpowers source compile failed:\n- ${errors.join("\n- ")}`);
|
|
147
|
-
}
|
|
226
|
+
function unique(values) {
|
|
227
|
+
return [...new Set(values.filter(Boolean))];
|
|
148
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);
|
|
@@ -107,13 +108,13 @@ function parseFieldValue(lines, index, rest, sourceFile, lineNumber, errors) {
|
|
|
107
108
|
let cursor = index + 1;
|
|
108
109
|
for (; cursor < lines.length; cursor++) {
|
|
109
110
|
const next = lines[cursor];
|
|
110
|
-
if (next.trim() &&
|
|
111
|
+
if (next.trim() && isTopLevelFieldOrHeading(next)) {
|
|
111
112
|
break;
|
|
112
113
|
}
|
|
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
|
}
|
|
@@ -127,18 +128,24 @@ function parseFieldValue(lines, index, rest, sourceFile, lineNumber, errors) {
|
|
|
127
128
|
if (!next.trim()) {
|
|
128
129
|
continue;
|
|
129
130
|
}
|
|
130
|
-
if (
|
|
131
|
+
if (isTopLevelFieldOrHeading(next)) {
|
|
131
132
|
break;
|
|
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]));
|
|
139
140
|
}
|
|
140
141
|
return { value: values, endIndex: cursor - 1 };
|
|
141
142
|
}
|
|
143
|
+
function isTopLevelFieldOrHeading(line) {
|
|
144
|
+
if (/^(#{1,6})\s+/.test(line)) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
return /^([a-z][a-z0-9_]*)\s*:/.test(line);
|
|
148
|
+
}
|
|
142
149
|
function rejectListStyleDefinitions(lines, options, errors) {
|
|
143
150
|
const idPattern = options.kind === "PI" ? "PI" : "AC";
|
|
144
151
|
const listPattern = new RegExp(`^\\s*[-*+]\\s+(${idPattern}-\\d{3,})\\b\\s*[:.-]?`, "i");
|
|
@@ -152,7 +159,7 @@ function rejectListStyleDefinitions(lines, options, errors) {
|
|
|
152
159
|
continue;
|
|
153
160
|
}
|
|
154
161
|
const id = match[1].toUpperCase();
|
|
155
|
-
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}: ..."`));
|
|
156
163
|
}
|
|
157
164
|
}
|
|
158
165
|
function listItemBlock(lines, start) {
|
|
@@ -211,8 +218,3 @@ function splitLines(content) {
|
|
|
211
218
|
function cleanValue(value) {
|
|
212
219
|
return value.replace(/^[-#*\s]+/, "").trim();
|
|
213
220
|
}
|
|
214
|
-
function throwIfErrors(errors) {
|
|
215
|
-
if (errors.length > 0) {
|
|
216
|
-
throw new Error(`Superpowers source compile failed:\n- ${errors.join("\n- ")}`);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
@@ -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
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type SuperpowersTaskState } from "./superpowers-task-state-schema.js";
|
|
2
2
|
import type { ValidatorReport } from "./validators.js";
|
|
3
3
|
export declare function validateSuperpowersState(projectRoot: string, args?: string[]): Promise<ValidatorReport>;
|
|
4
|
+
export declare function completionConditionErrors(state: SuperpowersTaskState): string[];
|
|
4
5
|
export declare function allCompletionConditionsSatisfied(state: SuperpowersTaskState): boolean;
|