@peterxiaoyang/superspec 0.1.11 → 0.1.12
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 +4 -4
- package/dist/src/apply_worker_chain.d.ts +6 -3
- package/dist/src/apply_worker_chain.js +125 -30
- package/dist/src/apply_worker_chain_lifecycle.d.ts +7 -5
- package/dist/src/apply_worker_chain_lifecycle.js +153 -22
- package/dist/src/cli_args.d.ts +1 -0
- package/dist/src/cli_args.js +33 -15
- package/dist/src/disclosure.js +2 -2
- package/dist/src/evidence.js +104 -8
- package/dist/src/gates.d.ts +1 -0
- package/dist/src/gates.js +160 -27
- package/dist/src/hooks/guard_api.js +2 -2
- package/dist/src/hooks/policy_event.js +11 -1
- package/dist/src/i18n.js +63 -3
- package/dist/src/init_cli.js +1 -1
- package/dist/src/openspec.d.ts +2 -0
- package/dist/src/openspec.js +31 -0
- package/dist/src/packet_render.d.ts +1 -0
- package/dist/src/packet_render.js +248 -31
- package/dist/src/packet_schema.d.ts +2 -0
- package/dist/src/tasks.d.ts +5 -0
- package/dist/src/tasks.js +62 -0
- package/dist/src/util.d.ts +4 -0
- package/dist/src/util.js +13 -0
- package/dist/superspec.js +4 -4
- package/package.json +3 -3
- package/templates/sidecar/discovery.md +9 -0
- package/templates/sidecar/test-contract.md +2 -1
- package/templates/workflow/prompts/code-reviewer.md +1 -1
- package/templates/workflow/prompts/executor.md +1 -1
- package/templates/workflow/prompts/test-engineer.md +1 -0
- package/templates/workflow/prompts/test-runner.md +3 -1
- package/templates/workflow/prompts/verifier.md +9 -9
- package/templates/workflow/skills/superspec-apply/SKILL.md +58 -33
- package/templates/workflow/skills/superspec-archive/SKILL.md +5 -5
- package/templates/workflow/skills/superspec-explore/SKILL.md +18 -12
- package/templates/workflow/skills/superspec-propose/SKILL.md +26 -15
- package/templates/workflow/skills/superspec-review/SKILL.md +17 -17
- package/dist/src/packet_measure.d.ts +0 -43
- package/dist/src/packet_measure.js +0 -382
- /package/bin/{superspec-guard.js → superspec-check.js} +0 -0
package/dist/src/cli_args.js
CHANGED
|
@@ -48,6 +48,13 @@ function isPacketCommand(command) {
|
|
|
48
48
|
|| command === "apply-verify-packet"
|
|
49
49
|
|| command === "ledger-render";
|
|
50
50
|
}
|
|
51
|
+
function isHookEventCommand(command) {
|
|
52
|
+
return command === "hook-check-write"
|
|
53
|
+
|| command === "hook-check-command"
|
|
54
|
+
|| command === "hook-record-test"
|
|
55
|
+
|| command === "hook-record-subagent-start"
|
|
56
|
+
|| command === "hook-record-subagent-stop";
|
|
57
|
+
}
|
|
51
58
|
function requiredValueFlags(command) {
|
|
52
59
|
const flags = ["--change"];
|
|
53
60
|
if (command === "check-artifact")
|
|
@@ -67,11 +74,10 @@ function requiredValueFlags(command) {
|
|
|
67
74
|
if (command === "apply-code-review-packet")
|
|
68
75
|
flags.push("--task-id", "--executor-report-ref");
|
|
69
76
|
if (command === "apply-verify-packet")
|
|
70
|
-
flags.push("--task-id", "--executor-report-ref", "--task-code-review-report-ref"
|
|
77
|
+
flags.push("--task-id", "--executor-report-ref", "--task-code-review-report-ref");
|
|
71
78
|
if (command === "ledger-render")
|
|
72
79
|
flags.push("--gate");
|
|
73
|
-
if (command
|
|
74
|
-
|| command === "hook-record-subagent-start" || command === "hook-record-subagent-stop")
|
|
80
|
+
if (isHookEventCommand(command))
|
|
75
81
|
flags.push("--event-ref");
|
|
76
82
|
if (command === "hook-session-begin")
|
|
77
83
|
flags.push("--workflow", "--entrypoint-token");
|
|
@@ -97,7 +103,7 @@ function optionalValueFlags(command) {
|
|
|
97
103
|
if (command === "apply-executor-packet")
|
|
98
104
|
return ["--apply-worker-chain-ref"];
|
|
99
105
|
if (command === "apply-verify-packet")
|
|
100
|
-
return ["--red-test-run-evidence-ref", "--characterization-test-run-evidence-ref"];
|
|
106
|
+
return ["--green-test-run-evidence-ref", "--alternative-verification-evidence-ref", "--red-test-run-evidence-ref", "--characterization-test-run-evidence-ref"];
|
|
101
107
|
if (command === "ledger-render")
|
|
102
108
|
return ["--round"];
|
|
103
109
|
if (command === "hook-session-end")
|
|
@@ -126,7 +132,7 @@ function requiresFormat(command) {
|
|
|
126
132
|
|| command === "apply-verify-packet";
|
|
127
133
|
}
|
|
128
134
|
function rootUsage() {
|
|
129
|
-
return `usage:
|
|
135
|
+
return `usage: superspec [-h]\n {${COMMAND_LIST}}\n ...\n`;
|
|
130
136
|
}
|
|
131
137
|
function rootHelp() {
|
|
132
138
|
const commandLines = COMMANDS.map((command) => {
|
|
@@ -144,7 +150,7 @@ function commandUsage(command) {
|
|
|
144
150
|
...requiredBooleanFlags(command),
|
|
145
151
|
...optionalBooleanFlags(command),
|
|
146
152
|
];
|
|
147
|
-
return `usage:
|
|
153
|
+
return `usage: superspec ${command} ${usageFlags.join(" ")}\n`;
|
|
148
154
|
}
|
|
149
155
|
function commandHelp(command) {
|
|
150
156
|
const zh = command_zh(command);
|
|
@@ -179,7 +185,7 @@ function missingRequiredFlags(command, args) {
|
|
|
179
185
|
}
|
|
180
186
|
export function emitArgparsePreamble(argv) {
|
|
181
187
|
if (argv.length === 0) {
|
|
182
|
-
process.stderr.write(`${rootUsage()}
|
|
188
|
+
process.stderr.write(`${rootUsage()}superspec:错误:缺少必填参数:command\n`);
|
|
183
189
|
return 2;
|
|
184
190
|
}
|
|
185
191
|
const command = argv[0];
|
|
@@ -188,7 +194,7 @@ export function emitArgparsePreamble(argv) {
|
|
|
188
194
|
return 0;
|
|
189
195
|
}
|
|
190
196
|
if (!COMMANDS.includes(command)) {
|
|
191
|
-
process.stderr.write(`${rootUsage()}
|
|
197
|
+
process.stderr.write(`${rootUsage()}superspec:错误:命令无效:'${command}';可选值:${COMMAND_CHOICES}\n`);
|
|
192
198
|
return 2;
|
|
193
199
|
}
|
|
194
200
|
const args = argv.slice(1);
|
|
@@ -199,7 +205,11 @@ export function emitArgparsePreamble(argv) {
|
|
|
199
205
|
if (!isPacketCommand(command)) {
|
|
200
206
|
const missing = missingRequiredFlags(command, args);
|
|
201
207
|
if (missing.length > 0) {
|
|
202
|
-
|
|
208
|
+
if (isHookEventCommand(command) && missing.length === 1 && missing[0] === "--event-ref") {
|
|
209
|
+
process.stderr.write(`${commandUsage(command)}superspec ${command}:错误:--event-ref must point to a readable hook event JSON file\n`);
|
|
210
|
+
return 2;
|
|
211
|
+
}
|
|
212
|
+
process.stderr.write(`${commandUsage(command)}superspec ${command}:错误:缺少必填参数:${missing.join(", ")}\n`);
|
|
203
213
|
return 2;
|
|
204
214
|
}
|
|
205
215
|
}
|
|
@@ -357,6 +367,7 @@ export function parse_argv(argv) {
|
|
|
357
367
|
const executorRefs = getValues("--executor-report-ref");
|
|
358
368
|
const codeReviewRefs = getValues("--task-code-review-report-ref");
|
|
359
369
|
const greenRefs = getValues("--green-test-run-evidence-ref");
|
|
370
|
+
const alternativeRefs = getValues("--alternative-verification-evidence-ref");
|
|
360
371
|
const redRefs = getValues("--red-test-run-evidence-ref");
|
|
361
372
|
const characterizationRefs = getValues("--characterization-test-run-evidence-ref");
|
|
362
373
|
if (!taskId)
|
|
@@ -365,11 +376,18 @@ export function parse_argv(argv) {
|
|
|
365
376
|
throw new GuardError("apply-verify-packet 缺少必填参数 --executor-report-ref");
|
|
366
377
|
if (codeReviewRefs.length === 0)
|
|
367
378
|
throw new GuardError("apply-verify-packet 缺少必填参数 --task-code-review-report-ref");
|
|
368
|
-
if (greenRefs.length === 0)
|
|
369
|
-
throw new GuardError("apply-verify-packet
|
|
370
|
-
|
|
379
|
+
if (greenRefs.length === 0 && alternativeRefs.length === 0) {
|
|
380
|
+
throw new GuardError("apply-verify-packet requires --green-test-run-evidence-ref or --alternative-verification-evidence-ref");
|
|
381
|
+
}
|
|
382
|
+
if (greenRefs.length > 0 && alternativeRefs.length > 0) {
|
|
383
|
+
throw new GuardError("apply-verify-packet requires either GREEN refs or alternative verification refs, not both");
|
|
384
|
+
}
|
|
385
|
+
if (greenRefs.length > 0 && redRefs.length === 0 && characterizationRefs.length === 0) {
|
|
371
386
|
throw new GuardError("apply-verify-packet requires --red-test-run-evidence-ref or --characterization-test-run-evidence-ref");
|
|
372
387
|
}
|
|
388
|
+
if (alternativeRefs.length > 0 && (redRefs.length > 0 || characterizationRefs.length > 0)) {
|
|
389
|
+
throw new GuardError("apply-verify-packet alternative verification branch must not include RED or characterization refs");
|
|
390
|
+
}
|
|
373
391
|
return {
|
|
374
392
|
command,
|
|
375
393
|
change,
|
|
@@ -377,6 +395,7 @@ export function parse_argv(argv) {
|
|
|
377
395
|
executor_report_refs: executorRefs,
|
|
378
396
|
task_code_review_report_refs: codeReviewRefs,
|
|
379
397
|
green_test_run_evidence_refs: greenRefs,
|
|
398
|
+
alternative_verification_evidence_refs: alternativeRefs,
|
|
380
399
|
red_test_run_evidence_refs: redRefs,
|
|
381
400
|
characterization_test_run_evidence_refs: characterizationRefs,
|
|
382
401
|
packet_format: parsePacketOutputFormat(selectedPacketFormat, { allowPrompt: true }),
|
|
@@ -394,11 +413,10 @@ export function parse_argv(argv) {
|
|
|
394
413
|
throw new GuardError("--round 必须是大于等于 1 的整数");
|
|
395
414
|
return { command, change, gate, round };
|
|
396
415
|
}
|
|
397
|
-
if (command
|
|
398
|
-
|| command === "hook-record-subagent-start" || command === "hook-record-subagent-stop") {
|
|
416
|
+
if (isHookEventCommand(command)) {
|
|
399
417
|
const eventRef = getValue("--event-ref");
|
|
400
418
|
if (!eventRef)
|
|
401
|
-
throw new
|
|
419
|
+
throw new GuardError("--event-ref must point to a readable hook event JSON file");
|
|
402
420
|
return { command, change, format, event_ref: eventRef };
|
|
403
421
|
}
|
|
404
422
|
if (command === "hook-session-begin") {
|
package/dist/src/disclosure.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
11
11
|
import { join, relative } from "node:path";
|
|
12
12
|
import { isObject, reason, renderList, repr, runtime, safe_within, toPosix, walkFiles } from "./util.js";
|
|
13
|
-
import { normalize_gate } from "./openspec.js";
|
|
13
|
+
import { effective_superseded_ids, normalize_gate } from "./openspec.js";
|
|
14
14
|
// Explicit target map (design §7); later phases extend this table gate by gate.
|
|
15
15
|
// Entries containing "*" are globs enumerated at check time; set equality (P1-6) means a spec
|
|
16
16
|
// file added after the digest makes the digest stale even though every pinned blob still matches.
|
|
@@ -462,7 +462,7 @@ export function review_disclosure_reasons(gate, changeRoot, evidences) {
|
|
|
462
462
|
return [reason("missing_review_digest", `${gate}: the disclosure loop is mandatory on this gate; need round-tagged role review evidence (review_round_id ${gate}-r1, findings[]) plus a main_review_digest`)];
|
|
463
463
|
}
|
|
464
464
|
const out = [];
|
|
465
|
-
const dead =
|
|
465
|
+
const dead = effective_superseded_ids(valid);
|
|
466
466
|
const byId = new Map();
|
|
467
467
|
for (const ev of valid) {
|
|
468
468
|
if (typeof ev.evidence_id === "string" && ev.evidence_id)
|
package/dist/src/evidence.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { join, relative } from "node:path";
|
|
3
|
-
import { CLAIM_ADJUDICATION_DECISIONS, EVIDENCE_KINDS, EVIDENCE_STATUSES, FINAL_VERIFICATION_ROLES, HUMAN_CONFIRMATION_GATES, FINAL_TEST_REQUIRED_FIELDS, FINDING_ADJUDICATION_DECISIONS, FORBIDDEN_FIELDS, MAIN_ADJUDICATION_DECISIONS, MAIN_ADJUDICATION_REQUIRED_FIELDS, REQUEST_CHANGES_ROUTES, ROLE_EVIDENCE_FIELDS, REVIEW_GUIDANCE_ROLES, SELF_REVIEW_MARKERS, SOURCE_GUIDANCE_REQUIRED_FIELDS, TASK_REOPEN_INVALIDITY_CLASSES, TASK_REOPEN_REQUIRED_FIELDS, TASK_REOPEN_RESOLVED_REQUIRED_FIELDS, VERIFY_EVIDENCE_REQUIRED_FIELDS, fingerprint_obj, isObject, reason, renderList, repr, pinned_ref_key, runtime, safe_within, sha256_file, toPosix, walkFiles, } from "./util.js";
|
|
4
|
-
import { normalize_gate } from "./openspec.js";
|
|
3
|
+
import { CLAIM_ADJUDICATION_DECISIONS, EVIDENCE_KINDS, EVIDENCE_STATUSES, FINAL_VERIFICATION_ROLES, HUMAN_CONFIRMATION_GATES, FINAL_TEST_REQUIRED_FIELDS, FINDING_ADJUDICATION_DECISIONS, FORBIDDEN_FIELDS, MAIN_ADJUDICATION_DECISIONS, MAIN_ADJUDICATION_REQUIRED_FIELDS, NO_TDD_REASONS, REQUEST_CHANGES_ROUTES, ROLE_EVIDENCE_FIELDS, REVIEW_GUIDANCE_ROLES, SELF_REVIEW_MARKERS, SOURCE_GUIDANCE_REQUIRED_FIELDS, TASK_REOPEN_INVALIDITY_CLASSES, TASK_REOPEN_REQUIRED_FIELDS, TASK_REOPEN_RESOLVED_REQUIRED_FIELDS, VERIFY_EVIDENCE_REQUIRED_FIELDS, confirmation_text_has_pending_wording, fingerprint_obj, isObject, reason, renderList, repr, pinned_ref_key, runtime, safe_within, sha256_file, toPosix, UNCHECKED_CHECKBOX_RE, walkFiles, } from "./util.js";
|
|
4
|
+
import { effective_superseded_ids, normalize_gate } from "./openspec.js";
|
|
5
5
|
import { superspec_dir } from "./paths.js";
|
|
6
6
|
import { findings_schema_reasons, review_digest_schema_reasons, standing_authorization_schema_reasons, user_decision_schema_reasons, } from "./disclosure.js";
|
|
7
7
|
import { pinned_artifact_ref_reasons as shared_pinned_artifact_ref_reasons, worker_test_run_reasons, } from "./apply_worker_chain.js";
|
|
@@ -143,6 +143,24 @@ function string_list_field_reasons(ev, field, code, opts = {}) {
|
|
|
143
143
|
}
|
|
144
144
|
return [];
|
|
145
145
|
}
|
|
146
|
+
const APPLY_WORKER_CHAIN_SURFACES = new Set(["implementation", "runtime_config"]);
|
|
147
|
+
function apply_worker_chain_ref_ids(ev, singleField, listField) {
|
|
148
|
+
const refs = new Set();
|
|
149
|
+
const single = ev[singleField];
|
|
150
|
+
if (isObject(single) && typeof single.evidence_id === "string" && single.evidence_id)
|
|
151
|
+
refs.add(single.evidence_id);
|
|
152
|
+
else if (typeof single === "string" && single)
|
|
153
|
+
refs.add(single);
|
|
154
|
+
if (Array.isArray(ev[listField])) {
|
|
155
|
+
for (const item of ev[listField]) {
|
|
156
|
+
if (isObject(item) && typeof item.evidence_id === "string" && item.evidence_id)
|
|
157
|
+
refs.add(item.evidence_id);
|
|
158
|
+
else if (typeof item === "string" && item)
|
|
159
|
+
refs.add(item);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return [...refs].sort();
|
|
163
|
+
}
|
|
146
164
|
function fingerprint_field_reasons(ev, field, code) {
|
|
147
165
|
const raw = ev[field];
|
|
148
166
|
if (typeof raw === "string") {
|
|
@@ -161,7 +179,37 @@ function pinned_artifact_ref_item_reasons(refItem, field, changeRoot, expected =
|
|
|
161
179
|
});
|
|
162
180
|
}
|
|
163
181
|
export function role_target_ref_reasons(ev, targetRoot) {
|
|
164
|
-
|
|
182
|
+
const problems = [];
|
|
183
|
+
const raw = ev.target_refs;
|
|
184
|
+
if (!Array.isArray(raw)) {
|
|
185
|
+
problems.push(reason("target_ref_invalid", `${ev._path}: target_refs must be a list of {path, blob_sha}`));
|
|
186
|
+
return problems;
|
|
187
|
+
}
|
|
188
|
+
for (const refItem of raw) {
|
|
189
|
+
if (!isObject(refItem)) {
|
|
190
|
+
problems.push(reason("target_ref_invalid", `${ev._path}: target_refs item must be object`));
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const rel = refItem.path;
|
|
194
|
+
const expected = refItem.blob_sha;
|
|
195
|
+
if (typeof rel !== "string" || !rel || typeof expected !== "string" || !expected) {
|
|
196
|
+
problems.push(reason("target_ref_invalid", `${ev._path}: target_refs requires path and blob_sha`));
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const target = safe_within(targetRoot, rel);
|
|
200
|
+
if (target === null) {
|
|
201
|
+
problems.push(reason("evidence_unsafe_ref", `${ev._path}: target_refs escapes allowed root: ${rel}`));
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (!existsSync(target) || !statSync(target).isFile()) {
|
|
205
|
+
problems.push(reason("stale_review", `${ev._path}: target_refs is not readable: ${rel}`));
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const actual = runtime.file_blob_sha(target);
|
|
209
|
+
if (actual !== expected)
|
|
210
|
+
problems.push(reason("stale_review", `${ev._path}: target_refs stale for ${rel}`));
|
|
211
|
+
}
|
|
212
|
+
return problems;
|
|
165
213
|
}
|
|
166
214
|
export function loaded_ref_reasons(ev, repoRoot) {
|
|
167
215
|
return pinned_ref_reasons(ev, "loaded_refs", repoRoot, "stale_loaded_ref");
|
|
@@ -358,6 +406,16 @@ function human_confirmation_reasons(ev) {
|
|
|
358
406
|
&& (typeof ev.tasks_structure_hash !== "string" || !ev.tasks_structure_hash.trim())) {
|
|
359
407
|
problems.push(reason("human_confirmation_invalid", `${ev._path}: ${gate} human_confirmation requires tasks_structure_hash pinning the approved tasks.md structure`));
|
|
360
408
|
}
|
|
409
|
+
// B3: a pass confirmation must not carry unresolved signals. Strong signal: an unchecked checkbox
|
|
410
|
+
// "[ ]" pasted into confirmation_text (the agent copied an open question list into the confirmation).
|
|
411
|
+
// Weak signal: confirmation-pending wording shared with the explore gate (仍需确认 / 待确认 / ...),
|
|
412
|
+
// scoped to confirmation semantics so legitimate "细节仍需在 propose 细化" prose is not flagged.
|
|
413
|
+
// Real incident: secondment-hour-unit-support sealed status:"pass" while its confirmation_text said
|
|
414
|
+
// "还需要评估..." — the gate's presence-check then let propose through with 8 open questions.
|
|
415
|
+
const ctext = typeof ev.confirmation_text === "string" ? ev.confirmation_text : "";
|
|
416
|
+
if (UNCHECKED_CHECKBOX_RE.test(ctext) || confirmation_text_has_pending_wording(ctext)) {
|
|
417
|
+
problems.push(reason("human_confirmation_invalid", `${ev._path}: confirmation_text carries unresolved signals (unchecked checkbox or 「仍需确认/待确认」wording); do not record pass while open items remain`));
|
|
418
|
+
}
|
|
361
419
|
return problems;
|
|
362
420
|
}
|
|
363
421
|
// FIX-10 (audit C-5/H-4): test_run evidence is archived per run, not as a bare self-report.
|
|
@@ -455,7 +513,28 @@ function apply_worker_chain_reasons(ev, changeRoot) {
|
|
|
455
513
|
problems.push(...fingerprint_field_reasons(ev, "executor_packet_fingerprint", "apply_worker_chain_invalid"));
|
|
456
514
|
problems.push(...fingerprint_field_reasons(ev, "source_implementation_fingerprint", "apply_worker_chain_invalid"));
|
|
457
515
|
problems.push(...string_list_field_reasons(ev, "declared_task_write_scope", "apply_worker_chain_invalid"));
|
|
458
|
-
|
|
516
|
+
const proofKind = String(ev.pre_edit_proof_kind ?? "red_or_characterization");
|
|
517
|
+
if (proofKind !== "red_or_characterization" && proofKind !== "no_tdd_declared") {
|
|
518
|
+
problems.push(reason("apply_worker_chain_invalid", `${ev._path}: active apply_worker_chain pre_edit_proof_kind must be red_or_characterization or no_tdd_declared`));
|
|
519
|
+
}
|
|
520
|
+
else if (proofKind === "no_tdd_declared") {
|
|
521
|
+
problems.push(...string_list_field_reasons(ev, "pre_edit_evidence_refs", "apply_worker_chain_invalid", { allowEmpty: true }));
|
|
522
|
+
if (Array.isArray(ev.pre_edit_evidence_refs) && ev.pre_edit_evidence_refs.length > 0) {
|
|
523
|
+
problems.push(reason("apply_worker_chain_invalid", `${ev._path}: no_tdd_declared active apply_worker_chain requires empty pre_edit_evidence_refs`));
|
|
524
|
+
}
|
|
525
|
+
if (ev.tdd_required !== false) {
|
|
526
|
+
problems.push(reason("apply_worker_chain_invalid", `${ev._path}: no_tdd_declared active apply_worker_chain requires tdd_required=false`));
|
|
527
|
+
}
|
|
528
|
+
if (!APPLY_WORKER_CHAIN_SURFACES.has(String(ev.apply_execution_surface ?? ""))) {
|
|
529
|
+
problems.push(reason("apply_worker_chain_invalid", `${ev._path}: no_tdd_declared active apply_worker_chain requires apply_execution_surface implementation or runtime_config`));
|
|
530
|
+
}
|
|
531
|
+
if (!NO_TDD_REASONS.has(String(ev.no_tdd_reason ?? ""))) {
|
|
532
|
+
problems.push(reason("apply_worker_chain_invalid", `${ev._path}: no_tdd_declared active apply_worker_chain requires valid no_tdd_reason`));
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
problems.push(...string_list_field_reasons(ev, "pre_edit_evidence_refs", "apply_worker_chain_invalid"));
|
|
537
|
+
}
|
|
459
538
|
}
|
|
460
539
|
if (state === "closed") {
|
|
461
540
|
const taskId = typeof ev.task_id === "string" ? ev.task_id : undefined;
|
|
@@ -468,8 +547,17 @@ function apply_worker_chain_reasons(ev, changeRoot) {
|
|
|
468
547
|
problems.push(...refProblems);
|
|
469
548
|
if (refProblems.length > 0)
|
|
470
549
|
problems.push(reason("apply_worker_chain_invalid", `${ev._path}: closed apply_worker_chain report refs must be pinned same-chain worker reports`));
|
|
471
|
-
|
|
472
|
-
|
|
550
|
+
const proofKind = String(ev.completion_proof_kind ?? "green_tests");
|
|
551
|
+
if (proofKind !== "green_tests" && proofKind !== "alternative_verification") {
|
|
552
|
+
problems.push(reason("apply_worker_chain_invalid", `${ev._path}: closed apply_worker_chain completion_proof_kind must be green_tests or alternative_verification`));
|
|
553
|
+
}
|
|
554
|
+
else if (proofKind === "green_tests") {
|
|
555
|
+
if (apply_worker_chain_ref_ids(ev, "green_test_run_evidence_ref", "green_test_run_evidence_refs").length === 0) {
|
|
556
|
+
problems.push(reason("apply_worker_chain_invalid", `${ev._path}: green_tests closed apply_worker_chain requires green_test_run_evidence_ref(s)`));
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
else if (apply_worker_chain_ref_ids(ev, "alternative_verification_evidence_ref", "alternative_verification_evidence_refs").length === 0) {
|
|
560
|
+
problems.push(reason("apply_worker_chain_invalid", `${ev._path}: alternative_verification closed apply_worker_chain requires alternative_verification_evidence_ref(s)`));
|
|
473
561
|
}
|
|
474
562
|
problems.push(...fingerprint_field_reasons(ev, "observed_freshness_fingerprint", "apply_worker_chain_invalid"));
|
|
475
563
|
}
|
|
@@ -479,6 +567,9 @@ function apply_worker_chain_reasons(ev, changeRoot) {
|
|
|
479
567
|
if ("restored_implementation_fingerprint" in ev) {
|
|
480
568
|
problems.push(...fingerprint_field_reasons(ev, "restored_implementation_fingerprint", "apply_worker_chain_invalid"));
|
|
481
569
|
}
|
|
570
|
+
if ("restored_implementation_fingerprint" in ev && "serial_takeover_baseline_ref" in ev) {
|
|
571
|
+
problems.push(reason("apply_worker_chain_invalid", `${ev._path}: abandoned apply_worker_chain restored_implementation_fingerprint and serial_takeover_baseline_ref are mutually exclusive`));
|
|
572
|
+
}
|
|
482
573
|
if (!("restored_implementation_fingerprint" in ev) && !("serial_takeover_baseline_ref" in ev)) {
|
|
483
574
|
problems.push(reason("apply_worker_chain_invalid", `${ev._path}: abandoned apply_worker_chain requires restored_implementation_fingerprint or serial_takeover_baseline_ref`));
|
|
484
575
|
}
|
|
@@ -487,7 +578,12 @@ function apply_worker_chain_reasons(ev, changeRoot) {
|
|
|
487
578
|
problems.push(...baselineProblems);
|
|
488
579
|
if (baselineProblems.length > 0)
|
|
489
580
|
problems.push(reason("apply_worker_chain_invalid", `${ev._path}: abandoned apply_worker_chain serial_takeover_baseline_ref must be a pinned same-chain status_report`));
|
|
490
|
-
|
|
581
|
+
if (typeof ev.takeover_confirmation_evidence_id !== "string" || !ev.takeover_confirmation_evidence_id) {
|
|
582
|
+
problems.push(reason("apply_worker_chain_invalid", `${ev._path}: abandoned apply_worker_chain serial takeover requires takeover_confirmation_evidence_id`));
|
|
583
|
+
}
|
|
584
|
+
if ("successor_green_evidence_refs" in ev) {
|
|
585
|
+
problems.push(...string_list_field_reasons(ev, "successor_green_evidence_refs", "apply_worker_chain_invalid"));
|
|
586
|
+
}
|
|
491
587
|
}
|
|
492
588
|
}
|
|
493
589
|
return problems;
|
|
@@ -915,7 +1011,7 @@ export function find_pass(evidences, filters = {}) {
|
|
|
915
1011
|
});
|
|
916
1012
|
}
|
|
917
1013
|
export function superseded_ids(evidences) {
|
|
918
|
-
return
|
|
1014
|
+
return effective_superseded_ids(evidences);
|
|
919
1015
|
}
|
|
920
1016
|
// FIX-6 (audit C-2): supersede is a rollback mechanism, so it needs an authorization model.
|
|
921
1017
|
// A supersede evidence must point at an existing evidence_id, and a cross-gate supersede
|
package/dist/src/gates.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export declare function superspec_agent_reasons(repoRoot: string): Reason[];
|
|
|
5
5
|
export declare function openspec_cli_capability_reasons(): Reason[];
|
|
6
6
|
export declare function evidence_schema_guard(change: string, changeRoot: string, repoRoot: string, evidences: JsonMap[]): Reason[];
|
|
7
7
|
export declare function check_init(change: string, status: JsonMap, repoRoot: string, changeRoot: string): Decision;
|
|
8
|
+
export declare function discovery_open_question_count(discoveryPath: string): number;
|
|
8
9
|
export declare function check_superspec_gate(change: string, status: JsonMap, changeRoot: string, evidences: JsonMap[], gateRaw: string): Decision;
|
|
9
10
|
export declare function check_artifact(change: string, status: JsonMap, changeRoot: string, evidences: JsonMap[], artifact: string): Decision;
|
|
10
11
|
export declare function check_task_reopen(change: string, status: JsonMap, changeRoot: string, evidences: JsonMap[], taskId: string): Decision;
|
package/dist/src/gates.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { join, relative } from "node:path";
|
|
3
|
-
import { ARTIFACT_ENTER_GATE, MAIN_ADJUDICATION_DECISIONS, REQUEST_CHANGES_ROUTES, NO_TDD_REASONS, OPENSPEC_ARTIFACTS, FINAL_VERIFICATION_ROLES, REQUIRED_SUPERSPEC_AGENT_ROLES, REQUIRED_SUPERSPEC_WORKFLOW_SKILLS, REVIEW_GUIDANCE_ROLES, REVIEW_EVIDENCE_REQUIRED_FIELDS, TDD_MODES, VERIFY_EVIDENCE_REQUIRED_FIELDS, allow, block, isObject, reason, renderList, repr, pinned_ref_key, safe_within, sha256_text, runtime, toPosix, } from "./util.js";
|
|
4
|
-
import { all_done, artifact_status_map, get_repo_root, is_done, normalize_gate, openspec_cli_probe } from "./openspec.js";
|
|
3
|
+
import { ARTIFACT_ENTER_GATE, MAIN_ADJUDICATION_DECISIONS, REQUEST_CHANGES_ROUTES, NO_TDD_REASONS, OPENSPEC_ARTIFACTS, FINAL_VERIFICATION_ROLES, REQUIRED_SUPERSPEC_AGENT_ROLES, REQUIRED_SUPERSPEC_WORKFLOW_SKILLS, REVIEW_GUIDANCE_ROLES, REVIEW_EVIDENCE_REQUIRED_FIELDS, TDD_MODES, VERIFY_EVIDENCE_REQUIRED_FIELDS, allow, block, isObject, reason, renderList, repr, pinned_ref_key, safe_within, sha256_text, runtime, toPosix, PENDING_CONFIRMATION_RE, UNCHECKED_CHECKBOX_RE, CHECKED_CHECKBOX_RE, } from "./util.js";
|
|
4
|
+
import { all_done, artifact_status_map, effective_superseded_ids, get_repo_root, is_done, normalize_gate, openspec_cli_probe } from "./openspec.js";
|
|
5
5
|
import { read_agent_toml_name, read_skill_frontmatter_name, sidecar_business_invariants_path, sidecar_discovery_path, sidecar_test_contract_path } from "./paths.js";
|
|
6
6
|
import { hookInitReasons } from "./hooks/health.js";
|
|
7
7
|
import { business_invariant_ids, business_invariant_validation_reasons, automated_hard_business_invariant_ids, evidence_invariant_refs, evidence_invariant_ref_reasons, evidence_test_contract_invariant_reasons, human_confirmation_business_invariant_ids, invariant_matrix_coverage_reasons, post_implementation_business_invariant_ids, red_green_invariant_ids, test_contract_invariant_ids, } from "./invariants.js";
|
|
8
|
-
import { evidence_test_id_reasons, declared_test_evidence_reasons, parse_spec_scenarios, parse_tasks, parse_test_contract_ids, parse_test_contract_records, red_green_test_ids, splitList, tasks_structure_hash, task_alternative_verification, task_test_evidence, task_test_refs, test_contract_covers_scenario, test_contract_invariant_refs_by_test, write_scope_conflict_reasons, } from "./tasks.js";
|
|
8
|
+
import { evidence_test_id_reasons, declared_test_evidence_reasons, parse_spec_scenarios, parse_tasks, parse_test_contract_ids, parse_test_contract_records, red_green_test_ids, splitList, task_apply_execution_surface, task_apply_execution_surface_reasons, tasks_structure_hash, task_alternative_verification, task_test_evidence, task_test_refs, test_contract_covers_scenario, test_contract_invariant_refs_by_test, write_scope_conflict_reasons, } from "./tasks.js";
|
|
9
9
|
import { duplicate_evidence_id_reasons, dangling_evidence_ref_reasons, final_verification_evidences, live_task_reopens, live_task_reopen_resolutions, live_pass, live_user_confirmations, pass_task_reopens, supersede_reasons, unresolved_live_task_reopens, validate_evidence_schema, verify_reference_reasons, } from "./evidence.js";
|
|
10
10
|
import { review_disclosure_reasons } from "./disclosure.js";
|
|
11
11
|
import { archive_manifest_path } from "./archive.js";
|
|
12
|
-
import {
|
|
13
|
-
import { apply_worker_chain_lifecycle_reasons as shared_apply_worker_chain_lifecycle_reasons, apply_worker_chain_lifecycle_state,
|
|
12
|
+
import { pre_edit_worker_test_run_reasons, } from "./apply_worker_chain.js";
|
|
13
|
+
import { apply_worker_chain_lifecycle_reasons as shared_apply_worker_chain_lifecycle_reasons, apply_worker_chain_lifecycle_state, } from "./apply_worker_chain_lifecycle.js";
|
|
14
14
|
function action_list(...items) {
|
|
15
15
|
return [...new Set(items.filter((item) => typeof item === "string" && item.length > 0))];
|
|
16
16
|
}
|
|
@@ -398,8 +398,15 @@ export function openspec_cli_capability_reasons() {
|
|
|
398
398
|
return [reason("openspec_native_surface_missing", probe.message)];
|
|
399
399
|
}
|
|
400
400
|
export function evidence_schema_guard(change, changeRoot, repoRoot, evidences) {
|
|
401
|
+
const effectiveDead = effective_superseded_ids(evidences);
|
|
401
402
|
return [
|
|
402
|
-
...evidences.flatMap((ev) =>
|
|
403
|
+
...evidences.flatMap((ev) => {
|
|
404
|
+
const problems = validate_evidence_schema(ev, change, changeRoot, repoRoot);
|
|
405
|
+
const id = typeof ev.evidence_id === "string" ? ev.evidence_id : "";
|
|
406
|
+
if (!id || !effectiveDead.has(id) || !ev.agent_role)
|
|
407
|
+
return problems;
|
|
408
|
+
return problems.filter((item) => item.code !== "stale_review");
|
|
409
|
+
}),
|
|
403
410
|
...duplicate_evidence_id_reasons(evidences),
|
|
404
411
|
...dangling_evidence_ref_reasons(evidences),
|
|
405
412
|
...supersede_reasons(evidences),
|
|
@@ -450,6 +457,54 @@ function stale_artifact_review_reasons(reviews, changeRoot, artifactRel, code, g
|
|
|
450
457
|
}
|
|
451
458
|
return reasons;
|
|
452
459
|
}
|
|
460
|
+
// B1: count unresolved open questions inside discovery.md's "待确认问题" section before explore_complete
|
|
461
|
+
// can pass. The section is located by a heading whose text carries confirmation semantics
|
|
462
|
+
// (待确认 / 需要用户确认 / open question / pending confirmation), tolerating inserted words such as
|
|
463
|
+
// 「用户」between 需要 and 确认 (the real secondment-hour-unit-support incident title was
|
|
464
|
+
// 「## 需要用户确认的问题」, which a naive contiguous 「需要确认」 substring would miss).
|
|
465
|
+
//
|
|
466
|
+
// Within a matched section, a list item ("- " / "* " / "+ " / "1. ") counts as open if it carries an
|
|
467
|
+
// unchecked checkbox ("[ ]", literal half-width space — full-width U+3000 is not treated as a box,
|
|
468
|
+
// matching GitHub task-list semantics) OR pending-confirmation wording (PENDING_CONFIRMATION_RE,
|
|
469
|
+
// shared with the B3 confirmation guard). An item is closed (not counted) if it begins with 「已确认」
|
|
470
|
+
// after its list marker or carries a checked box "[x]". Plain prose bullets with neither signal are
|
|
471
|
+
// ignored. When the section is absent the count is 0 (grandfathered: legacy changes with no such
|
|
472
|
+
// section are not retroactively blocked, matching this repo's treatment of legacy evidence).
|
|
473
|
+
const OPEN_QUESTION_SECTION_RE = /(待|需要|需)[^\n]{0,6}确认|待澄清|开放问题|open question|pending confirmation/iu;
|
|
474
|
+
const OPEN_QUESTION_LIST_ITEM_RE = /^\s*(?:[-*+]|\d+\.)\s+(.*)$/u;
|
|
475
|
+
function classify_open_question_line(line) {
|
|
476
|
+
const m = line.match(OPEN_QUESTION_LIST_ITEM_RE);
|
|
477
|
+
if (!m)
|
|
478
|
+
return "none";
|
|
479
|
+
const body = m[1].trimStart();
|
|
480
|
+
if (CHECKED_CHECKBOX_RE.test(line))
|
|
481
|
+
return "closed";
|
|
482
|
+
if (UNCHECKED_CHECKBOX_RE.test(line))
|
|
483
|
+
return "open";
|
|
484
|
+
if (/^已确认/u.test(body))
|
|
485
|
+
return "closed";
|
|
486
|
+
if (PENDING_CONFIRMATION_RE.test(line))
|
|
487
|
+
return "open";
|
|
488
|
+
return "none";
|
|
489
|
+
}
|
|
490
|
+
export function discovery_open_question_count(discoveryPath) {
|
|
491
|
+
if (!existsSync(discoveryPath) || !statSync(discoveryPath).isFile())
|
|
492
|
+
return 0;
|
|
493
|
+
const text = readFileSync(discoveryPath, "utf8");
|
|
494
|
+
if (!text.trim())
|
|
495
|
+
return 0;
|
|
496
|
+
let inSection = false;
|
|
497
|
+
let count = 0;
|
|
498
|
+
for (const line of text.split(/\r?\n/u)) {
|
|
499
|
+
if (/^#{1,6}\s/u.test(line)) {
|
|
500
|
+
inSection = OPEN_QUESTION_SECTION_RE.test(line);
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
if (inSection && classify_open_question_line(line) === "open")
|
|
504
|
+
count += 1;
|
|
505
|
+
}
|
|
506
|
+
return count;
|
|
507
|
+
}
|
|
453
508
|
export function check_superspec_gate(change, status, changeRoot, evidences, gateRaw) {
|
|
454
509
|
const gate = normalize_gate(gateRaw);
|
|
455
510
|
const amap = artifact_status_map(status);
|
|
@@ -470,6 +525,13 @@ export function check_superspec_gate(change, status, changeRoot, evidences, gate
|
|
|
470
525
|
// DISC Phase 1: material findings raised by explore reviews must be disclosed to the user
|
|
471
526
|
// (main_review_digest + user_review_decision) before the gate can pass.
|
|
472
527
|
reasons.push(...review_disclosure_reasons("explore_complete", changeRoot, evidences));
|
|
528
|
+
// B1: discovery.md must not still carry unresolved open questions. Without this the gate is a
|
|
529
|
+
// presence-check on human_confirmation and will pass even when discovery lists 「仍需确认」items
|
|
530
|
+
// (real incident: secondment-hour-unit-support entered propose with 8 open questions).
|
|
531
|
+
const openQuestionCount = discovery_open_question_count(discovery);
|
|
532
|
+
if (openQuestionCount > 0) {
|
|
533
|
+
reasons.push(reason("explore_open_questions_unresolved", `${discovery}: discovery.md 待确认问题段仍有 ${openQuestionCount} 条未勾选/「仍需确认」项,逐条与用户确认并改为已确认后才能进入 propose`));
|
|
534
|
+
}
|
|
473
535
|
}
|
|
474
536
|
else if (gate === "proposal_reviewed") {
|
|
475
537
|
// DISC Phase 2: proposal review is an internal gate, not an advisory note. It is born inside
|
|
@@ -626,6 +688,9 @@ export function check_superspec_gate(change, status, changeRoot, evidences, gate
|
|
|
626
688
|
if (Object.keys(tasks).length === 0)
|
|
627
689
|
reasons.push(reason("invalid_task_graph", "tasks.md has no structured tasks"));
|
|
628
690
|
reasons.push(...write_scope_conflict_reasons(tasks));
|
|
691
|
+
for (const [taskId, task] of Object.entries(tasks)) {
|
|
692
|
+
reasons.push(...task_apply_execution_surface_reasons(taskId, task));
|
|
693
|
+
}
|
|
629
694
|
// DISC Phase 3: tasks_complete enters the disclosure loop only once round-tagged review
|
|
630
695
|
// evidence appears (design: no mandatory role review on this gate until then).
|
|
631
696
|
reasons.push(...review_disclosure_reasons("tasks_complete", changeRoot, evidences));
|
|
@@ -702,6 +767,19 @@ function task_reopen_history(evidences, taskId) {
|
|
|
702
767
|
function task_reopen_match_key(ev) {
|
|
703
768
|
return `${String(ev.evidence_id ?? "")}\u0000${String(ev.reopen_id ?? "")}\u0000${String(ev.task_id ?? "")}`;
|
|
704
769
|
}
|
|
770
|
+
function worker_pre_edit_test_evidence(changeRoot, evidences, taskId, semanticStatus) {
|
|
771
|
+
const candidates = task_test_evidence(evidences, taskId, semanticStatus, "task_edit");
|
|
772
|
+
const valid = [];
|
|
773
|
+
const invalidReasons = [];
|
|
774
|
+
for (const ev of candidates) {
|
|
775
|
+
const problems = pre_edit_worker_test_run_reasons(changeRoot, ev, taskId, "pre_edit_test_run_invalid");
|
|
776
|
+
if (problems.length === 0)
|
|
777
|
+
valid.push(ev);
|
|
778
|
+
else
|
|
779
|
+
invalidReasons.push(...problems);
|
|
780
|
+
}
|
|
781
|
+
return { valid, invalid_reasons: invalidReasons, candidate_count: candidates.length };
|
|
782
|
+
}
|
|
705
783
|
function matching_reopen_resolutions(evidences, reopen) {
|
|
706
784
|
const key = task_reopen_match_key(reopen);
|
|
707
785
|
return live_task_reopen_resolutions(evidences)
|
|
@@ -1118,18 +1196,24 @@ export function check_task_edit(change, status, changeRoot, evidences, taskId) {
|
|
|
1118
1196
|
reasons.push(reason("invalid_no_tdd_reason", `task ${taskId}: no_tdd_reason=${repr(nr)}`));
|
|
1119
1197
|
}
|
|
1120
1198
|
else if (tddMode === "behavior-preserving-refactor") {
|
|
1121
|
-
const
|
|
1122
|
-
|
|
1123
|
-
|
|
1199
|
+
const characterizationProof = worker_pre_edit_test_evidence(changeRoot, evidences, taskId, "expected_success");
|
|
1200
|
+
const characterization = characterizationProof.valid;
|
|
1201
|
+
if (characterization.length === 0) {
|
|
1202
|
+
reasons.push(reason(characterizationProof.candidate_count === 0 ? "missing_characterization" : "missing_worker_characterization", `task ${taskId} (behavior-preserving-refactor) needs test-runner characterization evidence before edit`));
|
|
1203
|
+
reasons.push(...characterizationProof.invalid_reasons);
|
|
1204
|
+
}
|
|
1124
1205
|
reasons.push(...evidence_test_id_reasons(characterization, taskId, declared, contractIds, "characterization"));
|
|
1125
1206
|
reasons.push(...declared_test_evidence_reasons(characterization, taskId, declared, "characterization"));
|
|
1126
1207
|
reasons.push(...evidence_invariant_ref_reasons(characterization, taskId, declaredInvariants, validInvariantIds, "characterization"));
|
|
1127
1208
|
reasons.push(...evidence_test_contract_invariant_reasons(characterization, taskId, test_contract_invariant_refs_by_test(changeRoot), "characterization"));
|
|
1128
1209
|
}
|
|
1129
1210
|
else {
|
|
1130
|
-
const
|
|
1131
|
-
|
|
1132
|
-
|
|
1211
|
+
const redProof = worker_pre_edit_test_evidence(changeRoot, evidences, taskId, "expected_failure");
|
|
1212
|
+
const red = redProof.valid;
|
|
1213
|
+
if (red.length === 0) {
|
|
1214
|
+
reasons.push(reason(redProof.candidate_count === 0 ? "missing_red_evidence" : "missing_worker_red_evidence", `task ${taskId} requires test-runner RED evidence before edit`));
|
|
1215
|
+
reasons.push(...redProof.invalid_reasons);
|
|
1216
|
+
}
|
|
1133
1217
|
reasons.push(...evidence_test_id_reasons(red, taskId, declared, contractIds, "RED"));
|
|
1134
1218
|
reasons.push(...declared_test_evidence_reasons(red, taskId, declared, "RED"));
|
|
1135
1219
|
reasons.push(...evidence_invariant_ref_reasons(red, taskId, declaredInvariants, validInvariantIds, "RED"));
|
|
@@ -1149,6 +1233,34 @@ export function check_task_edit(change, status, changeRoot, evidences, taskId) {
|
|
|
1149
1233
|
export function apply_worker_chain_lifecycle_reasons(repoRoot, changeRoot, evidences, taskId) {
|
|
1150
1234
|
return shared_apply_worker_chain_lifecycle_reasons(repoRoot, changeRoot, evidences, taskId);
|
|
1151
1235
|
}
|
|
1236
|
+
function apply_worker_chain_ref_ids(ev, singular, plural) {
|
|
1237
|
+
const ids = new Set();
|
|
1238
|
+
if (Array.isArray(ev[plural])) {
|
|
1239
|
+
for (const item of ev[plural]) {
|
|
1240
|
+
if (isObject(item) && typeof item.evidence_id === "string" && item.evidence_id)
|
|
1241
|
+
ids.add(item.evidence_id);
|
|
1242
|
+
else if (typeof item === "string" && item)
|
|
1243
|
+
ids.add(item);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
const single = ev[singular];
|
|
1247
|
+
if (isObject(single) && typeof single.evidence_id === "string" && single.evidence_id)
|
|
1248
|
+
ids.add(single.evidence_id);
|
|
1249
|
+
else if (typeof single === "string" && single)
|
|
1250
|
+
ids.add(single);
|
|
1251
|
+
return [...ids].sort();
|
|
1252
|
+
}
|
|
1253
|
+
function active_no_tdd_metadata_matches(active, executionSurface, noTddReason) {
|
|
1254
|
+
const preIds = Array.isArray(active?.pre_edit_evidence_refs) ? active.pre_edit_evidence_refs.map(String).filter(Boolean) : [];
|
|
1255
|
+
return isObject(active)
|
|
1256
|
+
&& active.pre_edit_proof_kind === "no_tdd_declared"
|
|
1257
|
+
&& preIds.length === 0
|
|
1258
|
+
&& active.tdd_required === false
|
|
1259
|
+
&& active.apply_execution_surface === executionSurface
|
|
1260
|
+
&& typeof noTddReason === "string"
|
|
1261
|
+
&& active.no_tdd_reason === noTddReason
|
|
1262
|
+
&& NO_TDD_REASONS.has(noTddReason);
|
|
1263
|
+
}
|
|
1152
1264
|
export function check_task_complete(change, status, changeRoot, evidences, taskId) {
|
|
1153
1265
|
const gate = "task_complete";
|
|
1154
1266
|
const reasons = [];
|
|
@@ -1163,6 +1275,7 @@ export function check_task_complete(change, status, changeRoot, evidences, taskI
|
|
|
1163
1275
|
const task = parse_tasks(changeRoot)[taskId];
|
|
1164
1276
|
if (!task)
|
|
1165
1277
|
return block(change, gate, [reason("unknown_task", `task ${taskId} not found`)], { task_id: taskId });
|
|
1278
|
+
reasons.push(...task_apply_execution_surface_reasons(taskId, task));
|
|
1166
1279
|
const workerChainState = apply_worker_chain_lifecycle_state(get_repo_root(status), changeRoot, evidences, taskId);
|
|
1167
1280
|
reasons.push(...workerChainState.completionReasons);
|
|
1168
1281
|
const reopenHistory = task_reopen_history(evidences, taskId);
|
|
@@ -1185,21 +1298,11 @@ export function check_task_complete(change, status, changeRoot, evidences, taskI
|
|
|
1185
1298
|
reasons.push(reason("invalid_tdd_mode", `task ${taskId}: tdd_mode=${repr(tddMode)}`));
|
|
1186
1299
|
if (tddRequired) {
|
|
1187
1300
|
const allGreen = task_test_evidence(evidences, taskId, "expected_success", "task_complete");
|
|
1188
|
-
const green =
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
&& workerChainState.validClosedGreenEvidenceIds.has(String(ev.evidence_id ?? ""))))
|
|
1192
|
-
: workerChainState.takeoverBaselines.length > 0
|
|
1193
|
-
? allGreen.filter((ev) => serial_completion_green(ev) && workerChainState.takeoverBaselines.some((baseline) => (baseline.successorGreenRefs.includes(String(ev.evidence_id ?? ""))
|
|
1194
|
-
&& fingerprint_matches(ev.implementation_fingerprint, baseline.fingerprint))))
|
|
1195
|
-
: allGreen.filter(serial_completion_green);
|
|
1301
|
+
const green = allGreen.filter((ev) => (ev.apply_execution_chain === "executor_worker"
|
|
1302
|
+
&& workerChainState.validClosedChainIds.has(String(ev.apply_worker_chain_id ?? ""))
|
|
1303
|
+
&& workerChainState.validClosedGreenEvidenceIds.has(String(ev.evidence_id ?? ""))));
|
|
1196
1304
|
if (green.length === 0)
|
|
1197
1305
|
reasons.push(reason("missing_green_evidence", `task ${taskId} requires GREEN evidence (expected_success) before completion`));
|
|
1198
|
-
if (workerChainState.validClosedGreenEvidenceIds.size === 0 && workerChainState.takeoverBaselines.length > 0) {
|
|
1199
|
-
if (green.length === 0) {
|
|
1200
|
-
reasons.push(reason("apply_worker_chain_takeover_green_mismatch", `task ${taskId} requires declared successor serial GREEN evidence after takeover baseline with matching implementation fingerprint`));
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
1306
|
const declared = new Set(splitList(attrs.test_refs ?? ""));
|
|
1204
1307
|
const declaredInvariants = new Set(splitList(attrs.invariant_refs ?? ""));
|
|
1205
1308
|
if (declared.size === 0)
|
|
@@ -1235,9 +1338,39 @@ export function check_task_complete(change, status, changeRoot, evidences, taskI
|
|
|
1235
1338
|
const nr = attrs.no_tdd_reason;
|
|
1236
1339
|
if (!NO_TDD_REASONS.has(nr))
|
|
1237
1340
|
reasons.push(reason("invalid_no_tdd_reason", `task ${taskId}: no_tdd_reason=${repr(nr)}`));
|
|
1238
|
-
const
|
|
1239
|
-
|
|
1341
|
+
const executionSurface = task_apply_execution_surface(task);
|
|
1342
|
+
const directAlternativeAllowed = executionSurface === "docs_generated" || executionSurface === "no_code";
|
|
1343
|
+
const implementationAlternativeRequired = executionSurface === "implementation" || executionSurface === "runtime_config";
|
|
1344
|
+
const allVerifications = task_alternative_verification(evidences, taskId);
|
|
1345
|
+
let validAlternativeEvidenceIds = workerChainState.validClosedAlternativeEvidenceIds;
|
|
1346
|
+
if (implementationAlternativeRequired) {
|
|
1347
|
+
const activeByChain = new Map(live_pass(evidences, { gate: "task_complete", kind: "apply_worker_chain", task_id: taskId })
|
|
1348
|
+
.filter((ev) => ev.chain_state === "active" && typeof ev.apply_worker_chain_id === "string")
|
|
1349
|
+
.map((ev) => [String(ev.apply_worker_chain_id), ev]));
|
|
1350
|
+
const closedAlternativeChains = live_pass(evidences, { gate: "task_complete", kind: "apply_worker_chain", task_id: taskId })
|
|
1351
|
+
.filter((ev) => (ev.chain_state === "closed"
|
|
1352
|
+
&& String(ev.completion_proof_kind ?? "green_tests") === "alternative_verification"
|
|
1353
|
+
&& workerChainState.validClosedAlternativeChainIds.has(String(ev.apply_worker_chain_id ?? ""))));
|
|
1354
|
+
const matchingClosedAlternativeChains = closedAlternativeChains.filter((ev) => (active_no_tdd_metadata_matches(activeByChain.get(String(ev.apply_worker_chain_id ?? "")), executionSurface, nr)));
|
|
1355
|
+
const metadataMismatchChains = closedAlternativeChains
|
|
1356
|
+
.filter((ev) => !matchingClosedAlternativeChains.includes(ev))
|
|
1357
|
+
.map((ev) => String(ev.apply_worker_chain_id ?? ev.evidence_id ?? ""))
|
|
1358
|
+
.filter(Boolean)
|
|
1359
|
+
.sort();
|
|
1360
|
+
if (metadataMismatchChains.length > 0) {
|
|
1361
|
+
reasons.push(reason("apply_worker_chain_active_invalid", `task ${taskId} closed alternative apply_worker_chain active no-TDD metadata must match task metadata: ${renderList(metadataMismatchChains)}`, metadataMismatchChains));
|
|
1362
|
+
}
|
|
1363
|
+
validAlternativeEvidenceIds = new Set(matchingClosedAlternativeChains.flatMap((ev) => apply_worker_chain_ref_ids(ev, "alternative_verification_evidence_ref", "alternative_verification_evidence_refs")));
|
|
1364
|
+
}
|
|
1365
|
+
const verifications = directAlternativeAllowed
|
|
1366
|
+
? allVerifications
|
|
1367
|
+
: allVerifications.filter((ev) => validAlternativeEvidenceIds.has(String(ev.evidence_id ?? "")));
|
|
1368
|
+
if (implementationAlternativeRequired && verifications.length === 0) {
|
|
1369
|
+
reasons.push(reason("missing_alternative_verification", `task ${taskId} apply_execution_surface=${executionSurface} requires closed apply_worker_chain alternative_verification proof before completion`));
|
|
1370
|
+
}
|
|
1371
|
+
else if (!implementationAlternativeRequired && verifications.length === 0) {
|
|
1240
1372
|
reasons.push(reason("missing_alternative_verification", `task ${taskId} has tdd_required:false and needs alternative verification evidence`));
|
|
1373
|
+
}
|
|
1241
1374
|
if (reopenMode.mode === "reopened") {
|
|
1242
1375
|
const reopen = reopenMode.reopen;
|
|
1243
1376
|
const reopenId = String(reopen.reopen_id ?? "");
|