project-tiny-context-harness 0.2.74 → 0.2.76

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +48 -42
  2. package/assets/README.md +48 -42
  3. package/assets/README.zh-CN.md +56 -52
  4. package/assets/skills/superpowers-long-task/SKILL.md +586 -471
  5. package/dist/commands/index.js +16 -9
  6. package/dist/commands/superpowers.d.ts +1 -0
  7. package/dist/commands/superpowers.js +89 -0
  8. package/dist/commands/validate.js +6 -0
  9. package/dist/lib/plan-acceptance-artifacts.d.ts +1 -0
  10. package/dist/lib/plan-acceptance-artifacts.js +220 -0
  11. package/dist/lib/plan-acceptance-evidence.d.ts +1 -0
  12. package/dist/lib/plan-acceptance-evidence.js +40 -0
  13. package/dist/lib/plan-acceptance-json.js +4 -0
  14. package/dist/lib/plan-acceptance-validator.js +23 -4
  15. package/dist/lib/superpowers-task-compile.d.ts +1 -0
  16. package/dist/lib/superpowers-task-compile.js +114 -0
  17. package/dist/lib/superpowers-task-derive.d.ts +13 -0
  18. package/dist/lib/superpowers-task-derive.js +192 -0
  19. package/dist/lib/superpowers-task-events.d.ts +1 -0
  20. package/dist/lib/superpowers-task-events.js +13 -0
  21. package/dist/lib/superpowers-task-gates.d.ts +12 -0
  22. package/dist/lib/superpowers-task-gates.js +47 -0
  23. package/dist/lib/superpowers-task-next-slices.d.ts +1 -0
  24. package/dist/lib/superpowers-task-next-slices.js +12 -0
  25. package/dist/lib/superpowers-task-state-schema.d.ts +167 -0
  26. package/dist/lib/superpowers-task-state-schema.js +43 -0
  27. package/dist/lib/superpowers-task-state.d.ts +15 -0
  28. package/dist/lib/superpowers-task-state.js +223 -0
  29. package/dist/lib/superpowers-task-validator.d.ts +4 -0
  30. package/dist/lib/superpowers-task-validator.js +238 -0
  31. package/dist/lib/validators.d.ts +2 -0
  32. package/dist/lib/validators.js +6 -2
  33. package/package.json +69 -69
@@ -3,6 +3,7 @@ import { doctor } from "./doctor.js";
3
3
  import { exportContext } from "./export-context.js";
4
4
  import { init } from "./init.js";
5
5
  import { packageSource } from "./package-source.js";
6
+ import { superpowers } from "./superpowers.js";
6
7
  import { sync } from "./sync.js";
7
8
  import { upgrade } from "./upgrade.js";
8
9
  import { validate } from "./validate.js";
@@ -20,6 +21,8 @@ export const commands = {
20
21
  "validate-harness": (args) => validate(["validate-harness", ...args]),
21
22
  "validate-plan-contract": (args) => validate(["validate-plan-contract", ...args]),
22
23
  "validate-plan-acceptance": (args) => validate(["validate-plan-acceptance", ...args]),
24
+ "validate-superpowers-state": (args) => validate(["validate-superpowers-state", ...args]),
25
+ superpowers,
23
26
  package: packageSource
24
27
  };
25
28
  export function help() {
@@ -32,16 +35,20 @@ export function help() {
32
35
  doctor Diagnose project configuration and drift
33
36
  check-modularity --touched|--file <path>|--base <ref> [--limit 300] [--fail-on-warning]
34
37
  Warn when selected handwritten source files exceed a line-count limit
35
- export-context --full|--code|--all|--source-pack|--code-index|--task-context
36
- Export temporary Context, code snapshot or bounded Source Pack artifacts
38
+ export-context --full|--code|--all|--source-pack|--code-index|--task-context
39
+ Export temporary Context, code snapshot or bounded Source Pack artifacts
37
40
  validate <gate> Run a Harness validation gate
38
41
  validate-context Validate Minimal Context fact-source recoverability
39
- validate-code-modularity
40
- Enforce touched handwritten source file modularity
41
- validate-harness Run validate-context and validate-code-modularity
42
- validate-plan-contract <plan.md|dir>
43
- Validate workflow-contract plan surface consistency
44
- validate-plan-acceptance <dir>
45
- Validate plan-conformance matrix and final verdict consistency
42
+ validate-code-modularity
43
+ Enforce touched handwritten source file modularity
44
+ validate-harness Run validate-context and validate-code-modularity
45
+ validate-plan-contract <plan.md|dir>
46
+ Validate workflow-contract plan surface consistency
47
+ validate-plan-acceptance <dir>
48
+ Validate plan-conformance matrix and final verdict consistency
49
+ validate-superpowers-state <dir>
50
+ Validate canonical Superpowers task-state.json
51
+ superpowers <subcommand>
52
+ Manage explicit Superpowers long-task state workdirs
46
53
  package <subcommand> Maintain package canonical source`);
47
54
  }
@@ -0,0 +1 @@
1
+ export declare function superpowers(args: string[]): Promise<void>;
@@ -0,0 +1,89 @@
1
+ import path from "node:path";
2
+ import { applySliceDelta, initializeSuperpowersTask } from "../lib/superpowers-task-state.js";
3
+ import { compileSuperpowersTask } from "../lib/superpowers-task-compile.js";
4
+ import { deriveSuperpowersArtifacts } from "../lib/superpowers-task-derive.js";
5
+ import { runEpochGate, runFinalGate, runSliceGate } from "../lib/superpowers-task-gates.js";
6
+ import { nextSuperpowersSlices } from "../lib/superpowers-task-next-slices.js";
7
+ export async function superpowers(args) {
8
+ const subcommand = args[0] ?? "help";
9
+ const workdirArg = args[1];
10
+ if (!workdirArg || subcommand === "help") {
11
+ help();
12
+ return;
13
+ }
14
+ const workdir = path.resolve(process.cwd(), workdirArg);
15
+ if (subcommand === "init") {
16
+ await initializeSuperpowersTask(workdir, { planSlug: path.basename(workdir) });
17
+ console.log(`initialized superpowers task state at ${workdirArg}/task-state.json`);
18
+ return;
19
+ }
20
+ if (subcommand === "compile") {
21
+ const state = await compileSuperpowersTask(workdir);
22
+ console.log(`compiled superpowers task graph plan_items=${Object.keys(state.graph.plan_items).length} acs=${Object.keys(state.graph.acceptance_criteria).length}`);
23
+ return;
24
+ }
25
+ if (subcommand === "apply-slice-delta") {
26
+ const delta = args[2];
27
+ if (!delta) {
28
+ throw new Error("apply-slice-delta requires <slice-delta.json>");
29
+ }
30
+ await applySliceDelta(workdir, path.resolve(process.cwd(), delta));
31
+ const result = await deriveSuperpowersArtifacts(workdir);
32
+ console.log(`applied superpowers slice delta and derived files=${result.files.length}`);
33
+ return;
34
+ }
35
+ if (subcommand === "derive") {
36
+ const result = await deriveSuperpowersArtifacts(workdir);
37
+ console.log(`derived superpowers artifacts files=${result.files.length}`);
38
+ return;
39
+ }
40
+ if (subcommand === "slice-gate") {
41
+ const sliceId = optionValue(args, "--slice") ?? "";
42
+ const result = await runSliceGate(workdir, sliceId);
43
+ console.log(result.passed ? `slice gate passed ${sliceId}` : `slice gate blocked ${result.messages.join("; ")}`);
44
+ if (!result.passed) {
45
+ process.exitCode = 1;
46
+ }
47
+ return;
48
+ }
49
+ if (subcommand === "epoch-gate") {
50
+ const epochId = optionValue(args, "--epoch") ?? "";
51
+ const result = await runEpochGate(workdir, epochId);
52
+ console.log(result.passed ? `epoch gate passed ${epochId}` : `epoch gate blocked ${result.messages.join("; ")}`);
53
+ return;
54
+ }
55
+ if (subcommand === "final-gate") {
56
+ const result = await runFinalGate(workdir);
57
+ console.log(`final gate product_goal_complete=${result.product_goal_complete}`);
58
+ if (!result.product_goal_complete) {
59
+ process.exitCode = 1;
60
+ for (const error of result.errors) {
61
+ console.error(`error: ${error}`);
62
+ }
63
+ }
64
+ return;
65
+ }
66
+ if (subcommand === "next-slices") {
67
+ const limit = Number.parseInt(optionValue(args, "--limit") ?? "5", 10);
68
+ const slices = await nextSuperpowersSlices(workdir, Number.isFinite(limit) ? limit : 5);
69
+ console.log(`Next ${Math.min(Number.isFinite(limit) ? limit : 5, 5)} high-value clusters:`);
70
+ console.log(slices.join("\n"));
71
+ return;
72
+ }
73
+ help();
74
+ }
75
+ function help() {
76
+ console.log(`ty-context superpowers commands:
77
+ init <workdir> Initialize task-state.json and events.ndjson
78
+ compile <workdir> Compile sources into task graph
79
+ apply-slice-delta <workdir> <delta> Apply structured slice delta, evidence and derived views
80
+ derive <workdir> Generate derived/** views
81
+ slice-gate <workdir> --slice <id> Validate one slice has real progress
82
+ epoch-gate <workdir> --epoch <id> Refresh shared epoch evidence views
83
+ final-gate <workdir> Compute product_goal_complete
84
+ next-slices <workdir> --limit 5 Recommend next proof clusters`);
85
+ }
86
+ function optionValue(args, name) {
87
+ const index = args.indexOf(name);
88
+ return index >= 0 ? args[index + 1] : undefined;
89
+ }
@@ -5,6 +5,12 @@ export async function validate(args) {
5
5
  for (const line of report.info) {
6
6
  console.log(line);
7
7
  }
8
+ for (const warning of report.warnings ?? []) {
9
+ console.error(`warning: ${warning}`);
10
+ }
11
+ for (const hygiene of report.hygiene ?? []) {
12
+ console.error(`hygiene: ${hygiene}`);
13
+ }
8
14
  for (const error of report.errors) {
9
15
  console.error(`error: ${error}`);
10
16
  }
@@ -0,0 +1 @@
1
+ export declare function validateAcceptanceArtifactDiagnostics(projectRoot: string, targetDir: string, matrixRows: Record<string, unknown>[], verdictRows: Record<string, unknown>[], verdictOverall: string, errors: string[], warnings: string[], hygiene: string[]): Promise<void>;
@@ -0,0 +1,220 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { readText } from "./fs.js";
4
+ import { findSensitiveEvidence } from "./plan-acceptance-evidence.js";
5
+ import { isOutOfScope, statusOf } from "./plan-acceptance-json.js";
6
+ import { isBlankish, primitiveText, repoRelative, valuesAsArray } from "./plan-validator-common.js";
7
+ const MILESTONE_STATUSES = new Set([
8
+ "not_started",
9
+ "implemented_no_proof",
10
+ "proof_partial",
11
+ "proof_ready",
12
+ "complete",
13
+ "blocked",
14
+ "out_of_scope_NA"
15
+ ]);
16
+ const GENERATED_ACTIVE_COUNTS = /<!--\s*generated:active-counts:start\s*-->([\s\S]*?)<!--\s*generated:active-counts:end\s*-->/i;
17
+ export async function validateAcceptanceArtifactDiagnostics(projectRoot, targetDir, matrixRows, verdictRows, verdictOverall, errors, warnings, hygiene) {
18
+ validateRows(matrixRows, "plan-conformance matrix row", warnings, hygiene);
19
+ validateRows(verdictRows, "final acceptance verdict row", warnings, hygiene);
20
+ const manifestEvidenceIds = await validateEvidenceManifest(projectRoot, targetDir, errors, warnings);
21
+ if (manifestEvidenceIds.size > 0) {
22
+ validateEvidenceIdReferences(matrixRows, "plan-conformance matrix row", manifestEvidenceIds, errors);
23
+ validateEvidenceIdReferences(verdictRows, "final acceptance verdict row", manifestEvidenceIds, errors);
24
+ }
25
+ await validateFinalVerdictMarkdown(projectRoot, targetDir, verdictRows, verdictOverall, errors, warnings, hygiene);
26
+ }
27
+ function validateRows(rows, labelPrefix, warnings, hygiene) {
28
+ for (const [index, row] of rows.entries()) {
29
+ const label = `${labelPrefix} ${index + 1}`;
30
+ validateMilestones(row, label, warnings);
31
+ validateStalePartialText(row, label, hygiene);
32
+ }
33
+ }
34
+ function validateMilestones(row, label, warnings) {
35
+ const milestones = row.milestones ?? row.proof_layer_milestones ?? row.proofLayerMilestones;
36
+ if (isBlankish(milestones)) {
37
+ return;
38
+ }
39
+ for (const status of extractMilestoneStatuses(milestones)) {
40
+ if (!MILESTONE_STATUSES.has(status)) {
41
+ warnings.push(`${label} has unsupported milestone status: ${status || "<empty>"}`);
42
+ }
43
+ }
44
+ }
45
+ function extractMilestoneStatuses(value) {
46
+ if (Array.isArray(value)) {
47
+ return value.flatMap(extractMilestoneStatuses);
48
+ }
49
+ if (!value || typeof value !== "object") {
50
+ return valuesAsArray(value).map((item) => item.trim());
51
+ }
52
+ const object = value;
53
+ if (!isBlankish(object.status)) {
54
+ return [String(object.status).trim()];
55
+ }
56
+ return Object.values(object).flatMap(extractMilestoneStatuses);
57
+ }
58
+ function validateStalePartialText(row, label, hygiene) {
59
+ const status = statusOf(row);
60
+ if (status === "complete" || status === "out_of_scope_NA") {
61
+ return;
62
+ }
63
+ const text = primitiveText(row);
64
+ const hasCompletionClaim = /\b(accepted|complete|final passed|goal achieved|product_goal_complete=true)\b/i.test(text);
65
+ const hasStaleQualifier = /\b(old|stale|previously|prior|outdated|superseded|no longer current)\b/i.test(text);
66
+ if (hasCompletionClaim && hasStaleQualifier) {
67
+ hygiene.push(`${label} has stale or overclaim completion prose while status is ${status || "<empty>"}`);
68
+ }
69
+ }
70
+ async function validateEvidenceManifest(projectRoot, targetDir, errors, warnings) {
71
+ const manifestFile = await findFile(targetDir, "evidence-manifest", ".json");
72
+ const evidenceIds = new Set();
73
+ if (!manifestFile) {
74
+ return evidenceIds;
75
+ }
76
+ let parsed;
77
+ try {
78
+ parsed = JSON.parse(await readText(manifestFile));
79
+ }
80
+ catch (error) {
81
+ errors.push(`${repoRelative(projectRoot, manifestFile)} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
82
+ return evidenceIds;
83
+ }
84
+ const entries = findManifestEntries(parsed);
85
+ if (entries.length === 0) {
86
+ warnings.push(`${repoRelative(projectRoot, manifestFile)} has no evidence entries`);
87
+ return evidenceIds;
88
+ }
89
+ for (const [index, entry] of entries.entries()) {
90
+ const label = `evidence manifest entry ${index + 1}`;
91
+ const evidenceId = firstString(entry.evidence_id, entry.evidenceId, entry.id);
92
+ if (evidenceId) {
93
+ evidenceIds.add(evidenceId);
94
+ }
95
+ else {
96
+ errors.push(`${label} is missing evidence_id`);
97
+ }
98
+ warnIfBlank(entry, label, warnings, "slice_id", "sliceId");
99
+ warnIfBlank(entry, label, warnings, "slice_goal", "sliceGoal");
100
+ warnIfBlank(entry, label, warnings, "touched_plan_item_ids", "touched_plan_ids", "plan_item_ids");
101
+ warnIfBlank(entry, label, warnings, "touched_ac_ids", "acceptance_ids", "ac_ids");
102
+ warnIfBlank(entry, label, warnings, "missing_layer_classes");
103
+ warnIfBlank(entry, label, warnings, "proves");
104
+ warnIfBlank(entry, label, warnings, "explicitly_does_not_prove", "does_not_prove");
105
+ warnIfBlank(entry, label, warnings, "closed_missing_layers", "closed_layers");
106
+ warnIfBlank(entry, label, warnings, "remaining_missing_layers", "remaining_layers");
107
+ warnIfBlank(entry, label, warnings, "cleanup_status");
108
+ warnIfBlank(entry, label, warnings, "redaction_security_status", "security_redaction_status");
109
+ warnIfBlank(entry, label, warnings, "freshness");
110
+ const sensitiveEvidence = findSensitiveEvidence(primitiveText(entry));
111
+ if (sensitiveEvidence) {
112
+ errors.push(`${label} contains raw secret/token/cookie material: ${sensitiveEvidence}`);
113
+ }
114
+ if (entry.safe_to_sync_to_verdict === true && !isBlankish(entry.remaining_missing_layers ?? entry.remaining_layers)) {
115
+ warnings.push(`${label} is safe_to_sync_to_verdict but still has remaining missing layers`);
116
+ }
117
+ }
118
+ return evidenceIds;
119
+ }
120
+ function findManifestEntries(value) {
121
+ if (Array.isArray(value)) {
122
+ return value.filter(isRecord);
123
+ }
124
+ if (!isRecord(value)) {
125
+ return [];
126
+ }
127
+ for (const key of ["evidence", "entries", "items", "manifest"]) {
128
+ const rows = findManifestEntries(value[key]);
129
+ if (rows.length > 0) {
130
+ return rows;
131
+ }
132
+ }
133
+ return [];
134
+ }
135
+ function validateEvidenceIdReferences(rows, labelPrefix, evidenceIds, errors) {
136
+ for (const [index, row] of rows.entries()) {
137
+ for (const evidenceId of valuesAsArray(row.evidence_id ?? row.evidence_ids ?? row.evidenceId ?? row.evidenceIds)) {
138
+ if (!evidenceIds.has(evidenceId)) {
139
+ errors.push(`${labelPrefix} ${index + 1} references unknown evidence_id: ${evidenceId}`);
140
+ }
141
+ }
142
+ }
143
+ }
144
+ async function validateFinalVerdictMarkdown(projectRoot, targetDir, verdictRows, verdictOverall, errors, warnings, hygiene) {
145
+ const markdownFile = await findFile(targetDir, "final-acceptance-verdict", ".md");
146
+ if (!markdownFile) {
147
+ return;
148
+ }
149
+ const content = await readText(markdownFile);
150
+ const match = GENERATED_ACTIVE_COUNTS.exec(content);
151
+ if (!match) {
152
+ if (/\b(active[-_ ]?count|complete_count|acceptance_required_count|missing_layer_count)\b/i.test(content)) {
153
+ hygiene.push(`${repoRelative(projectRoot, markdownFile)} has active-count-like prose outside generated active-count markers`);
154
+ }
155
+ return;
156
+ }
157
+ const declared = parseCountBlock(match[1]);
158
+ const actual = activeCounts(verdictRows);
159
+ for (const [key, expected] of Object.entries(actual)) {
160
+ const declaredValue = declared[key];
161
+ if (declaredValue === undefined) {
162
+ warnings.push(`${repoRelative(projectRoot, markdownFile)} generated active-count block is missing ${key}`);
163
+ continue;
164
+ }
165
+ if (declaredValue !== expected) {
166
+ const message = `${repoRelative(projectRoot, markdownFile)} generated active-count ${key}=${declaredValue} but current verdict has ${expected}`;
167
+ if (verdictOverall === "complete") {
168
+ errors.push(message);
169
+ }
170
+ else {
171
+ warnings.push(message);
172
+ }
173
+ }
174
+ }
175
+ }
176
+ function parseCountBlock(content) {
177
+ const counts = {};
178
+ for (const line of content.split(/\r?\n/)) {
179
+ const match = /^\s*([A-Za-z0-9_-]+)\s*[:=]\s*(\d+)\s*$/.exec(line);
180
+ if (match) {
181
+ counts[match[1].replace(/-/g, "_")] = Number(match[2]);
182
+ }
183
+ }
184
+ return counts;
185
+ }
186
+ function activeCounts(rows) {
187
+ return {
188
+ complete_count: rows.filter((row) => statusOf(row) === "complete").length,
189
+ partial_count: rows.filter((row) => statusOf(row) === "partial").length,
190
+ acceptance_required_count: rows.filter((row) => !isOutOfScope(row)).length,
191
+ missing_layer_count: rows.reduce((sum, row) => sum + missingLayerCount(row), 0)
192
+ };
193
+ }
194
+ function missingLayerCount(row) {
195
+ return valuesAsArray(row.missing_required_layers ?? row.missingRequiredLayers ?? row.missing_proof_layers ?? row.missing_evidence).length;
196
+ }
197
+ async function findFile(targetDir, marker, extension) {
198
+ const entries = await fs.readdir(targetDir, { withFileTypes: true });
199
+ return entries
200
+ .filter((entry) => entry.isFile() && entry.name.endsWith(extension) && entry.name.includes(marker))
201
+ .map((entry) => path.join(targetDir, entry.name))
202
+ .sort()[0];
203
+ }
204
+ function warnIfBlank(entry, label, warnings, ...keys) {
205
+ if (keys.some((key) => !isBlankish(entry[key]))) {
206
+ return;
207
+ }
208
+ warnings.push(`${label} is missing ${keys[0]}`);
209
+ }
210
+ function firstString(...values) {
211
+ for (const value of values) {
212
+ if (!isBlankish(value)) {
213
+ return String(value).trim();
214
+ }
215
+ }
216
+ return undefined;
217
+ }
218
+ function isRecord(value) {
219
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
220
+ }
@@ -1 +1,2 @@
1
1
  export declare function assertExternalReviewerFields(label: string, row: Record<string, unknown>, evidenceText: string, errors: string[]): void;
2
+ export declare function findSensitiveEvidence(text: string): string | undefined;
@@ -1,6 +1,20 @@
1
1
  import { isOutOfScope } from "./plan-acceptance-json.js";
2
2
  import { isBlankish, primitiveText, valuesAsArray } from "./plan-validator-common.js";
3
3
  const BLOCKING_AUDITOR_STATUSES = new Set(["partial", "blocked", "invalidated"]);
4
+ const SENSITIVE_EVIDENCE_PATTERNS = [
5
+ {
6
+ label: "Authorization bearer token",
7
+ pattern: /\bauthorization\s*:\s*bearer\s+(?!<redacted>|redacted|\[redacted\])[A-Za-z0-9._~+/=-]{8,}/i
8
+ },
9
+ {
10
+ label: "cookie value",
11
+ pattern: /\bcookie\s*[:=]\s*(?!<redacted>|redacted|\[redacted\])[^;\s]{8,}/i
12
+ },
13
+ {
14
+ label: "secret assignment",
15
+ pattern: /\b(?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|secret|password)\s*[:=]\s*["']?(?!<redacted>|redacted|\[redacted\])[A-Za-z0-9_./+=-]{8,}/i
16
+ }
17
+ ];
4
18
  export function assertExternalReviewerFields(label, row, evidenceText, errors) {
5
19
  if (hasUnresolvedMissingRequiredLayers(row)) {
6
20
  errors.push(`${label} is complete but missing_required_layers is not empty`);
@@ -12,6 +26,9 @@ export function assertExternalReviewerFields(label, row, evidenceText, errors) {
12
26
  if (siblingSubstitutionUsed(row) && !hasSiblingSubstitutionApproval(row)) {
13
27
  errors.push(`${label} is complete but sibling_substitution_used without approval`);
14
28
  }
29
+ if (liveProofSubstitutionUsed(row) && !hasLiveProofSubstitutionApproval(row)) {
30
+ errors.push(`${label} is complete but live_proof_substitution_used without approval`);
31
+ }
15
32
  const auditorStatus = String(row.auditor_status ?? row.auditorStatus ?? "").trim().toLowerCase();
16
33
  if (BLOCKING_AUDITOR_STATUSES.has(auditorStatus)) {
17
34
  errors.push(`${label} is complete but auditor_status is ${auditorStatus}`);
@@ -19,6 +36,13 @@ export function assertExternalReviewerFields(label, row, evidenceText, errors) {
19
36
  if (hasOnlySelfCertifyingEvidence(evidenceText)) {
20
37
  errors.push(`${label} is complete but fresh_evidence contains only summary or self-certifying evidence`);
21
38
  }
39
+ const sensitiveEvidence = findSensitiveEvidence(evidenceText);
40
+ if (sensitiveEvidence) {
41
+ errors.push(`${label} is complete but evidence contains raw secret/token/cookie material: ${sensitiveEvidence}`);
42
+ }
43
+ }
44
+ export function findSensitiveEvidence(text) {
45
+ return SENSITIVE_EVIDENCE_PATTERNS.find((item) => item.pattern.test(text))?.label;
22
46
  }
23
47
  function hasUnresolvedMissingRequiredLayers(row) {
24
48
  const value = row.missing_required_layers ?? row.missingRequiredLayers ?? row.missing_proof_layers;
@@ -59,6 +83,22 @@ function hasSiblingSubstitutionApproval(row) {
59
83
  !isBlankish(row.substitution_approval_source) ||
60
84
  /\b(approved|explicitly allowed|out[_ -]?of[_ -]?scope[_ -]?NA)\b/i.test(primitiveText([row.sibling_substitution_approved, row.substitution_approval])));
61
85
  }
86
+ function liveProofSubstitutionUsed(row) {
87
+ const value = row.live_proof_substitution_used ?? row.liveProofSubstitutionUsed;
88
+ if (value === true) {
89
+ return true;
90
+ }
91
+ const text = primitiveText(value);
92
+ if (/\b(false|no|none|not used|not_used)\b/i.test(text)) {
93
+ return false;
94
+ }
95
+ return /\b(true|yes|used|present|substituted)\b/i.test(text);
96
+ }
97
+ function hasLiveProofSubstitutionApproval(row) {
98
+ return (!isBlankish(row.live_proof_substitution_approval_source) ||
99
+ !isBlankish(row.liveProofSubstitutionApprovalSource) ||
100
+ /\b(approved|explicitly allowed|out[_ -]?of[_ -]?scope[_ -]?NA)\b/i.test(primitiveText([row.live_proof_substitution_approved, row.liveProofSubstitutionApproved])));
101
+ }
62
102
  function hasOnlySelfCertifyingEvidence(text) {
63
103
  const entries = valuesAsArray(text);
64
104
  return entries.length > 0 && entries.every(isSelfCertifyingEvidence);
@@ -107,10 +107,14 @@ export function assertSurfaceConformance(row, label, errors) {
107
107
  errors.push(`${label} declares forbidden_primary_surfaces but negative_surface_checks is empty`);
108
108
  }
109
109
  const userPathText = primitiveText([row.required_user_paths, row.primary_user_paths]);
110
+ const evidenceText = primitiveText([row.real_page_evidence, row.user_path_evidence, row.fresh_evidence, row.runtime_evidence, row.artifact_evidence]);
110
111
  for (const forbiddenSurface of valuesAsArray(row.forbidden_primary_surfaces)) {
111
112
  if (userPathText.toLowerCase().includes(forbiddenSurface.toLowerCase())) {
112
113
  errors.push(`${label} routes a required/primary user path through forbidden surface: ${forbiddenSurface}`);
113
114
  }
115
+ if (evidenceText.toLowerCase().includes(forbiddenSurface.toLowerCase())) {
116
+ errors.push(`${label} uses wrong owner surface evidence from forbidden surface: ${forbiddenSurface}`);
117
+ }
114
118
  }
115
119
  if (row.default_visibility_required === true && !mentionsDefaultVisibility(primitiveText(row.real_page_evidence))) {
116
120
  errors.push(`${label} requires default visibility but real_page_evidence does not record default-visible proof`);
@@ -1,13 +1,31 @@
1
+ import path from "node:path";
1
2
  import { pathExists } from "./fs.js";
3
+ import { validateAcceptanceArtifactDiagnostics } from "./plan-acceptance-artifacts.js";
2
4
  import { assertExternalReviewerFields } from "./plan-acceptance-evidence.js";
3
5
  import { AC_STATUSES, MATRIX_STATUSES, NON_COMPLETE_AC, NON_COMPLETE_MATRIX, assertStructuredNa, assertSurfaceConformance, contextDeltaRequired, findJsonFile, findRows, hasExplicitNoTestScope, isOutOfScope, isSurfaceConformanceRow, overallStatus, readJson, statusOf } from "./plan-acceptance-json.js";
4
6
  import { assertReferencedPathsExist, hasRealPageEvidence, isBlankish, isUiFacing, primitiveText, repoRelative, resolveInputDir, valuesAsArray, weakProofHit } from "./plan-validator-common.js";
7
+ import { validateSuperpowersState } from "./superpowers-task-validator.js";
5
8
  export async function validatePlanAcceptance(projectRoot, args = []) {
6
9
  const info = [];
10
+ const warnings = [];
11
+ const hygiene = [];
7
12
  const errors = [];
8
13
  const targetDir = await resolveInputDir(projectRoot, args[0], "tmp/ty-context/plan-acceptance");
9
14
  if (!(await pathExists(targetDir))) {
10
- return { info, errors: [`plan acceptance directory is missing: ${repoRelative(projectRoot, targetDir)}`] };
15
+ return { info, warnings, hygiene, errors: [`plan acceptance directory is missing: ${repoRelative(projectRoot, targetDir)}`] };
16
+ }
17
+ if (await pathExists(path.join(targetDir, "task-state.json"))) {
18
+ const stateReport = await validateSuperpowersState(projectRoot, [targetDir]);
19
+ return {
20
+ info: [
21
+ `checked state-backed plan acceptance ${repoRelative(projectRoot, targetDir)}`,
22
+ ...stateReport.info,
23
+ ...(stateReport.errors.length === 0 ? ["Plan acceptance state-backed artifact consistency passed"] : [])
24
+ ],
25
+ warnings: stateReport.warnings,
26
+ hygiene: stateReport.hygiene,
27
+ errors: stateReport.errors
28
+ };
11
29
  }
12
30
  const matrixFile = await findJsonFile(targetDir, "plan-conformance-matrix");
13
31
  const verdictFile = await findJsonFile(targetDir, "final-acceptance-verdict");
@@ -18,12 +36,12 @@ export async function validatePlanAcceptance(projectRoot, args = []) {
18
36
  errors.push(`plan acceptance directory is missing *-final-acceptance-verdict.json`);
19
37
  }
20
38
  if (!matrixFile || !verdictFile) {
21
- return { info, errors };
39
+ return { info, warnings, hygiene, errors };
22
40
  }
23
41
  const matrix = await readJson(matrixFile, errors);
24
42
  const verdict = await readJson(verdictFile, errors);
25
43
  if (matrix === undefined || verdict === undefined) {
26
- return { info, errors };
44
+ return { info, warnings, hygiene, errors };
27
45
  }
28
46
  const matrixRows = findRows(matrix, ["plan_items", "items", "matrix", "entries", "plan_conformance"]);
29
47
  const verdictRows = findRows(verdict, ["acceptance_items", "ac_verdicts", "verdicts", "items", "entries", "acs"]);
@@ -31,11 +49,12 @@ export async function validatePlanAcceptance(projectRoot, args = []) {
31
49
  await validateVerdictRows(projectRoot, verdictRows, overallStatus(verdict), errors);
32
50
  validateCrossReferences(matrixRows, verdictRows, errors);
33
51
  validateContextFactReferences(matrix, verdict, matrixRows, verdictRows, errors);
52
+ await validateAcceptanceArtifactDiagnostics(projectRoot, targetDir, matrixRows, verdictRows, overallStatus(verdict), errors, warnings, hygiene);
34
53
  info.push(`checked plan acceptance ${repoRelative(projectRoot, targetDir)} matrix_rows=${matrixRows.length} verdict_rows=${verdictRows.length}`);
35
54
  if (errors.length === 0) {
36
55
  info.push("Plan acceptance artifact consistency passed");
37
56
  }
38
- return { info, errors };
57
+ return { info, warnings, hygiene, errors };
39
58
  }
40
59
  async function validateMatrixRows(projectRoot, rows, overall, errors) {
41
60
  if (rows.length === 0) {
@@ -0,0 +1 @@
1
+ export declare function compileSuperpowersTask(workdir: string): Promise<import("./superpowers-task-state-schema.js").SuperpowersTaskState>;
@@ -0,0 +1,114 @@
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 technicalPlan = await readText(path.join(workdir, state.sources.technical_realization_plan.path));
11
+ const checklist = await readText(path.join(workdir, state.sources.acceptance_checklist.path));
12
+ const planItems = parsePlanItems(technicalPlan);
13
+ const acceptanceCriteria = parseAcceptanceCriteria(checklist);
14
+ const acIds = Object.keys(acceptanceCriteria);
15
+ for (const [planId, item] of Object.entries(planItems)) {
16
+ if (item.related_acs.length === 0) {
17
+ item.related_acs = acIds;
18
+ }
19
+ item.required_proof_layers = item.related_acs.flatMap((acId) => (acceptanceCriteria[acId]?.required_proof_layers ?? DEFAULT_LAYERS).map((layer) => `${acId}.${layer}`));
20
+ }
21
+ for (const [acId, ac] of Object.entries(acceptanceCriteria)) {
22
+ if (ac.related_plan_items.length === 0) {
23
+ ac.related_plan_items = Object.keys(planItems);
24
+ }
25
+ }
26
+ state.graph.plan_items = planItems;
27
+ state.graph.acceptance_criteria = acceptanceCriteria;
28
+ state.graph.proof_layers = {};
29
+ for (const [acId, ac] of Object.entries(acceptanceCriteria)) {
30
+ for (const layer of ac.required_proof_layers) {
31
+ state.graph.proof_layers[`${acId}.${layer}`] = { required: true, status: "missing", evidence_ids: [] };
32
+ }
33
+ }
34
+ state.graph.edges = Object.entries(planItems).flatMap(([planId, item]) => item.related_acs.map((acId) => ({ from: planId, to: acId, type: "supports" })));
35
+ recomputeStatuses(state);
36
+ await saveSuperpowersState(workdir, state);
37
+ await appendSuperpowersEvent(workdir, "graph_compiled", {
38
+ plan_items: Object.keys(planItems).length,
39
+ acceptance_criteria: Object.keys(acceptanceCriteria).length
40
+ });
41
+ return state;
42
+ }
43
+ function parsePlanItems(content) {
44
+ const items = {};
45
+ const matches = [...content.matchAll(/\b(PI-\d{3,})\b\s*[:.-]?\s*([^\n]*)/gi)];
46
+ for (const [index, match] of matches.entries()) {
47
+ const id = match[1].toUpperCase();
48
+ const block = blockAfter(content, match.index ?? 0, matches[index + 1]?.index);
49
+ items[id] = {
50
+ requirement: cleanText(match[2]) || firstLine(block) || id,
51
+ owner_surfaces: field(block, "owner_surfaces"),
52
+ forbidden_surfaces: field(block, "forbidden_surfaces"),
53
+ implementation_paths: field(block, "implementation_paths"),
54
+ required_tests: field(block, "required_tests"),
55
+ status: "not_started",
56
+ related_acs: field(block, "related_acs").map((item) => item.toUpperCase()),
57
+ required_proof_layers: []
58
+ };
59
+ }
60
+ if (Object.keys(items).length === 0) {
61
+ items["PI-001"] = {
62
+ requirement: firstLine(content) || "Implement technical realization plan",
63
+ owner_surfaces: [],
64
+ forbidden_surfaces: [],
65
+ implementation_paths: [],
66
+ required_tests: [],
67
+ status: "not_started",
68
+ related_acs: [],
69
+ required_proof_layers: []
70
+ };
71
+ }
72
+ return items;
73
+ }
74
+ function parseAcceptanceCriteria(content) {
75
+ const items = {};
76
+ const matches = [...content.matchAll(/\b(AC-\d{3,})\b\s*[:.-]?\s*([^\n]*)/gi)];
77
+ for (const [index, match] of matches.entries()) {
78
+ const id = match[1].toUpperCase();
79
+ const block = blockAfter(content, match.index ?? 0, matches[index + 1]?.index);
80
+ const layers = field(block, "required_proof_layers").map(normalizeLayer).filter(Boolean);
81
+ items[id] = {
82
+ scope: cleanText(match[2]) || firstLine(block) || id,
83
+ related_plan_items: field(block, "related_plan_items").map((item) => item.toUpperCase()),
84
+ required_proof_layers: layers.length > 0 ? layers : DEFAULT_LAYERS,
85
+ status: "not_run"
86
+ };
87
+ }
88
+ if (Object.keys(items).length === 0) {
89
+ items["AC-001"] = {
90
+ scope: firstLine(content) || "Acceptance checklist item",
91
+ related_plan_items: [],
92
+ required_proof_layers: DEFAULT_LAYERS,
93
+ status: "not_run"
94
+ };
95
+ }
96
+ return items;
97
+ }
98
+ function blockAfter(content, start, end) {
99
+ return content.slice(start, end ?? content.length);
100
+ }
101
+ function field(block, name) {
102
+ const pattern = new RegExp(`${name}\\s*:\\s*([^\\n]+)`, "i");
103
+ const match = pattern.exec(block);
104
+ return match ? asStringArray(match[1]) : [];
105
+ }
106
+ function normalizeLayer(value) {
107
+ return value.trim().toLowerCase().replace(/[- ]+/g, "_");
108
+ }
109
+ function firstLine(content) {
110
+ return cleanText(content.split(/\r?\n/).find((line) => cleanText(line)) ?? "");
111
+ }
112
+ function cleanText(value) {
113
+ return value.replace(/^[-#*\s]+/, "").trim();
114
+ }
@@ -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[]>;