okstra 0.172.0 → 0.174.0
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 +8 -6
- package/docs/architecture/storage-model.md +24 -3
- package/docs/architecture.md +21 -35
- package/docs/cli.md +39 -7
- package/docs/container.md +1 -1
- package/docs/contributor-change-matrix.md +1 -1
- package/docs/performance-improvement-plan-v2.md +6 -5
- package/docs/project-structure-overview.md +33 -25
- package/docs/task-process/README.md +6 -4
- package/docs/task-process/error-analysis.md +2 -2
- package/docs/task-process/final-verification.md +2 -2
- package/docs/task-process/implementation-option-selection.md +70 -0
- package/docs/task-process/implementation-planning.md +24 -16
- package/docs/task-process/requirements-discovery.md +2 -2
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +1 -1
- package/runtime/agents/workers/report-writer-worker.md +30 -6
- package/runtime/bin/lib/okstra/cli.sh +5 -1
- package/runtime/bin/lib/okstra/globals.sh +2 -1
- package/runtime/bin/lib/okstra/usage.sh +3 -0
- package/runtime/bin/okstra-provider-exec.py +29 -12
- package/runtime/bin/okstra-trace-cleanup.sh +58 -129
- package/runtime/bin/okstra.sh +2 -0
- package/runtime/prompts/duties/direction-selection-worker.md +44 -0
- package/runtime/prompts/duties/planning-worker.md +12 -4
- package/runtime/prompts/lead/adapters/cmux.md +2 -0
- package/runtime/prompts/lead/context-loader.md +1 -1
- package/runtime/prompts/lead/convergence.md +5 -5
- package/runtime/prompts/lead/okstra-lead-contract.md +7 -6
- package/runtime/prompts/lead/plan-body-verification.md +23 -6
- package/runtime/prompts/lead/report-writer.md +33 -11
- package/runtime/prompts/profiles/_common-contract.md +3 -3
- package/runtime/prompts/profiles/_implementation-deliverable.md +2 -2
- package/runtime/prompts/profiles/_implementation-executor.md +2 -0
- package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
- package/runtime/prompts/profiles/error-analysis.md +4 -4
- package/runtime/prompts/profiles/final-verification.md +3 -3
- package/runtime/prompts/profiles/forbidden-actions.json +7 -0
- package/runtime/prompts/profiles/implementation-option-selection.md +35 -0
- package/runtime/prompts/profiles/implementation-planning.md +61 -46
- package/runtime/prompts/profiles/implementation.md +4 -2
- package/runtime/prompts/profiles/improvement-discovery.md +1 -1
- package/runtime/prompts/profiles/release-handoff.md +1 -1
- package/runtime/prompts/profiles/requirements-discovery.md +3 -3
- package/runtime/prompts/wizard/prompts.ko.json +9 -1
- package/runtime/python/okstra_ctl/adapters/dispatch/__init__.py +1 -6
- package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +4 -4
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +5 -0
- package/runtime/python/okstra_ctl/agent_invocation.py +1 -0
- package/runtime/python/okstra_ctl/analysis_packet.py +6 -0
- package/runtime/python/okstra_ctl/conformance.py +68 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +89 -39
- package/runtime/python/okstra_ctl/dispatch_state.py +142 -14
- package/runtime/python/okstra_ctl/doctor.py +2 -2
- package/runtime/python/okstra_ctl/domain/worker_exec.py +5 -0
- package/runtime/python/okstra_ctl/exact_coverage.py +128 -0
- package/runtime/python/okstra_ctl/final_report_schema.py +5 -4
- package/runtime/python/okstra_ctl/fix_cycles.py +3 -1
- package/runtime/python/okstra_ctl/implementation_direction.py +836 -0
- package/runtime/python/okstra_ctl/implementation_options.py +479 -0
- package/runtime/python/okstra_ctl/pane_reclaim.py +13 -22
- package/runtime/python/okstra_ctl/plan_items.py +51 -3
- package/runtime/python/okstra_ctl/render.py +1 -0
- package/runtime/python/okstra_ctl/render_final_report.py +16 -19
- package/runtime/python/okstra_ctl/report_contract.py +45 -14
- package/runtime/python/okstra_ctl/report_finalize.py +68 -9
- package/runtime/python/okstra_ctl/report_html/render.py +4 -2
- package/runtime/python/okstra_ctl/report_html/router.py +4 -0
- package/runtime/python/okstra_ctl/report_html/view_models/implementation_option_selection.py +32 -0
- package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +25 -10
- package/runtime/python/okstra_ctl/report_views.py +148 -12
- package/runtime/python/okstra_ctl/run.py +393 -4
- package/runtime/python/okstra_ctl/schema_excerpt.py +1 -1
- package/runtime/python/okstra_ctl/scope_provenance.py +16 -10
- package/runtime/python/okstra_ctl/session.py +69 -12
- package/runtime/python/okstra_ctl/team.py +51 -25
- package/runtime/python/okstra_ctl/tmux.py +19 -149
- package/runtime/python/okstra_ctl/user_response.py +75 -0
- package/runtime/python/okstra_ctl/wizard.py +144 -0
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +2 -0
- package/runtime/python/okstra_ctl/worker_request.py +2 -0
- package/runtime/python/okstra_ctl/workflow.py +29 -7
- package/runtime/python/okstra_ctl/worktree.py +69 -3
- package/runtime/python/okstra_token_usage/cli.py +1 -1
- package/runtime/python/okstra_token_usage/collect.py +66 -6
- package/runtime/schemas/final-report-v2.0.schema.json +1428 -137
- package/runtime/skills/okstra-setup/references/project-config.md +11 -0
- package/runtime/templates/reports/final-report-v2.template.md +4 -0
- package/runtime/templates/reports/final-verification-input.template.md +1 -1
- package/runtime/templates/reports/html/base.template.html +3 -2
- package/runtime/templates/reports/html/i18n/en.json +21 -1
- package/runtime/templates/reports/html/i18n/ko.json +21 -1
- package/runtime/templates/reports/html/macros/forms.html +21 -2
- package/runtime/templates/reports/html/tasks/implementation-option-selection.template.html +49 -0
- package/runtime/templates/reports/html/tasks/implementation-planning.template.html +36 -2
- package/runtime/templates/reports/i18n/en.json +13 -0
- package/runtime/templates/reports/implementation-input.template.md +4 -2
- package/runtime/templates/reports/implementation-planning-input.template.md +18 -4
- package/runtime/templates/reports/improvement-discovery-input.template.md +1 -1
- package/runtime/templates/reports/md/tasks/implementation-option-selection.template.md +13 -0
- package/runtime/templates/reports/md/tasks/implementation-planning.template.md +17 -0
- package/runtime/templates/reports/report.js +111 -4
- package/runtime/templates/reports/settings.template.json +0 -24
- package/runtime/templates/reports/task-brief.template.md +9 -3
- package/runtime/templates/reports/user-response.template.md +25 -4
- package/runtime/templates/worker-prompt-preamble.md +8 -0
- package/runtime/validators/lib/fixtures.sh +49 -17
- package/runtime/validators/validate-implementation-plan-stages.py +169 -4
- package/runtime/validators/validate-report-views.py +2 -2
- package/runtime/validators/validate-run.py +149 -498
- package/runtime/validators/validate_improvement_report.py +5 -1
- package/runtime/validators/validate_session_conformance.py +1 -1
- package/src/cli-registry.mjs +8 -1
- package/src/commands/execute/codex-run.mjs +1 -0
- package/src/commands/execute/render-bundle.mjs +1 -0
- package/src/commands/execute/team.mjs +3 -3
- package/src/commands/execute/worktree-status.mjs +109 -0
- package/src/commands/lifecycle/install.mjs +0 -2
- package/src/commands/report/finalize.mjs +13 -6
- package/runtime/bin/okstra-subagent-reclaim.sh +0 -26
- package/runtime/schemas/final-report-v1.0.schema.json +0 -6366
- package/runtime/templates/reports/final-report.template.md +0 -1258
|
@@ -44,6 +44,28 @@
|
|
|
44
44
|
return String(s == null ? "" : s).replace(/^\s+|\s+$/g, "");
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
var DIRECTION_IDENTITY_WHITESPACE = [
|
|
48
|
+
0x0009, 0x000A, 0x000B, 0x000C, 0x000D,
|
|
49
|
+
0x001C, 0x001D, 0x001E, 0x001F, 0x0020, 0x0085, 0x00A0, 0x1680,
|
|
50
|
+
0x2000, 0x2001, 0x2002, 0x2003, 0x2004, 0x2005, 0x2006, 0x2007,
|
|
51
|
+
0x2008, 0x2009, 0x200A, 0x2028, 0x2029, 0x202F, 0x205F, 0x3000,
|
|
52
|
+
0xFEFF,
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
function trimDirectionIdentity(value) {
|
|
56
|
+
var start = 0;
|
|
57
|
+
var end = value.length;
|
|
58
|
+
while (
|
|
59
|
+
start < end &&
|
|
60
|
+
DIRECTION_IDENTITY_WHITESPACE.indexOf(value.charCodeAt(start)) !== -1
|
|
61
|
+
) start += 1;
|
|
62
|
+
while (
|
|
63
|
+
end > start &&
|
|
64
|
+
DIRECTION_IDENTITY_WHITESPACE.indexOf(value.charCodeAt(end - 1)) !== -1
|
|
65
|
+
) end -= 1;
|
|
66
|
+
return value.slice(start, end);
|
|
67
|
+
}
|
|
68
|
+
|
|
47
69
|
// Read the user-supplied value and decision effect from one clarification
|
|
48
70
|
// row. Returns null when the row has no usable input.
|
|
49
71
|
function readRowInput(row) {
|
|
@@ -181,6 +203,58 @@
|
|
|
181
203
|
});
|
|
182
204
|
}
|
|
183
205
|
|
|
206
|
+
function validateDirectionSelection(selection) {
|
|
207
|
+
if (!selection) return null;
|
|
208
|
+
var optionId = String(selection.optionId == null ? "" : selection.optionId);
|
|
209
|
+
var optionName = String(selection.optionName == null ? "" : selection.optionName);
|
|
210
|
+
var normalizedId = trimDirectionIdentity(optionId);
|
|
211
|
+
var normalizedName = trimDirectionIdentity(optionName);
|
|
212
|
+
if (
|
|
213
|
+
!normalizedId ||
|
|
214
|
+
!normalizedName ||
|
|
215
|
+
/[\r\n]/.test(optionId) ||
|
|
216
|
+
/[\r\n]/.test(optionName)
|
|
217
|
+
) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
"DIRECTION SELECTION Option-ID and Option-Name must be " +
|
|
220
|
+
"non-empty single-line values"
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
if (selection.confirmed !== true) {
|
|
224
|
+
throw new Error("DIRECTION SELECTION requires Confirmed: true");
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
optionId: normalizedId,
|
|
228
|
+
optionName: normalizedName,
|
|
229
|
+
confirmed: selection.confirmed,
|
|
230
|
+
selectionNote: selection.selectionNote,
|
|
231
|
+
constraints: selection.constraints,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function collectDirectionSelection() {
|
|
236
|
+
var controls = document.querySelectorAll(
|
|
237
|
+
'input[name="direction-selection-option"]'
|
|
238
|
+
);
|
|
239
|
+
if (!controls.length) return null;
|
|
240
|
+
var selected = document.querySelector(
|
|
241
|
+
'input[name="direction-selection-option"]:checked'
|
|
242
|
+
);
|
|
243
|
+
if (!selected) {
|
|
244
|
+
throw new Error("Select an implementation direction");
|
|
245
|
+
}
|
|
246
|
+
var confirmed = document.getElementById("direction-selection-confirmed");
|
|
247
|
+
var note = document.getElementById("direction-selection-note");
|
|
248
|
+
var constraints = document.getElementById("direction-selection-constraints");
|
|
249
|
+
return validateDirectionSelection({
|
|
250
|
+
optionId: selected.value,
|
|
251
|
+
optionName: selected.getAttribute("data-option-name") || "",
|
|
252
|
+
confirmed: !!(confirmed && confirmed.checked),
|
|
253
|
+
selectionNote: note ? trimMultiline(note.value) : "",
|
|
254
|
+
constraints: constraints ? trimMultiline(constraints.value) : "",
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
184
258
|
// Toggle the visibility of the "기타" companion input next to each
|
|
185
259
|
// select whose current value is "__other__". Wired at bind() time and
|
|
186
260
|
// also called once for the initial state.
|
|
@@ -233,13 +307,34 @@
|
|
|
233
307
|
return chunk;
|
|
234
308
|
}
|
|
235
309
|
|
|
236
|
-
function
|
|
310
|
+
function serialiseDirectionSelection(selection) {
|
|
311
|
+
selection = validateDirectionSelection(selection);
|
|
312
|
+
return (
|
|
313
|
+
"\n## DIRECTION SELECTION\n" +
|
|
314
|
+
"- Status: selected\n" +
|
|
315
|
+
"- Option-ID: " + selection.optionId + "\n" +
|
|
316
|
+
"- Option-Name: " + selection.optionName + "\n" +
|
|
317
|
+
"- Confirmed: true\n" +
|
|
318
|
+
quotedReviewField("Selection-Note", selection.selectionNote) +
|
|
319
|
+
quotedReviewField("Constraints", selection.constraints)
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function buildUserResponseMarkdown(
|
|
324
|
+
runMeta, entries, createdAt, planDecision, analysisReview, directionSelection
|
|
325
|
+
) {
|
|
237
326
|
var head =
|
|
238
327
|
"---\n" +
|
|
239
328
|
"task-key: " + (runMeta["task-key"] || "") + "\n" +
|
|
240
329
|
"task-type: " + (runMeta["task-type"] || "") + "\n" +
|
|
241
330
|
"seq: " + (runMeta["seq"] || "") + "\n" +
|
|
242
331
|
"source-report: " + (runMeta["source-report"] || "") + "\n" +
|
|
332
|
+
(runMeta["source-data"]
|
|
333
|
+
? "source-data: " + runMeta["source-data"] + "\n"
|
|
334
|
+
: "") +
|
|
335
|
+
(runMeta["source-data-sha256"]
|
|
336
|
+
? "source-data-sha256: " + runMeta["source-data-sha256"] + "\n"
|
|
337
|
+
: "") +
|
|
243
338
|
"created-by: user\n" +
|
|
244
339
|
"created-at: " + createdAt + "\n" +
|
|
245
340
|
"---\n" +
|
|
@@ -249,6 +344,7 @@
|
|
|
249
344
|
entries = entries || [];
|
|
250
345
|
var hasPlanDecision = !!planDecision;
|
|
251
346
|
var hasAnalysisReview = !!analysisReview;
|
|
347
|
+
var hasDirectionSelection = !!directionSelection;
|
|
252
348
|
var chunks = "";
|
|
253
349
|
for (var i = 0; i < entries.length; i++) {
|
|
254
350
|
var e = entries[i];
|
|
@@ -265,7 +361,12 @@
|
|
|
265
361
|
}
|
|
266
362
|
chunks += chunk;
|
|
267
363
|
}
|
|
268
|
-
if (
|
|
364
|
+
if (
|
|
365
|
+
entries.length === 0 &&
|
|
366
|
+
!hasPlanDecision &&
|
|
367
|
+
!hasAnalysisReview &&
|
|
368
|
+
!hasDirectionSelection
|
|
369
|
+
) {
|
|
269
370
|
chunks += "\n_(No user responses recorded.)_\n";
|
|
270
371
|
}
|
|
271
372
|
if (hasPlanDecision) {
|
|
@@ -274,6 +375,9 @@
|
|
|
274
375
|
if (hasAnalysisReview) {
|
|
275
376
|
chunks += serialiseAnalysisReview(analysisReview);
|
|
276
377
|
}
|
|
378
|
+
if (hasDirectionSelection) {
|
|
379
|
+
chunks += serialiseDirectionSelection(directionSelection);
|
|
380
|
+
}
|
|
277
381
|
return head + chunks;
|
|
278
382
|
}
|
|
279
383
|
|
|
@@ -308,22 +412,24 @@
|
|
|
308
412
|
var out = document.getElementById("user-response-output");
|
|
309
413
|
var planDecision;
|
|
310
414
|
var analysisReview;
|
|
415
|
+
var directionSelection;
|
|
311
416
|
try {
|
|
312
417
|
planDecision = collectPlanDecision();
|
|
313
418
|
analysisReview = collectAnalysisReview();
|
|
419
|
+
directionSelection = collectDirectionSelection();
|
|
314
420
|
} catch (e) {
|
|
315
421
|
showOutput(out, e.message || String(e));
|
|
316
422
|
showDismiss(true);
|
|
317
423
|
return "";
|
|
318
424
|
}
|
|
319
425
|
var md = buildUserResponseMarkdown(
|
|
320
|
-
runMeta, entries, isoNowUtc(), planDecision, analysisReview
|
|
426
|
+
runMeta, entries, isoNowUtc(), planDecision, analysisReview, directionSelection
|
|
321
427
|
);
|
|
322
428
|
showOutput(out, md);
|
|
323
429
|
showDismiss(true);
|
|
324
430
|
// Nothing answered yet — show the empty serialisation as feedback but
|
|
325
431
|
// don't download a useless file.
|
|
326
|
-
if (entries.length > 0 || planDecision || analysisReview) {
|
|
432
|
+
if (entries.length > 0 || planDecision || analysisReview || directionSelection) {
|
|
327
433
|
downloadUserResponse(md, runMeta);
|
|
328
434
|
}
|
|
329
435
|
return md;
|
|
@@ -455,6 +561,7 @@
|
|
|
455
561
|
collectEntries: collectEntries,
|
|
456
562
|
collectPlanDecision: collectPlanDecision,
|
|
457
563
|
collectAnalysisReview: collectAnalysisReview,
|
|
564
|
+
collectDirectionSelection: collectDirectionSelection,
|
|
458
565
|
exportUserResponse: exportUserResponse,
|
|
459
566
|
setReaderMode: setReaderMode,
|
|
460
567
|
};
|
|
@@ -52,36 +52,12 @@
|
|
|
52
52
|
"SessionEnd": [
|
|
53
53
|
{
|
|
54
54
|
"hooks": [
|
|
55
|
-
{
|
|
56
|
-
"type": "command",
|
|
57
|
-
"command": "$HOME/.okstra/bin/okstra-trace-cleanup.sh --reap"
|
|
58
|
-
},
|
|
59
55
|
{
|
|
60
56
|
"type": "command",
|
|
61
57
|
"command": "$HOME/.okstra/bin/okstra-team-reconcile.sh --session-end"
|
|
62
58
|
}
|
|
63
59
|
]
|
|
64
60
|
}
|
|
65
|
-
],
|
|
66
|
-
"SubagentStop": [
|
|
67
|
-
{
|
|
68
|
-
"hooks": [
|
|
69
|
-
{
|
|
70
|
-
"type": "command",
|
|
71
|
-
"command": "$HOME/.okstra/bin/okstra-subagent-reclaim.sh"
|
|
72
|
-
}
|
|
73
|
-
]
|
|
74
|
-
}
|
|
75
|
-
],
|
|
76
|
-
"TaskCompleted": [
|
|
77
|
-
{
|
|
78
|
-
"hooks": [
|
|
79
|
-
{
|
|
80
|
-
"type": "command",
|
|
81
|
-
"command": "$HOME/.okstra/bin/okstra-subagent-reclaim.sh"
|
|
82
|
-
}
|
|
83
|
-
]
|
|
84
|
-
}
|
|
85
61
|
]
|
|
86
62
|
}
|
|
87
63
|
}
|
|
@@ -60,9 +60,14 @@ taskType: "{{FM_TASK_TYPE}}"
|
|
|
60
60
|
- What is the visible symptom?
|
|
61
61
|
- What are the expected vs actual results?
|
|
62
62
|
- What are the current hypotheses and missing evidence?
|
|
63
|
+
- If `Task Type` is `implementation-option-selection`:
|
|
64
|
+
- Is this a comparison among possible directions or validation of one preselected direction?
|
|
65
|
+
- Which stable `EB-NNN`, `PB-NNN`, and `EO-NNN` requirements form the coverage denominator?
|
|
66
|
+
- Which mechanism, architecture boundary, preservation rule, or implementation constraint must each direction address?
|
|
63
67
|
- If `Task Type` is `implementation-planning`:
|
|
64
|
-
-
|
|
65
|
-
-
|
|
68
|
+
- Which validated selection report and `selectedDirectionRef` authorise this new plan?
|
|
69
|
+
- Which mechanism, architecture boundary, invariants, and user constraints must the plan preserve?
|
|
70
|
+
- Which dependencies or migrations matter while realizing that one direction?
|
|
66
71
|
- What validation and rollback approach is expected?
|
|
67
72
|
- If `Task Type` is `implementation`:
|
|
68
73
|
- Which approved `implementation-planning` final report authorises this run, and is its frontmatter `approved: true` cited verbatim?
|
|
@@ -141,6 +146,7 @@ taskType: "{{FM_TASK_TYPE}}"
|
|
|
141
146
|
- Allowed and forbidden actions for each task type are listed in `Lifecycle Phase Boundaries` of the okstra skill (`prompts/lead/okstra-lead-contract.md`). The lead and every worker stay inside that boundary.
|
|
142
147
|
- "proceed to the next step" or any equivalent user phrase is interpreted as "complete the remaining outputs of the current phase," never as "start the next lifecycle phase." The next phase begins only via a fresh okstra invocation with the new `--task-type`.
|
|
143
148
|
- For `implementation-planning` specifically: produce a plan document with the sections listed in `okstra-implementation-planning-input.template.md` `## Required Plan Deliverable`. Do not edit project source code, run builds/migrations/deployments, or write artifacts outside the run's own directories.
|
|
149
|
+
- For `implementation-option-selection` specifically: compare or validate implementation directions without editing source, running tests/builds, or writing exact file lists, stage maps, and test commands. Only directions with 100% requirement coverage and 100% scope precision may be displayed.
|
|
144
150
|
- For `implementation` specifically: edits are bounded by the approved plan's file list (the `--approved-plan` reference). The run MUST refuse to start if the approved plan path is missing or its frontmatter `approved` field is not `true`. `git push`, publish, deploy, real migrations, and any third-party write API call remain forbidden; only local `git add`/`git commit` are allowed. Verifier roles stay read-only — they record fix recommendations rather than applying edits — and acceptance verdicts belong to `final-verification`, not this phase.
|
|
145
151
|
|
|
146
152
|
## Available MCP Servers
|
|
@@ -157,7 +163,7 @@ How to invoke (worker-by-worker):
|
|
|
157
163
|
|
|
158
164
|
Usage policy:
|
|
159
165
|
|
|
160
|
-
- **Allowed phases**: `requirements-discovery`, `error-analysis`, `implementation-planning`, `final-verification`. Use only when local schema/data evidence improves the answer. Always cite the server, table, and the SELECT used as evidence in worker output.
|
|
166
|
+
- **Allowed phases**: `requirements-discovery`, `error-analysis`, `implementation-option-selection`, `implementation-planning`, `final-verification`. Use only when local schema/data evidence improves the answer. Always cite the server, table, and the SELECT used as evidence in worker output.
|
|
161
167
|
- **`implementation` phase**: read-only MCP queries are permitted as cross-checks; MCP MUST NOT be used as a write path even if a write tool becomes available — schema/data mutations belong in the codebase migration files reviewed by humans.
|
|
162
168
|
- **CLI-wrapper workers**: can use these MCP servers only if their provider CLI configuration mirrors the same servers. If not configured, the worker should record `MCP not available in this CLI` in `Missing Information or Assumptions` rather than guessing.
|
|
163
169
|
- **Forbidden**: connecting to non-listed databases, running anything that mutates state (server is read-only — flagged write attempts are a contract violation), persisting query results outside the run's own artifact directories.
|
|
@@ -9,9 +9,11 @@ This file defines the standard format of the markdown produced by the **Export u
|
|
|
9
9
|
|
|
10
10
|
```yaml
|
|
11
11
|
task-key: <task-group>/<task-id>
|
|
12
|
-
task-type: <requirements-discovery | error-analysis | implementation-planning | implementation | final-verification | release-handoff | project-analysis | feature-analysis | change-impact-analysis>
|
|
12
|
+
task-type: <requirements-discovery | error-analysis | implementation-option-selection | implementation-planning | implementation | final-verification | release-handoff | improvement-discovery | project-analysis | feature-analysis | change-impact-analysis>
|
|
13
13
|
seq: <3-digit zero-padded run sequence>
|
|
14
14
|
source-report: <project-relative path to the final-report .md the HTML was derived from>
|
|
15
|
+
source-data: <project-relative path to the final-report data.json; omit for legacy reports without one>
|
|
16
|
+
source-data-sha256: <SHA-256 of the exact source-data bytes; omit when source-data is omitted>
|
|
15
17
|
created-by: user
|
|
16
18
|
created-at: <ISO 8601 UTC timestamp>
|
|
17
19
|
```
|
|
@@ -72,15 +74,16 @@ When you pick a verdict in the Plan Decision widget and Export, the following bl
|
|
|
72
74
|
```markdown
|
|
73
75
|
## PLAN DECISION
|
|
74
76
|
- Status: <approved | revision-requested | rejected>
|
|
75
|
-
- Implementation-Option: <the name exactly as in Option Candidates>
|
|
77
|
+
- Implementation-Option: <legacy plans only: the name exactly as in Option Candidates>
|
|
76
78
|
- Reason:
|
|
77
79
|
> <one quoted line per input line>
|
|
78
80
|
```
|
|
79
81
|
|
|
80
82
|
- `revision-requested` and `rejected` require a reason: a plan sent back without one leaves the next run guessing at what to change, so Export refuses to serialise it.
|
|
81
|
-
- `
|
|
83
|
+
- A `planningContract: selected-direction` plan never emits `Implementation-Option:`. The direction was confirmed before planning, and this block decides only whether the detailed plan is approved, revised, or rejected.
|
|
84
|
+
- A legacy candidate plan emits `Implementation-Option:` only for `approved`, and only when you moved off the recommended default. Otherwise implementation falls back to the legacy plan's Recommended Option.
|
|
82
85
|
- `Reason:` is optional for `approved`, and the line is omitted when empty.
|
|
83
|
-
- The consumer of an `approved` block is the approve-confirm step of the implementation start wizard (`scripts/okstra_ctl/wizard.py`). After user confirmation the wizard applies
|
|
86
|
+
- The consumer of an `approved` block is the approve-confirm step of the implementation start wizard (`scripts/okstra_ctl/wizard.py`). After user confirmation the wizard applies `--approve`; it applies `--implementation-option` only for a legacy plan. The sidecar itself does not bypass approval-gate validation. A non-approved status is ignored there — that step only ever looks for an approval.
|
|
84
87
|
- The parser is `parse_plan_decision` in `scripts/okstra_ctl/user_response.py`, and it accepts only the lowercase statuses that are byte-identical to the producer output (hand-edited values such as `Approved` are rejected fail-closed).
|
|
85
88
|
- `--resume-clarification` attaches the sidecar verbatim, so the PLAN DECISION block reaches the next planning run — which is how a rejection and its reason get acted on.
|
|
86
89
|
|
|
@@ -102,6 +105,24 @@ The Analysis Review control appends one decision block. `revision-requested` and
|
|
|
102
105
|
|
|
103
106
|
The block is the review decision's sole storage location. Export never changes the source final-report markdown. A later analysis rerun receives the sidecar through `--clarification-response` and records one `analysisReviewResolution` row for each imported affected ID.
|
|
104
107
|
|
|
108
|
+
## DIRECTION SELECTION block (implementation-option-selection comparison only)
|
|
109
|
+
|
|
110
|
+
Selecting one ranked direction and checking `Confirmed` appends this block. `Selection-Note` and `Constraints` keep their field lines when empty, but do not emit empty quote lines.
|
|
111
|
+
|
|
112
|
+
```markdown
|
|
113
|
+
## DIRECTION SELECTION
|
|
114
|
+
- Status: selected
|
|
115
|
+
- Option-ID: IO-002
|
|
116
|
+
- Option-Name: Adapter boundary
|
|
117
|
+
- Confirmed: true
|
|
118
|
+
- Selection-Note:
|
|
119
|
+
> Use the existing port.
|
|
120
|
+
- Constraints:
|
|
121
|
+
> Keep PB-001 unchanged.
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The parser accepts exactly one block, lowercase `selected`, and lowercase `true`. A `preselected-validation` report displays its confirmed upstream direction read-only and does not export a new selection block.
|
|
125
|
+
|
|
105
126
|
## Compatibility Rules
|
|
106
127
|
|
|
107
128
|
- If `Kind` has an unknown value, the form renders with a `<textarea>` fallback, and the received `Kind` string is preserved as-is during serialization.
|
|
@@ -61,6 +61,14 @@ Every analysis result starts with YAML frontmatter containing the task identity
|
|
|
61
61
|
|
|
62
62
|
Every item has a worker-local ID and file:line evidence where code evidence exists. Sections 1–5 are the common core: feasibility, requirement interpretation, hidden assumptions, alternatives, and execution risk. Section 6 is the only legal home for specialization and is not consensus input.
|
|
63
63
|
|
|
64
|
+
### Selected-direction planning ownership
|
|
65
|
+
|
|
66
|
+
For `implementation-planning` with `selected-direction.json`, sections 1–5 read that snapshot and the original requirements, then assess only its realization into files, interfaces, stages, validation, rollback, bidirectional requirement links, and `direction-invalidated` evidence. Direction selection remains upstream of this prompt.
|
|
67
|
+
|
|
68
|
+
### Legacy candidate-comparison ownership
|
|
69
|
+
|
|
70
|
+
For an implementation-planning compatibility rerun without the snapshot, sections 1–5 retain candidate comparison, trade-off, recommendation, and `P-Opt-*` evidence.
|
|
71
|
+
|
|
64
72
|
### Ticket Tagging
|
|
65
73
|
|
|
66
74
|
For `requirements-discovery`, `error-analysis`, and `implementation-planning`, tag every section 1–5 item with its related ticket. Use `Issue / Ticket`, fall back to Task ID, then `unknown`; comma-separate multiple tickets.
|
|
@@ -428,7 +428,7 @@ if WORKSPACE_ROOT:
|
|
|
428
428
|
task_type = str(task_manifest.get("taskType", ""))
|
|
429
429
|
sample_path = (
|
|
430
430
|
Path(WORKSPACE_ROOT)
|
|
431
|
-
/ "tests" / "fixtures" / "final-report-data"
|
|
431
|
+
/ "tests" / "fixtures" / "final-report-data-v2"
|
|
432
432
|
/ f"{task_type}-001.data.json"
|
|
433
433
|
)
|
|
434
434
|
if sample_path.is_file():
|
|
@@ -439,6 +439,25 @@ if WORKSPACE_ROOT:
|
|
|
439
439
|
sample["frontmatter"]["projectId"] = str(task_manifest.get("projectId", ""))
|
|
440
440
|
sample["header"]["taskKey"] = str(task_manifest.get("taskKey", ""))
|
|
441
441
|
sample["header"]["taskType"] = task_type
|
|
442
|
+
# The shipped fixture carries its own roster; this run's contract names
|
|
443
|
+
# a different one, and the validator compares the report's agent rows
|
|
444
|
+
# against that contract. Restate the rows under the contract's names so
|
|
445
|
+
# the fixture exercises the check instead of tripping over it.
|
|
446
|
+
_required = (
|
|
447
|
+
(run_manifest.get("teamContract") or {}).get("requiredAgentStatusEntries")
|
|
448
|
+
or (task_manifest.get("resultContract") or {}).get(
|
|
449
|
+
"requiredAgentStatusEntries"
|
|
450
|
+
)
|
|
451
|
+
or []
|
|
452
|
+
)
|
|
453
|
+
_rows = sample.get("executionStatus") or []
|
|
454
|
+
if _required and _rows:
|
|
455
|
+
# `agent` is the runtime (a schema enum); `role` carries the label the
|
|
456
|
+
# validator looks for in the rendered markdown.
|
|
457
|
+
sample["executionStatus"] = [
|
|
458
|
+
{**_rows[min(i, len(_rows) - 1)], "role": role}
|
|
459
|
+
for i, role in enumerate(_required)
|
|
460
|
+
]
|
|
442
461
|
name = report_path.name
|
|
443
462
|
data_path = (
|
|
444
463
|
report_path.with_name(name[:-3] + ".data.json")
|
|
@@ -452,23 +471,36 @@ if WORKSPACE_ROOT:
|
|
|
452
471
|
|
|
453
472
|
import sys as _sys
|
|
454
473
|
_sys.path.insert(0, str(Path(WORKSPACE_ROOT) / "scripts"))
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
474
|
+
# The markdown seeded above this block predates the data.json contract, so
|
|
475
|
+
# re-render it from the data.json the fixture just wrote. Otherwise the pair
|
|
476
|
+
# disagrees and the validator's AI-handoff heading scan fails on a fixture
|
|
477
|
+
# that never claimed to be hand-authored.
|
|
478
|
+
if sample_path.is_file():
|
|
479
|
+
try:
|
|
480
|
+
from okstra_ctl.render_final_report import render_to_file
|
|
481
|
+
|
|
482
|
+
render_to_file(data_path, report_path)
|
|
483
|
+
except Exception as exc: # pragma: no cover — fixture path only
|
|
484
|
+
raise SystemExit(f"failed to render final report in fixture: {exc}")
|
|
485
|
+
# Go through the same CLI the run uses, so the fixture picks the schema's
|
|
486
|
+
# own view (v2 task template) rather than a second copy of that choice.
|
|
487
|
+
import subprocess as _subprocess
|
|
488
|
+
_views = _subprocess.run(
|
|
489
|
+
[
|
|
490
|
+
_sys.executable,
|
|
491
|
+
str(Path(WORKSPACE_ROOT) / "scripts" / "okstra-render-report-views.py"),
|
|
492
|
+
str(report_path),
|
|
493
|
+
"--task-key", str(task_manifest.get("taskKey", "validation/fixture")),
|
|
494
|
+
"--task-type", str(task_manifest.get("taskType", "validation")),
|
|
495
|
+
"--seq", "001",
|
|
496
|
+
],
|
|
497
|
+
capture_output=True,
|
|
498
|
+
text=True,
|
|
499
|
+
)
|
|
500
|
+
if _views.returncode != 0:
|
|
501
|
+
raise SystemExit(
|
|
502
|
+
f"failed to render report views in fixture: {_views.stderr or _views.stdout}"
|
|
469
503
|
)
|
|
470
|
-
except Exception as exc: # pragma: no cover — fixture path only
|
|
471
|
-
raise SystemExit(f"failed to render report views in fixture: {exc}")
|
|
472
504
|
|
|
473
505
|
if final_status_path.exists():
|
|
474
506
|
final_status_path.unlink()
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""S1–
|
|
2
|
+
"""S1–S13 checks for the Stage Map structure of an approved
|
|
3
3
|
implementation-planning final-report.md. Run from prepare_task_bundle
|
|
4
4
|
of `implementation` task or standalone."""
|
|
5
5
|
|
|
@@ -8,6 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
import argparse
|
|
9
9
|
import re
|
|
10
10
|
import sys
|
|
11
|
+
from collections import Counter
|
|
11
12
|
from dataclasses import dataclass
|
|
12
13
|
from pathlib import Path
|
|
13
14
|
from typing import List, Tuple
|
|
@@ -42,9 +43,26 @@ EXIT_CONTRACT_HEADING = re.compile(r"^###\s+Stage Exit Contract\b", re.M)
|
|
|
42
43
|
PATH_TOKEN = re.compile(r"(?:[\w.@-]+/)+[\w.@-]+")
|
|
43
44
|
|
|
44
45
|
|
|
46
|
+
# S12 — a step command that reads an okstra artifact back out of a git object
|
|
47
|
+
# (`git cat-file -e <rev>:.okstra/...`, `git show <rev>:.okstra/...`). `.okstra/**`
|
|
48
|
+
# is never committed: `_implementation-executor.md` forbids `git add -f` and makes
|
|
49
|
+
# a staged ignored path abort the commit, and `_implementation-verifier.md` reports
|
|
50
|
+
# a committed `.okstra` path as a branch defect. A verification step built on such
|
|
51
|
+
# a read can never pass, whatever the stage does.
|
|
52
|
+
GIT_OBJECT_OKSTRA_READ = re.compile(
|
|
53
|
+
r"\bgit\b[^&|;]*?\b(?:cat-file|show|ls-tree|archive|grep)\b[^&|;]*?"
|
|
54
|
+
r"(?<![\w./@'\"-])[\w./@{}~^-]+:\.okstra/"
|
|
55
|
+
)
|
|
56
|
+
# S13 — a clean-worktree assertion built on a bare `git status`. okstra provisions
|
|
57
|
+
# `.okstra`, the configured sync entries, and (for implementation) a nested stage
|
|
58
|
+
# worktree into every task worktree, so a bare status is never empty there.
|
|
59
|
+
BARE_GIT_STATUS = re.compile(r"\bgit\b[^&|;]*?\bstatus\b[^&|;]*?--(?:porcelain|short)\b")
|
|
60
|
+
CLEAN_GATE_COMMAND = "okstra worktree-status --check-clean"
|
|
61
|
+
|
|
62
|
+
|
|
45
63
|
@dataclass
|
|
46
64
|
class ValidationError:
|
|
47
|
-
code: str # S1..
|
|
65
|
+
code: str # S1..S13
|
|
48
66
|
stage: int # 0 = global
|
|
49
67
|
message: str
|
|
50
68
|
|
|
@@ -91,10 +109,14 @@ def _slice_stage_section(text: str, stage_number: int) -> str:
|
|
|
91
109
|
return text[start: start + nxt.start()] if nxt else text[start:]
|
|
92
110
|
|
|
93
111
|
|
|
112
|
+
STEP_COMMAND_CELL = 3
|
|
113
|
+
|
|
114
|
+
|
|
94
115
|
def _effective_step_rows(section: str) -> List[List[str]]:
|
|
95
116
|
"""Effective (non header/divider/comment) rows of the `### Stepwise
|
|
96
117
|
Execution Order` table, each as a list of stripped cells. Columns are
|
|
97
|
-
`step | action | files | command | expected`, so action is
|
|
118
|
+
`step | action | files | command | outcome | expected`, so action is
|
|
119
|
+
index 1, command index 3, outcome index 4."""
|
|
98
120
|
m = re.search(r"^###\s+Stepwise Execution Order\b", section, re.M)
|
|
99
121
|
if not m:
|
|
100
122
|
return []
|
|
@@ -266,6 +288,40 @@ def _check_red_green_steps(section: str, stage_number: int) -> List[ValidationEr
|
|
|
266
288
|
return errs
|
|
267
289
|
|
|
268
290
|
|
|
291
|
+
def _check_step_command(command: str, stage_number: int) -> List[ValidationError]:
|
|
292
|
+
"""S12 / S13 over one step's `command` cell.
|
|
293
|
+
|
|
294
|
+
The command cell is what actually closes a step, so it has to be runnable
|
|
295
|
+
inside the worktree layout okstra provisions. Both rules reject a command
|
|
296
|
+
that can never pass there, regardless of what the stage implements.
|
|
297
|
+
"""
|
|
298
|
+
errs: List[ValidationError] = []
|
|
299
|
+
if GIT_OBJECT_OKSTRA_READ.search(command):
|
|
300
|
+
errs.append(ValidationError("S12", stage_number,
|
|
301
|
+
"S12: step command reads an `.okstra/` path out of a git object — "
|
|
302
|
+
"`.okstra/**` is gitignored and never committed, so the read can "
|
|
303
|
+
"never resolve. Pass the artifact forward through the stage carry "
|
|
304
|
+
"sidecar / verifier result, or read it from the working tree"))
|
|
305
|
+
if BARE_GIT_STATUS.search(command):
|
|
306
|
+
errs.append(ValidationError("S13", stage_number,
|
|
307
|
+
"S13: step command asserts a clean worktree with a bare `git status` — "
|
|
308
|
+
"okstra provisions `.okstra`, the synced entries, and any nested stage "
|
|
309
|
+
f"worktree there, so it is never empty. Use `{CLEAN_GATE_COMMAND}`"))
|
|
310
|
+
return errs
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _check_markdown_step_commands(
|
|
314
|
+
text: str, stages: List[StageMapStage]
|
|
315
|
+
) -> List[ValidationError]:
|
|
316
|
+
"""S12 / S13 over the rendered `### Stepwise Execution Order` rows."""
|
|
317
|
+
errs: List[ValidationError] = []
|
|
318
|
+
for s in stages:
|
|
319
|
+
for row in _effective_step_rows(_slice_stage_section(text, s.stage_number)):
|
|
320
|
+
if len(row) > STEP_COMMAND_CELL:
|
|
321
|
+
errs.extend(_check_step_command(row[STEP_COMMAND_CELL], s.stage_number))
|
|
322
|
+
return errs
|
|
323
|
+
|
|
324
|
+
|
|
269
325
|
def _check_conformance_declaration(
|
|
270
326
|
text: str, stages: List[StageMapStage]
|
|
271
327
|
) -> List[ValidationError]:
|
|
@@ -382,6 +438,7 @@ def collect_validation_errors(text: str) -> List[ValidationError]:
|
|
|
382
438
|
if stages:
|
|
383
439
|
errors.extend(_check_each_stage_section(text, stages))
|
|
384
440
|
errors.extend(_check_slice_tdd(text, stages))
|
|
441
|
+
errors.extend(_check_markdown_step_commands(text, stages))
|
|
385
442
|
errors.extend(_check_conformance_declaration(text, stages))
|
|
386
443
|
errors.extend(_check_depends_on(stages))
|
|
387
444
|
errors.extend(_check_parallel_safety(text, stages))
|
|
@@ -472,6 +529,99 @@ def _check_data_step_counts(
|
|
|
472
529
|
return errs
|
|
473
530
|
|
|
474
531
|
|
|
532
|
+
def _check_data_stage_identities(
|
|
533
|
+
raw_stage_map: List[dict], stages: List[dict]
|
|
534
|
+
) -> List[ValidationError]:
|
|
535
|
+
"""Reject ambiguous schema-v2 identities before building stage indexes."""
|
|
536
|
+
errs: List[ValidationError] = []
|
|
537
|
+
stage_map_numbers = [
|
|
538
|
+
row["stage"]
|
|
539
|
+
for row in raw_stage_map
|
|
540
|
+
if isinstance(row, dict) and isinstance(row.get("stage"), int)
|
|
541
|
+
]
|
|
542
|
+
stage_numbers = [
|
|
543
|
+
row["stage"]
|
|
544
|
+
for row in stages
|
|
545
|
+
if isinstance(row, dict) and isinstance(row.get("stage"), int)
|
|
546
|
+
]
|
|
547
|
+
duplicate_stage_map = {
|
|
548
|
+
number for number, count in Counter(stage_map_numbers).items() if count > 1
|
|
549
|
+
}
|
|
550
|
+
duplicate_stages = {
|
|
551
|
+
number for number, count in Counter(stage_numbers).items() if count > 1
|
|
552
|
+
}
|
|
553
|
+
errs.extend(
|
|
554
|
+
ValidationError("S3", number, f"stageMap duplicate stage {number}")
|
|
555
|
+
for number in sorted(duplicate_stage_map)
|
|
556
|
+
)
|
|
557
|
+
errs.extend(
|
|
558
|
+
ValidationError("S3", number, f"stages[] duplicate stage {number}")
|
|
559
|
+
for number in sorted(duplicate_stages)
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
stage_map_set = set(stage_map_numbers)
|
|
563
|
+
stage_set = set(stage_numbers)
|
|
564
|
+
errs.extend(
|
|
565
|
+
ValidationError(
|
|
566
|
+
"S3",
|
|
567
|
+
number,
|
|
568
|
+
f"stageMap is missing stages[] stage {number}",
|
|
569
|
+
)
|
|
570
|
+
for number in sorted(stage_set - stage_map_set)
|
|
571
|
+
)
|
|
572
|
+
errs.extend(
|
|
573
|
+
ValidationError(
|
|
574
|
+
"S3",
|
|
575
|
+
number,
|
|
576
|
+
f"stages[] is missing stageMap stage {number}",
|
|
577
|
+
)
|
|
578
|
+
for number in sorted(stage_map_set - stage_set)
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
stage_map_by_number = {
|
|
582
|
+
row["stage"]: row
|
|
583
|
+
for row in raw_stage_map
|
|
584
|
+
if isinstance(row, dict)
|
|
585
|
+
and isinstance(row.get("stage"), int)
|
|
586
|
+
and row["stage"] not in duplicate_stage_map
|
|
587
|
+
}
|
|
588
|
+
stages_by_number = {
|
|
589
|
+
row["stage"]: row
|
|
590
|
+
for row in stages
|
|
591
|
+
if isinstance(row, dict)
|
|
592
|
+
and isinstance(row.get("stage"), int)
|
|
593
|
+
and row["stage"] not in duplicate_stages
|
|
594
|
+
}
|
|
595
|
+
errs.extend(
|
|
596
|
+
ValidationError(
|
|
597
|
+
"S3",
|
|
598
|
+
number,
|
|
599
|
+
f"stageMap and stages[] stage {number} title must match",
|
|
600
|
+
)
|
|
601
|
+
for number in sorted(stage_map_by_number.keys() & stages_by_number.keys())
|
|
602
|
+
if stage_map_by_number[number].get("title")
|
|
603
|
+
!= stages_by_number[number].get("title")
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
for stage in stages:
|
|
607
|
+
number = stage.get("stage")
|
|
608
|
+
step_numbers = [
|
|
609
|
+
step["step"]
|
|
610
|
+
for step in stage.get("stepwiseExecution") or []
|
|
611
|
+
if isinstance(step, dict) and isinstance(step.get("step"), int)
|
|
612
|
+
]
|
|
613
|
+
errs.extend(
|
|
614
|
+
ValidationError(
|
|
615
|
+
"S4",
|
|
616
|
+
number if isinstance(number, int) else 0,
|
|
617
|
+
f"stages[] stage {number} step {step} is duplicate",
|
|
618
|
+
)
|
|
619
|
+
for step, count in sorted(Counter(step_numbers).items())
|
|
620
|
+
if count > 1
|
|
621
|
+
)
|
|
622
|
+
return errs
|
|
623
|
+
|
|
624
|
+
|
|
475
625
|
def collect_data_validation_errors(planning: dict) -> List[ValidationError]:
|
|
476
626
|
"""The S-checks that schema v2 cannot express, over `implementationPlanning`.
|
|
477
627
|
|
|
@@ -482,11 +632,22 @@ def collect_data_validation_errors(planning: dict) -> List[ValidationError]:
|
|
|
482
632
|
covers presence and cardinality; this covers the relationships between
|
|
483
633
|
fields, which is what a JSON Schema has no way to say.
|
|
484
634
|
"""
|
|
485
|
-
|
|
635
|
+
if (
|
|
636
|
+
planning.get("planningContract") == "selected-direction"
|
|
637
|
+
and planning.get("outcome") == "direction-invalidated"
|
|
638
|
+
):
|
|
639
|
+
return []
|
|
640
|
+
|
|
641
|
+
raw_stage_map = planning.get("stageMap") or []
|
|
642
|
+
stage_map, errors = _data_stage_metas(raw_stage_map)
|
|
486
643
|
stages = [s for s in (planning.get("stages") or []) if isinstance(s, dict)]
|
|
487
644
|
if not stage_map and not stages:
|
|
488
645
|
return errors
|
|
489
646
|
|
|
647
|
+
identity_errors = _check_data_stage_identities(raw_stage_map, stages)
|
|
648
|
+
errors.extend(identity_errors)
|
|
649
|
+
if identity_errors:
|
|
650
|
+
return errors
|
|
490
651
|
errors.extend(_check_data_step_counts(stage_map, stages))
|
|
491
652
|
errors.extend(_check_depends_on(stage_map))
|
|
492
653
|
errors.extend(_report_shared_parallel_files({
|
|
@@ -500,6 +661,10 @@ def collect_data_validation_errors(planning: dict) -> List[ValidationError]:
|
|
|
500
661
|
}))
|
|
501
662
|
for stage in stages:
|
|
502
663
|
errors.extend(_check_data_slice_tdd(stage))
|
|
664
|
+
number = stage.get("stage") if isinstance(stage.get("stage"), int) else 0
|
|
665
|
+
for step in stage.get("stepwiseExecution") or []:
|
|
666
|
+
if isinstance(step, dict):
|
|
667
|
+
errors.extend(_check_step_command(str(step.get("command") or ""), number))
|
|
503
668
|
return errors
|
|
504
669
|
|
|
505
670
|
|