@chenmk/superflow 0.4.0 → 0.5.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/INSTALL.en.md +54 -12
- package/INSTALL.md +59 -33
- package/README.md +178 -54
- package/README.zh-CN.md +156 -51
- package/assets/brand/managed-review-loop.png +0 -0
- package/assets/manifest.json +10 -5
- package/assets/scripts/superflow-coding-ready.mjs +135 -0
- package/assets/scripts/superflow-dependency-update-hook.sh +81 -8
- package/assets/scripts/superflow-document-audit.mjs +240 -0
- package/assets/scripts/superflow-environment-preflight.mjs +398 -0
- package/assets/scripts/superflow-hook-guard.sh +40 -1
- package/assets/scripts/superflow-managed-command-guard.py +309 -0
- package/assets/scripts/superflow-managed-executor-preflight.mjs +421 -0
- package/assets/scripts/superflow-managed-owner-verification.sh +117 -0
- package/assets/scripts/superflow-managed-work-check.mjs +287 -26
- package/assets/scripts/superflow-managed-work-guard.sh +6 -92
- package/assets/skills/superflow-clarify/SKILL.md +14 -0
- package/assets/skills/superflow-design/SKILL.md +38 -13
- package/assets/skills/superflow-docs/SKILL.md +93 -2
- package/assets/skills/superflow-pipeline/SKILL.md +167 -10
- package/assets/skills/superflow-pipeline/references/batch-prompt-template.md +17 -9
- package/assets/skills/superflow-pipeline/references/document-readiness.md +157 -0
- package/assets/skills/superflow-pipeline/references/document-templates.md +9 -5
- package/assets/skills/superflow-pipeline/references/feature-gated-workflow.md +14 -0
- package/assets/skills/superflow-pipeline/references/implementation-prompt-template.md +20 -11
- package/assets/skills/superflow-pipeline/references/managed-work.md +123 -15
- package/assets/skills/superflow-pipeline/references/orchestration.md +2 -2
- package/assets/skills/superflow-pipeline/references/p0-baseline-template.md +3 -2
- package/assets/skills/superflow-pipeline/references/quality-gate.md +7 -5
- package/assets/skills/superflow-pipeline/references/reviewer-checklist.md +7 -0
- package/assets/skills/superflow-pipeline/scripts/superflow-guard.sh +205 -9
- package/assets/skills/superflow-pipeline/scripts/superflow-handoff.sh +7 -4
- package/assets/skills/superflow-requirement-review/SKILL.md +178 -0
- package/assets/skills/superflow-requirement-review/agents/openai.yaml +4 -0
- package/assets/skills-en/superflow-clarify/SKILL.md +17 -0
- package/assets/skills-en/superflow-design/SKILL.md +31 -12
- package/assets/skills-en/superflow-docs/SKILL.md +99 -0
- package/assets/skills-en/superflow-pipeline/SKILL.md +163 -9
- package/assets/skills-en/superflow-pipeline/references/batch-prompt-template.md +4 -0
- package/assets/skills-en/superflow-pipeline/references/document-readiness.md +125 -0
- package/assets/skills-en/superflow-pipeline/references/document-templates.md +10 -0
- package/assets/skills-en/superflow-pipeline/references/feature-gated-workflow.md +7 -0
- package/assets/skills-en/superflow-pipeline/references/implementation-prompt-template.md +7 -0
- package/assets/skills-en/superflow-pipeline/references/managed-work.md +160 -10
- package/assets/skills-en/superflow-pipeline/references/quality-gate.md +12 -0
- package/assets/skills-en/superflow-pipeline/references/reviewer-checklist.md +7 -0
- package/assets/skills-en/superflow-pipeline/scripts/superflow-guard.sh +205 -9
- package/assets/skills-en/superflow-pipeline/scripts/superflow-handoff.sh +7 -4
- package/assets/skills-en/superflow-requirement-review/SKILL.md +99 -0
- package/assets/skills-en/superflow-requirement-review/agents/openai.yaml +4 -0
- package/dist/app/cli.js +64 -9
- package/dist/app/cli.js.map +1 -1
- package/dist/app/commands/change-guard.js +19 -2
- package/dist/app/commands/change-guard.js.map +1 -1
- package/dist/app/commands/check.js +107 -45
- package/dist/app/commands/check.js.map +1 -1
- package/dist/app/commands/docs.js +7 -3
- package/dist/app/commands/docs.js.map +1 -1
- package/dist/app/commands/doctor.js +174 -198
- package/dist/app/commands/doctor.js.map +1 -1
- package/dist/app/commands/eval.js +64 -0
- package/dist/app/commands/eval.js.map +1 -0
- package/dist/app/commands/implement.js +7 -3
- package/dist/app/commands/implement.js.map +1 -1
- package/dist/app/commands/init.js +12 -24
- package/dist/app/commands/init.js.map +1 -1
- package/dist/app/commands/mcp.js +167 -0
- package/dist/app/commands/mcp.js.map +1 -0
- package/dist/app/commands/pipeline.js +652 -18
- package/dist/app/commands/pipeline.js.map +1 -1
- package/dist/app/commands/requirement-review.js +6 -0
- package/dist/app/commands/requirement-review.js.map +1 -0
- package/dist/app/commands/status.js +66 -3
- package/dist/app/commands/status.js.map +1 -1
- package/dist/app/commands/uninstall.js +3 -3
- package/dist/app/commands/uninstall.js.map +1 -1
- package/dist/app/commands/update.js +83 -9
- package/dist/app/commands/update.js.map +1 -1
- package/dist/domains/agent.js +3 -11
- package/dist/domains/agent.js.map +1 -1
- package/dist/domains/config/cli-help.js +54 -12
- package/dist/domains/config/cli-help.js.map +1 -1
- package/dist/domains/config/i18n.js +6 -8
- package/dist/domains/config/i18n.js.map +1 -1
- package/dist/domains/config/manifest.js +0 -3
- package/dist/domains/config/manifest.js.map +1 -1
- package/dist/domains/deps.js +68 -7
- package/dist/domains/deps.js.map +1 -1
- package/dist/domains/managed-work/completion-policy.js +174 -0
- package/dist/domains/managed-work/completion-policy.js.map +1 -0
- package/dist/domains/managed-work/context-manifest.js +208 -0
- package/dist/domains/managed-work/context-manifest.js.map +1 -0
- package/dist/domains/managed-work/contract.js +135 -4
- package/dist/domains/managed-work/contract.js.map +1 -1
- package/dist/domains/managed-work/control-signals.js +34 -0
- package/dist/domains/managed-work/control-signals.js.map +1 -0
- package/dist/domains/managed-work/control.js +628 -0
- package/dist/domains/managed-work/control.js.map +1 -0
- package/dist/domains/managed-work/evaluation.js +497 -0
- package/dist/domains/managed-work/evaluation.js.map +1 -0
- package/dist/domains/managed-work/evidence-path.js +14 -0
- package/dist/domains/managed-work/evidence-path.js.map +1 -0
- package/dist/domains/managed-work/execution-contract.js +167 -0
- package/dist/domains/managed-work/execution-contract.js.map +1 -0
- package/dist/domains/managed-work/executor-policy.js +65 -0
- package/dist/domains/managed-work/executor-policy.js.map +1 -0
- package/dist/domains/managed-work/failure.js +65 -0
- package/dist/domains/managed-work/failure.js.map +1 -0
- package/dist/domains/managed-work/host-review.js +77 -0
- package/dist/domains/managed-work/host-review.js.map +1 -0
- package/dist/domains/managed-work/human-messages.js +31 -0
- package/dist/domains/managed-work/human-messages.js.map +1 -0
- package/dist/domains/managed-work/input.js +29 -3
- package/dist/domains/managed-work/input.js.map +1 -1
- package/dist/domains/managed-work/journal.js +77 -3
- package/dist/domains/managed-work/journal.js.map +1 -1
- package/dist/domains/managed-work/lock.js +67 -1
- package/dist/domains/managed-work/lock.js.map +1 -1
- package/dist/domains/managed-work/notifications.js +0 -15
- package/dist/domains/managed-work/notifications.js.map +1 -1
- package/dist/domains/managed-work/pair-admission.js +108 -0
- package/dist/domains/managed-work/pair-admission.js.map +1 -0
- package/dist/domains/managed-work/prompts.js +257 -22
- package/dist/domains/managed-work/prompts.js.map +1 -1
- package/dist/domains/managed-work/redaction.js +90 -0
- package/dist/domains/managed-work/redaction.js.map +1 -0
- package/dist/domains/managed-work/review-convergence.js +41 -0
- package/dist/domains/managed-work/review-convergence.js.map +1 -0
- package/dist/domains/managed-work/review-facts.js +181 -0
- package/dist/domains/managed-work/review-facts.js.map +1 -0
- package/dist/domains/managed-work/rule-selection.js +84 -0
- package/dist/domains/managed-work/rule-selection.js.map +1 -0
- package/dist/domains/managed-work/runner.js +1967 -175
- package/dist/domains/managed-work/runner.js.map +1 -1
- package/dist/domains/managed-work/schemas.js +72 -2
- package/dist/domains/managed-work/schemas.js.map +1 -1
- package/dist/domains/managed-work/service.js +82 -13
- package/dist/domains/managed-work/service.js.map +1 -1
- package/dist/domains/managed-work/state.js +195 -5
- package/dist/domains/managed-work/state.js.map +1 -1
- package/dist/domains/managed-work/storage.js +66 -24
- package/dist/domains/managed-work/storage.js.map +1 -1
- package/dist/domains/managed-work/types.js +12 -9
- package/dist/domains/managed-work/types.js.map +1 -1
- package/dist/domains/managed-work/verification-categories.js +39 -0
- package/dist/domains/managed-work/verification-categories.js.map +1 -0
- package/dist/domains/managed-work/verification-policy.js +116 -0
- package/dist/domains/managed-work/verification-policy.js.map +1 -0
- package/dist/domains/managed-work/wait.js +21 -3
- package/dist/domains/managed-work/wait.js.map +1 -1
- package/dist/domains/managed-work/workspace-binding.js +133 -0
- package/dist/domains/managed-work/workspace-binding.js.map +1 -0
- package/dist/domains/sdd-readiness.js +44 -0
- package/dist/domains/sdd-readiness.js.map +1 -0
- package/dist/domains/skill/deploy.js +0 -6
- package/dist/domains/skill/deploy.js.map +1 -1
- package/dist/domains/skill/scripts.js +0 -8
- package/dist/domains/skill/scripts.js.map +1 -1
- package/dist/domains/state.js +1 -2
- package/dist/domains/state.js.map +1 -1
- package/dist/mcp/runtime-identity.js +21 -0
- package/dist/mcp/runtime-identity.js.map +1 -0
- package/dist/mcp/server.js +369 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/platform/agent-process.js +499 -38
- package/dist/platform/agent-process.js.map +1 -1
- package/dist/platform/executable.js +28 -0
- package/dist/platform/executable.js.map +1 -0
- package/dist/platform/paths.js +1 -17
- package/dist/platform/paths.js.map +1 -1
- package/dist/platform/process-tree.js +44 -0
- package/dist/platform/process-tree.js.map +1 -0
- package/dist/platform/process.js +1 -1
- package/dist/platform/process.js.map +1 -1
- package/package.json +16 -7
- package/scripts/check-design-principles.mjs +107 -0
- package/scripts/check-managed-work-design.mjs +507 -0
- package/scripts/clean-dist.js +9 -0
- package/scripts/ensure-bin-modes.js +11 -0
- package/dist/cli/index.js +0 -193
- package/dist/cli/index.js.map +0 -1
- package/dist/commands/archive.js +0 -6
- package/dist/commands/archive.js.map +0 -1
- package/dist/commands/clarify.js +0 -6
- package/dist/commands/clarify.js.map +0 -1
- package/dist/commands/design.js +0 -28
- package/dist/commands/design.js.map +0 -1
- package/dist/commands/docs.js +0 -6
- package/dist/commands/docs.js.map +0 -1
- package/dist/commands/doctor.js +0 -494
- package/dist/commands/doctor.js.map +0 -1
- package/dist/commands/implement.js +0 -6
- package/dist/commands/implement.js.map +0 -1
- package/dist/commands/init.js +0 -520
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/pipeline.js +0 -6
- package/dist/commands/pipeline.js.map +0 -1
- package/dist/commands/scan.js +0 -63
- package/dist/commands/scan.js.map +0 -1
- package/dist/commands/status.js +0 -173
- package/dist/commands/status.js.map +0 -1
- package/dist/commands/uninstall.js +0 -214
- package/dist/commands/uninstall.js.map +0 -1
- package/dist/commands/update.js +0 -193
- package/dist/commands/update.js.map +0 -1
- package/dist/commands/verify.js +0 -6
- package/dist/commands/verify.js.map +0 -1
- package/dist/core/assets.js +0 -27
- package/dist/core/assets.js.map +0 -1
- package/dist/core/cli-help.js +0 -98
- package/dist/core/cli-help.js.map +0 -1
- package/dist/core/context.js +0 -123
- package/dist/core/context.js.map +0 -1
- package/dist/core/dependencies.js +0 -146
- package/dist/core/dependencies.js.map +0 -1
- package/dist/core/detect.js +0 -64
- package/dist/core/detect.js.map +0 -1
- package/dist/core/i18n.js +0 -105
- package/dist/core/i18n.js.map +0 -1
- package/dist/core/integrity.js +0 -46
- package/dist/core/integrity.js.map +0 -1
- package/dist/core/manifest.js +0 -21
- package/dist/core/manifest.js.map +0 -1
- package/dist/core/prompts.js +0 -20
- package/dist/core/prompts.js.map +0 -1
- package/dist/core/registry.js +0 -134
- package/dist/core/registry.js.map +0 -1
- package/dist/core/rules.js +0 -17
- package/dist/core/rules.js.map +0 -1
- package/dist/core/scripts.js +0 -48
- package/dist/core/scripts.js.map +0 -1
- package/dist/core/skill-check.js +0 -32
- package/dist/core/skill-check.js.map +0 -1
- package/dist/core/skills.js +0 -62
- package/dist/core/skills.js.map +0 -1
- package/dist/core/state.js +0 -54
- package/dist/core/state.js.map +0 -1
- package/dist/utils/path.js +0 -11
- package/dist/utils/path.js.map +0 -1
- package/dist/utils/shell.js +0 -29
- package/dist/utils/shell.js.map +0 -1
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
const changeArg = args.find((arg) => !arg.startsWith("--"));
|
|
8
|
+
const jsonOutput = args.includes("--json");
|
|
9
|
+
|
|
10
|
+
if (!changeArg) {
|
|
11
|
+
fail(["用法: superflow-document-audit.mjs <change-dir> [--json]"]);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const changeDir = fs.realpathSync(path.resolve(changeArg));
|
|
15
|
+
const issues = [];
|
|
16
|
+
const prompts = markdownFiles(path.join(changeDir, "prompt"));
|
|
17
|
+
const handoffHash = readHandoffHash(changeDir, issues);
|
|
18
|
+
const aggregateDocuments = [
|
|
19
|
+
"tasks.md",
|
|
20
|
+
"traceability-matrix.md",
|
|
21
|
+
"sdd-quality-gate.md",
|
|
22
|
+
"test-report.md",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
if (prompts.length === 0) {
|
|
26
|
+
issues.push("prompt/ 目录至少需要一份 Markdown 实现 Prompt");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for (const document of aggregateDocuments) {
|
|
30
|
+
const documentPath = path.join(changeDir, document);
|
|
31
|
+
if (!fs.existsSync(documentPath)) {
|
|
32
|
+
issues.push(`缺少文档: ${document}`);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const links = markdownTargets(fs.readFileSync(documentPath, "utf-8"));
|
|
36
|
+
for (const prompt of prompts) {
|
|
37
|
+
const relativePrompt = path.relative(changeDir, prompt).replaceAll("\\", "/");
|
|
38
|
+
if (!links.has(relativePrompt)) {
|
|
39
|
+
issues.push(`${document} 未链接 ${relativePrompt}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const review = readJson(
|
|
45
|
+
path.join(changeDir, ".sdd", "reviews", "document-review.json"),
|
|
46
|
+
"文档多轮评审凭证",
|
|
47
|
+
issues,
|
|
48
|
+
);
|
|
49
|
+
validateReview(review, handoffHash, issues);
|
|
50
|
+
validateMermaid(changeDir, issues);
|
|
51
|
+
|
|
52
|
+
const result = {
|
|
53
|
+
schemaVersion: "superflow.document-audit.v1",
|
|
54
|
+
ready: issues.length === 0,
|
|
55
|
+
handoffHash,
|
|
56
|
+
promptCount: prompts.length,
|
|
57
|
+
reviewRounds: Array.isArray(review?.rounds) ? review.rounds.length : 0,
|
|
58
|
+
issues,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (issues.length > 0) {
|
|
62
|
+
fail(issues, result);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
print(result);
|
|
66
|
+
|
|
67
|
+
function validateReview(review, currentHash, targetIssues) {
|
|
68
|
+
if (!review) return;
|
|
69
|
+
if (review.schemaVersion !== "superflow.document-review.v1") {
|
|
70
|
+
targetIssues.push("文档评审凭证 schemaVersion 必须为 superflow.document-review.v1");
|
|
71
|
+
}
|
|
72
|
+
if (review.handoffHash !== currentHash) {
|
|
73
|
+
targetIssues.push("文档评审凭证未绑定当前 handoff hash");
|
|
74
|
+
}
|
|
75
|
+
if (review.verdict !== "PASS") {
|
|
76
|
+
targetIssues.push("文档评审最终结论必须为 PASS");
|
|
77
|
+
}
|
|
78
|
+
if ((review.openOwnerDecisions?.length ?? 0) > 0) {
|
|
79
|
+
targetIssues.push("仍有 owner 决策未关闭,应集中询问用户后重新评审");
|
|
80
|
+
}
|
|
81
|
+
if (!Array.isArray(review.rounds) || review.rounds.length < 3) {
|
|
82
|
+
targetIssues.push("文档交付前至少三轮独立视角评审");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const requiredLenses = new Set([
|
|
86
|
+
"source-contract",
|
|
87
|
+
"architecture-minimality",
|
|
88
|
+
"e2e-environment",
|
|
89
|
+
]);
|
|
90
|
+
for (const round of review.rounds) {
|
|
91
|
+
requiredLenses.delete(round.lens);
|
|
92
|
+
if (round.inputHash !== currentHash) {
|
|
93
|
+
targetIssues.push(`第 ${round.round ?? "?"} 轮评审不是基于当前 handoff hash`);
|
|
94
|
+
}
|
|
95
|
+
const openFindings = (round.findings ?? []).filter(
|
|
96
|
+
(finding) => finding.status !== "closed",
|
|
97
|
+
);
|
|
98
|
+
if (openFindings.length > 0) {
|
|
99
|
+
targetIssues.push(`第 ${round.round ?? "?"} 轮仍有未关闭评审发现`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (requiredLenses.size > 0) {
|
|
103
|
+
targetIssues.push(`缺少评审视角: ${[...requiredLenses].join(", ")}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function validateMermaid(root, targetIssues) {
|
|
108
|
+
const configPath = path.join(root, ".openspec.yaml");
|
|
109
|
+
const config = fs.existsSync(configPath)
|
|
110
|
+
? fs.readFileSync(configPath, "utf-8")
|
|
111
|
+
: "";
|
|
112
|
+
const required =
|
|
113
|
+
/^\s*mermaid:\s*required\s*$/m.test(config) ||
|
|
114
|
+
/^\s*complex_logic:\s*true\s*$/m.test(config) ||
|
|
115
|
+
/^\s*cross_repo:\s*true\s*$/m.test(config);
|
|
116
|
+
if (!required) return;
|
|
117
|
+
|
|
118
|
+
const markdown = ["design.md", "tests.md"]
|
|
119
|
+
.map((file) => path.join(root, file))
|
|
120
|
+
.filter((file) => fs.existsSync(file))
|
|
121
|
+
.map((file) => fs.readFileSync(file, "utf-8"))
|
|
122
|
+
.join("\n");
|
|
123
|
+
const blocks = [...markdown.matchAll(/```mermaid\s*\n([\s\S]*?)```/g)].map(
|
|
124
|
+
(match) => match[1].trim(),
|
|
125
|
+
);
|
|
126
|
+
for (const block of blocks) {
|
|
127
|
+
validateMermaidBlock(block, targetIssues);
|
|
128
|
+
}
|
|
129
|
+
if (!blocks.some((block) => /^sequenceDiagram\b/m.test(block))) {
|
|
130
|
+
targetIssues.push("复杂逻辑必须包含 Mermaid sequenceDiagram 端到端时序图");
|
|
131
|
+
}
|
|
132
|
+
if (
|
|
133
|
+
!blocks.some((block) =>
|
|
134
|
+
/^(flowchart\b|graph\b|stateDiagram(?:-v2)?\b)/m.test(block),
|
|
135
|
+
)
|
|
136
|
+
) {
|
|
137
|
+
targetIssues.push("复杂逻辑必须包含 Mermaid flowchart 或 stateDiagram");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function validateMermaidBlock(block, targetIssues) {
|
|
142
|
+
const lines = block
|
|
143
|
+
.split("\n")
|
|
144
|
+
.map((line) => line.trim())
|
|
145
|
+
.filter((line) => line && !line.startsWith("%%"));
|
|
146
|
+
const directive = lines[0] ?? "";
|
|
147
|
+
if (/^sequenceDiagram\b/.test(directive)) {
|
|
148
|
+
const arrows = lines.filter((line) => /-{1,2}>>|--?>/.test(line));
|
|
149
|
+
if (arrows.length === 0) {
|
|
150
|
+
targetIssues.push("Mermaid sequenceDiagram 缺少有效消息箭头");
|
|
151
|
+
}
|
|
152
|
+
const openings = lines.filter((line) => /^(alt|opt|loop|par|critical)\b/.test(line)).length;
|
|
153
|
+
const endings = lines.filter((line) => line === "end").length;
|
|
154
|
+
if (openings !== endings) {
|
|
155
|
+
targetIssues.push("Mermaid sequenceDiagram 分支块未正确闭合");
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (/^(flowchart|graph)\s+(TD|TB|BT|LR|RL)\b/.test(directive)) {
|
|
160
|
+
if (!lines.slice(1).some((line) => /-->|---|==>|-.->/.test(line))) {
|
|
161
|
+
targetIssues.push("Mermaid flowchart 缺少有效连线");
|
|
162
|
+
}
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (/^stateDiagram(?:-v2)?\b/.test(directive)) {
|
|
166
|
+
if (!lines.slice(1).some((line) => /-->/.test(line))) {
|
|
167
|
+
targetIssues.push("Mermaid stateDiagram 缺少状态迁移");
|
|
168
|
+
}
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
targetIssues.push(`不支持或不完整的 Mermaid 图声明: ${directive || "空图"}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function markdownFiles(directory) {
|
|
175
|
+
if (!fs.existsSync(directory)) return [];
|
|
176
|
+
return fs
|
|
177
|
+
.readdirSync(directory, { withFileTypes: true })
|
|
178
|
+
.flatMap((entry) => {
|
|
179
|
+
const target = path.join(directory, entry.name);
|
|
180
|
+
if (entry.isDirectory()) return markdownFiles(target);
|
|
181
|
+
return entry.isFile() && entry.name.endsWith(".md") ? [target] : [];
|
|
182
|
+
})
|
|
183
|
+
.sort();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function markdownTargets(content) {
|
|
187
|
+
return new Set(
|
|
188
|
+
[...content.matchAll(/\[[^\]]*\]\(([^)]+)\)/g)].map((match) =>
|
|
189
|
+
match[1].split("#", 1)[0].replace(/^\.\//, ""),
|
|
190
|
+
),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function readHandoffHash(root, targetIssues) {
|
|
195
|
+
const hashPath = path.join(root, ".sdd", "handoff", "sdd-context.sha256");
|
|
196
|
+
if (!fs.existsSync(hashPath)) {
|
|
197
|
+
targetIssues.push("缺少当前 handoff hash");
|
|
198
|
+
return "";
|
|
199
|
+
}
|
|
200
|
+
const match = fs.readFileSync(hashPath, "utf-8").match(/[a-f0-9]{64}/i);
|
|
201
|
+
if (!match) {
|
|
202
|
+
targetIssues.push("handoff hash 格式不正确");
|
|
203
|
+
return "";
|
|
204
|
+
}
|
|
205
|
+
return match[0].toLowerCase();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function readJson(file, label, targetIssues) {
|
|
209
|
+
if (!fs.existsSync(file)) {
|
|
210
|
+
targetIssues.push(`缺少${label}: ${path.relative(changeDir, file)}`);
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
return JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
215
|
+
} catch {
|
|
216
|
+
targetIssues.push(`${label}不是合法 JSON`);
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function print(result) {
|
|
222
|
+
if (jsonOutput) {
|
|
223
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
process.stdout.write(
|
|
227
|
+
`文档交付审计通过:${result.reviewRounds} 轮评审,${result.promptCount} 份 Prompt\n`,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function fail(targetIssues, result = { ready: false, issues: targetIssues }) {
|
|
232
|
+
if (jsonOutput) {
|
|
233
|
+
process.stderr.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
234
|
+
} else {
|
|
235
|
+
process.stderr.write(
|
|
236
|
+
`文档交付审计失败:\n${targetIssues.map((item) => `- ${item}`).join("\n")}\n`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import net from "node:net";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const changeArg = args.find((arg) => !arg.startsWith("--"));
|
|
9
|
+
const jsonOutput = args.includes("--json");
|
|
10
|
+
|
|
11
|
+
if (!changeArg) {
|
|
12
|
+
fail(["用法: superflow-environment-preflight.mjs <change-dir> [--json]"]);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const changeDir = fs.realpathSync(path.resolve(changeArg));
|
|
16
|
+
const reportPath = path.join(
|
|
17
|
+
changeDir,
|
|
18
|
+
".sdd",
|
|
19
|
+
"readiness",
|
|
20
|
+
"environment.json",
|
|
21
|
+
);
|
|
22
|
+
const issues = [];
|
|
23
|
+
const report = readReport(reportPath, issues);
|
|
24
|
+
const handoffHash = readHandoffHash(changeDir, issues);
|
|
25
|
+
|
|
26
|
+
if (report) {
|
|
27
|
+
if (report.schemaVersion !== "superflow.environment-readiness.v2") {
|
|
28
|
+
issues.push(
|
|
29
|
+
"环境报告 schemaVersion 必须为 superflow.environment-readiness.v2",
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
if (report.handoffHash !== handoffHash) {
|
|
33
|
+
issues.push("环境报告未绑定当前 handoff hash");
|
|
34
|
+
}
|
|
35
|
+
if (!report.scope) issues.push("环境报告必须声明 local/dev/test 等实际范围");
|
|
36
|
+
if (!['READY', 'LOCAL_FIXTURE_READY'].includes(report.overall)) {
|
|
37
|
+
issues.push(`环境整体状态不是可执行状态: ${report.overall ?? "未填写"}`);
|
|
38
|
+
}
|
|
39
|
+
if ((report.ownerHelpRequired?.length ?? 0) > 0) {
|
|
40
|
+
issues.push("仍有需要用户协助的环境问题,必须集中澄清后再交付开发");
|
|
41
|
+
}
|
|
42
|
+
const contractTargets = validateExecutionContract(
|
|
43
|
+
report.executionContract,
|
|
44
|
+
issues,
|
|
45
|
+
);
|
|
46
|
+
const referencedTargets = new Set();
|
|
47
|
+
if (!Array.isArray(report.checks) || report.checks.length === 0) {
|
|
48
|
+
issues.push("环境报告至少需要一项可执行检查");
|
|
49
|
+
} else {
|
|
50
|
+
for (const check of report.checks) {
|
|
51
|
+
validateContractRef(check, contractTargets, referencedTargets, issues);
|
|
52
|
+
await probe(check, issues);
|
|
53
|
+
}
|
|
54
|
+
for (const target of contractTargets) {
|
|
55
|
+
if (!referencedTargets.has(target)) {
|
|
56
|
+
issues.push(`${target}: 缺少对应的失败安全检查`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const result = {
|
|
63
|
+
schemaVersion: "superflow.environment-preflight.v1",
|
|
64
|
+
ready: issues.length === 0,
|
|
65
|
+
scope: report?.scope ?? null,
|
|
66
|
+
handoffHash,
|
|
67
|
+
checks: (report?.checks ?? []).map((check) => ({
|
|
68
|
+
id: check.id,
|
|
69
|
+
type: check.type,
|
|
70
|
+
status: check.status,
|
|
71
|
+
})),
|
|
72
|
+
issues,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (issues.length > 0) fail(issues, result);
|
|
76
|
+
if (jsonOutput) {
|
|
77
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
78
|
+
} else {
|
|
79
|
+
process.stdout.write(`环境预检通过:${result.checks.length} 项检查\n`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function probe(check, targetIssues) {
|
|
83
|
+
if (!check?.id || !check?.type) {
|
|
84
|
+
targetIssues.push("环境检查缺少 id 或 type");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (!['READY', 'LOCAL_FIXTURE_READY'].includes(check.status)) {
|
|
88
|
+
targetIssues.push(`${check.id}: 状态不是 READY 或 LOCAL_FIXTURE_READY`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
switch (check.type) {
|
|
93
|
+
case "file": {
|
|
94
|
+
const target = resolveSafeTarget(check.target);
|
|
95
|
+
if (!fs.existsSync(target) || !fs.statSync(target).isFile()) {
|
|
96
|
+
targetIssues.push(`${check.id}: 文件不存在或已失效`);
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
case "directory": {
|
|
101
|
+
const target = resolveSafeTarget(check.target);
|
|
102
|
+
if (!fs.existsSync(target) || !fs.statSync(target).isDirectory()) {
|
|
103
|
+
targetIssues.push(`${check.id}: 目录不存在或已失效`);
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
case "executable":
|
|
108
|
+
if (!findExecutable(check.target)) {
|
|
109
|
+
targetIssues.push(`${check.id}: 可执行程序不可用`);
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
112
|
+
case "tcp":
|
|
113
|
+
await probeTcp(check.host, check.port, check.timeoutMs ?? 1500);
|
|
114
|
+
break;
|
|
115
|
+
case "http":
|
|
116
|
+
await probeHttp(check);
|
|
117
|
+
break;
|
|
118
|
+
default:
|
|
119
|
+
targetIssues.push(`${check.id}: 不支持的环境检查类型 ${check.type}`);
|
|
120
|
+
}
|
|
121
|
+
} catch (error) {
|
|
122
|
+
targetIssues.push(`${check.id}: ${safeError(error)}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function validateExecutionContract(contract, targetIssues) {
|
|
127
|
+
const targets = new Set();
|
|
128
|
+
if (!contract || typeof contract !== "object") {
|
|
129
|
+
targetIssues.push("缺少结构化环境执行合同 executionContract");
|
|
130
|
+
return targets;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const applicationLocations = new Set(["local", "dev", "test"]);
|
|
134
|
+
if (!applicationLocations.has(contract.applicationLocation)) {
|
|
135
|
+
targetIssues.push("executionContract.applicationLocation 必须为 local/dev/test");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const dependencyPolicies = new Set([
|
|
139
|
+
"local-isolated",
|
|
140
|
+
"shared-dev",
|
|
141
|
+
"shared-test",
|
|
142
|
+
"mixed",
|
|
143
|
+
]);
|
|
144
|
+
if (!dependencyPolicies.has(contract.dependencyPolicy)) {
|
|
145
|
+
targetIssues.push(
|
|
146
|
+
"executionContract.dependencyPolicy 必须为 local-isolated/shared-dev/shared-test/mixed",
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
if (typeof contract.allowLocalProvisioning !== "boolean") {
|
|
150
|
+
targetIssues.push("executionContract.allowLocalProvisioning 必须为布尔值");
|
|
151
|
+
}
|
|
152
|
+
if (typeof contract.allowRemoteDevDependencies !== "boolean") {
|
|
153
|
+
targetIssues.push("executionContract.allowRemoteDevDependencies 必须为布尔值");
|
|
154
|
+
}
|
|
155
|
+
if (
|
|
156
|
+
contract.dependencyPolicy === "shared-dev" &&
|
|
157
|
+
contract.allowLocalProvisioning === true
|
|
158
|
+
) {
|
|
159
|
+
targetIssues.push("dependencyPolicy=shared-dev 禁止自建本地依赖");
|
|
160
|
+
}
|
|
161
|
+
if (
|
|
162
|
+
contract.dependencyPolicy === "local-isolated" &&
|
|
163
|
+
contract.allowRemoteDevDependencies === true
|
|
164
|
+
) {
|
|
165
|
+
targetIssues.push("dependencyPolicy=local-isolated 禁止使用远程开发依赖");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
validateOverrides(contract, targetIssues);
|
|
169
|
+
validateContractItems(
|
|
170
|
+
contract.services,
|
|
171
|
+
"service",
|
|
172
|
+
contract.dependencyPolicy,
|
|
173
|
+
targets,
|
|
174
|
+
targetIssues,
|
|
175
|
+
);
|
|
176
|
+
validateContractItems(
|
|
177
|
+
contract.dependencies,
|
|
178
|
+
"dependency",
|
|
179
|
+
contract.dependencyPolicy,
|
|
180
|
+
targets,
|
|
181
|
+
targetIssues,
|
|
182
|
+
);
|
|
183
|
+
return targets;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function validateOverrides(contract, targetIssues) {
|
|
187
|
+
const allowed = contract.allowedOverrides;
|
|
188
|
+
const forbidden = contract.forbiddenOverrides;
|
|
189
|
+
if (!isStringArray(allowed)) {
|
|
190
|
+
targetIssues.push("executionContract.allowedOverrides 必须为字符串数组");
|
|
191
|
+
}
|
|
192
|
+
if (!isStringArray(forbidden)) {
|
|
193
|
+
targetIssues.push("executionContract.forbiddenOverrides 必须为字符串数组");
|
|
194
|
+
}
|
|
195
|
+
if (!isStringArray(allowed) || !isStringArray(forbidden)) return;
|
|
196
|
+
const forbiddenSet = new Set(forbidden);
|
|
197
|
+
const overlap = [...new Set(allowed)].filter((item) => forbiddenSet.has(item));
|
|
198
|
+
if (overlap.length > 0) {
|
|
199
|
+
targetIssues.push(`允许覆盖项与禁止覆盖项重复: ${overlap.join(", ")}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function validateContractItems(
|
|
204
|
+
items,
|
|
205
|
+
type,
|
|
206
|
+
dependencyPolicy,
|
|
207
|
+
targets,
|
|
208
|
+
targetIssues,
|
|
209
|
+
) {
|
|
210
|
+
const label = type === "service" ? "services" : "dependencies";
|
|
211
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
212
|
+
targetIssues.push(`executionContract.${label} 至少需要一项`);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
for (const item of items) {
|
|
216
|
+
if (!item?.id || typeof item.id !== "string") {
|
|
217
|
+
targetIssues.push(`${label}: 缺少 id`);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const reference = `${type}:${item.id}`;
|
|
221
|
+
if (targets.has(reference)) {
|
|
222
|
+
targetIssues.push(`${reference}: id 重复`);
|
|
223
|
+
}
|
|
224
|
+
targets.add(reference);
|
|
225
|
+
if (!isConfigSource(item.configSource)) {
|
|
226
|
+
targetIssues.push(`${item.id}: 缺少 configSource`);
|
|
227
|
+
}
|
|
228
|
+
if (
|
|
229
|
+
type === "service" &&
|
|
230
|
+
(!item.startupCommandSource ||
|
|
231
|
+
typeof item.startupCommandSource !== "string")
|
|
232
|
+
) {
|
|
233
|
+
targetIssues.push(`${item.id}: 缺少 startupCommandSource`);
|
|
234
|
+
}
|
|
235
|
+
if (type === "dependency") {
|
|
236
|
+
validateDependency(item, dependencyPolicy, targetIssues);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function validateDependency(dependency, dependencyPolicy, targetIssues) {
|
|
242
|
+
const provisioningValues = new Set([
|
|
243
|
+
"local-isolated",
|
|
244
|
+
"shared-dev",
|
|
245
|
+
"shared-test",
|
|
246
|
+
]);
|
|
247
|
+
if (!dependency.kind || typeof dependency.kind !== "string") {
|
|
248
|
+
targetIssues.push(`${dependency.id}: 缺少 kind`);
|
|
249
|
+
}
|
|
250
|
+
if (!provisioningValues.has(dependency.provisioning)) {
|
|
251
|
+
targetIssues.push(`${dependency.id}: provisioning 不合法`);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (
|
|
255
|
+
dependencyPolicy !== "mixed" &&
|
|
256
|
+
dependencyPolicy !== dependency.provisioning
|
|
257
|
+
) {
|
|
258
|
+
targetIssues.push(
|
|
259
|
+
`${dependency.id}: provisioning 与 dependencyPolicy 不一致`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function validateContractRef(
|
|
265
|
+
check,
|
|
266
|
+
contractTargets,
|
|
267
|
+
referencedTargets,
|
|
268
|
+
targetIssues,
|
|
269
|
+
) {
|
|
270
|
+
if (!check?.id) return;
|
|
271
|
+
if (!check.contractRef || typeof check.contractRef !== "string") {
|
|
272
|
+
targetIssues.push(`${check.id}: 缺少 contractRef`);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (!contractTargets.has(check.contractRef)) {
|
|
276
|
+
targetIssues.push(`${check.id}: contractRef 未指向已声明的服务或依赖`);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
referencedTargets.add(check.contractRef);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function isStringArray(value) {
|
|
283
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function isConfigSource(value) {
|
|
287
|
+
const types = new Set([
|
|
288
|
+
"bundled-profile",
|
|
289
|
+
"external-file",
|
|
290
|
+
"environment",
|
|
291
|
+
"cli-override",
|
|
292
|
+
"generated-fixture",
|
|
293
|
+
]);
|
|
294
|
+
return Boolean(
|
|
295
|
+
value &&
|
|
296
|
+
typeof value === "object" &&
|
|
297
|
+
types.has(value.type) &&
|
|
298
|
+
typeof value.ref === "string" &&
|
|
299
|
+
value.ref.length > 0,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function resolveSafeTarget(target) {
|
|
304
|
+
if (typeof target !== "string" || target.length === 0) {
|
|
305
|
+
throw new Error("缺少检查目标");
|
|
306
|
+
}
|
|
307
|
+
return path.isAbsolute(target) ? target : path.resolve(changeDir, target);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function findExecutable(name) {
|
|
311
|
+
if (typeof name !== "string" || !/^[A-Za-z0-9._-]+$/.test(name)) return false;
|
|
312
|
+
return (process.env.PATH ?? "")
|
|
313
|
+
.split(path.delimiter)
|
|
314
|
+
.some((directory) => {
|
|
315
|
+
try {
|
|
316
|
+
fs.accessSync(path.join(directory, name), fs.constants.X_OK);
|
|
317
|
+
return true;
|
|
318
|
+
} catch {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function probeTcp(host, port, timeoutMs) {
|
|
325
|
+
if (typeof host !== "string" || !Number.isInteger(port)) {
|
|
326
|
+
return Promise.reject(new Error("TCP 检查缺少 host/port"));
|
|
327
|
+
}
|
|
328
|
+
return new Promise((resolve, reject) => {
|
|
329
|
+
const socket = net.createConnection({ host, port });
|
|
330
|
+
const timer = setTimeout(() => socket.destroy(new Error("连接超时")), timeoutMs);
|
|
331
|
+
socket.once("connect", () => {
|
|
332
|
+
clearTimeout(timer);
|
|
333
|
+
socket.end();
|
|
334
|
+
resolve();
|
|
335
|
+
});
|
|
336
|
+
socket.once("error", (error) => {
|
|
337
|
+
clearTimeout(timer);
|
|
338
|
+
reject(error);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function probeHttp(check) {
|
|
344
|
+
const url = new URL(check.url);
|
|
345
|
+
if (url.username || url.password) throw new Error("HTTP URL 禁止内嵌凭据");
|
|
346
|
+
const response = await fetch(url, {
|
|
347
|
+
method: "GET",
|
|
348
|
+
signal: AbortSignal.timeout(check.timeoutMs ?? 2000),
|
|
349
|
+
});
|
|
350
|
+
const configured = check.expectedStatus ?? [200, 204];
|
|
351
|
+
const expected = Array.isArray(configured) ? configured : [configured];
|
|
352
|
+
if (!expected.includes(response.status)) {
|
|
353
|
+
throw new Error(`HTTP 状态不符合预期: ${response.status}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function readReport(file, targetIssues) {
|
|
358
|
+
if (!fs.existsSync(file)) {
|
|
359
|
+
targetIssues.push("缺少环境预检合同 .sdd/readiness/environment.json");
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
return JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
364
|
+
} catch {
|
|
365
|
+
targetIssues.push("环境预检合同不是合法 JSON");
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function readHandoffHash(root, targetIssues) {
|
|
371
|
+
const file = path.join(root, ".sdd", "handoff", "sdd-context.sha256");
|
|
372
|
+
const content = fs.existsSync(file) ? fs.readFileSync(file, "utf-8") : "";
|
|
373
|
+
const match = content.match(/[a-f0-9]{64}/i);
|
|
374
|
+
if (!match) {
|
|
375
|
+
targetIssues.push("缺少合法的当前 handoff hash");
|
|
376
|
+
return "";
|
|
377
|
+
}
|
|
378
|
+
return match[0].toLowerCase();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function safeError(error) {
|
|
382
|
+
const message = error instanceof Error ? error.message : "检查失败";
|
|
383
|
+
return message.replace(
|
|
384
|
+
/\b(password|secret|token|authorization)=[^\s]+/gi,
|
|
385
|
+
(_match, key) => `${key}=[REDACTED]`,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function fail(targetIssues, result = { ready: false, issues: targetIssues }) {
|
|
390
|
+
if (jsonOutput) {
|
|
391
|
+
process.stderr.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
392
|
+
} else {
|
|
393
|
+
process.stderr.write(
|
|
394
|
+
`环境预检失败:\n${targetIssues.map((item) => `- ${item}`).join("\n")}\n`,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
process.exit(1);
|
|
398
|
+
}
|
|
@@ -86,7 +86,7 @@ is_root_markdown_or_config() {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
case "$REL" in
|
|
89
|
-
*.java|*.xml|*.sql|*.yml|*.yaml|*.properties)
|
|
89
|
+
*.java|*.kt|*.kts|*.xml|*.sql|*.yml|*.yaml|*.properties|*.ts|*.tsx|*.js|*.jsx|*.mjs|*.cjs|*.py|*.go|*.rs|*.vue)
|
|
90
90
|
RUNTIME=1
|
|
91
91
|
;;
|
|
92
92
|
*)
|
|
@@ -94,6 +94,42 @@ case "$REL" in
|
|
|
94
94
|
;;
|
|
95
95
|
esac
|
|
96
96
|
|
|
97
|
+
require_coding_ready() {
|
|
98
|
+
local change_dir receipt expected_hash
|
|
99
|
+
change_dir=$(dirname "$(dirname "$STATE_FILE")")
|
|
100
|
+
receipt="$change_dir/.sdd/readiness/coding-ready.json"
|
|
101
|
+
expected_hash=$(awk -F':' '$1=="handoff_hash"{gsub(/^[ \t]+|[ \t]+$/, "", $2); print $2}' "$STATE_FILE" 2>/dev/null)
|
|
102
|
+
if [ ! -f "$receipt" ] || [ -z "$expected_hash" ]; then
|
|
103
|
+
cat <<'BLOCK_MSG' >&2
|
|
104
|
+
[SDD Coding Ready] 当前任务没有可用的 Coding Ready 凭证,禁止修改运行时代码。
|
|
105
|
+
请先完成多轮文档评审、环境预检和 docs/design/implement 门禁,再执行:
|
|
106
|
+
superflow check <change> --level coding-ready
|
|
107
|
+
BLOCK_MSG
|
|
108
|
+
return 2
|
|
109
|
+
fi
|
|
110
|
+
python3 - "$receipt" "$expected_hash" <<'PY'
|
|
111
|
+
import json, sys
|
|
112
|
+
try:
|
|
113
|
+
with open(sys.argv[1], encoding="utf-8") as handle:
|
|
114
|
+
receipt = json.load(handle)
|
|
115
|
+
except Exception:
|
|
116
|
+
raise SystemExit(2)
|
|
117
|
+
if receipt.get("codingReady") is not True:
|
|
118
|
+
raise SystemExit(2)
|
|
119
|
+
if receipt.get("schemaVersion") != "superflow.coding-ready.v1":
|
|
120
|
+
raise SystemExit(2)
|
|
121
|
+
if receipt.get("handoffHash") != sys.argv[2]:
|
|
122
|
+
raise SystemExit(2)
|
|
123
|
+
PY
|
|
124
|
+
if [ $? -ne 0 ]; then
|
|
125
|
+
cat <<'BLOCK_MSG' >&2
|
|
126
|
+
[SDD Coding Ready] 凭证已过期或与当前 handoff hash 不一致。
|
|
127
|
+
请重新执行 superflow check <change> --level coding-ready。
|
|
128
|
+
BLOCK_MSG
|
|
129
|
+
return 2
|
|
130
|
+
fi
|
|
131
|
+
}
|
|
132
|
+
|
|
97
133
|
case "$PHASE" in
|
|
98
134
|
docs)
|
|
99
135
|
if is_sdd_doc_path || is_root_markdown_or_config; then
|
|
@@ -108,6 +144,9 @@ BLOCK_MSG
|
|
|
108
144
|
fi
|
|
109
145
|
;;
|
|
110
146
|
implement|verify)
|
|
147
|
+
if [ "$RUNTIME" -eq 1 ]; then
|
|
148
|
+
require_coding_ready || exit 2
|
|
149
|
+
fi
|
|
111
150
|
exit 0
|
|
112
151
|
;;
|
|
113
152
|
archive)
|