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.
@@ -0,0 +1,252 @@
1
+ import path from "node:path";
2
+ import { pathExists, readText } from "./fs.js";
3
+ import { findSensitiveEvidence } from "./plan-acceptance-evidence.js";
4
+ import { primitiveText, repoRelative, resolveInputDir } from "./plan-validator-common.js";
5
+ import { derivedMatchesState } from "./superpowers-task-derive.js";
6
+ import { fullPopulationRequired, validateDeliveryContract, validateScopeConflicts } from "./superpowers-task-delivery.js";
7
+ import { loadSuperpowersState, sha256 } from "./superpowers-task-state.js";
8
+ import { isRecord } from "./superpowers-task-state-schema.js";
9
+ export async function validateSuperpowersState(projectRoot, args = []) {
10
+ const info = [];
11
+ const warnings = [];
12
+ const hygiene = [];
13
+ const errors = [];
14
+ const targetDir = await resolveInputDir(projectRoot, args[0], "tmp/ty-context/plan-acceptance");
15
+ const statePath = path.join(targetDir, "task-state.json");
16
+ if (!(await pathExists(statePath))) {
17
+ return { info, warnings, hygiene, errors: [`superpowers task state is missing: ${repoRelative(projectRoot, statePath)}`] };
18
+ }
19
+ let state;
20
+ try {
21
+ state = await loadSuperpowersState(targetDir);
22
+ }
23
+ catch (error) {
24
+ return {
25
+ info,
26
+ warnings,
27
+ hygiene,
28
+ errors: [`${repoRelative(projectRoot, statePath)} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`]
29
+ };
30
+ }
31
+ validateShape(state, errors);
32
+ if (!hasUsableShape(state)) {
33
+ info.push("checked superpowers task state with unusable or incomplete shape");
34
+ return { info, warnings, hygiene, errors };
35
+ }
36
+ await validateSourceHashes(targetDir, state, errors);
37
+ validateDeliveryContract(state, errors);
38
+ validateGraphReferences(state, errors);
39
+ validateScopeConflicts(state, errors);
40
+ validateEvidenceRecords(state, errors);
41
+ validateProofLayers(state, errors);
42
+ validateAuditor(state, errors);
43
+ validateFinalCompletion(state, errors);
44
+ errors.push(...(await derivedMatchesState(targetDir, state)));
45
+ info.push(`checked superpowers task state ${repoRelative(projectRoot, targetDir)} plan_items=${Object.keys(state.graph?.plan_items ?? {}).length} acs=${Object.keys(state.graph?.acceptance_criteria ?? {}).length} evidence=${state.evidence?.length ?? 0}`);
46
+ if (errors.length === 0) {
47
+ info.push("Superpowers task state validation passed");
48
+ }
49
+ return { info, warnings, hygiene, errors };
50
+ }
51
+ async function validateSourceHashes(workdir, state, errors) {
52
+ for (const [key, source] of Object.entries(state.sources ?? {})) {
53
+ const file = path.join(workdir, source.path);
54
+ if (!(await pathExists(file))) {
55
+ errors.push(`source file is missing for ${key}: ${source.path}`);
56
+ continue;
57
+ }
58
+ const actual = sha256(await readText(file));
59
+ if (actual !== source.sha256) {
60
+ errors.push(`source hash mismatch for ${key}: expected ${source.sha256}, actual ${actual}; recompile graph before continuing`);
61
+ }
62
+ }
63
+ }
64
+ function validateShape(state, errors) {
65
+ if (state.meta?.schema_version !== "superpowers-task-state-v1") {
66
+ errors.push("task-state.json schema_version must be superpowers-task-state-v1");
67
+ }
68
+ for (const key of ["meta", "sources", "context", "delivery", "graph", "slices", "evidence", "gates", "progress", "blockers", "final"]) {
69
+ if (!(key in state)) {
70
+ errors.push(`task-state.json is missing section: ${key}`);
71
+ }
72
+ }
73
+ }
74
+ function hasUsableShape(state) {
75
+ const candidate = state;
76
+ return (isRecord(candidate.meta) &&
77
+ isRecord(candidate.sources) &&
78
+ isRecord(candidate.context) &&
79
+ isRecord(candidate.delivery) &&
80
+ isRecord(candidate.graph) &&
81
+ isRecord(candidate.graph.plan_items) &&
82
+ isRecord(candidate.graph.acceptance_criteria) &&
83
+ isRecord(candidate.graph.proof_layers) &&
84
+ Array.isArray(candidate.slices) &&
85
+ Array.isArray(candidate.evidence) &&
86
+ isRecord(candidate.gates) &&
87
+ isRecord(candidate.progress) &&
88
+ Array.isArray(candidate.blockers) &&
89
+ isRecord(candidate.final));
90
+ }
91
+ function validateGraphReferences(state, errors) {
92
+ const planIds = new Set(Object.keys(state.graph?.plan_items ?? {}));
93
+ const acIds = new Set(Object.keys(state.graph?.acceptance_criteria ?? {}));
94
+ for (const [planId, item] of Object.entries(state.graph?.plan_items ?? {})) {
95
+ for (const acId of item.related_acs ?? []) {
96
+ if (!acIds.has(acId)) {
97
+ errors.push(`plan item ${planId} references unknown AC: ${acId}`);
98
+ }
99
+ }
100
+ for (const layerId of item.required_proof_layers ?? []) {
101
+ if (!state.graph.proof_layers[layerId]) {
102
+ errors.push(`plan item ${planId} references unknown proof layer: ${layerId}`);
103
+ }
104
+ }
105
+ }
106
+ for (const [acId, ac] of Object.entries(state.graph?.acceptance_criteria ?? {})) {
107
+ for (const planId of ac.related_plan_items ?? []) {
108
+ if (!planIds.has(planId)) {
109
+ errors.push(`AC ${acId} references unknown plan item: ${planId}`);
110
+ }
111
+ }
112
+ for (const layer of ac.required_proof_layers ?? []) {
113
+ const layerId = `${acId}.${layer}`;
114
+ if (!state.graph.proof_layers[layerId]) {
115
+ errors.push(`AC ${acId} references unknown proof layer: ${layerId}`);
116
+ }
117
+ }
118
+ }
119
+ }
120
+ function validateEvidenceRecords(state, errors) {
121
+ const ids = new Set();
122
+ for (const [index, evidence] of (state.evidence ?? []).entries()) {
123
+ const label = `evidence ${evidence.evidence_id || index + 1}`;
124
+ if (!evidence.evidence_id) {
125
+ errors.push(`${label} is missing evidence_id`);
126
+ }
127
+ else if (ids.has(evidence.evidence_id)) {
128
+ errors.push(`${label} duplicates evidence_id`);
129
+ }
130
+ ids.add(evidence.evidence_id);
131
+ if (!evidence.slice_id) {
132
+ errors.push(`${label} is missing slice_id`);
133
+ }
134
+ if ((evidence.proves ?? []).length === 0) {
135
+ errors.push(`${label} is missing proves`);
136
+ }
137
+ if ((evidence.does_not_prove ?? []).length === 0) {
138
+ errors.push(`${label} is missing does_not_prove`);
139
+ }
140
+ if (!evidence.freshness?.created_at || !evidence.freshness.valid_for) {
141
+ errors.push(`${label} is missing freshness`);
142
+ }
143
+ if (evidence.freshness?.stale_after && Date.parse(evidence.freshness.stale_after) < Date.now()) {
144
+ errors.push(`${label} is stale evidence: ${evidence.freshness.stale_after}`);
145
+ }
146
+ if (evidence.redaction?.checked !== true) {
147
+ errors.push(`${label} redaction.checked must be true`);
148
+ }
149
+ if (evidence.redaction?.contains_secret === true) {
150
+ errors.push(`${label} redaction contains_secret is true`);
151
+ }
152
+ if (evidence.reviewability?.external_reviewer_can_reproduce !== true || !evidence.reviewability.reproduction_steps) {
153
+ errors.push(`${label} is not reviewable by an external reviewer`);
154
+ }
155
+ const sensitive = findSensitiveEvidence(primitiveText(evidence));
156
+ if (sensitive) {
157
+ errors.push(`${label} contains raw secret/token/cookie material: ${sensitive}`);
158
+ }
159
+ if (evidence.sibling_substitution_used === true && !evidence.sibling_substitution_approval_source) {
160
+ errors.push(`${label} uses sibling substitution without approval`);
161
+ }
162
+ for (const proofLayer of evidence.proves ?? []) {
163
+ if (proofLayer.endsWith(".runtime") && /\b(mock|unit|viewmodel)\b/i.test(evidence.type)) {
164
+ errors.push(`${label} runtime proof cannot be mock/unit/viewmodel only`);
165
+ }
166
+ if (proofLayer.endsWith(".ui_browser") && !/\b(browser|ui_browser|screenshot)\b/i.test(evidence.type)) {
167
+ errors.push(`${label} UI proof must use browser owner surface evidence`);
168
+ }
169
+ }
170
+ }
171
+ }
172
+ function validateProofLayers(state, errors) {
173
+ const evidenceById = new Map((state.evidence ?? []).map((item) => [item.evidence_id, item]));
174
+ for (const [layerId, layer] of Object.entries(state.graph?.proof_layers ?? {})) {
175
+ if (layer.status === "satisfied" && layer.evidence_ids.length === 0) {
176
+ errors.push(`proof layer ${layerId} is satisfied but has no evidence_ids`);
177
+ }
178
+ for (const evidenceId of layer.evidence_ids ?? []) {
179
+ const evidence = evidenceById.get(evidenceId);
180
+ if (!evidence) {
181
+ errors.push(`proof layer ${layerId} references unknown evidence_id: ${evidenceId}`);
182
+ continue;
183
+ }
184
+ if (!evidence.proves.includes(layerId)) {
185
+ errors.push(`proof layer ${layerId} references ${evidenceId} but that evidence does not prove it`);
186
+ }
187
+ }
188
+ }
189
+ }
190
+ function validateAuditor(state, errors) {
191
+ const auditor = state.gates?.auditor;
192
+ if (!isRecord(auditor)) {
193
+ return;
194
+ }
195
+ const status = String(auditor.auditor_status ?? "").toLowerCase();
196
+ const findings = Array.isArray(auditor.findings) ? auditor.findings : [];
197
+ if (status === "blocking_gap" || findings.some((finding) => isRecord(finding) && String(finding.severity ?? "").toLowerCase() === "blocking")) {
198
+ errors.push(`auditor blocker remains: ${findings.map((finding) => (isRecord(finding) ? finding.id : "")).filter(Boolean).join(", ") || status}`);
199
+ }
200
+ }
201
+ function validateFinalCompletion(state, errors) {
202
+ const finalComplete = state.final?.product_goal_complete === true || state.meta?.product_goal_complete === true;
203
+ if (!finalComplete) {
204
+ return;
205
+ }
206
+ const planEntries = Object.entries(state.graph.plan_items);
207
+ const acEntries = Object.entries(state.graph.acceptance_criteria);
208
+ const layerEntries = Object.entries(state.graph.proof_layers);
209
+ if (planEntries.length === 0 || acEntries.length === 0 || layerEntries.length === 0) {
210
+ errors.push("product_goal_complete=true but task graph is empty or uncompiled");
211
+ }
212
+ const incompleteAcs = acEntries.filter(([, ac]) => ac.status !== "complete" && ac.status !== "out_of_scope_NA");
213
+ const incompletePlans = planEntries.filter(([, item]) => item.status !== "complete" && item.status !== "out_of_scope_NA");
214
+ const incompleteLayers = layerEntries.filter(([, layer]) => layer.required && layer.status !== "satisfied");
215
+ if (incompleteAcs.length > 0 || incompletePlans.length > 0 || incompleteLayers.length > 0) {
216
+ errors.push("product_goal_complete=true but required plan items, ACs or proof layers are incomplete");
217
+ }
218
+ if (state.context.product_context_delta === "required" || state.context.technical_context_delta === "required") {
219
+ const unresolvedCoverage = (state.context.source_to_context_coverage ?? []).filter((row) => /\b(new_context_required|needs_user_decision|under_scoped)\b/i.test(primitiveText(row)));
220
+ if (unresolvedCoverage.length > 0) {
221
+ errors.push("product_goal_complete=true but Context Delta coverage is unresolved");
222
+ }
223
+ }
224
+ if (fullPopulationRequired(state)) {
225
+ const sampleOnlyEvidence = state.evidence.filter((evidence) => evidence.does_not_prove.some((claim) => /\b(full[-_ ]?population|all[-_ ]?provider|all[-_ ]?interface|all[-_ ]?platform)\b/i.test(claim)));
226
+ if (sampleOnlyEvidence.length > 0) {
227
+ errors.push(`product_goal_complete=true but full-population completion relies on evidence that explicitly does not prove full population coverage: ${sampleOnlyEvidence
228
+ .map((evidence) => evidence.evidence_id)
229
+ .join(", ")}`);
230
+ }
231
+ }
232
+ }
233
+ export function allCompletionConditionsSatisfied(state) {
234
+ const errors = [];
235
+ validateShape(state, errors);
236
+ if (!hasUsableShape(state)) {
237
+ return false;
238
+ }
239
+ validateDeliveryContract(state, errors);
240
+ validateScopeConflicts(state, errors);
241
+ validateGraphReferences(state, errors);
242
+ validateEvidenceRecords(state, errors);
243
+ validateProofLayers(state, errors);
244
+ validateAuditor(state, errors);
245
+ const planItems = Object.values(state.graph.plan_items);
246
+ const acceptanceCriteria = Object.values(state.graph.acceptance_criteria);
247
+ const proofLayers = Object.values(state.graph.proof_layers);
248
+ const allPlansComplete = planItems.every((item) => item.status === "complete" || item.status === "out_of_scope_NA");
249
+ const allAcsComplete = acceptanceCriteria.every((ac) => ac.status === "complete" || ac.status === "out_of_scope_NA");
250
+ const allLayersSatisfied = proofLayers.every((layer) => !layer.required || layer.status === "satisfied");
251
+ return errors.length === 0 && planItems.length > 0 && acceptanceCriteria.length > 0 && proofLayers.length > 0 && allPlansComplete && allAcsComplete && allLayersSatisfied;
252
+ }
@@ -5,13 +5,15 @@ import { listFiles, pathExists, readText } from "./fs.js";
5
5
  import { runModularityCheck } from "./modularity.js";
6
6
  import { validatePlanAcceptance } from "./plan-acceptance-validator.js";
7
7
  import { validatePlanContract } from "./plan-contract-validator.js";
8
+ import { validateSuperpowersState } from "./superpowers-task-validator.js";
8
9
  import { unsupportedSchemaMessage } from "./schema-guard.js";
9
10
  const VALIDATORS = {
10
11
  "validate-context": validateContext,
11
12
  "validate-code-modularity": validateCodeModularity,
12
13
  "validate-harness": validateHarness,
13
14
  "validate-plan-contract": validatePlanContract,
14
- "validate-plan-acceptance": validatePlanAcceptance
15
+ "validate-plan-acceptance": validatePlanAcceptance,
16
+ "validate-superpowers-state": validateSuperpowersState
15
17
  };
16
18
  const GLOBAL_REQUIRED_SECTIONS = [
17
19
  ...sectionSpecs([
@@ -77,7 +79,7 @@ export async function runValidator(projectRoot, gate, args = []) {
77
79
  return {
78
80
  info: [],
79
81
  errors: [
80
- `unknown validator: ${gate}. Minimal Context Harness supports validate-context, validate-code-modularity, validate-harness, validate-plan-contract and validate-plan-acceptance only.`
82
+ `unknown validator: ${gate}. Minimal Context Harness supports validate-context, validate-code-modularity, validate-harness, validate-plan-contract, validate-plan-acceptance and validate-superpowers-state only.`
81
83
  ]
82
84
  };
83
85
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-tiny-context-harness",
3
- "version": "0.2.75",
3
+ "version": "0.2.77",
4
4
  "description": "Minimal project memory and validation harness for AI coding agents.",
5
5
  "license": "MIT",
6
6
  "author": "Seven128",