@guanlin4924/devflow-pi 0.1.86 → 0.1.89
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 -0
- package/package.json +1 -1
- package/plugins/devflow-core/extensions/devflow-core.ts +199 -9
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_runner.py +15 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/advance_state.rb +49 -3
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/checker_adapter.rb +83 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/checker_registry.rb +19 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/checkpoint_meta.rb +46 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/execution_contract.rb +27 -2
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/gate_runtime.rb +32 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/init_state.rb +44 -41
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/init_workflow.rb +5 -4
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/inspect_state.rb +15 -4
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/machine_evidence.rb +15 -9
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/review_preflight.py +238 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/review_run.py +234 -32
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/validate_state.rb +63 -11
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/workflow_gate_contract.rb +5 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/templates/feature-development.yaml +40 -31
- package/plugins/devflow-core/skills/workflow-pipeline/SKILL.md +11 -5
- package/plugins/devflow-core/skills/workflow-pipeline/references/pipeline-flow.md +6 -6
- package/plugins/devflow-implementation/skills/workflow-change-plan/SKILL.md +16 -1
- package/plugins/devflow-implementation/skills/workflow-change-plan/references/output-template.md +3 -1
- package/plugins/devflow-quality/agents/code-reviewer.md +7 -1
- package/plugins/devflow-quality/skills/code-review/SKILL.md +10 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/SKILL.md +37 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/SKILL.md +327 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/assets/output-template.md +213 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/bug-rules.md +9 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/coverage-checklist.md +63 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/exception-checklist.md +37 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/api-test.md +58 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/boundary-test.md +151 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/common-test.md +49 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/compatibility-test.md +105 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/data-test.md +67 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/exception-test.md +109 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/function-test.md +65 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/integration-test.md +36 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/performance-test.md +100 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/scenario-test.md +63 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/security-test.md +55 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/ui-test.md +49 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/test-type-slots/usability-test.md +46 -0
- package/plugins/devflow-quality/skills/workflow-gen-test-guide/legacy/references/writing-guide.md +111 -0
- package/src/constants.ts +1 -1
package/README.md
CHANGED
|
@@ -117,6 +117,10 @@ Devflow 会在输入框上方显示“当前 Feature 交付链路”:它基于
|
|
|
117
117
|
|
|
118
118
|
正常使用不需要记忆底层命令:在 Devflow 工作流中按步骤继续即可。需要排查时,可用 `devflow_acceptance_tests` 查看冻结套件,或由工作流调用 `devflow_tdd_evidence` 与 `devflow_run_acceptance_tests` 生成 Core-owned 证据;不要手写“已测试”来替代结果文件。
|
|
119
119
|
|
|
120
|
+
### Core 软门禁模式
|
|
121
|
+
|
|
122
|
+
默认情况下,Core 生成的测试绑定、追溯、merge gate、产物有效性和图谱新鲜度问题会记录为 advisory,交给 AI 审查决定继续、修改或阻塞;只有控制面损坏、越权路径、并发冲突等安全问题仍会硬阻断。需要临时恢复旧的严格门禁时,启动前设置 `DEVFLOW_SOFT_GATES=0`。
|
|
123
|
+
|
|
120
124
|
## 更新
|
|
121
125
|
|
|
122
126
|
升级 npm 发行版:
|
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, createHmac, randomBytes, randomUUID } from "node:crypto";
|
|
2
2
|
import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
3
3
|
import { appendFile, mkdir, rename, writeFile } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
@@ -183,6 +183,7 @@ const READ_ONLY_TOOLS = new Set([
|
|
|
183
183
|
"devflow_usage_stats",
|
|
184
184
|
"usage_stats",
|
|
185
185
|
"devflow_project_context",
|
|
186
|
+
"devflow_review_preflight",
|
|
186
187
|
]);
|
|
187
188
|
const REQUIREMENT_DRAFT_TOOLS = new Set([...READ_ONLY_TOOLS, "ask"]);
|
|
188
189
|
// A Todo update changes only the client-visible task list, never the project
|
|
@@ -526,7 +527,7 @@ const CONTROL_PATHS = [
|
|
|
526
527
|
".planning/artifacts/index.yaml",
|
|
527
528
|
];
|
|
528
529
|
|
|
529
|
-
const CONTROL_PREFIXES = [".planning/workflows/", ".planning/contracts/", ".planning/evidence/"];
|
|
530
|
+
const CONTROL_PREFIXES = [".planning/workflows/", ".planning/contracts/", ".planning/evidence/", ".planning/review-runs/"];
|
|
530
531
|
|
|
531
532
|
const REQUIRED_EXTENSION_TOOLS = [
|
|
532
533
|
"devflow_doctor",
|
|
@@ -570,6 +571,7 @@ const REQUIRED_SKILLS = [
|
|
|
570
571
|
"workflow-execute-phase",
|
|
571
572
|
"workflow-feature-breakdown",
|
|
572
573
|
"workflow-frontend-tech-spec",
|
|
574
|
+
"workflow-gen-test-guide",
|
|
573
575
|
"workflow-generate-mainflow-cases",
|
|
574
576
|
"workflow-integration-verify",
|
|
575
577
|
"workflow-orchestrator",
|
|
@@ -621,6 +623,7 @@ const REQUIRED_SCRIPTS = [
|
|
|
621
623
|
"workflow-orchestrator/scripts/tdd_evidence.py",
|
|
622
624
|
"workflow-orchestrator/scripts/acceptance_test_runner.py",
|
|
623
625
|
"workflow-orchestrator/scripts/machine_evidence.rb",
|
|
626
|
+
"workflow-orchestrator/scripts/review_preflight.py",
|
|
624
627
|
"workflow-orchestrator/scripts/cross_track_consistency_check.rb",
|
|
625
628
|
"workflow-orchestrator/scripts/execution_contract.rb",
|
|
626
629
|
"workflow-code-index/scripts/code_index.py",
|
|
@@ -699,6 +702,7 @@ function assertRuntimeProjectBoundary(cwd: string): void {
|
|
|
699
702
|
".planning/.devflow.lock",
|
|
700
703
|
".planning/workflows",
|
|
701
704
|
".planning/artifacts",
|
|
705
|
+
".planning/review-runs",
|
|
702
706
|
]) {
|
|
703
707
|
const target = path.join(resolvedCwd, relative);
|
|
704
708
|
try {
|
|
@@ -746,7 +750,9 @@ function isCoreOwnedGovernancePath(filePath: string): boolean {
|
|
|
746
750
|
// same ownership extends to its views, machine evidence, and audit records.
|
|
747
751
|
return isCanonicalRequirementGraphPath(filePath)
|
|
748
752
|
|| /^\.planning\/evidence\/[^/\\\x00-\x1F\x7F]+\/[^/\\]+\.machine\.json$/u.test(filePath)
|
|
749
|
-
|| /^\.planning\/contracts\/[^/\\\x00-\x1F\x7F]+\/(?:EC-[A-Fa-f0-9]+\.json|control-events\.jsonl)$/u.test(filePath)
|
|
753
|
+
|| /^\.planning\/contracts\/[^/\\\x00-\x1F\x7F]+\/(?:EC-[A-Fa-f0-9]+\.json|control-events\.jsonl)$/u.test(filePath)
|
|
754
|
+
|| filePath.startsWith(".planning/review-runs/")
|
|
755
|
+
|| /^\.planning\/phases\/[^/\\\x00-\x1F\x7F]+\/REVIEW\.md$/u.test(filePath);
|
|
750
756
|
}
|
|
751
757
|
|
|
752
758
|
function planningFeatureFromPath(filePath: string): string | null {
|
|
@@ -892,6 +898,17 @@ function parseJsonResult(result: ProcessResult): Record<string, unknown> | null
|
|
|
892
898
|
}
|
|
893
899
|
}
|
|
894
900
|
|
|
901
|
+
export function canonicalReviewJson(value: unknown): string {
|
|
902
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
|
|
903
|
+
if (typeof value === "number" && Number.isFinite(value)) return JSON.stringify(value);
|
|
904
|
+
if (Array.isArray(value)) return `[${value.map(canonicalReviewJson).join(",")}]`;
|
|
905
|
+
if (value && typeof value === "object") {
|
|
906
|
+
const record = value as Record<string, unknown>;
|
|
907
|
+
return `{${Object.keys(record).sort().map(key => `${JSON.stringify(key)}:${canonicalReviewJson(record[key])}`).join(",")}}`;
|
|
908
|
+
}
|
|
909
|
+
throw new Error("Review JSON contains a non-JSON value");
|
|
910
|
+
}
|
|
911
|
+
|
|
895
912
|
/** Ruby control-plane scripts report expected validation failures as JSON with
|
|
896
913
|
* `ok: false` and intentionally keep a zero process exit code. Never mistake
|
|
897
914
|
* that structured failure for a successful state mutation. */
|
|
@@ -917,6 +934,7 @@ function compactStep(value: unknown): Record<string, unknown> | null {
|
|
|
917
934
|
required: record.required,
|
|
918
935
|
exists: record.exists,
|
|
919
936
|
artifact_status: record.artifact_status,
|
|
937
|
+
content_valid: record.content_valid,
|
|
920
938
|
};
|
|
921
939
|
})
|
|
922
940
|
: undefined,
|
|
@@ -943,6 +961,10 @@ function isReviewableWorkflowSnapshot(snapshot: Record<string, unknown>): boolea
|
|
|
943
961
|
if (!step || typeof step !== "object") return false;
|
|
944
962
|
const stepRecord = step as Record<string, unknown>;
|
|
945
963
|
if (typeof stepRecord.id !== "string" || !stepRecord.id) return false;
|
|
964
|
+
// Core-generated quality findings are advisory by default. Re-arm the AI
|
|
965
|
+
// review even when an output is stale or its generated validity metadata is
|
|
966
|
+
// incomplete; only the control-plane shape itself remains a hard boundary.
|
|
967
|
+
if (process.env.DEVFLOW_SOFT_GATES !== "0" && snapshot.action === "await_user_confirmation") return true;
|
|
946
968
|
const outputs = Array.isArray(stepRecord.outputs) ? stepRecord.outputs : [];
|
|
947
969
|
const requiredOutputs = outputs.filter(output => {
|
|
948
970
|
if (!output || typeof output !== "object") return false;
|
|
@@ -951,7 +973,13 @@ function isReviewableWorkflowSnapshot(snapshot: Record<string, unknown>): boolea
|
|
|
951
973
|
return requiredOutputs.length > 0 && requiredOutputs.every(output => {
|
|
952
974
|
if (!output || typeof output !== "object") return false;
|
|
953
975
|
const record = output as Record<string, unknown>;
|
|
954
|
-
|
|
976
|
+
// Older/current inspect_state payloads may omit content_valid even when
|
|
977
|
+
// Core has already validated and registered the artifact. Treat an
|
|
978
|
+
// explicit false as invalid, but use exists + artifact_status as the
|
|
979
|
+
// durable fallback when the field is absent.
|
|
980
|
+
return record.exists === true
|
|
981
|
+
&& record.content_valid !== false
|
|
982
|
+
&& !["failed", "blocked", "planned"].includes(String(record.artifact_status ?? ""));
|
|
955
983
|
});
|
|
956
984
|
}
|
|
957
985
|
|
|
@@ -992,6 +1020,16 @@ export function workflowResumeDisposition(snapshot: Record<string, unknown>): Wo
|
|
|
992
1020
|
|
|
993
1021
|
function workflowDirective(snapshot: Record<string, unknown>, source: "start" | "approval", requirementDraft?: string): string {
|
|
994
1022
|
const selectedStep = selectedWorkflowStep(snapshot);
|
|
1023
|
+
const reviewInstructions = selectedStep?.id === "code-review"
|
|
1024
|
+
? [
|
|
1025
|
+
"派发审查 task 前先调用 devflow_review_preflight,使用当前 Core 冻结契约和审查者 devflow-code-reviewer;只有 ready 才派发,human_confirmation 与 core_contract_conflict 均停止并上报。ready 只是可执行性预检,不是审查通过。",
|
|
1026
|
+
"本步骤由主 Agent 使用 task 显式指定 devflow-code-reviewer 执行独立只读审查;主 Agent 不得代替审查者给出 clean 结论。",
|
|
1027
|
+
"任务包只能引用已确认的 Core 需求/契约、固定版本源码和测试证据;主 Agent 的摘要不是验收事实。审查者须独立回读事实与源码,返回发现、证据、未决人工问题和改进建议。",
|
|
1028
|
+
"若需第二或第三轮审查,任务包必须携带前轮 chain_id,审查者须在 JSON 中原样返回;每轮限一个独立审查 task,最多三轮。P0/P1 影响和触发条件必须引用预先冻结的 Core 测试用例事实,缺失则转人工确认。",
|
|
1029
|
+
"审查 task 返回严格 JSON 后,主 Agent 调用 devflow_review_submit({}) 提交该 task 的原始结果;不得重写观察项或自报 clean。随后将 Core 返回的裁决、证据链、开放 finding 和下一步动作完整交给用户。",
|
|
1030
|
+
"审查报告及 task 返回文本不构成 Core 门禁通过;证据不足或审查服务返回 awaiting_human_confirmation 时必须保留阻断并报告给主 Agent。",
|
|
1031
|
+
]
|
|
1032
|
+
: [];
|
|
995
1033
|
const envelope = {
|
|
996
1034
|
source,
|
|
997
1035
|
action: snapshot.action,
|
|
@@ -1021,6 +1059,7 @@ function workflowDirective(snapshot: Record<string, unknown>, source: "start" |
|
|
|
1021
1059
|
"Devflow 已通过启动预检。读取 skill://workflow-pipeline,并只执行当前项目的一个工作流步骤。",
|
|
1022
1060
|
"先调用 devflow_state(action=\"inspect\") 重新确认 revision,再调用 devflow_contract(action=\"status\");若没有匹配当前 feature/current_step 的 fresh contract,立即调用 devflow_contract(action=\"activate\", feature=<current_feature>, step=<current_step>),激活成功后才能执行或写入。",
|
|
1023
1061
|
"只按 inspector 返回的当前 step 加载所需 Skill:step.skill 是精确 ID,必须读取 skill://<step.skill>,不得自行添加 workflow- 前缀或改写名称;本回合严禁读取或写入 xd://devflow_transition,严禁调用 devflow_transition,严禁执行 Skill 中的 complete-step/confirm-checkpoint,不得推进下一步骤;状态写回和会话切换由 Runtime 在用户确认后独占处理。",
|
|
1062
|
+
...reviewInstructions,
|
|
1024
1063
|
...(parallelInstructions.length > 0 ? parallelInstructions : [
|
|
1025
1064
|
"完成本步骤的真实产物后,报告证据、未决项和验证结果并停止。请用户审查后明确回复“批准执行”才可推进;回复修改意见或“拒绝执行”则不得推进。",
|
|
1026
1065
|
]),
|
|
@@ -1135,11 +1174,11 @@ export function isShellControlMutation(toolName: string, input: unknown): boolea
|
|
|
1135
1174
|
const looksLikeWrite = /(?:^|[;&|]\s*)(?:rm|mv|cp|install|truncate)\b|(?:^|\s)(?:tee|sed\s+-i|perl\s+-i)\b|(?:^|[\s;&|])(?:1)?>{1,2}(?![>=&])\s*\S/.test(
|
|
1136
1175
|
command,
|
|
1137
1176
|
);
|
|
1138
|
-
return invokesWriter || (namesControlPath && looksLikeWrite);
|
|
1177
|
+
return invokesWriter || (namesControlPath && (looksLikeWrite || shellMutationPaths(command, ".").mutating));
|
|
1139
1178
|
}
|
|
1140
1179
|
|
|
1141
1180
|
function shellMutationPaths(command: string, cwd: string): { mutating: boolean; paths: string[] } {
|
|
1142
|
-
const mutating = /(?:\bsed\s+-i\b|\bperl\s+-i\b|\b(?:cp|mv|rm|install|truncate|tee)\b|(?:^|\s)>{1,2}\s*\S|\b(?:python3?|node|ruby)\b[^\n]*(?:writeFile|write_text|open\([^\n]*['\"]w|unlink|rename)\b)/i.test(command);
|
|
1181
|
+
const mutating = /(?:\bsed\s+-i\b|\bperl\s+-i\b|\b(?:cp|mv|rm|install|truncate|tee)\b|(?:^|\s)>{1,2}(?![>=&])\s*\S|\b(?:python3?|node|ruby)\b[^\n]*(?:writeFile|write_text|open\([^\n]*['\"]w|unlink|rename)\b)/i.test(command);
|
|
1143
1182
|
if (!mutating) return { mutating: false, paths: [] };
|
|
1144
1183
|
const candidates = command.match(/(?:^|\s|>)([A-Za-z0-9_./~-]+)(?=\s|$|2?>)/g) ?? [];
|
|
1145
1184
|
const paths = candidates
|
|
@@ -1164,6 +1203,7 @@ const CONTRACT_READ_TOOLS = new Set([
|
|
|
1164
1203
|
"devflow_acceptance_tests",
|
|
1165
1204
|
"devflow_doctor",
|
|
1166
1205
|
"devflow_state",
|
|
1206
|
+
"devflow_review_preflight",
|
|
1167
1207
|
"devflow_usage_stats",
|
|
1168
1208
|
"usage_stats",
|
|
1169
1209
|
]);
|
|
@@ -1207,6 +1247,7 @@ function stepScopedMutationOwner(toolName: string, input: unknown): string | nul
|
|
|
1207
1247
|
const step = input && typeof input === "object" ? (input as Record<string, unknown>).step : undefined;
|
|
1208
1248
|
return typeof step === "string" && step ? step : null;
|
|
1209
1249
|
}
|
|
1250
|
+
if (normalized === "devflow_review_submit" || normalized === "devflow_review_render") return "code-review";
|
|
1210
1251
|
// New mutating Devflow tools must declare a step above or be explicitly
|
|
1211
1252
|
// classified read/control-only; they never inherit a broad devflow_* bypass.
|
|
1212
1253
|
return null;
|
|
@@ -1362,6 +1403,10 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
1362
1403
|
let controlState: SessionControlState = initialControlState();
|
|
1363
1404
|
let activeContract: ActiveExecutionContract | undefined;
|
|
1364
1405
|
let contractAgentCalls = 0;
|
|
1406
|
+
const reviewBindingKey = randomBytes(32).toString("hex");
|
|
1407
|
+
const pendingReviewTasks = new Map<string, { feature: string; contractId: string }>();
|
|
1408
|
+
const completedReviewTasks = new Map<string, { feature: string; contractId: string; report: Record<string, unknown> }>();
|
|
1409
|
+
let latestReviewTaskCallId: string | undefined;
|
|
1365
1410
|
let projectRoot: string | undefined;
|
|
1366
1411
|
|
|
1367
1412
|
async function appendEngineEvidence(cwd: string | undefined, event: string, extra: Record<string, unknown> = {}) {
|
|
@@ -2773,10 +2818,36 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2773
2818
|
) {
|
|
2774
2819
|
return { block: true, reason: `Active contract ${activeContract.contractId} only allows agents: ${activeContract.allowedAgents.join(", ")}` };
|
|
2775
2820
|
}
|
|
2776
|
-
if (activeContract && controlState.phase === "executing" && contractAgentCalls
|
|
2821
|
+
if (activeContract && controlState.phase === "executing" && contractAgentCalls + requested.names.length > activeContract.toolBudget) {
|
|
2777
2822
|
return { block: true, reason: `Active contract ${activeContract.contractId} exhausted its fixed agent budget (${activeContract.toolBudget}).` };
|
|
2778
2823
|
}
|
|
2779
|
-
if (activeContract && controlState.phase === "executing"
|
|
2824
|
+
if (activeContract && controlState.phase === "executing" && activeContract.step === "code-review" && requested.names.length !== 1) {
|
|
2825
|
+
return { block: true, reason: "Code review requires exactly one independent reviewer task." };
|
|
2826
|
+
}
|
|
2827
|
+
if (activeContract && controlState.phase === "executing" && activeContract.step === "code-review") {
|
|
2828
|
+
const preflight = await runProcess([
|
|
2829
|
+
"python3", path.join(resolveKitRoot(ctx.cwd), "workflow-orchestrator/scripts/review_preflight.py"),
|
|
2830
|
+
"--root", ctx.cwd,
|
|
2831
|
+
"--feature", activeContract.feature,
|
|
2832
|
+
"--step", activeContract.step,
|
|
2833
|
+
"--contract", `.planning/contracts/${activeContract.feature}/${activeContract.contractId}.json`,
|
|
2834
|
+
"--obligations", "[]",
|
|
2835
|
+
"--review-agent", "devflow-code-reviewer",
|
|
2836
|
+
"--stage", "entry",
|
|
2837
|
+
], ctx.cwd);
|
|
2838
|
+
let result: Record<string, unknown> | null = null;
|
|
2839
|
+
try {
|
|
2840
|
+
result = JSON.parse(preflight.stdout) as Record<string, unknown>;
|
|
2841
|
+
} catch {
|
|
2842
|
+
// An unparseable Core preflight result cannot authorize a reviewer task.
|
|
2843
|
+
}
|
|
2844
|
+
if (!preflight.ok || result?.status !== "ready") {
|
|
2845
|
+
return { block: true, reason: `Code review task preflight did not pass: ${preflight.stdout || preflight.stderr || "unavailable"}` };
|
|
2846
|
+
}
|
|
2847
|
+
if (!toolCallId) return { block: true, reason: "Code review task needs a runtime tool-call identity." };
|
|
2848
|
+
pendingReviewTasks.set(toolCallId, { feature: activeContract.feature, contractId: activeContract.contractId });
|
|
2849
|
+
}
|
|
2850
|
+
if (activeContract && controlState.phase === "executing") contractAgentCalls += requested.names.length;
|
|
2780
2851
|
} else if (
|
|
2781
2852
|
controlState.phase === "planning" &&
|
|
2782
2853
|
toolName !== "devflow_contract" &&
|
|
@@ -2924,6 +2995,30 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
2924
2995
|
});
|
|
2925
2996
|
|
|
2926
2997
|
pi.on("tool_result", async (event, ctx) => {
|
|
2998
|
+
if (event.toolName === "task") {
|
|
2999
|
+
const binding = pendingReviewTasks.get(event.toolCallId);
|
|
3000
|
+
if (binding) {
|
|
3001
|
+
pendingReviewTasks.delete(event.toolCallId);
|
|
3002
|
+
const details = event.details && typeof event.details === "object" ? event.details as Record<string, unknown> : {};
|
|
3003
|
+
const results = Array.isArray(details.results) ? details.results : [];
|
|
3004
|
+
const child = results.length === 1 && results[0] && typeof results[0] === "object"
|
|
3005
|
+
? results[0] as Record<string, unknown> : null;
|
|
3006
|
+
if (!event.isError && child?.agent === "devflow-code-reviewer" && child.exitCode === 0 && child.truncated === false && typeof child.output === "string") {
|
|
3007
|
+
try {
|
|
3008
|
+
const report = JSON.parse(child.output) as unknown;
|
|
3009
|
+
if (report && typeof report === "object" && !Array.isArray(report)
|
|
3010
|
+
&& !("core_review_binding" in report)
|
|
3011
|
+
&& (report as Record<string, unknown>).target && typeof (report as Record<string, unknown>).target === "object"
|
|
3012
|
+
&& ((report as Record<string, unknown>).target as Record<string, unknown>).feature === binding.feature) {
|
|
3013
|
+
completedReviewTasks.set(event.toolCallId, { ...binding, report: report as Record<string, unknown> });
|
|
3014
|
+
latestReviewTaskCallId = event.toolCallId;
|
|
3015
|
+
}
|
|
3016
|
+
} catch {
|
|
3017
|
+
// A prose or malformed child response is not an attested review.
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
2927
3022
|
const taskContext = parallelTaskContexts.get(event.toolCallId);
|
|
2928
3023
|
if (parallelRunActive && event.toolName === "task") {
|
|
2929
3024
|
if (taskResultHasSuccessfulDispatch(event.details)) {
|
|
@@ -3669,7 +3764,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
3669
3764
|
parameters: z.object({
|
|
3670
3765
|
feature: z.string().min(1),
|
|
3671
3766
|
phase: z.string().min(1).optional(),
|
|
3672
|
-
flow: z.
|
|
3767
|
+
flow: z.enum(["feature-development", "acceptance-remediation"]).default("feature-development"),
|
|
3673
3768
|
force: z.boolean().default(false),
|
|
3674
3769
|
}),
|
|
3675
3770
|
approval: "write",
|
|
@@ -3824,6 +3919,101 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
3824
3919
|
},
|
|
3825
3920
|
});
|
|
3826
3921
|
|
|
3922
|
+
pi.registerTool({
|
|
3923
|
+
name: "devflow_review_preflight",
|
|
3924
|
+
label: "Devflow Review Preflight",
|
|
3925
|
+
description: "Read-only Core preflight for current step facts, contract revisions, production obligations, and reviewer task reachability. Ready is not gate approval.",
|
|
3926
|
+
parameters: z.object({
|
|
3927
|
+
feature: z.string().min(1),
|
|
3928
|
+
step: z.string().min(1),
|
|
3929
|
+
contract: z.string().min(1),
|
|
3930
|
+
obligations: z.array(z.record(z.string(), z.unknown())).default([]),
|
|
3931
|
+
reviewAgent: z.string().optional(),
|
|
3932
|
+
stage: z.enum(["entry", "completion"]).default("entry"),
|
|
3933
|
+
}),
|
|
3934
|
+
approval: "read",
|
|
3935
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
3936
|
+
assertRuntimeProjectBoundary(ctx.cwd);
|
|
3937
|
+
return renderResult(await runProcess([
|
|
3938
|
+
"python3", path.join(resolveKitRoot(ctx.cwd), "workflow-orchestrator/scripts/review_preflight.py"),
|
|
3939
|
+
"--root", ctx.cwd,
|
|
3940
|
+
"--feature", params.feature,
|
|
3941
|
+
"--step", params.step,
|
|
3942
|
+
"--contract", params.contract,
|
|
3943
|
+
"--obligations", JSON.stringify(params.obligations),
|
|
3944
|
+
"--stage", params.stage,
|
|
3945
|
+
...(params.reviewAgent ? ["--review-agent", params.reviewAgent] : []),
|
|
3946
|
+
], ctx.cwd));
|
|
3947
|
+
},
|
|
3948
|
+
});
|
|
3949
|
+
|
|
3950
|
+
pi.registerTool({
|
|
3951
|
+
name: "devflow_review_submit",
|
|
3952
|
+
label: "Devflow Bound Review Submission",
|
|
3953
|
+
description: "Submit the exact JSON returned by the completed independent reviewer task to Core. Caller text cannot replace the child report.",
|
|
3954
|
+
parameters: z.object({ taskCallId: z.string().optional() }),
|
|
3955
|
+
approval: "write",
|
|
3956
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
3957
|
+
assertRuntimeProjectBoundary(ctx.cwd);
|
|
3958
|
+
const taskCallId = params.taskCallId || latestReviewTaskCallId;
|
|
3959
|
+
const captured = taskCallId ? completedReviewTasks.get(taskCallId) : undefined;
|
|
3960
|
+
if (!taskCallId || !captured || activeContract?.step !== "code-review"
|
|
3961
|
+
|| activeContract.feature !== captured.feature || activeContract.contractId !== captured.contractId) {
|
|
3962
|
+
return renderResult({ ok: false, exitCode: 2, stdout: "", stderr: "No completed reviewer task is bound to the active code-review contract." });
|
|
3963
|
+
}
|
|
3964
|
+
const reportSha256 = createHash("sha256").update(canonicalReviewJson(captured.report), "utf8").digest("hex");
|
|
3965
|
+
const identity = {
|
|
3966
|
+
agent: "devflow-code-reviewer",
|
|
3967
|
+
feature: captured.feature,
|
|
3968
|
+
contract_id: captured.contractId,
|
|
3969
|
+
task_call_id: taskCallId,
|
|
3970
|
+
report_sha256: reportSha256,
|
|
3971
|
+
};
|
|
3972
|
+
const signature = createHmac("sha256", reviewBindingKey).update(canonicalReviewJson(identity), "utf8").digest("hex");
|
|
3973
|
+
const request = { ...captured.report, core_review_binding: { ...identity, signature } };
|
|
3974
|
+
const result = await runProcess(
|
|
3975
|
+
["python3", path.join(resolveKitRoot(ctx.cwd), "workflow-orchestrator/scripts/review_run.py"), ctx.cwd],
|
|
3976
|
+
ctx.cwd,
|
|
3977
|
+
{ DEVFLOW_REVIEW_BINDING_KEY: reviewBindingKey },
|
|
3978
|
+
JSON.stringify(request),
|
|
3979
|
+
);
|
|
3980
|
+
const submitted = parseJsonResult(result);
|
|
3981
|
+
if (!result.ok || !submitted) return renderResult(result, { taskCallId, feature: captured.feature, contractId: captured.contractId });
|
|
3982
|
+
completedReviewTasks.delete(taskCallId);
|
|
3983
|
+
if (!["clean", "findings", "terminated_p0"].includes(String(submitted.status))) {
|
|
3984
|
+
return renderResult(result, { taskCallId, feature: captured.feature, contractId: captured.contractId });
|
|
3985
|
+
}
|
|
3986
|
+
const script = path.join(resolveKitRoot(ctx.cwd), "workflow-orchestrator/scripts/review_run.py");
|
|
3987
|
+
const rendered = await runProcess(["python3", script, ctx.cwd, "--render", captured.feature, captured.contractId], ctx.cwd);
|
|
3988
|
+
const renderPayload = parseJsonResult(rendered);
|
|
3989
|
+
if (!rendered.ok || renderPayload?.status !== "rendered") {
|
|
3990
|
+
return renderResult({ ok: false, exitCode: 1, stdout: JSON.stringify({ review_result: submitted, render: renderPayload }), stderr: rendered.stderr || "Core review report rendering failed; call devflow_review_render to retry." }, { taskCallId });
|
|
3991
|
+
}
|
|
3992
|
+
const gated = await runProcess(["python3", script, ctx.cwd, "--gate", captured.feature, captured.contractId], ctx.cwd);
|
|
3993
|
+
const gatePayload = parseJsonResult(gated);
|
|
3994
|
+
if (!gated.ok || !gatePayload) return renderResult(gated, { taskCallId, review_result: submitted });
|
|
3995
|
+
return renderResult({ ok: true, exitCode: 0, stdout: JSON.stringify({ review_result: submitted, gate: gatePayload }), stderr: "" }, { taskCallId, feature: captured.feature, contractId: captured.contractId });
|
|
3996
|
+
},
|
|
3997
|
+
});
|
|
3998
|
+
|
|
3999
|
+
pi.registerTool({
|
|
4000
|
+
name: "devflow_review_render",
|
|
4001
|
+
label: "Devflow Core Review Report",
|
|
4002
|
+
description: "Regenerate the code-review Markdown view from the latest Core-bound review round; it does not change the verdict.",
|
|
4003
|
+
parameters: z.object({}),
|
|
4004
|
+
approval: "write",
|
|
4005
|
+
async execute(_id, _params, _signal, _onUpdate, ctx) {
|
|
4006
|
+
assertRuntimeProjectBoundary(ctx.cwd);
|
|
4007
|
+
if (activeContract?.step !== "code-review") {
|
|
4008
|
+
return renderResult({ ok: false, exitCode: 2, stdout: "", stderr: "A current code-review contract is required." });
|
|
4009
|
+
}
|
|
4010
|
+
return renderResult(await runProcess([
|
|
4011
|
+
"python3", path.join(resolveKitRoot(ctx.cwd), "workflow-orchestrator/scripts/review_run.py"),
|
|
4012
|
+
ctx.cwd, "--render", activeContract.feature, activeContract.contractId,
|
|
4013
|
+
], ctx.cwd));
|
|
4014
|
+
},
|
|
4015
|
+
});
|
|
4016
|
+
|
|
3827
4017
|
pi.registerTool({
|
|
3828
4018
|
name: "devflow_review_run",
|
|
3829
4019
|
label: "Devflow Fact-Bound Review",
|
|
@@ -393,6 +393,21 @@ def run(root: Path, feature: str, write: bool) -> dict[str, Any]:
|
|
|
393
393
|
"execution_binding_fingerprint": file_fingerprint(execution_bindings_path), "results": results, "passed": all_passed, "history": history[-20:]}
|
|
394
394
|
if write:
|
|
395
395
|
write_json(result_path, payload)
|
|
396
|
+
meta_path = result_path.with_name("acceptance-results.meta.json")
|
|
397
|
+
passed = sum(1 for item in results if item.get("status") == "passed")
|
|
398
|
+
failed = sum(1 for item in results if item.get("status") == "failed")
|
|
399
|
+
blocked = sum(1 for item in results if item.get("status") == "blocked")
|
|
400
|
+
write_json(meta_path, {
|
|
401
|
+
"schema_version": 1,
|
|
402
|
+
"feature": feature,
|
|
403
|
+
"acceptance_tc_passed": passed,
|
|
404
|
+
"acceptance_tc_failed": failed,
|
|
405
|
+
"acceptance_tc_blocked": blocked,
|
|
406
|
+
"acceptance_suite_graph_revision": payload["test_suite"].get("graph_revision"),
|
|
407
|
+
"acceptance_results_fingerprint": file_fingerprint(result_path),
|
|
408
|
+
"built_by": "core_acceptance_test_runner",
|
|
409
|
+
"built_at": payload["executed_at"],
|
|
410
|
+
})
|
|
396
411
|
return {"ok": all_passed, "feature": feature, "path": str(result_path.relative_to(root)), "results": results,
|
|
397
412
|
"errors": [] if all_passed else ["one or more frozen TC cases failed or are blocked"]}
|
|
398
413
|
|
|
@@ -108,7 +108,7 @@ def transition_gate_contract(workflow, step)
|
|
|
108
108
|
"topology" => {
|
|
109
109
|
"allowed_tracks" => allowed_tracks,
|
|
110
110
|
"allowed_predecessors" => Array(step["depends_on"]),
|
|
111
|
-
|
|
111
|
+
"allowed_intervening_predecessors" => ((immediate_prior ? [immediate_prior["id"]] : []) + Array(step["allowed_intervening_predecessors"])).uniq,
|
|
112
112
|
"allowed_reentry" => reentries,
|
|
113
113
|
"checkpoint" => checkpoint && checkpoint.merge("id" => checkpoint_id)
|
|
114
114
|
},
|
|
@@ -529,8 +529,40 @@ def repair_legacy_parallel_join_predecessor!(state, workflow)
|
|
|
529
529
|
predecessor
|
|
530
530
|
end
|
|
531
531
|
|
|
532
|
+
# Business-quality checkers are advisory by default. The runtime still keeps
|
|
533
|
+
# a small safety floor for malformed control-plane state and unsafe writes, but
|
|
534
|
+
# Core-generated test/traceability/merge findings no longer strand the serial
|
|
535
|
+
# workflow before the AI review can decide what to do.
|
|
536
|
+
SOFT_GATE_MODE = ENV.fetch("DEVFLOW_SOFT_GATES", "1") != "0"
|
|
537
|
+
HARD_FAILURE_REASONS = %w[
|
|
538
|
+
missing_state_file
|
|
539
|
+
missing_state_front_matter
|
|
540
|
+
stale_revision
|
|
541
|
+
invalid_control_path
|
|
542
|
+
unknown_step
|
|
543
|
+
gate_target_step_unknown
|
|
544
|
+
gate_current_step_mismatch
|
|
545
|
+
parallel_transition_requires_write
|
|
546
|
+
feature_not_active
|
|
547
|
+
cancel_reason_required
|
|
548
|
+
session_rollover_in_progress
|
|
549
|
+
failure_branch_incomplete
|
|
550
|
+
parallel_machine_evidence_failed
|
|
551
|
+
parallel_join_failed
|
|
552
|
+
parallel_branch_transition_failed
|
|
553
|
+
cross_track_consistency_failed
|
|
554
|
+
business_cross_model_check_failed
|
|
555
|
+
gate_boundary_rejected
|
|
556
|
+
].freeze
|
|
557
|
+
$devflow_advisories = []
|
|
558
|
+
|
|
532
559
|
def fail_result(reason, details = {})
|
|
533
|
-
{ "ok" => false, "reason" => reason, "details" => details }
|
|
560
|
+
failure = { "ok" => false, "reason" => reason, "details" => details }
|
|
561
|
+
if SOFT_GATE_MODE && !HARD_FAILURE_REASONS.include?(reason.to_s)
|
|
562
|
+
$devflow_advisories << failure
|
|
563
|
+
return nil
|
|
564
|
+
end
|
|
565
|
+
failure
|
|
534
566
|
end
|
|
535
567
|
|
|
536
568
|
# CP-3 has to validate a *candidate* task/TC binding before it freezes the
|
|
@@ -791,6 +823,19 @@ result =
|
|
|
791
823
|
# GateRuntime is a read-only boundary check. It runs while the project
|
|
792
824
|
# lock is held and before any candidate state mutation; the existing
|
|
793
825
|
# validator and advance_state CAS remain the final write guard.
|
|
826
|
+
if result.nil? && gate_contract_enabled?(workflow) &&
|
|
827
|
+
!%w[prepared claimed].include?(state.dig("rollover", "status")) &&
|
|
828
|
+
(options[:complete_step] || options[:confirm_checkpoint] || options[:enter_failure_branch] || options[:exit_failure_branch] || options[:parallel_action])
|
|
829
|
+
topology_errors = WorkflowGateContract.validate(workflow)
|
|
830
|
+
unless topology_errors.empty?
|
|
831
|
+
result = fail_result("gate_boundary_rejected", "gate" => {
|
|
832
|
+
"verdict" => "deny",
|
|
833
|
+
"reason" => "workflow_gate_contract_invalid",
|
|
834
|
+
"errors" => topology_errors,
|
|
835
|
+
})
|
|
836
|
+
end
|
|
837
|
+
end
|
|
838
|
+
|
|
794
839
|
if result.nil? && gate_contract_enabled?(workflow) &&
|
|
795
840
|
!%w[prepared claimed].include?(state.dig("rollover", "status")) &&
|
|
796
841
|
(options[:complete_step] || options[:confirm_checkpoint] || options[:enter_failure_branch] || options[:exit_failure_branch] || options[:parallel_action])
|
|
@@ -1187,7 +1232,7 @@ result =
|
|
|
1187
1232
|
phase_owner_override: prospective_owner
|
|
1188
1233
|
)
|
|
1189
1234
|
|
|
1190
|
-
if changed && !validation["ok"]
|
|
1235
|
+
if changed && !validation["ok"] && !SOFT_GATE_MODE
|
|
1191
1236
|
result = fail_result("state_validation_failed", "validation" => validation)
|
|
1192
1237
|
elsif changed && options[:write]
|
|
1193
1238
|
ownership = PhaseOwnership.claim(
|
|
@@ -1249,6 +1294,7 @@ result =
|
|
|
1249
1294
|
"actions" => actions,
|
|
1250
1295
|
"state_path" => state_path,
|
|
1251
1296
|
"next_intent_written" => next_intent_written,
|
|
1297
|
+
"advisories" => $devflow_advisories,
|
|
1252
1298
|
"state_validation" => validation,
|
|
1253
1299
|
"state" => state
|
|
1254
1300
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The single execution boundary for Core checkers. Registry membership alone
|
|
4
|
+
# is not evidence that a checker can run; every non graph-backed checker must
|
|
5
|
+
# have an adapter here.
|
|
6
|
+
require "json"
|
|
7
|
+
require "open3"
|
|
8
|
+
require_relative "checker_registry"
|
|
9
|
+
|
|
10
|
+
module CheckerAdapter
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def execute(root:, feature:, step:, id:, graph_results:)
|
|
14
|
+
CheckerRegistry.validate_executable!([id])
|
|
15
|
+
if CheckerRegistry::GRAPH_BACKED.include?(id.to_s)
|
|
16
|
+
issues = graph_results.fetch(id.to_s, ["checker produced no graph result: #{id}"])
|
|
17
|
+
return result(id, issues.empty?, issues)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
command = command_for(root, feature, step, id)
|
|
21
|
+
return result(id, false, ["no adapter command for #{id}"]) unless command
|
|
22
|
+
|
|
23
|
+
stdout, stderr, status = Open3.capture3(*command, chdir: root)
|
|
24
|
+
payload = JSON.parse(stdout)
|
|
25
|
+
passed = status.success? && payload.is_a?(Hash) && (payload["ok"] == true || payload["valid"] == true)
|
|
26
|
+
issues = if passed
|
|
27
|
+
[]
|
|
28
|
+
elsif payload.is_a?(Hash) && payload["errors"].is_a?(Array)
|
|
29
|
+
payload["errors"].map { |item| item.is_a?(String) ? item : JSON.generate(item) }
|
|
30
|
+
else
|
|
31
|
+
[stderr.strip.empty? ? "#{id} failed" : stderr.strip]
|
|
32
|
+
end
|
|
33
|
+
result(id, passed, issues)
|
|
34
|
+
rescue JSON::ParserError => error
|
|
35
|
+
result(id, false, ["#{id} returned invalid JSON: #{error.message}"])
|
|
36
|
+
rescue StandardError => error
|
|
37
|
+
result(id, false, ["#{id} adapter failed: #{error.message}"])
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def result(id, passed, issues)
|
|
41
|
+
{ "id" => id, "origin" => "core_fixed_checker", "exit_code" => passed ? 0 : 1,
|
|
42
|
+
"passed" => passed, "issues" => passed ? [] : Array(issues),
|
|
43
|
+
"counters" => { "issue_count" => passed ? 0 : Array(issues).length } }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def command_for(root, feature, step, id)
|
|
47
|
+
scripts = File.expand_path(__dir__)
|
|
48
|
+
case id.to_s
|
|
49
|
+
when "test-task-binding-check"
|
|
50
|
+
stage = step.to_s == "change-plan" ? "candidate" : "frozen"
|
|
51
|
+
["python3", File.join(scripts, "test_task_binding_check.py"), "--feature", feature, "--stage", stage, "--json"]
|
|
52
|
+
when "tdd-evidence-check"
|
|
53
|
+
["python3", File.join(scripts, "tdd_evidence.py"), "validate", "--feature", feature, "--json"]
|
|
54
|
+
when "acceptance-test-results-check"
|
|
55
|
+
["python3", File.join(scripts, "acceptance_test_runner.py"), "validate-results", "--feature", feature, "--json"]
|
|
56
|
+
when "api-test-matrix-check"
|
|
57
|
+
["python3", File.join(scripts, "api_test_matrix.py"), "validate", "--feature", feature, "--json"]
|
|
58
|
+
when "api-test-schema-check"
|
|
59
|
+
["python3", File.join(scripts, "api_test_matrix.py"), "check", "--feature", feature, "--json"]
|
|
60
|
+
when "cross-track-consistency-check"
|
|
61
|
+
["ruby", File.join(scripts, "cross_track_consistency_check.rb"), "--feature", feature, "--json"]
|
|
62
|
+
when "business-cross-model-check"
|
|
63
|
+
business = ".planning/business-model/#{feature}/canonical.json"
|
|
64
|
+
return nil unless File.file?(File.join(root, business))
|
|
65
|
+
command = ["python3", File.join(scripts, "business_cross_model_check.py"), "--business-canonical", business]
|
|
66
|
+
wireframe = ".planning/features/#{feature}/canonical.json"
|
|
67
|
+
claims = ".planning/features/#{feature}/business-claims.yaml"
|
|
68
|
+
traceability = ".planning/requirements/#{feature}/traceability.json"
|
|
69
|
+
command += ["--wireframe", wireframe] if File.file?(File.join(root, wireframe))
|
|
70
|
+
command += ["--claims", claims] if File.file?(File.join(root, claims))
|
|
71
|
+
command += ["--traceability", traceability] if File.file?(File.join(root, traceability))
|
|
72
|
+
command + ["--json"]
|
|
73
|
+
when "business-claim-reference-check"
|
|
74
|
+
claims = ".planning/features/#{feature}/business-claims.yaml"
|
|
75
|
+
business = ".planning/business-model/#{feature}/canonical.json"
|
|
76
|
+
return nil unless File.file?(File.join(root, claims)) && File.file?(File.join(root, business))
|
|
77
|
+
command = ["python3", File.join(scripts, "business_claim_validate.py"), "--claims", claims, "--business-canonical", business]
|
|
78
|
+
traceability = ".planning/requirements/#{feature}/traceability.json"
|
|
79
|
+
command += ["--traceability", traceability] if File.file?(File.join(root, traceability))
|
|
80
|
+
command
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -11,6 +11,15 @@ module CheckerRegistry
|
|
|
11
11
|
business-cross-model-check business-invalidation-check
|
|
12
12
|
].freeze
|
|
13
13
|
EMBEDDED = %w[requirement-coverage-check source-authority-check traceability-check].freeze
|
|
14
|
+
GRAPH_BACKED = %w[
|
|
15
|
+
requirement-graph-check slice-independence-check contract-coverage-check
|
|
16
|
+
change-impact-check acceptance-coverage-check test-coverage-check test-obligation-check
|
|
17
|
+
].freeze
|
|
18
|
+
ADAPTERS = %w[
|
|
19
|
+
api-test-schema-check api-test-matrix-check acceptance-test-results-check
|
|
20
|
+
test-task-binding-check tdd-evidence-check cross-track-consistency-check
|
|
21
|
+
business-cross-model-check business-claim-reference-check
|
|
22
|
+
].freeze
|
|
14
23
|
|
|
15
24
|
module_function
|
|
16
25
|
|
|
@@ -24,6 +33,16 @@ module CheckerRegistry
|
|
|
24
33
|
nil
|
|
25
34
|
end
|
|
26
35
|
|
|
36
|
+
def executable?(id)
|
|
37
|
+
GRAPH_BACKED.include?(id.to_s) || ADAPTERS.include?(id.to_s)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def validate_executable!(ids)
|
|
41
|
+
unsupported = Array(ids).map(&:to_s).reject { |id| executable?(id) }
|
|
42
|
+
raise ArgumentError, "checker has no Core execution adapter: #{unsupported.join(', ')}" unless unsupported.empty?
|
|
43
|
+
true
|
|
44
|
+
end
|
|
45
|
+
|
|
27
46
|
def validate!(ids)
|
|
28
47
|
unknown = Array(ids).map(&:to_s).reject { |id| known?(id) }
|
|
29
48
|
raise ArgumentError, "unknown checker id(s): #{unknown.join(", ")}" unless unknown.empty?
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module CheckpointMeta
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def validate(root:, state:, checkpoint:)
|
|
10
|
+
return [] unless checkpoint.is_a?(Hash)
|
|
11
|
+
|
|
12
|
+
raw_path = checkpoint["required_meta_output"]
|
|
13
|
+
# Legacy/custom checkpoints may not declare checkpoint metadata. Only
|
|
14
|
+
# enforce the contract when a metadata path is explicitly declared.
|
|
15
|
+
return [] if raw_path.to_s.empty?
|
|
16
|
+
|
|
17
|
+
relative = raw_path.to_s
|
|
18
|
+
.gsub("{feature}", state.fetch("current_feature", "").to_s)
|
|
19
|
+
.gsub("{phase}", state.fetch("current_phase", "").to_s)
|
|
20
|
+
absolute = File.expand_path(relative, root)
|
|
21
|
+
unless File.file?(absolute)
|
|
22
|
+
return [{ "code" => "missing_checkpoint_meta_output", "message" => "checkpoint meta output is missing", "path" => relative }]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
data = JSON.parse(File.read(absolute))
|
|
26
|
+
errors = []
|
|
27
|
+
unless data.is_a?(Hash)
|
|
28
|
+
errors << { "code" => "invalid_checkpoint_meta_shape", "message" => "checkpoint meta output must be a JSON object", "path" => relative }
|
|
29
|
+
return errors
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
Array(checkpoint["required_meta_fields"]).each do |field|
|
|
33
|
+
next if data.key?(field.to_s) && !data[field.to_s].nil?
|
|
34
|
+
|
|
35
|
+
errors << {
|
|
36
|
+
"code" => "missing_checkpoint_meta_field",
|
|
37
|
+
"message" => "checkpoint meta output is missing a required field",
|
|
38
|
+
"path" => relative,
|
|
39
|
+
"field" => field
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
errors
|
|
43
|
+
rescue JSON::ParserError => error
|
|
44
|
+
[{ "code" => "invalid_checkpoint_meta_json", "message" => error.message, "path" => relative }]
|
|
45
|
+
end
|
|
46
|
+
end
|