agent-project-sdlc 0.1.22 → 0.1.24
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 +13 -5
- package/assets/agents/AGENTS_CORE.md +7 -1
- package/assets/docs/README.md +14 -6
- package/assets/policies/phase_contracts.yaml +136 -12
- package/assets/skills/pjsdlc_architect_design/SKILL.md +7 -1
- package/assets/skills/pjsdlc_dev_sprint/SKILL.md +10 -6
- package/assets/skills/pjsdlc_implementation_doc/SKILL.md +5 -4
- package/assets/skills/pjsdlc_manager/SKILL.md +9 -6
- package/assets/skills/pjsdlc_reviewer/SKILL.md +2 -2
- package/assets/skills/pjsdlc_rfc_recalibrate/SKILL.md +3 -3
- package/assets/skills/pjsdlc_tester/SKILL.md +8 -5
- package/assets/templates/EVIDENCE_INDEX_TEMPLATE.md +2 -1
- package/assets/templates/EXPLORATION_APPENDIX_TEMPLATE.md +2 -0
- package/assets/templates/IMPLEMENTATION_DOC_TEMPLATE.md +27 -6
- package/assets/templates/PLAN_TEMPLATE.yaml +31 -1
- package/assets/templates/RUNBOOK_TEMPLATE.md +10 -5
- package/assets/templates/TEST_REPORT_TEMPLATE.md +1 -0
- package/assets/tools/harness_utils.py +388 -18
- package/assets/tools/transition.py +24 -31
- package/assets/tools/validate_design.py +5 -0
- package/assets/tools/validate_harness.py +14 -1
- package/assets/tools/validate_prompt_language.py +1 -1
- package/assets/tools/validate_rfc.py +5 -0
- package/dist/lib/init.js +1 -1
- package/dist/lib/validators.js +567 -6
- package/package.json +1 -1
package/dist/lib/validators.js
CHANGED
|
@@ -9,6 +9,8 @@ const PARALLEL_MODES = new Set(["runtime_managed", "user_orchestrated"]);
|
|
|
9
9
|
const PARALLEL_TRIGGERS = new Set(["user_requested", "workflow_default"]);
|
|
10
10
|
const PARALLEL_RUNTIME_PROVIDERS = new Set(["codex_native_subagents", "user_orchestrated", "codex_exec_worktree"]);
|
|
11
11
|
const TASK_PHASES = new Set(["REQUIREMENT_GATHERING", "ARCHITECTING", "SPRINTING", "REVIEWING", "TESTING", "RELEASING", "RFC_RECALIBRATION"]);
|
|
12
|
+
const RESERVED_SUSPENDED_PHASE_TARGET = "<suspended_phase>";
|
|
13
|
+
const TRANSITION_KINDS = new Set(["normal", "return", "interrupt", "resume"]);
|
|
12
14
|
const PARALLEL_ALLOWED_PHASES = new Set(["REQUIREMENT_GATHERING", "ARCHITECTING", "SPRINTING", "REVIEWING", "TESTING", "RELEASING", "RFC_RECALIBRATION"]);
|
|
13
15
|
const PARALLEL_READ_ONLY_PHASES = new Set(["REQUIREMENT_GATHERING", "ARCHITECTING", "REVIEWING", "RELEASING", "RFC_RECALIBRATION"]);
|
|
14
16
|
const PARALLEL_PROTECTED_WRITE_PATTERNS = [
|
|
@@ -104,6 +106,7 @@ const DEVELOPMENT_SELF_TEST_CONTRACT_TERMS = ["development self-test contract",
|
|
|
104
106
|
const DEVELOPMENT_SELF_TEST_REPORT_TERMS = ["development self-test report", "开发自测报告"];
|
|
105
107
|
const DEVELOPMENT_SELF_TEST_IMPACT_TERMS = ["development self-test impact", "开发自测影响"];
|
|
106
108
|
const MODULE_KEY_TEST_PATH_TERMS = ["module key test path", "模块关键测试路径"];
|
|
109
|
+
const MODULE_KEY_TEST_GRAPH_TERMS = ["module key test graph", "module_key_test_graph", "模块关键测试图"];
|
|
107
110
|
const GATE_BREAKDOWN_TERMS = ["gate breakdown", "gate 分层", "gate breakdown(gate 分层)"];
|
|
108
111
|
const CURRENT_OPERATOR_PATH_TERMS = ["current operator path", "operator path", "当前操作路径", "当前 operator path"];
|
|
109
112
|
const TESTING_HANDOFF_TERMS = ["testing handoff contract", "测试交接合同"];
|
|
@@ -125,9 +128,27 @@ const SELF_TEST_REPORT_PLACEHOLDER_TERMS = [
|
|
|
125
128
|
"all self-test scenarios",
|
|
126
129
|
"all task/module promised runnable entries",
|
|
127
130
|
"actual internal key paths",
|
|
128
|
-
"observable completion evidence"
|
|
131
|
+
"observable completion evidence",
|
|
132
|
+
"required only when",
|
|
133
|
+
"compact dag pointer",
|
|
134
|
+
"复杂 / high-risk",
|
|
135
|
+
"只记录实际 handoff"
|
|
129
136
|
];
|
|
130
137
|
const SELF_TEST_REPORT_STATUSES = new Set(["PASS", "BLOCKED", "IN_PROGRESS", "STALE"]);
|
|
138
|
+
const SELF_TEST_REPORT_REQUIRED_FIELDS = [
|
|
139
|
+
"Contract Source",
|
|
140
|
+
"Module Application Entry",
|
|
141
|
+
"Module Key Test Path",
|
|
142
|
+
"Scenario Results",
|
|
143
|
+
"Executed Gates",
|
|
144
|
+
"Observable Exit",
|
|
145
|
+
"Current Blocker",
|
|
146
|
+
"Testing Handoff Readiness",
|
|
147
|
+
"Evidence Index Refs"
|
|
148
|
+
];
|
|
149
|
+
const SELF_TEST_REPORT_NONE_ALLOWED_FIELDS = new Set(["Current Blocker"]);
|
|
150
|
+
const MAX_SELF_TEST_REPORT_LINES = 80;
|
|
151
|
+
const MAX_HIGH_RISK_SELF_TEST_REPORT_LINES = 120;
|
|
131
152
|
const SELF_TEST_REPORT_DISALLOWED_SECTION_TERMS = [
|
|
132
153
|
"debug log",
|
|
133
154
|
"operator log",
|
|
@@ -146,6 +167,94 @@ const SELF_TEST_REPORT_DISALLOWED_SECTION_TERMS = [
|
|
|
146
167
|
"诊断尝试",
|
|
147
168
|
"历史流水"
|
|
148
169
|
];
|
|
170
|
+
const SELF_TEST_REPORT_DISALLOWED_FIELD_TERMS = [
|
|
171
|
+
"Actual Evidence"
|
|
172
|
+
];
|
|
173
|
+
const IMPLEMENTATION_MAINLINE_DISALLOWED_SECTION_TERMS = [
|
|
174
|
+
"debug log",
|
|
175
|
+
"operator log",
|
|
176
|
+
"operation log",
|
|
177
|
+
"evidence dump",
|
|
178
|
+
"screenshot index",
|
|
179
|
+
"full screenshot",
|
|
180
|
+
"runbook body",
|
|
181
|
+
"failed attempts",
|
|
182
|
+
"failure exploration",
|
|
183
|
+
"diagnostic attempts",
|
|
184
|
+
"diagnostic path",
|
|
185
|
+
"remote operation log",
|
|
186
|
+
"recovery notes",
|
|
187
|
+
"task changelog",
|
|
188
|
+
"old task",
|
|
189
|
+
"historical task",
|
|
190
|
+
"调试日志",
|
|
191
|
+
"操作日志",
|
|
192
|
+
"证据正文",
|
|
193
|
+
"截图索引",
|
|
194
|
+
"失败探索",
|
|
195
|
+
"诊断尝试",
|
|
196
|
+
"诊断路径",
|
|
197
|
+
"恢复备注",
|
|
198
|
+
"旧 task",
|
|
199
|
+
"历史 task"
|
|
200
|
+
];
|
|
201
|
+
const HARD_CONSTRAINT_TERMS = [
|
|
202
|
+
"hard constraint",
|
|
203
|
+
"hard constraints",
|
|
204
|
+
"strategy constraint",
|
|
205
|
+
"strategy constraints",
|
|
206
|
+
"恢复硬约束",
|
|
207
|
+
"硬约束",
|
|
208
|
+
"策略约束"
|
|
209
|
+
];
|
|
210
|
+
const STRATEGY_CHANGING_DECISION_GROUPS = [
|
|
211
|
+
[
|
|
212
|
+
"session persistence/reset",
|
|
213
|
+
[
|
|
214
|
+
"session persistence",
|
|
215
|
+
"session reset",
|
|
216
|
+
"session_reset",
|
|
217
|
+
"session 异常",
|
|
218
|
+
"rule_assumption_gap",
|
|
219
|
+
"operator_induced_logout_or_session_reset",
|
|
220
|
+
"operator induced logout",
|
|
221
|
+
"logout",
|
|
222
|
+
"logged out",
|
|
223
|
+
"退登",
|
|
224
|
+
"qr",
|
|
225
|
+
"二维码",
|
|
226
|
+
"扫码",
|
|
227
|
+
"重新扫码"
|
|
228
|
+
]
|
|
229
|
+
],
|
|
230
|
+
[
|
|
231
|
+
"canonical path change",
|
|
232
|
+
[
|
|
233
|
+
"canonical path changed",
|
|
234
|
+
"canonical path change",
|
|
235
|
+
"canonical path switched",
|
|
236
|
+
"canonical path reset",
|
|
237
|
+
"主路径变化",
|
|
238
|
+
"主路径切换",
|
|
239
|
+
"恢复路径变化"
|
|
240
|
+
]
|
|
241
|
+
],
|
|
242
|
+
[
|
|
243
|
+
"do-not-retry decision",
|
|
244
|
+
[
|
|
245
|
+
"do not retry",
|
|
246
|
+
"do-not-retry",
|
|
247
|
+
"don't retry",
|
|
248
|
+
"not retry",
|
|
249
|
+
"must not retry",
|
|
250
|
+
"不得重试",
|
|
251
|
+
"不要重试",
|
|
252
|
+
"不要再试",
|
|
253
|
+
"不要重复",
|
|
254
|
+
"不得直接"
|
|
255
|
+
]
|
|
256
|
+
]
|
|
257
|
+
];
|
|
149
258
|
const SELF_TEST_OBSERVABLE_EVIDENCE_TERMS = [
|
|
150
259
|
"pass output",
|
|
151
260
|
"response",
|
|
@@ -305,6 +414,27 @@ const REVIEW_READINESS_FIELDS = [
|
|
|
305
414
|
"Testing Handoff Readiness"
|
|
306
415
|
];
|
|
307
416
|
const SELF_TEST_CONTRACT_STATUSES = new Set(["required", "not_applicable"]);
|
|
417
|
+
const SELF_TEST_GRAPH_NODE_KINDS = new Set(["entry", "checkpoint", "branch", "scenario", "observable_exit"]);
|
|
418
|
+
const SELF_TEST_GRAPH_ORDINARY_NODE_LIMIT = 12;
|
|
419
|
+
const SELF_TEST_GRAPH_HIGH_RISK_NODE_LIMIT = 25;
|
|
420
|
+
const SELF_TEST_GRAPH_EVIDENCE_BODY_TERMS = [
|
|
421
|
+
"```",
|
|
422
|
+
"|---",
|
|
423
|
+
"actual evidence",
|
|
424
|
+
"command transcript",
|
|
425
|
+
"full command output",
|
|
426
|
+
"stdout",
|
|
427
|
+
"stderr",
|
|
428
|
+
"traceback",
|
|
429
|
+
"debug log",
|
|
430
|
+
"operator log",
|
|
431
|
+
"evidence dump",
|
|
432
|
+
"screenshot process",
|
|
433
|
+
"截图过程",
|
|
434
|
+
"调试日志",
|
|
435
|
+
"操作日志",
|
|
436
|
+
"证据正文"
|
|
437
|
+
];
|
|
308
438
|
const RFC_SELF_TEST_TRIGGER_TERMS = [
|
|
309
439
|
"entry/exit",
|
|
310
440
|
"runnable entry",
|
|
@@ -319,6 +449,11 @@ const RFC_SELF_TEST_TRIGGER_TERMS = [
|
|
|
319
449
|
"handoff",
|
|
320
450
|
"blocker",
|
|
321
451
|
"module key test path",
|
|
452
|
+
"module key test graph",
|
|
453
|
+
"module_key_test_graph",
|
|
454
|
+
"checkpoint",
|
|
455
|
+
"observable exit",
|
|
456
|
+
"evidence refs",
|
|
322
457
|
"test route",
|
|
323
458
|
"test path",
|
|
324
459
|
"debug path",
|
|
@@ -372,6 +507,9 @@ async function validateHarness(projectRoot) {
|
|
|
372
507
|
errors.push(`missing ${required}`);
|
|
373
508
|
}
|
|
374
509
|
}
|
|
510
|
+
const phaseContractPath = path.join(projectRoot, harnessPath(root, "pjsdlc_managed", "policies", "phase_contracts.yaml"));
|
|
511
|
+
const phaseContracts = await readYamlObject(phaseContractPath);
|
|
512
|
+
errors.push(...validatePhaseTransitionContract(phaseContracts));
|
|
375
513
|
return { info: [`validate-harness checked ${projectRoot} (${root})`], errors };
|
|
376
514
|
}
|
|
377
515
|
async function validateCurrent(projectRoot) {
|
|
@@ -1002,6 +1140,205 @@ function isNotApplicableRuntimeTask(task) {
|
|
|
1002
1140
|
const targetRuntime = isRecord(task.target_runtime_environment) ? task.target_runtime_environment : undefined;
|
|
1003
1141
|
return String(evidenceLevel?.required ?? "") === "unit" && String(targetRuntime?.kind ?? "") === "not_applicable";
|
|
1004
1142
|
}
|
|
1143
|
+
function selfTestGraphEvidenceRefIsBody(value) {
|
|
1144
|
+
const trimmed = value.trim();
|
|
1145
|
+
const lowered = trimmed.toLowerCase();
|
|
1146
|
+
return trimmed.includes("\n") || trimmed.length > 180 || SELF_TEST_GRAPH_EVIDENCE_BODY_TERMS.some((term) => lowered.includes(term));
|
|
1147
|
+
}
|
|
1148
|
+
function graphHasCycle(nodeIds, adjacency) {
|
|
1149
|
+
const visiting = new Set();
|
|
1150
|
+
const visited = new Set();
|
|
1151
|
+
const visit = (nodeId) => {
|
|
1152
|
+
if (visiting.has(nodeId))
|
|
1153
|
+
return true;
|
|
1154
|
+
if (visited.has(nodeId))
|
|
1155
|
+
return false;
|
|
1156
|
+
visiting.add(nodeId);
|
|
1157
|
+
for (const target of adjacency.get(nodeId) ?? []) {
|
|
1158
|
+
if (visit(target))
|
|
1159
|
+
return true;
|
|
1160
|
+
}
|
|
1161
|
+
visiting.delete(nodeId);
|
|
1162
|
+
visited.add(nodeId);
|
|
1163
|
+
return false;
|
|
1164
|
+
};
|
|
1165
|
+
for (const nodeId of nodeIds) {
|
|
1166
|
+
if (!visited.has(nodeId) && visit(nodeId))
|
|
1167
|
+
return true;
|
|
1168
|
+
}
|
|
1169
|
+
return false;
|
|
1170
|
+
}
|
|
1171
|
+
function reachableFrom(entryId, adjacency) {
|
|
1172
|
+
const reached = new Set();
|
|
1173
|
+
const stack = [entryId];
|
|
1174
|
+
while (stack.length > 0) {
|
|
1175
|
+
const nodeId = stack.pop();
|
|
1176
|
+
if (reached.has(nodeId))
|
|
1177
|
+
continue;
|
|
1178
|
+
reached.add(nodeId);
|
|
1179
|
+
stack.push(...(adjacency.get(nodeId) ?? []));
|
|
1180
|
+
}
|
|
1181
|
+
return reached;
|
|
1182
|
+
}
|
|
1183
|
+
function nodesThatCanReachExits(nodeIds, adjacency, exits) {
|
|
1184
|
+
const reverse = new Map();
|
|
1185
|
+
for (const nodeId of nodeIds)
|
|
1186
|
+
reverse.set(nodeId, []);
|
|
1187
|
+
for (const [source, targets] of adjacency.entries()) {
|
|
1188
|
+
for (const target of targets) {
|
|
1189
|
+
const incoming = reverse.get(target) ?? [];
|
|
1190
|
+
incoming.push(source);
|
|
1191
|
+
reverse.set(target, incoming);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
const reached = new Set();
|
|
1195
|
+
const stack = [...exits];
|
|
1196
|
+
while (stack.length > 0) {
|
|
1197
|
+
const nodeId = stack.pop();
|
|
1198
|
+
if (reached.has(nodeId))
|
|
1199
|
+
continue;
|
|
1200
|
+
reached.add(nodeId);
|
|
1201
|
+
stack.push(...(reverse.get(nodeId) ?? []));
|
|
1202
|
+
}
|
|
1203
|
+
return reached;
|
|
1204
|
+
}
|
|
1205
|
+
function validateSelfTestGraph(taskId, contract, scenarioIds, highRiskRuntime) {
|
|
1206
|
+
const errors = [];
|
|
1207
|
+
const graphRequired = contract.graph_required;
|
|
1208
|
+
if (graphRequired !== undefined && typeof graphRequired !== "boolean") {
|
|
1209
|
+
errors.push(`${taskId} self_test_contract.graph_required must be a boolean when set`);
|
|
1210
|
+
}
|
|
1211
|
+
const graph = contract.module_key_test_graph;
|
|
1212
|
+
if (graphRequired === true && !isRecord(graph)) {
|
|
1213
|
+
errors.push(`${taskId} self_test_contract.module_key_test_graph is required when graph_required is true`);
|
|
1214
|
+
}
|
|
1215
|
+
if (graph === undefined)
|
|
1216
|
+
return errors;
|
|
1217
|
+
if (!isRecord(graph)) {
|
|
1218
|
+
errors.push(`${taskId} self_test_contract.module_key_test_graph must be a mapping`);
|
|
1219
|
+
return errors;
|
|
1220
|
+
}
|
|
1221
|
+
const nodes = graph.nodes;
|
|
1222
|
+
const edges = graph.edges;
|
|
1223
|
+
if (!Array.isArray(nodes) || nodes.length === 0) {
|
|
1224
|
+
errors.push(`${taskId} self_test_contract.module_key_test_graph.nodes must be a non-empty list`);
|
|
1225
|
+
return errors;
|
|
1226
|
+
}
|
|
1227
|
+
if (!Array.isArray(edges) || edges.length === 0) {
|
|
1228
|
+
errors.push(`${taskId} self_test_contract.module_key_test_graph.edges must be a non-empty list`);
|
|
1229
|
+
return errors;
|
|
1230
|
+
}
|
|
1231
|
+
const nodeLimit = highRiskRuntime ? SELF_TEST_GRAPH_HIGH_RISK_NODE_LIMIT : SELF_TEST_GRAPH_ORDINARY_NODE_LIMIT;
|
|
1232
|
+
if (nodes.length > nodeLimit) {
|
|
1233
|
+
errors.push(`${taskId} self_test_contract.module_key_test_graph has ${nodes.length} nodes; keep ordinary graphs <= ${SELF_TEST_GRAPH_ORDINARY_NODE_LIMIT} nodes and high-risk graphs <= ${SELF_TEST_GRAPH_HIGH_RISK_NODE_LIMIT} nodes`);
|
|
1234
|
+
}
|
|
1235
|
+
const nodeIds = new Set();
|
|
1236
|
+
const entryIds = [];
|
|
1237
|
+
const observableExitIds = new Set();
|
|
1238
|
+
const scenarioNodesByRef = new Map();
|
|
1239
|
+
const adjacency = new Map();
|
|
1240
|
+
nodes.forEach((node, index) => {
|
|
1241
|
+
if (!isRecord(node)) {
|
|
1242
|
+
errors.push(`${taskId} self_test_contract.module_key_test_graph.nodes[${index}] must be a mapping`);
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
const nodeId = String(node.id ?? "").trim();
|
|
1246
|
+
const kind = String(node.kind ?? "").trim();
|
|
1247
|
+
const label = String(node.label ?? "").trim();
|
|
1248
|
+
if (!nodeId) {
|
|
1249
|
+
errors.push(`${taskId} self_test_contract.module_key_test_graph.nodes[${index}].id must be set`);
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
if (nodeIds.has(nodeId)) {
|
|
1253
|
+
errors.push(`${taskId} self_test_contract.module_key_test_graph node id must be unique: ${nodeId}`);
|
|
1254
|
+
}
|
|
1255
|
+
nodeIds.add(nodeId);
|
|
1256
|
+
adjacency.set(nodeId, adjacency.get(nodeId) ?? []);
|
|
1257
|
+
if (!SELF_TEST_GRAPH_NODE_KINDS.has(kind)) {
|
|
1258
|
+
errors.push(`${taskId} self_test_contract.module_key_test_graph.nodes[${nodeId}].kind must be one of ${[...SELF_TEST_GRAPH_NODE_KINDS].join(", ")}`);
|
|
1259
|
+
}
|
|
1260
|
+
if (!label || isPlaceholderEvidence(label)) {
|
|
1261
|
+
errors.push(`${taskId} self_test_contract.module_key_test_graph.nodes[${nodeId}].label must be concrete`);
|
|
1262
|
+
}
|
|
1263
|
+
if (kind === "entry")
|
|
1264
|
+
entryIds.push(nodeId);
|
|
1265
|
+
if (kind === "observable_exit")
|
|
1266
|
+
observableExitIds.add(nodeId);
|
|
1267
|
+
if (kind === "scenario") {
|
|
1268
|
+
const scenarioRef = String(node.scenario_ref ?? "").trim();
|
|
1269
|
+
if (!scenarioRef) {
|
|
1270
|
+
errors.push(`${taskId} self_test_contract.module_key_test_graph scenario node ${nodeId} must set scenario_ref`);
|
|
1271
|
+
}
|
|
1272
|
+
else if (!scenarioIds.has(scenarioRef)) {
|
|
1273
|
+
errors.push(`${taskId} self_test_contract.module_key_test_graph scenario node ${nodeId} references unknown scenario: ${scenarioRef}`);
|
|
1274
|
+
}
|
|
1275
|
+
else {
|
|
1276
|
+
const current = scenarioNodesByRef.get(scenarioRef) ?? [];
|
|
1277
|
+
current.push(nodeId);
|
|
1278
|
+
scenarioNodesByRef.set(scenarioRef, current);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
if (node.evidence_ref !== undefined) {
|
|
1282
|
+
const evidenceRef = String(node.evidence_ref).trim();
|
|
1283
|
+
if (!evidenceRef || isPlaceholderEvidence(evidenceRef) || selfTestGraphEvidenceRefIsBody(evidenceRef)) {
|
|
1284
|
+
errors.push(`${taskId} self_test_contract.module_key_test_graph.nodes[${nodeId}].evidence_ref must be a short evidence pointer, not evidence body`);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
});
|
|
1288
|
+
if (entryIds.length !== 1) {
|
|
1289
|
+
errors.push(`${taskId} self_test_contract.module_key_test_graph must have exactly one entry node`);
|
|
1290
|
+
}
|
|
1291
|
+
if (observableExitIds.size === 0) {
|
|
1292
|
+
errors.push(`${taskId} self_test_contract.module_key_test_graph must have at least one observable_exit node`);
|
|
1293
|
+
}
|
|
1294
|
+
edges.forEach((edge, index) => {
|
|
1295
|
+
if (!isRecord(edge)) {
|
|
1296
|
+
errors.push(`${taskId} self_test_contract.module_key_test_graph.edges[${index}] must be a mapping`);
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
const source = String(edge.from ?? "").trim();
|
|
1300
|
+
const target = String(edge.to ?? "").trim();
|
|
1301
|
+
if (!nodeIds.has(source)) {
|
|
1302
|
+
errors.push(`${taskId} self_test_contract.module_key_test_graph edge references unknown from node: ${source || "<empty>"}`);
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
if (!nodeIds.has(target)) {
|
|
1306
|
+
errors.push(`${taskId} self_test_contract.module_key_test_graph edge references unknown to node: ${target || "<empty>"}`);
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
const current = adjacency.get(source) ?? [];
|
|
1310
|
+
current.push(target);
|
|
1311
|
+
adjacency.set(source, current);
|
|
1312
|
+
});
|
|
1313
|
+
if (graphHasCycle(nodeIds, adjacency)) {
|
|
1314
|
+
errors.push(`${taskId} self_test_contract.module_key_test_graph must be a DAG; cycles are not allowed`);
|
|
1315
|
+
}
|
|
1316
|
+
let reachedFromEntry = new Set();
|
|
1317
|
+
if (entryIds.length === 1) {
|
|
1318
|
+
reachedFromEntry = reachableFrom(entryIds[0], adjacency);
|
|
1319
|
+
const unreachable = [...nodeIds].filter((nodeId) => !reachedFromEntry.has(nodeId)).sort();
|
|
1320
|
+
if (unreachable.length > 0) {
|
|
1321
|
+
errors.push(`${taskId} self_test_contract.module_key_test_graph nodes must be reachable from entry: ${unreachable.join(", ")}`);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
const canReachExit = nodesThatCanReachExits(nodeIds, adjacency, observableExitIds);
|
|
1325
|
+
for (const scenarioId of [...scenarioIds].sort()) {
|
|
1326
|
+
const scenarioNodeIds = scenarioNodesByRef.get(scenarioId) ?? [];
|
|
1327
|
+
if (scenarioNodeIds.length === 0) {
|
|
1328
|
+
errors.push(`${taskId} self_test_contract.module_key_test_graph must include a scenario node for ${scenarioId}`);
|
|
1329
|
+
continue;
|
|
1330
|
+
}
|
|
1331
|
+
for (const nodeId of scenarioNodeIds) {
|
|
1332
|
+
if (reachedFromEntry.size > 0 && !reachedFromEntry.has(nodeId)) {
|
|
1333
|
+
errors.push(`${taskId} self_test_contract.module_key_test_graph scenario ${scenarioId} must be reachable from entry`);
|
|
1334
|
+
}
|
|
1335
|
+
if (!canReachExit.has(nodeId)) {
|
|
1336
|
+
errors.push(`${taskId} self_test_contract.module_key_test_graph scenario ${scenarioId} must reach an observable_exit`);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
return errors;
|
|
1341
|
+
}
|
|
1005
1342
|
function validateSelfTestContract(task, requiredForRunnableBoundary) {
|
|
1006
1343
|
const errors = [];
|
|
1007
1344
|
const taskId = String(task.id ?? "Task");
|
|
@@ -1055,6 +1392,7 @@ function validateSelfTestContract(task, requiredForRunnableBoundary) {
|
|
|
1055
1392
|
errors.push(`${taskId} self_test_contract.scenarios must be a non-empty list`);
|
|
1056
1393
|
}
|
|
1057
1394
|
const seen = new Set();
|
|
1395
|
+
const scenarioIds = new Set();
|
|
1058
1396
|
scenarios.forEach((scenario, index) => {
|
|
1059
1397
|
if (!isRecord(scenario)) {
|
|
1060
1398
|
errors.push(`${taskId} self_test_contract.scenarios[${index}] must be a mapping`);
|
|
@@ -1067,6 +1405,9 @@ function validateSelfTestContract(task, requiredForRunnableBoundary) {
|
|
|
1067
1405
|
else if (seen.has(scenarioId)) {
|
|
1068
1406
|
errors.push(`${taskId} self_test_contract scenario id must be unique: ${scenarioId}`);
|
|
1069
1407
|
}
|
|
1408
|
+
else {
|
|
1409
|
+
scenarioIds.add(scenarioId);
|
|
1410
|
+
}
|
|
1070
1411
|
seen.add(scenarioId);
|
|
1071
1412
|
for (const field of ["entry", "expected_exit", "evidence"]) {
|
|
1072
1413
|
if (typeof scenario[field] !== "string" || !String(scenario[field]).trim() || isPlaceholderEvidence(String(scenario[field]))) {
|
|
@@ -1074,6 +1415,7 @@ function validateSelfTestContract(task, requiredForRunnableBoundary) {
|
|
|
1074
1415
|
}
|
|
1075
1416
|
}
|
|
1076
1417
|
});
|
|
1418
|
+
errors.push(...validateSelfTestGraph(taskId, contract, scenarioIds, requiresResumeCapsule(task)));
|
|
1077
1419
|
return errors;
|
|
1078
1420
|
}
|
|
1079
1421
|
async function validateSelfTestContractTechPlanBinding(projectRoot, task, normalizedTechRefs) {
|
|
@@ -1101,6 +1443,9 @@ async function validateSelfTestContractTechPlanBinding(projectRoot, task, normal
|
|
|
1101
1443
|
if (!containsAny(section, MODULE_KEY_TEST_PATH_TERMS)) {
|
|
1102
1444
|
errors.push(`${taskId} tech plan Development Self-Test Contract must include Module key test path: ${source}`);
|
|
1103
1445
|
}
|
|
1446
|
+
if (contract.graph_required === true && !containsAny(section, MODULE_KEY_TEST_GRAPH_TERMS)) {
|
|
1447
|
+
errors.push(`${taskId} tech plan Development Self-Test Contract must include Module Key Test Graph when graph_required is true: ${source}`);
|
|
1448
|
+
}
|
|
1104
1449
|
for (const scenario of Array.isArray(contract.scenarios) ? contract.scenarios.filter(isRecord) : []) {
|
|
1105
1450
|
const scenarioId = String(scenario.id ?? "").trim();
|
|
1106
1451
|
if (scenarioId && !section.includes(scenarioId)) {
|
|
@@ -1317,6 +1662,95 @@ function matchesGlob(file, pattern) {
|
|
|
1317
1662
|
function isRecord(value) {
|
|
1318
1663
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1319
1664
|
}
|
|
1665
|
+
function validatePhaseTransitionContract(contract) {
|
|
1666
|
+
const errors = [];
|
|
1667
|
+
const phases = contract.phases;
|
|
1668
|
+
if (!isRecord(phases)) {
|
|
1669
|
+
return ["phase_contracts.yaml must contain phases"];
|
|
1670
|
+
}
|
|
1671
|
+
for (const [phaseName, phaseContract] of Object.entries(phases)) {
|
|
1672
|
+
if (!isRecord(phaseContract)) {
|
|
1673
|
+
errors.push(`${phaseName} phase contract must be a mapping`);
|
|
1674
|
+
continue;
|
|
1675
|
+
}
|
|
1676
|
+
for (const legacyKey of ["next", "returns"]) {
|
|
1677
|
+
if (legacyKey in phaseContract) {
|
|
1678
|
+
errors.push(`${phaseName} must not define legacy ${legacyKey}; use top-level transitions`);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
const transitions = contract.transitions;
|
|
1683
|
+
if (!Array.isArray(transitions)) {
|
|
1684
|
+
errors.push("phase_contracts.yaml must contain top-level transitions");
|
|
1685
|
+
return errors;
|
|
1686
|
+
}
|
|
1687
|
+
const phaseNames = new Set(Object.keys(phases));
|
|
1688
|
+
const seen = new Set();
|
|
1689
|
+
const outgoing = new Set();
|
|
1690
|
+
transitions.forEach((transition, index) => {
|
|
1691
|
+
const prefix = `transition #${index + 1}`;
|
|
1692
|
+
if (!isRecord(transition)) {
|
|
1693
|
+
errors.push(`${prefix} must be a mapping`);
|
|
1694
|
+
return;
|
|
1695
|
+
}
|
|
1696
|
+
const fromPhase = String(transition.from ?? "");
|
|
1697
|
+
const toPhase = String(transition.to ?? "");
|
|
1698
|
+
const trigger = String(transition.trigger ?? "");
|
|
1699
|
+
const kind = String(transition.kind ?? "");
|
|
1700
|
+
for (const [field, value] of [
|
|
1701
|
+
["from", fromPhase],
|
|
1702
|
+
["to", toPhase],
|
|
1703
|
+
["trigger", trigger],
|
|
1704
|
+
["kind", kind]
|
|
1705
|
+
]) {
|
|
1706
|
+
if (!value.trim())
|
|
1707
|
+
errors.push(`${prefix} missing ${field}`);
|
|
1708
|
+
}
|
|
1709
|
+
if (!fromPhase || !toPhase || !trigger || !kind)
|
|
1710
|
+
return;
|
|
1711
|
+
if (!phaseNames.has(fromPhase)) {
|
|
1712
|
+
errors.push(`${prefix} from references unknown phase: ${fromPhase}`);
|
|
1713
|
+
}
|
|
1714
|
+
if (toPhase === RESERVED_SUSPENDED_PHASE_TARGET) {
|
|
1715
|
+
if (fromPhase !== "BLOCKED" || kind !== "resume") {
|
|
1716
|
+
errors.push(`${prefix} may use ${RESERVED_SUSPENDED_PHASE_TARGET} only for BLOCKED resume`);
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
else if (!phaseNames.has(toPhase)) {
|
|
1720
|
+
errors.push(`${prefix} to references unknown phase: ${toPhase}`);
|
|
1721
|
+
}
|
|
1722
|
+
if (!TRANSITION_KINDS.has(kind)) {
|
|
1723
|
+
errors.push(`${prefix} has invalid kind: ${kind}`);
|
|
1724
|
+
}
|
|
1725
|
+
const duplicateKey = `${fromPhase}\0${toPhase}\0${trigger}`;
|
|
1726
|
+
if (seen.has(duplicateKey)) {
|
|
1727
|
+
errors.push(`${prefix} duplicates transition ${fromPhase} -> ${toPhase} (${trigger})`);
|
|
1728
|
+
}
|
|
1729
|
+
seen.add(duplicateKey);
|
|
1730
|
+
outgoing.add(fromPhase);
|
|
1731
|
+
if ("effects" in transition && transition.effects !== undefined) {
|
|
1732
|
+
if (!isRecord(transition.effects)) {
|
|
1733
|
+
errors.push(`${prefix} effects must be a mapping`);
|
|
1734
|
+
}
|
|
1735
|
+
else {
|
|
1736
|
+
for (const [effectName, effectValue] of Object.entries(transition.effects)) {
|
|
1737
|
+
if (!["set_suspended_phase", "clear_suspended_phase"].includes(effectName)) {
|
|
1738
|
+
errors.push(`${prefix} has unknown effect: ${effectName}`);
|
|
1739
|
+
}
|
|
1740
|
+
if (typeof effectValue !== "boolean") {
|
|
1741
|
+
errors.push(`${prefix} effect ${effectName} must be boolean`);
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
});
|
|
1747
|
+
for (const phaseName of phaseNames) {
|
|
1748
|
+
if (!outgoing.has(phaseName)) {
|
|
1749
|
+
errors.push(`${phaseName} must have at least one outgoing transition`);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
return errors;
|
|
1753
|
+
}
|
|
1320
1754
|
async function readYamlObject(filePath) {
|
|
1321
1755
|
if (!(await pathExists(filePath)))
|
|
1322
1756
|
return {};
|
|
@@ -1375,7 +1809,10 @@ async function validateCurrentTaskDevelopmentEvidence(projectRoot, plan) {
|
|
|
1375
1809
|
return [`${taskId} implementation_doc is missing: ${implementationDoc}`];
|
|
1376
1810
|
}
|
|
1377
1811
|
const text = await readText(docPath);
|
|
1378
|
-
return
|
|
1812
|
+
return [
|
|
1813
|
+
...validateDevelopmentEvidenceText(text, currentTask, implementationDoc),
|
|
1814
|
+
...(await validateHighRiskRecoveryBoundaries(projectRoot, text, plan, currentTask, implementationDoc))
|
|
1815
|
+
];
|
|
1379
1816
|
}
|
|
1380
1817
|
function currentOpenSprintTask(plan) {
|
|
1381
1818
|
const currentTaskId = String(plan.current_task_id ?? "");
|
|
@@ -1459,18 +1896,24 @@ function validateDevelopmentSelfTestReport(fullText, developmentEvidenceSection,
|
|
|
1459
1896
|
else if (reportStatus !== "PASS") {
|
|
1460
1897
|
errors.push(`${taskId} Development Self-Test Report Report Status is ${reportStatus}; validate-dev cannot handoff until the report status is PASS`);
|
|
1461
1898
|
}
|
|
1899
|
+
const highRiskRuntime = requiresResumeCapsule(task);
|
|
1462
1900
|
errors.push(...validateSelfTestReportBoundary(report, taskId, implementationDoc));
|
|
1901
|
+
errors.push(...validateSelfTestReportLength(report, taskId, implementationDoc, highRiskRuntime));
|
|
1463
1902
|
const basicSelfTest = evidenceFieldValue(developmentEvidenceSection, "Basic Self-test Evidence") ?? "";
|
|
1464
1903
|
if (!containsAny(basicSelfTest, ["Development Self-Test Report", "开发自测报告", "self-test report"])) {
|
|
1465
1904
|
errors.push(`${taskId} Basic Self-test Evidence must reference the Development Self-Test Report in ${implementationDoc}`);
|
|
1466
1905
|
}
|
|
1467
|
-
for (const field of
|
|
1906
|
+
for (const field of SELF_TEST_REPORT_REQUIRED_FIELDS) {
|
|
1468
1907
|
const value = evidenceFieldValue(report, field);
|
|
1469
|
-
const allowsNone = field
|
|
1908
|
+
const allowsNone = SELF_TEST_REPORT_NONE_ALLOWED_FIELDS.has(field);
|
|
1470
1909
|
if (!value || (!allowsNone && isPlaceholderEvidence(value))) {
|
|
1471
1910
|
errors.push(`${taskId} Development Self-Test Report ${field} must contain executed evidence in ${implementationDoc}`);
|
|
1472
1911
|
}
|
|
1473
1912
|
}
|
|
1913
|
+
const evidenceIndexRefs = evidenceFieldValue(report, "Evidence Index Refs") ?? "";
|
|
1914
|
+
if (highRiskRuntime && !evidenceIndexRefs.includes(RUNBOOK_DOC_PREFIX)) {
|
|
1915
|
+
errors.push(`${taskId} high-risk Development Self-Test Report Evidence Index Refs must link evidence under ${RUNBOOK_DOC_PREFIX} in ${implementationDoc}`);
|
|
1916
|
+
}
|
|
1474
1917
|
const source = String(contract.source ?? "").trim();
|
|
1475
1918
|
if (source && !report.includes(source)) {
|
|
1476
1919
|
errors.push(`${taskId} Development Self-Test Report must reference contract source ${source} in ${implementationDoc}`);
|
|
@@ -1484,10 +1927,18 @@ function validateDevelopmentSelfTestReport(fullText, developmentEvidenceSection,
|
|
|
1484
1927
|
if (isPlaceholderSelfTestReportValue(moduleKeyTestPath) || isTemplateModuleKeyTestPath(moduleKeyTestPath)) {
|
|
1485
1928
|
errors.push(`${taskId} Development Self-Test Report Module Key Test Path must replace template placeholders with actual executed path evidence in ${implementationDoc}`);
|
|
1486
1929
|
}
|
|
1930
|
+
const graphRequired = contract.graph_required === true || isRecord(contract.module_key_test_graph);
|
|
1931
|
+
const moduleKeyTestGraph = (markdownSection(report, MODULE_KEY_TEST_GRAPH_TERMS) ?? evidenceFieldValue(report, "Module Key Test Graph") ?? "").trim();
|
|
1932
|
+
if (graphRequired && (!moduleKeyTestGraph || isPlaceholderSelfTestReportValue(moduleKeyTestGraph))) {
|
|
1933
|
+
errors.push(`${taskId} Development Self-Test Report must include Module Key Test Graph because self_test_contract graph is required or present in ${implementationDoc}`);
|
|
1934
|
+
}
|
|
1487
1935
|
const runnableEntry = String(contract.runnable_entry ?? "").trim();
|
|
1488
1936
|
if (runnableEntry && !moduleKeyTestPath.includes(runnableEntry)) {
|
|
1489
1937
|
errors.push(`${taskId} Development Self-Test Report Module Key Test Path must include runnable entry ${runnableEntry} in ${implementationDoc}`);
|
|
1490
1938
|
}
|
|
1939
|
+
if (graphRequired && moduleKeyTestGraph && runnableEntry && !moduleKeyTestGraph.includes(runnableEntry)) {
|
|
1940
|
+
errors.push(`${taskId} Development Self-Test Report Module Key Test Graph must include runnable entry ${runnableEntry} in ${implementationDoc}`);
|
|
1941
|
+
}
|
|
1491
1942
|
const scenarios = Array.isArray(contract.scenarios) ? contract.scenarios.filter(isRecord) : [];
|
|
1492
1943
|
const exitEvidenceTerms = [
|
|
1493
1944
|
String(contract.observable_exit ?? "").trim(),
|
|
@@ -1501,6 +1952,13 @@ function validateDevelopmentSelfTestReport(fullText, developmentEvidenceSection,
|
|
|
1501
1952
|
&& !containsAny(moduleKeyTestPath, SELF_TEST_OBSERVABLE_EVIDENCE_TERMS)) {
|
|
1502
1953
|
errors.push(`${taskId} Development Self-Test Report Module Key Test Path must include observable exit or evidence from self_test_contract in ${implementationDoc}`);
|
|
1503
1954
|
}
|
|
1955
|
+
if (graphRequired
|
|
1956
|
+
&& moduleKeyTestGraph
|
|
1957
|
+
&& exitEvidenceTerms.length > 0
|
|
1958
|
+
&& !exitEvidenceTerms.some((term) => normalizedIncludes(moduleKeyTestGraph, term))
|
|
1959
|
+
&& !containsAny(moduleKeyTestGraph, ["observable_exit", "observable exit", "exit", "出口"])) {
|
|
1960
|
+
errors.push(`${taskId} Development Self-Test Report Module Key Test Graph must include observable exit or evidence from self_test_contract in ${implementationDoc}`);
|
|
1961
|
+
}
|
|
1504
1962
|
for (const scenario of scenarios) {
|
|
1505
1963
|
const scenarioId = String(scenario.id ?? "").trim();
|
|
1506
1964
|
if (!scenarioId)
|
|
@@ -1508,6 +1966,9 @@ function validateDevelopmentSelfTestReport(fullText, developmentEvidenceSection,
|
|
|
1508
1966
|
if (!moduleKeyTestPath.includes(scenarioId)) {
|
|
1509
1967
|
errors.push(`${taskId} Development Self-Test Report Module Key Test Path must include scenario ${scenarioId} in ${implementationDoc}`);
|
|
1510
1968
|
}
|
|
1969
|
+
if (graphRequired && moduleKeyTestGraph && !moduleKeyTestGraph.includes(scenarioId)) {
|
|
1970
|
+
errors.push(`${taskId} Development Self-Test Report Module Key Test Graph must include scenario ${scenarioId} in ${implementationDoc}`);
|
|
1971
|
+
}
|
|
1511
1972
|
const status = scenarioStatus(report, scenarioId);
|
|
1512
1973
|
if (!status) {
|
|
1513
1974
|
errors.push(`${taskId} Development Self-Test Report must record scenario ${scenarioId} as PASS, BLOCKED, IN_PROGRESS, or STALE in ${implementationDoc}`);
|
|
@@ -1531,7 +1992,7 @@ function validateDevelopmentSelfTestReport(fullText, developmentEvidenceSection,
|
|
|
1531
1992
|
errors.push(`${taskId} page Development Self-Test Report must include browser, Playwright, screenshot, or equivalent interaction evidence in ${implementationDoc}`);
|
|
1532
1993
|
}
|
|
1533
1994
|
}
|
|
1534
|
-
if (
|
|
1995
|
+
if (highRiskRuntime) {
|
|
1535
1996
|
errors.push(...validateCurrentOperatorPath(fullText, taskId, implementationDoc));
|
|
1536
1997
|
errors.push(...validateGateBreakdown(fullText, taskId, implementationDoc));
|
|
1537
1998
|
}
|
|
@@ -1546,6 +2007,14 @@ function normalizeSelfTestReportStatus(value) {
|
|
|
1546
2007
|
function validateSelfTestReportBoundary(report, taskId, implementationDoc) {
|
|
1547
2008
|
const errors = [];
|
|
1548
2009
|
for (const line of report.split(/\r?\n/)) {
|
|
2010
|
+
const fieldMatch = line.match(/^\s*[-*]\s*([^:]+)\s*:/);
|
|
2011
|
+
if (fieldMatch) {
|
|
2012
|
+
const field = fieldMatch[1].trim().toLowerCase();
|
|
2013
|
+
const blockedField = SELF_TEST_REPORT_DISALLOWED_FIELD_TERMS.find((term) => field === term.toLowerCase());
|
|
2014
|
+
if (blockedField) {
|
|
2015
|
+
errors.push(`${taskId} Development Self-Test Report must not use ${blockedField}; put evidence bodies in an Evidence Index and reference them with Evidence Index Refs in ${implementationDoc}`);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
1549
2018
|
const match = line.match(/^(#{1,6})\s+(.+)$/);
|
|
1550
2019
|
if (!match)
|
|
1551
2020
|
continue;
|
|
@@ -1557,6 +2026,94 @@ function validateSelfTestReportBoundary(report, taskId, implementationDoc) {
|
|
|
1557
2026
|
}
|
|
1558
2027
|
return errors;
|
|
1559
2028
|
}
|
|
2029
|
+
function validateSelfTestReportLength(report, taskId, implementationDoc, highRiskRuntime) {
|
|
2030
|
+
const limit = highRiskRuntime ? MAX_HIGH_RISK_SELF_TEST_REPORT_LINES : MAX_SELF_TEST_REPORT_LINES;
|
|
2031
|
+
const lineCount = report.split(/\r?\n/).filter((line) => line.trim()).length;
|
|
2032
|
+
if (lineCount <= limit)
|
|
2033
|
+
return [];
|
|
2034
|
+
return [
|
|
2035
|
+
`${taskId} Development Self-Test Report must stay as a short handoff card (${limit} non-empty lines max); move logs, evidence bodies, runbook steps, and exploration history out of ${implementationDoc}`
|
|
2036
|
+
];
|
|
2037
|
+
}
|
|
2038
|
+
async function validateHighRiskRecoveryBoundaries(projectRoot, implementationText, plan, task, implementationDoc) {
|
|
2039
|
+
if (!requiresResumeCapsule(task))
|
|
2040
|
+
return [];
|
|
2041
|
+
const taskId = String(task.id ?? "current task");
|
|
2042
|
+
const errors = validateImplementationDocMainlineBoundary(implementationText, taskId, implementationDoc);
|
|
2043
|
+
errors.push(...(await validateStrategyChangingDecisionPromotion(projectRoot, implementationText, plan, task, implementationDoc)));
|
|
2044
|
+
return errors;
|
|
2045
|
+
}
|
|
2046
|
+
function validateImplementationDocMainlineBoundary(fullText, taskId, implementationDoc) {
|
|
2047
|
+
const errors = [];
|
|
2048
|
+
for (const line of fullText.split(/\r?\n/)) {
|
|
2049
|
+
const match = line.match(/^(#{1,6})\s+(.+)$/);
|
|
2050
|
+
if (!match)
|
|
2051
|
+
continue;
|
|
2052
|
+
const title = match[2].trim().toLowerCase();
|
|
2053
|
+
const blockedTerm = IMPLEMENTATION_MAINLINE_DISALLOWED_SECTION_TERMS.find((term) => title.includes(term));
|
|
2054
|
+
if (blockedTerm) {
|
|
2055
|
+
errors.push(`${taskId} implementation_doc must not keep ${blockedTerm} as a mainline section in ${implementationDoc}; move it to a runbook, evidence index, exploration appendix, or external artifact`);
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
return errors;
|
|
2059
|
+
}
|
|
2060
|
+
async function validateStrategyChangingDecisionPromotion(projectRoot, implementationText, plan, task, implementationDoc) {
|
|
2061
|
+
const taskId = String(task.id ?? "current task");
|
|
2062
|
+
const capsule = isRecord(plan.resume_capsule) ? plan.resume_capsule : {};
|
|
2063
|
+
const recoveryRefs = asStringList(capsule.recovery_refs);
|
|
2064
|
+
const runbookTexts = await readExistingRunbookRefs(projectRoot, recoveryRefs);
|
|
2065
|
+
const promotionText = [
|
|
2066
|
+
asStringList(capsule.do_not_retry).join("\n"),
|
|
2067
|
+
markdownSection(implementationText, CURRENT_OPERATOR_PATH_TERMS) ?? "",
|
|
2068
|
+
...runbookTexts.map((text) => markdownSection(text, HARD_CONSTRAINT_TERMS) ?? "")
|
|
2069
|
+
].join("\n");
|
|
2070
|
+
const sourceText = [
|
|
2071
|
+
asStringList(task.working_notes).join("\n"),
|
|
2072
|
+
removeMarkdownSections(implementationText, CURRENT_OPERATOR_PATH_TERMS),
|
|
2073
|
+
...runbookTexts.map((text) => removeMarkdownSections(text, HARD_CONSTRAINT_TERMS))
|
|
2074
|
+
].join("\n");
|
|
2075
|
+
const errors = [];
|
|
2076
|
+
for (const [label, terms] of STRATEGY_CHANGING_DECISION_GROUPS) {
|
|
2077
|
+
if (containsAny(sourceText, terms) && !containsAny(promotionText, terms)) {
|
|
2078
|
+
errors.push(`${taskId} strategy-changing ${label} judgment appears outside promoted hard constraints; promote it to resume_capsule.do_not_retry, runbook Hard Constraints, or Current Operator Path in ${implementationDoc}`);
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
return errors;
|
|
2082
|
+
}
|
|
2083
|
+
async function readExistingRunbookRefs(projectRoot, refs) {
|
|
2084
|
+
const texts = [];
|
|
2085
|
+
for (const ref of refs) {
|
|
2086
|
+
if (!ref.startsWith(RUNBOOK_DOC_PREFIX) || !ref.endsWith(".md"))
|
|
2087
|
+
continue;
|
|
2088
|
+
const fullPath = path.join(projectRoot, ref);
|
|
2089
|
+
if (await pathExists(fullPath)) {
|
|
2090
|
+
texts.push(await readText(fullPath));
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
return texts;
|
|
2094
|
+
}
|
|
2095
|
+
function removeMarkdownSections(text, headerTerms) {
|
|
2096
|
+
const lines = text.split(/\r?\n/);
|
|
2097
|
+
const kept = [];
|
|
2098
|
+
let skippingLevel = 0;
|
|
2099
|
+
for (const line of lines) {
|
|
2100
|
+
const match = line.match(/^(#{1,6})\s+(.+)$/);
|
|
2101
|
+
if (match) {
|
|
2102
|
+
const level = match[1].length;
|
|
2103
|
+
if (skippingLevel > 0 && level <= skippingLevel) {
|
|
2104
|
+
skippingLevel = 0;
|
|
2105
|
+
}
|
|
2106
|
+
const title = match[2].toLowerCase();
|
|
2107
|
+
if (skippingLevel === 0 && headerTerms.some((term) => title.includes(term.toLowerCase()))) {
|
|
2108
|
+
skippingLevel = level;
|
|
2109
|
+
continue;
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
if (skippingLevel === 0)
|
|
2113
|
+
kept.push(line);
|
|
2114
|
+
}
|
|
2115
|
+
return kept.join("\n");
|
|
2116
|
+
}
|
|
1560
2117
|
function validateCurrentOperatorPath(fullText, taskId, implementationDoc) {
|
|
1561
2118
|
const section = markdownSection(fullText, CURRENT_OPERATOR_PATH_TERMS);
|
|
1562
2119
|
if (!section) {
|
|
@@ -1568,7 +2125,8 @@ function validateCurrentOperatorPath(fullText, taskId, implementationDoc) {
|
|
|
1568
2125
|
["runbook link", ["Operator runbook", "Runbook"]],
|
|
1569
2126
|
["credential reference name", ["Credential reference", "Credential reference name"]],
|
|
1570
2127
|
["command/UI channel", ["Command/UI channel", "Command channel", "UI channel"]],
|
|
1571
|
-
["do-not-retry summary", ["Do-not-retry summary", "Do not retry summary"]]
|
|
2128
|
+
["do-not-retry summary", ["Do-not-retry summary", "Do not retry summary"]],
|
|
2129
|
+
["hard constraints", ["Hard Constraints", "Hard Constraint", "Strategy Constraints", "Strategy Constraint"]]
|
|
1572
2130
|
];
|
|
1573
2131
|
for (const [label, fields] of requiredFields) {
|
|
1574
2132
|
const value = fields.map((field) => evidenceFieldValue(section, field)).find((candidate) => candidate && candidate.trim());
|
|
@@ -1583,6 +2141,9 @@ function validateCurrentOperatorPath(fullText, taskId, implementationDoc) {
|
|
|
1583
2141
|
if (!runbookValue.includes(RUNBOOK_DOC_PREFIX)) {
|
|
1584
2142
|
errors.push(`${taskId} Current Operator Path must link a runbook/evidence document under ${RUNBOOK_DOC_PREFIX} in ${implementationDoc}`);
|
|
1585
2143
|
}
|
|
2144
|
+
if (!containsAny(section, HARD_CONSTRAINT_TERMS)) {
|
|
2145
|
+
errors.push(`${taskId} Current Operator Path must promote strategy-changing recovery decisions as Hard Constraints in ${implementationDoc}`);
|
|
2146
|
+
}
|
|
1586
2147
|
return errors;
|
|
1587
2148
|
}
|
|
1588
2149
|
function validateGateBreakdown(fullText, taskId, implementationDoc) {
|