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.
- package/README.md +48 -42
- package/assets/README.md +48 -42
- package/assets/README.zh-CN.md +56 -52
- package/assets/skills/superpowers-long-task/SKILL.md +586 -471
- package/dist/commands/index.js +16 -9
- package/dist/commands/superpowers.d.ts +1 -0
- package/dist/commands/superpowers.js +89 -0
- package/dist/commands/validate.js +6 -0
- package/dist/lib/plan-acceptance-artifacts.d.ts +1 -0
- package/dist/lib/plan-acceptance-artifacts.js +220 -0
- package/dist/lib/plan-acceptance-evidence.d.ts +1 -0
- package/dist/lib/plan-acceptance-evidence.js +40 -0
- package/dist/lib/plan-acceptance-json.js +4 -0
- package/dist/lib/plan-acceptance-validator.js +23 -4
- package/dist/lib/superpowers-task-compile.d.ts +1 -0
- package/dist/lib/superpowers-task-compile.js +114 -0
- package/dist/lib/superpowers-task-derive.d.ts +13 -0
- package/dist/lib/superpowers-task-derive.js +192 -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 +167 -0
- package/dist/lib/superpowers-task-state-schema.js +43 -0
- package/dist/lib/superpowers-task-state.d.ts +15 -0
- package/dist/lib/superpowers-task-state.js +223 -0
- package/dist/lib/superpowers-task-validator.d.ts +4 -0
- package/dist/lib/superpowers-task-validator.js +238 -0
- package/dist/lib/validators.d.ts +2 -0
- package/dist/lib/validators.js +6 -2
- package/package.json +69 -69
package/dist/commands/index.js
CHANGED
|
@@ -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,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[]>;
|