project-tiny-context-harness 0.2.75 → 0.2.77
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 +14 -10
- package/assets/README.md +15 -11
- package/assets/README.zh-CN.md +8 -6
- package/assets/skills/context_development_engineer/SKILL.md +44 -42
- package/assets/skills/superpowers-long-task/SKILL.md +135 -73
- package/dist/commands/index.js +16 -9
- package/dist/commands/superpowers.d.ts +1 -0
- package/dist/commands/superpowers.js +89 -0
- package/dist/lib/plan-acceptance-validator.js +15 -0
- package/dist/lib/superpowers-task-compile.d.ts +3 -0
- package/dist/lib/superpowers-task-compile.js +223 -0
- package/dist/lib/superpowers-task-delivery.d.ts +4 -0
- package/dist/lib/superpowers-task-delivery.js +84 -0
- package/dist/lib/superpowers-task-derive.d.ts +13 -0
- package/dist/lib/superpowers-task-derive.js +204 -0
- package/dist/lib/superpowers-task-events.d.ts +1 -0
- package/dist/lib/superpowers-task-events.js +13 -0
- package/dist/lib/superpowers-task-gates.d.ts +12 -0
- package/dist/lib/superpowers-task-gates.js +47 -0
- package/dist/lib/superpowers-task-next-slices.d.ts +1 -0
- package/dist/lib/superpowers-task-next-slices.js +12 -0
- package/dist/lib/superpowers-task-state-schema.d.ts +202 -0
- package/dist/lib/superpowers-task-state-schema.js +44 -0
- package/dist/lib/superpowers-task-state.d.ts +16 -0
- package/dist/lib/superpowers-task-state.js +241 -0
- package/dist/lib/superpowers-task-validator.d.ts +4 -0
- package/dist/lib/superpowers-task-validator.js +252 -0
- package/dist/lib/validators.js +4 -2
- package/package.json +1 -1
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { readText } from "./fs.js";
|
|
3
|
+
import { appendSuperpowersEvent } from "./superpowers-task-events.js";
|
|
4
|
+
import { loadSuperpowersState, recomputeStatuses, saveSuperpowersState, refreshSourceHashes } from "./superpowers-task-state.js";
|
|
5
|
+
import { asStringArray } from "./superpowers-task-state-schema.js";
|
|
6
|
+
const DEFAULT_LAYERS = ["code", "test"];
|
|
7
|
+
export async function compileSuperpowersTask(workdir) {
|
|
8
|
+
const state = await loadSuperpowersState(workdir);
|
|
9
|
+
await refreshSourceHashes(workdir, state);
|
|
10
|
+
const productSource = await readText(path.join(workdir, state.sources.product_architecture_source.path));
|
|
11
|
+
const technicalPlan = await readText(path.join(workdir, state.sources.technical_realization_plan.path));
|
|
12
|
+
const checklist = await readText(path.join(workdir, state.sources.acceptance_checklist.path));
|
|
13
|
+
state.delivery = {
|
|
14
|
+
product_architecture_scope: parseProductArchitectureScope(productSource),
|
|
15
|
+
scope_conflicts: []
|
|
16
|
+
};
|
|
17
|
+
const planItems = parsePlanItems(technicalPlan);
|
|
18
|
+
const acceptanceCriteria = parseAcceptanceCriteria(checklist);
|
|
19
|
+
const acIds = Object.keys(acceptanceCriteria);
|
|
20
|
+
for (const [planId, item] of Object.entries(planItems)) {
|
|
21
|
+
if (item.related_acs.length === 0) {
|
|
22
|
+
item.related_acs = acIds;
|
|
23
|
+
}
|
|
24
|
+
item.required_proof_layers = item.related_acs.flatMap((acId) => (acceptanceCriteria[acId]?.required_proof_layers ?? DEFAULT_LAYERS).map((layer) => `${acId}.${layer}`));
|
|
25
|
+
}
|
|
26
|
+
for (const [acId, ac] of Object.entries(acceptanceCriteria)) {
|
|
27
|
+
if (ac.related_plan_items.length === 0) {
|
|
28
|
+
ac.related_plan_items = Object.keys(planItems);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
state.graph.plan_items = planItems;
|
|
32
|
+
state.graph.acceptance_criteria = acceptanceCriteria;
|
|
33
|
+
state.graph.proof_layers = {};
|
|
34
|
+
for (const [acId, ac] of Object.entries(acceptanceCriteria)) {
|
|
35
|
+
for (const layer of ac.required_proof_layers) {
|
|
36
|
+
state.graph.proof_layers[`${acId}.${layer}`] = { required: true, status: "missing", evidence_ids: [] };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
state.graph.edges = Object.entries(planItems).flatMap(([planId, item]) => item.related_acs.map((acId) => ({ from: planId, to: acId, type: "supports" })));
|
|
40
|
+
state.delivery.scope_conflicts = computeScopeConflicts(state);
|
|
41
|
+
state.progress = compileProgress(state);
|
|
42
|
+
recomputeStatuses(state);
|
|
43
|
+
await saveSuperpowersState(workdir, state);
|
|
44
|
+
await appendSuperpowersEvent(workdir, "graph_compiled", {
|
|
45
|
+
plan_items: Object.keys(planItems).length,
|
|
46
|
+
acceptance_criteria: Object.keys(acceptanceCriteria).length
|
|
47
|
+
});
|
|
48
|
+
return state;
|
|
49
|
+
}
|
|
50
|
+
function parseProductArchitectureScope(content) {
|
|
51
|
+
return {
|
|
52
|
+
delivery_scope: fieldText(content, "delivery_scope"),
|
|
53
|
+
full_population_required: fieldBoolean(content, "full_population_required"),
|
|
54
|
+
representative_samples_validate: field(content, "representative_samples_validate"),
|
|
55
|
+
representative_samples_do_not_validate: field(content, "representative_samples_do_not_validate"),
|
|
56
|
+
out_of_scope_backlog: field(content, "out_of_scope_backlog")
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function parsePlanItems(content) {
|
|
60
|
+
const items = {};
|
|
61
|
+
const matches = [...content.matchAll(/\b(PI-\d{3,})\b\s*[:.-]?\s*([^\n]*)/gi)];
|
|
62
|
+
for (const [index, match] of matches.entries()) {
|
|
63
|
+
const id = match[1].toUpperCase();
|
|
64
|
+
const block = blockAfter(content, match.index ?? 0, matches[index + 1]?.index);
|
|
65
|
+
items[id] = {
|
|
66
|
+
requirement: cleanText(match[2]) || firstLine(block) || id,
|
|
67
|
+
delivery_scope: fieldText(block, "delivery_scope"),
|
|
68
|
+
capability_target: fieldText(block, "capability_target"),
|
|
69
|
+
representative_samples: field(block, "representative_samples"),
|
|
70
|
+
full_population_boundary: fieldText(block, "full_population_boundary"),
|
|
71
|
+
non_required_population: field(block, "non_required_population"),
|
|
72
|
+
owner_surfaces: field(block, "owner_surfaces"),
|
|
73
|
+
forbidden_surfaces: field(block, "forbidden_surfaces"),
|
|
74
|
+
implementation_paths: field(block, "implementation_paths"),
|
|
75
|
+
required_tests: field(block, "required_tests"),
|
|
76
|
+
status: "not_started",
|
|
77
|
+
related_acs: field(block, "related_acs").map((item) => item.toUpperCase()),
|
|
78
|
+
required_proof_layers: []
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (Object.keys(items).length === 0) {
|
|
82
|
+
items["PI-001"] = {
|
|
83
|
+
requirement: firstLine(content) || "Implement technical realization plan",
|
|
84
|
+
delivery_scope: "",
|
|
85
|
+
capability_target: "",
|
|
86
|
+
representative_samples: [],
|
|
87
|
+
full_population_boundary: "",
|
|
88
|
+
non_required_population: [],
|
|
89
|
+
owner_surfaces: [],
|
|
90
|
+
forbidden_surfaces: [],
|
|
91
|
+
implementation_paths: [],
|
|
92
|
+
required_tests: [],
|
|
93
|
+
status: "not_started",
|
|
94
|
+
related_acs: [],
|
|
95
|
+
required_proof_layers: []
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return items;
|
|
99
|
+
}
|
|
100
|
+
function parseAcceptanceCriteria(content) {
|
|
101
|
+
const items = {};
|
|
102
|
+
const matches = [...content.matchAll(/\b(AC-\d{3,})\b\s*[:.-]?\s*([^\n]*)/gi)];
|
|
103
|
+
for (const [index, match] of matches.entries()) {
|
|
104
|
+
const id = match[1].toUpperCase();
|
|
105
|
+
const block = blockAfter(content, match.index ?? 0, matches[index + 1]?.index);
|
|
106
|
+
const layers = field(block, "required_proof_layers").map(normalizeLayer).filter(Boolean);
|
|
107
|
+
items[id] = {
|
|
108
|
+
scope: cleanText(match[2]) || firstLine(block) || id,
|
|
109
|
+
acceptance_scope: fieldText(block, "acceptance_scope"),
|
|
110
|
+
ac_validates: field(block, "ac_validates"),
|
|
111
|
+
ac_does_not_validate: field(block, "ac_does_not_validate"),
|
|
112
|
+
sample_boundary: fieldText(block, "sample_boundary"),
|
|
113
|
+
full_population_required: fieldBoolean(block, "full_population_required"),
|
|
114
|
+
related_plan_items: field(block, "related_plan_items").map((item) => item.toUpperCase()),
|
|
115
|
+
required_proof_layers: layers.length > 0 ? layers : DEFAULT_LAYERS,
|
|
116
|
+
status: "not_run"
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (Object.keys(items).length === 0) {
|
|
120
|
+
items["AC-001"] = {
|
|
121
|
+
scope: firstLine(content) || "Acceptance checklist item",
|
|
122
|
+
acceptance_scope: "",
|
|
123
|
+
ac_validates: [],
|
|
124
|
+
ac_does_not_validate: [],
|
|
125
|
+
sample_boundary: "",
|
|
126
|
+
full_population_required: null,
|
|
127
|
+
related_plan_items: [],
|
|
128
|
+
required_proof_layers: DEFAULT_LAYERS,
|
|
129
|
+
status: "not_run"
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return items;
|
|
133
|
+
}
|
|
134
|
+
export function computeScopeConflicts(state) {
|
|
135
|
+
const conflicts = [];
|
|
136
|
+
const product = state.delivery?.product_architecture_scope;
|
|
137
|
+
const productScope = product?.delivery_scope ?? "";
|
|
138
|
+
const productRequiresFullPopulation = productScope === "full_population_operation" || product?.full_population_required === true;
|
|
139
|
+
const productIsCapabilityOnly = productScope === "system_capability_build" ||
|
|
140
|
+
productScope === "representative_sample_validation" ||
|
|
141
|
+
product?.full_population_required === false;
|
|
142
|
+
for (const [planId, item] of Object.entries(state.graph?.plan_items ?? {})) {
|
|
143
|
+
if (productRequiresFullPopulation && item.delivery_scope !== "full_population_operation" && item.delivery_scope !== "out_of_scope_backlog") {
|
|
144
|
+
conflicts.push(`scope_conflict_requires_decision: Product / Architecture Source requires full_population_operation but ${planId} delivery_scope=${item.delivery_scope || "missing"}`);
|
|
145
|
+
}
|
|
146
|
+
if (productIsCapabilityOnly && item.delivery_scope === "full_population_operation" && productScope !== "mixed_scope_requires_boundary") {
|
|
147
|
+
conflicts.push(`scope_conflict_requires_decision: Product / Architecture Source delivery_scope=${productScope || "missing"} but ${planId} delivery_scope=full_population_operation`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
for (const [acId, ac] of Object.entries(state.graph?.acceptance_criteria ?? {})) {
|
|
151
|
+
if (productRequiresFullPopulation && (ac.acceptance_scope === "full_population_not_required" || ac.full_population_required === false)) {
|
|
152
|
+
conflicts.push(`scope_conflict_requires_decision: Product / Architecture Source requires full_population_operation but ${acId} full_population_required=false`);
|
|
153
|
+
}
|
|
154
|
+
if (productIsCapabilityOnly && (ac.acceptance_scope === "full_population_operation" || ac.full_population_required === true) && productScope !== "mixed_scope_requires_boundary") {
|
|
155
|
+
conflicts.push(`scope_conflict_requires_decision: Product / Architecture Source delivery_scope=${productScope || "missing"} but ${acId} acceptance_scope=full_population_operation`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return [...new Set(conflicts)];
|
|
159
|
+
}
|
|
160
|
+
function compileProgress(state) {
|
|
161
|
+
const planEntries = Object.entries(state.graph.plan_items);
|
|
162
|
+
const sampleNames = unique([
|
|
163
|
+
...state.delivery.product_architecture_scope.representative_samples_validate,
|
|
164
|
+
...planEntries.flatMap(([, item]) => item.representative_samples)
|
|
165
|
+
]);
|
|
166
|
+
const systemPlanIds = planEntries.filter(([, item]) => item.delivery_scope === "system_capability_build").map(([planId]) => planId);
|
|
167
|
+
const representativePlanIds = planEntries.filter(([, item]) => item.delivery_scope === "representative_sample_validation").map(([planId]) => planId);
|
|
168
|
+
const fullPopulationRequired = state.delivery.product_architecture_scope.delivery_scope === "full_population_operation" ||
|
|
169
|
+
state.delivery.product_architecture_scope.full_population_required === true ||
|
|
170
|
+
Object.values(state.graph.acceptance_criteria).some((ac) => ac.acceptance_scope === "full_population_operation" || ac.full_population_required === true);
|
|
171
|
+
return {
|
|
172
|
+
system_capability_progress: {
|
|
173
|
+
status: systemPlanIds.length > 0 ? "not_started" : "not_in_scope",
|
|
174
|
+
plan_items: systemPlanIds
|
|
175
|
+
},
|
|
176
|
+
representative_sample_progress: {
|
|
177
|
+
status: sampleNames.length > 0 || representativePlanIds.length > 0 ? "not_started" : "not_in_scope",
|
|
178
|
+
plan_items: representativePlanIds,
|
|
179
|
+
samples: sampleNames
|
|
180
|
+
},
|
|
181
|
+
real_object_coverage: {
|
|
182
|
+
status: sampleNames.length > 0 ? "sampled_only" : "unknown",
|
|
183
|
+
covered_objects: sampleNames
|
|
184
|
+
},
|
|
185
|
+
full_population_operation_progress: {
|
|
186
|
+
status: fullPopulationRequired ? "not_started" : "not_in_scope"
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function blockAfter(content, start, end) {
|
|
191
|
+
return content.slice(start, end ?? content.length);
|
|
192
|
+
}
|
|
193
|
+
function field(block, name) {
|
|
194
|
+
const text = fieldText(block, name);
|
|
195
|
+
return text ? asStringArray(text) : [];
|
|
196
|
+
}
|
|
197
|
+
function fieldText(block, name) {
|
|
198
|
+
const pattern = new RegExp(`${name}\\s*:\\s*([^\\n]+)`, "i");
|
|
199
|
+
const match = pattern.exec(block);
|
|
200
|
+
return match ? cleanText(match[1]) : "";
|
|
201
|
+
}
|
|
202
|
+
function fieldBoolean(block, name) {
|
|
203
|
+
const value = fieldText(block, name).toLowerCase();
|
|
204
|
+
if (value === "true") {
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
if (value === "false") {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
function normalizeLayer(value) {
|
|
213
|
+
return value.trim().toLowerCase().replace(/[- ]+/g, "_");
|
|
214
|
+
}
|
|
215
|
+
function firstLine(content) {
|
|
216
|
+
return cleanText(content.split(/\r?\n/).find((line) => cleanText(line)) ?? "");
|
|
217
|
+
}
|
|
218
|
+
function cleanText(value) {
|
|
219
|
+
return value.replace(/^[-#*\s]+/, "").trim();
|
|
220
|
+
}
|
|
221
|
+
function unique(values) {
|
|
222
|
+
return [...new Set(values.filter(Boolean))];
|
|
223
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type SuperpowersTaskState } from "./superpowers-task-state-schema.js";
|
|
2
|
+
export declare function validateDeliveryContract(state: SuperpowersTaskState, errors: string[]): void;
|
|
3
|
+
export declare function validateScopeConflicts(state: SuperpowersTaskState, errors: string[]): void;
|
|
4
|
+
export declare function fullPopulationRequired(state: SuperpowersTaskState): boolean;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { computeScopeConflicts } from "./superpowers-task-compile.js";
|
|
2
|
+
import { isRecord } from "./superpowers-task-state-schema.js";
|
|
3
|
+
const PRODUCT_DELIVERY_SCOPES = new Set([
|
|
4
|
+
"system_capability_build",
|
|
5
|
+
"representative_sample_validation",
|
|
6
|
+
"full_population_operation",
|
|
7
|
+
"mixed_scope_requires_boundary"
|
|
8
|
+
]);
|
|
9
|
+
const PLAN_DELIVERY_SCOPES = new Set([
|
|
10
|
+
"system_capability_build",
|
|
11
|
+
"representative_sample_validation",
|
|
12
|
+
"full_population_operation",
|
|
13
|
+
"out_of_scope_backlog"
|
|
14
|
+
]);
|
|
15
|
+
const ACCEPTANCE_SCOPES = new Set([
|
|
16
|
+
"system_capability_build",
|
|
17
|
+
"representative_sample_validation",
|
|
18
|
+
"full_population_operation",
|
|
19
|
+
"full_population_not_required"
|
|
20
|
+
]);
|
|
21
|
+
export function validateDeliveryContract(state, errors) {
|
|
22
|
+
const product = state.delivery?.product_architecture_scope;
|
|
23
|
+
if (!isRecord(product)) {
|
|
24
|
+
errors.push("Product / Architecture Source delivery scope is missing");
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
requireEnum(errors, "Product / Architecture Source delivery_scope", product.delivery_scope, PRODUCT_DELIVERY_SCOPES);
|
|
28
|
+
requireBoolean(errors, "Product / Architecture Source full_population_required", product.full_population_required);
|
|
29
|
+
requireArray(errors, "Product / Architecture Source representative_samples_validate", product.representative_samples_validate);
|
|
30
|
+
requireArray(errors, "Product / Architecture Source representative_samples_do_not_validate", product.representative_samples_do_not_validate);
|
|
31
|
+
requireArray(errors, "Product / Architecture Source out_of_scope_backlog", product.out_of_scope_backlog);
|
|
32
|
+
}
|
|
33
|
+
for (const [planId, item] of Object.entries(state.graph?.plan_items ?? {})) {
|
|
34
|
+
requireEnum(errors, `${planId} delivery_scope`, item.delivery_scope, PLAN_DELIVERY_SCOPES);
|
|
35
|
+
requireText(errors, `${planId} capability_target`, item.capability_target);
|
|
36
|
+
requireArray(errors, `${planId} representative_samples`, item.representative_samples);
|
|
37
|
+
requireText(errors, `${planId} full_population_boundary`, item.full_population_boundary);
|
|
38
|
+
requireArray(errors, `${planId} non_required_population`, item.non_required_population);
|
|
39
|
+
}
|
|
40
|
+
for (const [acId, ac] of Object.entries(state.graph?.acceptance_criteria ?? {})) {
|
|
41
|
+
requireEnum(errors, `${acId} acceptance_scope`, ac.acceptance_scope, ACCEPTANCE_SCOPES);
|
|
42
|
+
requireArray(errors, `${acId} ac_validates`, ac.ac_validates);
|
|
43
|
+
requireArray(errors, `${acId} ac_does_not_validate`, ac.ac_does_not_validate);
|
|
44
|
+
requireText(errors, `${acId} sample_boundary`, ac.sample_boundary);
|
|
45
|
+
requireBoolean(errors, `${acId} full_population_required`, ac.full_population_required);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function validateScopeConflicts(state, errors) {
|
|
49
|
+
const conflicts = [...new Set([...(state.delivery?.scope_conflicts ?? []), ...computeScopeConflicts(state)])].filter(Boolean);
|
|
50
|
+
for (const conflict of conflicts) {
|
|
51
|
+
if (/scope_conflict_requires_decision/i.test(conflict)) {
|
|
52
|
+
errors.push(conflict);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function fullPopulationRequired(state) {
|
|
57
|
+
return (state.delivery?.product_architecture_scope?.delivery_scope === "full_population_operation" ||
|
|
58
|
+
state.delivery?.product_architecture_scope?.full_population_required === true ||
|
|
59
|
+
Object.values(state.graph?.acceptance_criteria ?? {}).some((ac) => ac.acceptance_scope === "full_population_operation" || ac.full_population_required === true));
|
|
60
|
+
}
|
|
61
|
+
function requireEnum(errors, label, value, allowed) {
|
|
62
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
63
|
+
errors.push(`${label} is missing`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (!allowed.has(value)) {
|
|
67
|
+
errors.push(`${label} has unknown value: ${value}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function requireText(errors, label, value) {
|
|
71
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
72
|
+
errors.push(`${label} is missing`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function requireBoolean(errors, label, value) {
|
|
76
|
+
if (typeof value !== "boolean") {
|
|
77
|
+
errors.push(`${label} must be true or false`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function requireArray(errors, label, value) {
|
|
81
|
+
if (!Array.isArray(value)) {
|
|
82
|
+
errors.push(`${label} is missing`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type SuperpowersTaskState } from "./superpowers-task-state-schema.js";
|
|
2
|
+
export interface DerivedSuperpowersArtifacts {
|
|
3
|
+
matrix: Record<string, unknown>;
|
|
4
|
+
verdict: Record<string, unknown>;
|
|
5
|
+
files: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare function deriveSuperpowersArtifacts(workdir: string): Promise<DerivedSuperpowersArtifacts>;
|
|
8
|
+
export declare function deriveObjects(state: SuperpowersTaskState): {
|
|
9
|
+
matrix: Record<string, unknown>;
|
|
10
|
+
verdict: Record<string, unknown>;
|
|
11
|
+
progress: Record<string, unknown>;
|
|
12
|
+
};
|
|
13
|
+
export declare function derivedMatchesState(workdir: string, state: SuperpowersTaskState): Promise<string[]>;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { ensureDir, pathExists, readText, writeTextIfChanged } from "./fs.js";
|
|
3
|
+
import { stableJson, loadSuperpowersState } from "./superpowers-task-state.js";
|
|
4
|
+
export async function deriveSuperpowersArtifacts(workdir) {
|
|
5
|
+
const state = await loadSuperpowersState(workdir);
|
|
6
|
+
const derived = deriveObjects(state);
|
|
7
|
+
const derivedDir = path.join(workdir, "derived");
|
|
8
|
+
await ensureDir(derivedDir);
|
|
9
|
+
const files = [];
|
|
10
|
+
await writeDerived(files, path.join(derivedDir, "plan-conformance-matrix.json"), stableJson(derived.matrix));
|
|
11
|
+
await writeDerived(files, path.join(derivedDir, "final-acceptance-verdict.json"), stableJson(derived.verdict));
|
|
12
|
+
await writeDerived(files, path.join(derivedDir, "progress-ledger.json"), stableJson(derived.progress));
|
|
13
|
+
await writeDerived(files, path.join(derivedDir, "plan-conformance-matrix.md"), matrixMarkdown(derived.matrix));
|
|
14
|
+
await writeDerived(files, path.join(derivedDir, "final-acceptance-verdict.md"), verdictMarkdown(derived.verdict));
|
|
15
|
+
await writeDerived(files, path.join(derivedDir, "local-audit.md"), localAuditMarkdown(state));
|
|
16
|
+
await writeDerived(files, path.join(derivedDir, "progress-ledger.md"), progressMarkdown(derived.progress));
|
|
17
|
+
await writeDerived(files, path.join(derivedDir, "evidence-index.md"), evidenceMarkdown(state));
|
|
18
|
+
await writeDerived(files, path.join(derivedDir, "context-alignment.md"), contextMarkdown(state));
|
|
19
|
+
await writeDerived(files, path.join(derivedDir, "final-summary.md"), finalSummaryMarkdown(state, derived.verdict));
|
|
20
|
+
return { matrix: derived.matrix, verdict: derived.verdict, files };
|
|
21
|
+
}
|
|
22
|
+
export function deriveObjects(state) {
|
|
23
|
+
const matrixRows = Object.entries(state.graph.plan_items).map(([planItemId, item]) => {
|
|
24
|
+
const relatedAcs = item.related_acs;
|
|
25
|
+
const requiredLayers = item.required_proof_layers;
|
|
26
|
+
const missingLayers = requiredLayers.filter((layerId) => state.graph.proof_layers[layerId]?.status !== "satisfied");
|
|
27
|
+
const evidenceIds = evidenceForLayers(state, requiredLayers);
|
|
28
|
+
return {
|
|
29
|
+
plan_item_id: planItemId,
|
|
30
|
+
plan_requirement: item.requirement,
|
|
31
|
+
acceptance_ids: relatedAcs,
|
|
32
|
+
status: missingLayers.length === 0 && requiredLayers.length > 0 ? "complete" : evidenceIds.length > 0 ? "partial" : item.status,
|
|
33
|
+
delivery_scope: item.delivery_scope,
|
|
34
|
+
capability_target: item.capability_target,
|
|
35
|
+
representative_samples: item.representative_samples,
|
|
36
|
+
full_population_boundary: item.full_population_boundary,
|
|
37
|
+
non_required_population: item.non_required_population,
|
|
38
|
+
conformance_type: item.owner_surfaces.length > 0 ? "product_surface" : "implementation",
|
|
39
|
+
owner_surface: item.owner_surfaces[0] ?? "",
|
|
40
|
+
forbidden_primary_surfaces: item.forbidden_surfaces,
|
|
41
|
+
negative_surface_checks: item.forbidden_surfaces.map((surface) => `confirmed ${surface} is not the owner surface`),
|
|
42
|
+
default_visibility_required: item.owner_surfaces.length > 0,
|
|
43
|
+
real_page_evidence: evidenceText(state, evidenceIds, "browser"),
|
|
44
|
+
context_fact_refs: [],
|
|
45
|
+
expected_surfaces: item.owner_surfaces.length > 0 ? ["ui", "runtime"] : ["code"],
|
|
46
|
+
implemented_paths: item.implementation_paths,
|
|
47
|
+
missing_paths: [],
|
|
48
|
+
tests: item.required_tests,
|
|
49
|
+
runtime_evidence: evidenceText(state, evidenceIds, "runtime"),
|
|
50
|
+
artifact_evidence: evidenceText(state, evidenceIds, "artifact"),
|
|
51
|
+
required_proof_layers: requiredLayers,
|
|
52
|
+
satisfied_proof_layers: requiredLayers.filter((layerId) => state.graph.proof_layers[layerId]?.status === "satisfied"),
|
|
53
|
+
missing_required_layers: missingLayers,
|
|
54
|
+
evidence_ids: evidenceIds,
|
|
55
|
+
scope_assessment: missingLayers.length === 0 ? "full" : "partial",
|
|
56
|
+
drift: missingLayers.length === 0 ? "no drift detected" : "missing required proof layers"
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
const verdictRows = Object.entries(state.graph.acceptance_criteria).map(([acId, ac]) => {
|
|
60
|
+
const requiredLayers = ac.required_proof_layers.map((layer) => `${acId}.${layer}`);
|
|
61
|
+
const missingLayers = requiredLayers.filter((layerId) => state.graph.proof_layers[layerId]?.status !== "satisfied");
|
|
62
|
+
const evidenceIds = evidenceForLayers(state, requiredLayers);
|
|
63
|
+
const status = missingLayers.length === 0 && requiredLayers.length > 0 ? "complete" : evidenceIds.length > 0 ? "partial" : ac.status;
|
|
64
|
+
return {
|
|
65
|
+
ac_id: acId,
|
|
66
|
+
related_plan_item_ids: ac.related_plan_items,
|
|
67
|
+
status,
|
|
68
|
+
acceptance_scope: ac.acceptance_scope,
|
|
69
|
+
ac_validates: ac.ac_validates,
|
|
70
|
+
ac_does_not_validate: ac.ac_does_not_validate,
|
|
71
|
+
sample_boundary: ac.sample_boundary,
|
|
72
|
+
full_population_required: ac.full_population_required,
|
|
73
|
+
full_population_status: ac.full_population_required === true || ac.acceptance_scope === "full_population_operation" ? status : "not_in_scope",
|
|
74
|
+
required_evidence: requiredLayers,
|
|
75
|
+
required_proof_chain: requiredLayers,
|
|
76
|
+
fresh_evidence: evidenceText(state, evidenceIds),
|
|
77
|
+
missing_evidence: [],
|
|
78
|
+
missing_required_layers: missingLayers,
|
|
79
|
+
contradictions: [],
|
|
80
|
+
context_fact_refs: [],
|
|
81
|
+
evidence_ids: evidenceIds,
|
|
82
|
+
drift_severity: "none",
|
|
83
|
+
sibling_substitution_used: false,
|
|
84
|
+
auditor_status: auditorStatus(state),
|
|
85
|
+
decision: status === "complete" ? "accept" : "continue"
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
const allComplete = verdictRows.length > 0 && verdictRows.every((row) => row.status === "complete");
|
|
89
|
+
const progress = {
|
|
90
|
+
...state.progress,
|
|
91
|
+
complete_count: verdictRows.filter((row) => row.status === "complete").length,
|
|
92
|
+
partial_count: verdictRows.filter((row) => row.status === "partial").length,
|
|
93
|
+
acceptance_required_count: verdictRows.filter((row) => row.status !== "out_of_scope_NA").length,
|
|
94
|
+
missing_layer_count: verdictRows.reduce((sum, row) => sum + row.missing_required_layers.length, 0),
|
|
95
|
+
next_clusters: Object.entries(state.graph.proof_layers)
|
|
96
|
+
.filter(([, layer]) => layer.status !== "satisfied")
|
|
97
|
+
.slice(0, 5)
|
|
98
|
+
.map(([layerId]) => layerId)
|
|
99
|
+
};
|
|
100
|
+
return {
|
|
101
|
+
matrix: { overall_status: allComplete ? "complete" : "partial", items: matrixRows },
|
|
102
|
+
verdict: { overall_status: allComplete ? "complete" : "partial", acceptance_items: verdictRows },
|
|
103
|
+
progress
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export async function derivedMatchesState(workdir, state) {
|
|
107
|
+
const errors = [];
|
|
108
|
+
const expected = deriveObjects(state);
|
|
109
|
+
await assertDerivedJson(workdir, "plan-conformance-matrix", expected.matrix, errors);
|
|
110
|
+
await assertDerivedJson(workdir, "final-acceptance-verdict", expected.verdict, errors);
|
|
111
|
+
return errors;
|
|
112
|
+
}
|
|
113
|
+
async function assertDerivedJson(workdir, basename, expected, errors) {
|
|
114
|
+
const file = path.join(workdir, "derived", `${basename}.json`);
|
|
115
|
+
if (!(await pathExists(file))) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const actual = JSON.parse(await readText(file));
|
|
119
|
+
if (stableJson(actual) !== stableJson(expected)) {
|
|
120
|
+
errors.push(`derived/${basename}.json does not match task-state.json; rerun ty-context superpowers derive`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function evidenceForLayers(state, layerIds) {
|
|
124
|
+
return [...new Set(layerIds.flatMap((layerId) => state.graph.proof_layers[layerId]?.evidence_ids ?? []))];
|
|
125
|
+
}
|
|
126
|
+
function evidenceText(state, evidenceIds, type) {
|
|
127
|
+
return evidenceIds
|
|
128
|
+
.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)))
|
|
130
|
+
.map((item) => {
|
|
131
|
+
const artifacts = item?.artifact_paths.join(", ");
|
|
132
|
+
return `${item?.type} ${item?.command ?? ""} ${artifacts}`.trim();
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
function auditorStatus(state) {
|
|
136
|
+
const auditor = state.gates.auditor;
|
|
137
|
+
if (auditor && typeof auditor === "object" && !Array.isArray(auditor) && typeof auditor.auditor_status === "string") {
|
|
138
|
+
return String(auditor.auditor_status);
|
|
139
|
+
}
|
|
140
|
+
return "not_run";
|
|
141
|
+
}
|
|
142
|
+
async function writeDerived(files, file, content) {
|
|
143
|
+
await writeTextIfChanged(file, `${content.trimEnd()}\n`);
|
|
144
|
+
files.push(file.split(path.sep).join("/"));
|
|
145
|
+
}
|
|
146
|
+
function matrixMarkdown(matrix) {
|
|
147
|
+
const rows = matrix.items ?? [];
|
|
148
|
+
return ["# Plan Conformance Matrix", "", ...rows.map((row) => `- ${row.plan_item_id}: ${row.status}`)].join("\n");
|
|
149
|
+
}
|
|
150
|
+
function verdictMarkdown(verdict) {
|
|
151
|
+
const rows = verdict.acceptance_items ?? [];
|
|
152
|
+
const complete = rows.filter((row) => row.status === "complete").length;
|
|
153
|
+
const partial = rows.filter((row) => row.status === "partial").length;
|
|
154
|
+
const required = rows.filter((row) => row.status !== "out_of_scope_NA").length;
|
|
155
|
+
const missing = rows.reduce((sum, row) => sum + ((row.missing_required_layers ?? []).length), 0);
|
|
156
|
+
return `# Final Acceptance Verdict
|
|
157
|
+
|
|
158
|
+
<!-- generated:active-counts:start -->
|
|
159
|
+
complete_count: ${complete}
|
|
160
|
+
partial_count: ${partial}
|
|
161
|
+
acceptance_required_count: ${required}
|
|
162
|
+
missing_layer_count: ${missing}
|
|
163
|
+
<!-- generated:active-counts:end -->
|
|
164
|
+
|
|
165
|
+
${rows.map((row) => `- ${row.ac_id}: ${row.status}`).join("\n")}
|
|
166
|
+
`;
|
|
167
|
+
}
|
|
168
|
+
function localAuditMarkdown(state) {
|
|
169
|
+
return `# Local Audit
|
|
170
|
+
|
|
171
|
+
audit_task_complete: ${state.final.audit_task_complete}
|
|
172
|
+
acceptance_target_status: ${state.final.acceptance_target_status}
|
|
173
|
+
product_goal_complete: ${state.final.product_goal_complete}
|
|
174
|
+
`;
|
|
175
|
+
}
|
|
176
|
+
function progressMarkdown(progress) {
|
|
177
|
+
return `# Progress Ledger
|
|
178
|
+
|
|
179
|
+
\`\`\`json
|
|
180
|
+
${stableJson(progress)}
|
|
181
|
+
\`\`\`
|
|
182
|
+
`;
|
|
183
|
+
}
|
|
184
|
+
function evidenceMarkdown(state) {
|
|
185
|
+
return [
|
|
186
|
+
"# Evidence Index",
|
|
187
|
+
"",
|
|
188
|
+
...state.evidence.map((item) => `- ${item.evidence_id}: proves ${item.proves.join(", ")}; does_not_prove ${item.does_not_prove.join(", ")}`)
|
|
189
|
+
].join("\n");
|
|
190
|
+
}
|
|
191
|
+
function contextMarkdown(state) {
|
|
192
|
+
return `# Context Alignment
|
|
193
|
+
|
|
194
|
+
Product Context Delta: ${state.context.product_context_delta}
|
|
195
|
+
Technical Context Delta: ${state.context.technical_context_delta}
|
|
196
|
+
`;
|
|
197
|
+
}
|
|
198
|
+
function finalSummaryMarkdown(state, verdict) {
|
|
199
|
+
return `# Final Summary
|
|
200
|
+
|
|
201
|
+
overall_status: ${verdict.overall_status}
|
|
202
|
+
product_goal_complete: ${state.final.product_goal_complete}
|
|
203
|
+
`;
|
|
204
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function appendSuperpowersEvent(workdir: string, eventType: string, payload?: Record<string, unknown>): Promise<void>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { ensureDir, readText, writeTextIfChanged, pathExists } from "./fs.js";
|
|
3
|
+
export async function appendSuperpowersEvent(workdir, eventType, payload = {}) {
|
|
4
|
+
const eventsPath = path.join(workdir, "events.ndjson");
|
|
5
|
+
await ensureDir(workdir);
|
|
6
|
+
const event = {
|
|
7
|
+
event_type: eventType,
|
|
8
|
+
created_at: new Date().toISOString(),
|
|
9
|
+
...payload
|
|
10
|
+
};
|
|
11
|
+
const previous = (await pathExists(eventsPath)) ? await readText(eventsPath) : "";
|
|
12
|
+
await writeTextIfChanged(eventsPath, `${previous}${JSON.stringify(event)}\n`);
|
|
13
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare function runSliceGate(workdir: string, sliceId: string): Promise<{
|
|
2
|
+
passed: boolean;
|
|
3
|
+
messages: string[];
|
|
4
|
+
}>;
|
|
5
|
+
export declare function runEpochGate(workdir: string, epochId: string): Promise<{
|
|
6
|
+
passed: boolean;
|
|
7
|
+
messages: string[];
|
|
8
|
+
}>;
|
|
9
|
+
export declare function runFinalGate(workdir: string): Promise<{
|
|
10
|
+
product_goal_complete: boolean;
|
|
11
|
+
errors: string[];
|
|
12
|
+
}>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { appendSuperpowersEvent } from "./superpowers-task-events.js";
|
|
2
|
+
import { deriveSuperpowersArtifacts } from "./superpowers-task-derive.js";
|
|
3
|
+
import { loadSuperpowersState, recomputeStatuses, saveSuperpowersState } from "./superpowers-task-state.js";
|
|
4
|
+
import { allCompletionConditionsSatisfied, validateSuperpowersState } from "./superpowers-task-validator.js";
|
|
5
|
+
export async function runSliceGate(workdir, sliceId) {
|
|
6
|
+
const state = await loadSuperpowersState(workdir);
|
|
7
|
+
const slice = state.slices.find((item) => item.slice_id === sliceId);
|
|
8
|
+
const messages = [];
|
|
9
|
+
if (!slice) {
|
|
10
|
+
messages.push(`slice not found: ${sliceId}`);
|
|
11
|
+
}
|
|
12
|
+
else if (!slice.progress_value?.type || slice.progress_value.closed_items.length === 0) {
|
|
13
|
+
messages.push(`slice ${sliceId} has no progress_value`);
|
|
14
|
+
}
|
|
15
|
+
await deriveSuperpowersArtifacts(workdir);
|
|
16
|
+
await appendSuperpowersEvent(workdir, "slice_gate", { slice_id: sliceId, passed: messages.length === 0 });
|
|
17
|
+
return { passed: messages.length === 0, messages };
|
|
18
|
+
}
|
|
19
|
+
export async function runEpochGate(workdir, epochId) {
|
|
20
|
+
await deriveSuperpowersArtifacts(workdir);
|
|
21
|
+
await appendSuperpowersEvent(workdir, "epoch_gate", { epoch_id: epochId, passed: true });
|
|
22
|
+
return { passed: true, messages: ["epoch derived artifacts refreshed"] };
|
|
23
|
+
}
|
|
24
|
+
export async function runFinalGate(workdir) {
|
|
25
|
+
const state = await loadSuperpowersState(workdir);
|
|
26
|
+
recomputeStatuses(state);
|
|
27
|
+
state.final.audit_task_complete = true;
|
|
28
|
+
state.meta.audit_task_complete = true;
|
|
29
|
+
state.gates.validator = { status: "not_run" };
|
|
30
|
+
await saveSuperpowersState(workdir, state);
|
|
31
|
+
await deriveSuperpowersArtifacts(workdir);
|
|
32
|
+
const report = await validateSuperpowersState(workdir, [workdir]);
|
|
33
|
+
const latest = await loadSuperpowersState(workdir);
|
|
34
|
+
const complete = report.errors.length === 0 && allCompletionConditionsSatisfied(latest);
|
|
35
|
+
latest.final.product_goal_complete = complete;
|
|
36
|
+
latest.meta.product_goal_complete = complete;
|
|
37
|
+
latest.final.acceptance_target_status = complete ? "complete" : "partial";
|
|
38
|
+
latest.meta.acceptance_target_status = latest.final.acceptance_target_status;
|
|
39
|
+
latest.final.audit_task_complete = true;
|
|
40
|
+
latest.meta.audit_task_complete = true;
|
|
41
|
+
latest.final.completion_basis = complete ? ["all_required_acs_complete", "validator_passed", "auditor_no_blocker"] : [];
|
|
42
|
+
latest.gates.validator = { status: report.errors.length === 0 ? "pass" : "blocked", errors: report.errors };
|
|
43
|
+
await saveSuperpowersState(workdir, latest);
|
|
44
|
+
await deriveSuperpowersArtifacts(workdir);
|
|
45
|
+
await appendSuperpowersEvent(workdir, "final_gate", { product_goal_complete: complete });
|
|
46
|
+
return { product_goal_complete: complete, errors: report.errors };
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function nextSuperpowersSlices(workdir: string, limit?: number): Promise<string[]>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { loadSuperpowersState } from "./superpowers-task-state.js";
|
|
2
|
+
export async function nextSuperpowersSlices(workdir, limit = 5) {
|
|
3
|
+
const state = await loadSuperpowersState(workdir);
|
|
4
|
+
return Object.entries(state.graph.proof_layers)
|
|
5
|
+
.filter(([, layer]) => layer.required && layer.status !== "satisfied")
|
|
6
|
+
.slice(0, limit)
|
|
7
|
+
.map(([layerId], index) => {
|
|
8
|
+
const acId = layerId.split(".")[0];
|
|
9
|
+
const planId = Object.entries(state.graph.plan_items).find(([, item]) => item.related_acs.includes(acId))?.[0] ?? "PI";
|
|
10
|
+
return `${index + 1}. ${planId}/${acId}: close ${layerId}`;
|
|
11
|
+
});
|
|
12
|
+
}
|