specpi 0.10.0 → 0.11.2
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/CHANGELOG.md +169 -150
- package/LICENSE +21 -21
- package/NPM_RELEASE.md +110 -110
- package/README.md +223 -155
- package/SECURITY.md +86 -85
- package/SECURITY_MODEL.md +123 -107
- package/THIRD_PARTY.md +61 -61
- package/browser-runtime/package-lock.json +86 -86
- package/browser-runtime/package.json +15 -15
- package/extensions/command-guard/powershell-parser.ps1 +47 -47
- package/extensions/spec.ts +236 -10
- package/extensions/tool-wishlist/capabilities.json +114 -114
- package/extensions/tool-wishlist/core.mjs +285 -24
- package/extensions/tool-wishlist/index.ts +939 -90
- package/extensions/tool-wishlist/verification.mjs +810 -0
- package/extensions/workflow-controls/challenge.mjs +139 -13
- package/extensions/workflow-controls/index.ts +811 -35
- package/extensions/workflow-controls/scope.mjs +41 -5
- package/extensions/workflow-controls/smoke.mjs +55 -0
- package/extensions/workflow-controls/task-contract.mjs +439 -0
- package/package.json +98 -98
- package/scripts/check-package.mjs +3 -0
- package/scripts/check-pi-package.mjs +103 -4
- package/scripts/pi-test-harness.mjs +309 -0
- package/scripts/specpi.mjs +31 -2
- package/skills/specpi-improve/SKILL.md +60 -54
- package/specpi +0 -0
- package/templates/AGENTS.md +25 -23
- package/templates/settings.json +10 -10
- package/themes/specpi-spec.json +96 -96
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { markdownPathLabel } from "./task-contract.mjs";
|
|
2
|
+
|
|
1
3
|
const MAX_REQUIREMENTS = 16;
|
|
2
4
|
const MAX_LIST = 12;
|
|
5
|
+
const MAX_REQUIREMENT_ID = 120;
|
|
3
6
|
|
|
4
7
|
function compact(value, maximum = 360) {
|
|
5
8
|
return String(value ?? "")
|
|
@@ -18,11 +21,78 @@ function textList(value, name) {
|
|
|
18
21
|
return value.map((item) => compact(item)).filter(Boolean);
|
|
19
22
|
}
|
|
20
23
|
|
|
24
|
+
function boundedRequirementId(value) {
|
|
25
|
+
return compact(value, MAX_REQUIREMENT_ID);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function boundedPath(value) {
|
|
29
|
+
return typeof value === "string" ? value.slice(0, 240) : "";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function boundedTaskContract(value) {
|
|
33
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const requirements = Array.isArray(value.requirements)
|
|
38
|
+
? value.requirements
|
|
39
|
+
.map((item) => ({
|
|
40
|
+
id: boundedRequirementId(item?.id),
|
|
41
|
+
description: compact(item?.description, 360),
|
|
42
|
+
acceptance: compact(item?.acceptance, 600),
|
|
43
|
+
}))
|
|
44
|
+
.filter((item) => item.id && item.description && item.acceptance)
|
|
45
|
+
.slice(0, MAX_REQUIREMENTS)
|
|
46
|
+
: [];
|
|
47
|
+
if (requirements.length === 0) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
id: compact(value.id, 120),
|
|
53
|
+
digest: compact(value.digest, 64),
|
|
54
|
+
origin: compact(value.origin, 24),
|
|
55
|
+
objective: compact(value.objective, 600),
|
|
56
|
+
hypothesis: compact(value.hypothesis, 600),
|
|
57
|
+
requirements,
|
|
58
|
+
paths: Array.isArray(value.paths)
|
|
59
|
+
? value.paths
|
|
60
|
+
.map((item) => compact(item, 240))
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
.slice(0, 40)
|
|
63
|
+
: [],
|
|
64
|
+
rollback: compact(value.rollback, 600),
|
|
65
|
+
nonGoals: Array.isArray(value.nonGoals)
|
|
66
|
+
? value.nonGoals
|
|
67
|
+
.map((item) => compact(item, 360))
|
|
68
|
+
.filter(Boolean)
|
|
69
|
+
.slice(0, 16)
|
|
70
|
+
: [],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function contractRequirements(facts) {
|
|
75
|
+
const contract = boundedTaskContract(facts?.taskContract);
|
|
76
|
+
if (!contract) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return contract.requirements;
|
|
81
|
+
}
|
|
82
|
+
|
|
21
83
|
export function validateChallengeSubmission(value, facts = {}) {
|
|
22
84
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
23
85
|
throw new Error("Completion challenge submission is malformed");
|
|
24
86
|
}
|
|
25
87
|
|
|
88
|
+
if (facts.challengeGeneration && value.generation !== facts.challengeGeneration) {
|
|
89
|
+
throw new Error("Completion challenge generation is stale");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (facts.taskContractDigest && value.taskContractDigest !== facts.taskContractDigest) {
|
|
93
|
+
throw new Error("Task contract digest is stale");
|
|
94
|
+
}
|
|
95
|
+
|
|
26
96
|
const verdicts = new Set(["ready-for-human-review", "incomplete", "blocked"]);
|
|
27
97
|
if (!verdicts.has(value.verdict)) {
|
|
28
98
|
throw new Error("Completion challenge verdict is invalid");
|
|
@@ -36,19 +106,55 @@ export function validateChallengeSubmission(value, facts = {}) {
|
|
|
36
106
|
throw new Error(`Completion challenge requires 1-${MAX_REQUIREMENTS} requirement assessments`);
|
|
37
107
|
}
|
|
38
108
|
|
|
109
|
+
const expectedRequirements = contractRequirements(facts);
|
|
39
110
|
const requirements = value.requirements.map((item) => {
|
|
40
|
-
const
|
|
111
|
+
const id = boundedRequirementId(item?.id);
|
|
112
|
+
const expected = expectedRequirements?.find((candidate) => candidate.id === id);
|
|
113
|
+
if (expectedRequirements) {
|
|
114
|
+
if (!id) {
|
|
115
|
+
throw new Error("Task contract requirement assessment must include its original ID");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!expected) {
|
|
119
|
+
throw new Error(`Unknown task contract requirement ID: ${id}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const requirement = compact(item?.requirement) || expected?.description || "";
|
|
41
124
|
const evidence = compact(item?.evidence, 600);
|
|
42
125
|
if (!requirement || !["proven", "partial", "unproven"].includes(item?.status)) {
|
|
43
126
|
throw new Error("Completion challenge requirement assessment is invalid");
|
|
44
127
|
}
|
|
45
128
|
|
|
129
|
+
if (expected && requirement !== expected.description) {
|
|
130
|
+
throw new Error(`Task contract requirement ${id} text was altered`);
|
|
131
|
+
}
|
|
132
|
+
|
|
46
133
|
if (item.status === "proven" && !evidence) {
|
|
47
134
|
throw new Error("A proven requirement must cite concise evidence");
|
|
48
135
|
}
|
|
49
136
|
|
|
50
|
-
return {
|
|
137
|
+
return {
|
|
138
|
+
...(expectedRequirements ? { id, acceptance: expected.acceptance } : {}),
|
|
139
|
+
requirement,
|
|
140
|
+
status: item.status,
|
|
141
|
+
evidence,
|
|
142
|
+
};
|
|
51
143
|
});
|
|
144
|
+
if (expectedRequirements) {
|
|
145
|
+
const submittedIds = requirements.map((item) => item.id);
|
|
146
|
+
if (new Set(submittedIds).size !== submittedIds.length) {
|
|
147
|
+
throw new Error("Task contract requirement IDs must be unique");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const expectedIds = expectedRequirements.map((item) => item.id);
|
|
151
|
+
if (submittedIds.length !== expectedIds.length || expectedIds.some((id) => !submittedIds.includes(id))) {
|
|
152
|
+
throw new Error("Task contract requirement assessments must cover the exact original IDs");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
requirements.sort((left, right) => expectedIds.indexOf(left.id) - expectedIds.indexOf(right.id));
|
|
156
|
+
}
|
|
157
|
+
|
|
52
158
|
const contradictions = textList(value.contradictions, "contradictions");
|
|
53
159
|
const falsePositiveChecks = textList(value.falsePositiveChecks, "falsePositiveChecks");
|
|
54
160
|
const scopeFindings = textList(value.scopeFindings, "scopeFindings");
|
|
@@ -78,6 +184,10 @@ export function validateChallengeSubmission(value, facts = {}) {
|
|
|
78
184
|
throw new Error("Ready verdict rejected: scope drift remains pending");
|
|
79
185
|
}
|
|
80
186
|
|
|
187
|
+
if (facts.scopeTaskStale === true) {
|
|
188
|
+
throw new Error("Ready verdict rejected: task-bound scope is stale");
|
|
189
|
+
}
|
|
190
|
+
|
|
81
191
|
// An indeterminate snapshot means the facts the review was handed may be incomplete. It is a permanent
|
|
82
192
|
// condition outside a Git worktree, so rejecting outright would make a ready verdict unreachable there;
|
|
83
193
|
// instead the limitation must be disclosed rather than silently dropped.
|
|
@@ -101,13 +211,11 @@ export function validateChallengeSubmission(value, facts = {}) {
|
|
|
101
211
|
}
|
|
102
212
|
|
|
103
213
|
export function boundedChallengeFacts(value = {}) {
|
|
104
|
-
const paths = (items) =>
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
.slice(0, 40)
|
|
110
|
-
: [];
|
|
214
|
+
const paths = (items) => (Array.isArray(items) ? items.map(boundedPath).filter(Boolean).slice(0, 40) : []);
|
|
215
|
+
|
|
216
|
+
const taskContract = boundedTaskContract(value.taskContract);
|
|
217
|
+
const taskContractDigest = value.taskContractDigest ?? taskContract?.digest;
|
|
218
|
+
const challengeGeneration = value.challengeGeneration;
|
|
111
219
|
|
|
112
220
|
return {
|
|
113
221
|
changedPaths: paths(value.changedPaths),
|
|
@@ -123,6 +231,10 @@ export function boundedChallengeFacts(value = {}) {
|
|
|
123
231
|
: undefined,
|
|
124
232
|
observedToolFailures: Math.max(0, Math.min(99, Number(value.observedToolFailures) || 0)),
|
|
125
233
|
snapshotIndeterminate: Boolean(value.snapshotIndeterminate),
|
|
234
|
+
taskContract,
|
|
235
|
+
taskContractDigest: taskContractDigest ? compact(taskContractDigest, 64) : undefined,
|
|
236
|
+
challengeGeneration: challengeGeneration ? compact(challengeGeneration, 64) : undefined,
|
|
237
|
+
scopeTaskStale: Boolean(value.scopeTaskStale),
|
|
126
238
|
};
|
|
127
239
|
}
|
|
128
240
|
|
|
@@ -140,12 +252,25 @@ export function challengePrompt(generation, facts) {
|
|
|
140
252
|
"5. Was runtime, visual, or platform validation required but omitted?",
|
|
141
253
|
"6. What residual risk must be disclosed?",
|
|
142
254
|
"",
|
|
143
|
-
`Changed paths: ${bounded.changedPaths.join(", ") || "none observed"}`,
|
|
144
|
-
`Declared scope: ${bounded.scopeEntries.join(", ") || "inactive"}`,
|
|
145
|
-
`Pending scope drift: ${bounded.pendingScope.join(", ") || "none"}`,
|
|
255
|
+
`Changed paths: ${bounded.changedPaths.map(markdownPathLabel).join(", ") || "none observed"}`,
|
|
256
|
+
`Declared scope: ${bounded.scopeEntries.map(markdownPathLabel).join(", ") || "inactive"}`,
|
|
257
|
+
`Pending scope drift: ${bounded.pendingScope.map(markdownPathLabel).join(", ") || "none"}`,
|
|
146
258
|
`Observed tool failures: ${bounded.observedToolFailures}`,
|
|
147
259
|
`Snapshot indeterminate: ${bounded.snapshotIndeterminate ? "yes" : "no"}`,
|
|
260
|
+
`Task-bound scope: ${bounded.scopeTaskStale ? "stale" : "not stale or not bound"}`,
|
|
148
261
|
];
|
|
262
|
+
if (bounded.taskContract) {
|
|
263
|
+
lines.push(
|
|
264
|
+
`Task contract: ${bounded.taskContract.id} (digest ${bounded.taskContract.digest})`,
|
|
265
|
+
"Include this exact task contract digest as taskContractDigest in the submission.",
|
|
266
|
+
"The task contract requirements are fixed. Use every original requirement ID exactly once and do not rewrite requirement text.",
|
|
267
|
+
"Fixed requirements:",
|
|
268
|
+
...bounded.taskContract.requirements.map(
|
|
269
|
+
(item) => `- ${item.id}: ${item.description} (acceptance: ${item.acceptance})`,
|
|
270
|
+
),
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
149
274
|
if (bounded.experiment) {
|
|
150
275
|
lines.push(
|
|
151
276
|
`Experiment: ${bounded.experiment.name} (${bounded.experiment.id.slice(0, 8)})`,
|
|
@@ -171,7 +296,8 @@ export function renderChallengeMarkdown(result, metadata = {}) {
|
|
|
171
296
|
const lines = [`## Completion Challenge — ${heading}`, "", `Generation: \`${metadata.generation ?? "unknown"}\``];
|
|
172
297
|
lines.push("", "### Requirements");
|
|
173
298
|
for (const item of result.requirements) {
|
|
174
|
-
|
|
299
|
+
const id = item.id ? ` ${item.id}` : "";
|
|
300
|
+
lines.push(`- **${item.status}**${id} — ${item.requirement}${item.evidence ? ` — ${item.evidence}` : ""}`);
|
|
175
301
|
}
|
|
176
302
|
|
|
177
303
|
const sections = [
|