@guanlin4924/devflow-pi 0.1.23 → 0.1.25
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 -24
- package/package.json +1 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/acceptance_test_runner.py +50 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/api_test_matrix.py +2 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/architecture_brief.rb +19 -3
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/architecture_constraints.rb +24 -0
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/architecture_feasibility.rb +25 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/requirement_graph.rb +136 -15
- package/plugins/devflow-design/skills/workflow-api-contract/SKILL.md +4 -3
- package/plugins/devflow-design/skills/workflow-architecture-feasibility/SKILL.md +4 -0
- package/plugins/devflow-design/skills/workflow-backend-tech-spec/SKILL.md +2 -0
- package/plugins/devflow-quality/skills/workflow-acceptance-test-design/SKILL.md +5 -1
- package/plugins/devflow-quality/skills/workflow-run-acceptance-tests/SKILL.md +2 -0
- package/src/constants.ts +1 -1
package/README.md
CHANGED
|
@@ -2,19 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
面向项目代码开发的 OMP 原生 Devflow 能力仓库。仓库同时提供 Agent Skills、运行时 extension、task agents、项目配置模板和 marketplace 插件目录。Devflow 的流程真相由需求图谱、冻结测试集、机器证据和控制面状态组成,Markdown 只负责展示。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
本项目不依赖外部会话式验收或里程碑工作流;验收和版本收口均由 Devflow 自有节点完成。架构设计材料仅保留在内部源码仓库,不随 npm 包发布。
|
|
6
6
|
|
|
7
|
-
## Devflow
|
|
7
|
+
## Devflow Pi
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Devflow Pi 是一个面向项目研发交付的独立 OMP 产品入口,内置固定版本的 OMP Runtime 和六个 Devflow 插件。安装后可以直接在目标项目中进行初始化、需求梳理、方案设计、代码执行和验收,不需要把本仓库复制到业务项目中。
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
安装与启动:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
|
|
15
|
-
npm install
|
|
16
|
-
npm link
|
|
17
|
-
|
|
14
|
+
npm install -g @guanlin4924/devflow-pi@0.1.23
|
|
18
15
|
cd /path/to/your-project
|
|
19
16
|
devflow-pi init
|
|
20
17
|
devflow-pi doctor
|
|
@@ -50,14 +47,12 @@ devflow-pi
|
|
|
50
47
|
- MCP 默认关闭,避免自动连接用户全局服务器;确需复用时显式加 `devflow-pi --mcp`;
|
|
51
48
|
- `task` 只允许六个 Devflow 角色 Agent,且必须显式写明 agent;同名项目/用户 Agent 与产品定义不一致时拒绝执行;
|
|
52
49
|
- `devflow-pi usage`、`devflow_usage_stats` 与 `usage_stats` 默认只统计当前项目的隔离 Session;`usage_stats` 显式传 `source=omp` 时才读取 `~/.omp` 历史会话。
|
|
53
|
-
- 以 `devflow-pi`
|
|
50
|
+
- 以 `devflow-pi` 启动时,确定性工作流和代码索引脚本只从产品包内六个插件解析;项目遗留 `.skills` 不会覆盖产品版本。
|
|
54
51
|
|
|
55
|
-
`devflow-pi doctor` 是产品壳和资源检查;TUI 内的 `/devflow doctor` 还会验证活动 Extension、30 个必需 Skills、6 个 Agents,以及 `@smol/@task/@slow` 三个不同的已认证模型。专项 Pi 默认用 `@slow` 处理用户交互和规划,`@smol` 仅负责只读侦察,`@task`
|
|
52
|
+
`devflow-pi doctor` 是产品壳和资源检查;TUI 内的 `/devflow doctor` 还会验证活动 Extension、30 个必需 Skills、6 个 Agents,以及 `@smol/@task/@slow` 三个不同的已认证模型。专项 Pi 默认用 `@slow` 处理用户交互和规划,`@smol` 仅负责只读侦察,`@task` 仅负责受限执行与验证。
|
|
56
53
|
|
|
57
54
|
专项 Pi 会复用 `~/.omp/agent/` 里的登录凭据和可用模型列表,但模型角色选择写在 `~/.devflow-pi/agent/` 这个产品级目录里,不写入目标项目,也不读取官方 OMP 的 Session。首次进入专项 Pi 后,先在模型角色界面把 `smol`、`task`、`slow` 配成三个不同模型,再执行 `/devflow doctor`;未配好时 `/devflow start` 会被拒绝。
|
|
58
55
|
|
|
59
|
-
现有 `install.sh --apply` 模式仍保留,作为未迁移项目的兼容入口;下文描述的是该兼容模式。
|
|
60
|
-
|
|
61
56
|
## Skill 如何被加载
|
|
62
57
|
|
|
63
58
|
Skill 仍采用标准目录结构:
|
|
@@ -80,18 +75,6 @@ skills:
|
|
|
80
75
|
# 其余四个插件目录同理
|
|
81
76
|
```
|
|
82
77
|
|
|
83
|
-
## npm 安装:推荐入口
|
|
84
|
-
|
|
85
|
-
```bash
|
|
86
|
-
npm install -g @guanlin4924/devflow-pi@0.1.22
|
|
87
|
-
cd /path/to/your-project
|
|
88
|
-
devflow-pi init
|
|
89
|
-
devflow-pi doctor
|
|
90
|
-
devflow-pi
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
npm 包已经内置完整 Devflow Runtime、Extension、task agents、项目策略和生命周期配置,不需要复制私有 Git 仓库,也不需要把本仓库作为 submodule 加入业务项目。
|
|
94
|
-
|
|
95
78
|
## 私有源码兼容模式
|
|
96
79
|
|
|
97
80
|
`install.sh --apply` 只用于已经获得内部源码访问权限的开发环境。本文档不提供私有仓库地址;在内部工作副本中执行:
|
|
@@ -147,6 +130,7 @@ bash .skills/install.sh --apply --force-config
|
|
|
147
130
|
- `TC-*` 的步骤和 oracle 在代码出现前冻结;CP-3 在同一次确认中先校验候选绑定,再冻结 TC 并固定绑定哈希。需求或契约变化会使它 stale,不能继续以旧测试集宣称通过。
|
|
148
131
|
- `tdd_required` 任务必须先由 Core 记录准备快照,再记录 RED(命中预期失败)、同一目标的 GREEN 和回归;缺失或环境失败不算完成。
|
|
149
132
|
- API、跨数据域、边界、迁移、工作负载与架构风险会派生相应的 `api`、`permission`、`boundary`、`migration`、`performance`、`resilience` TC 义务。
|
|
133
|
+
- 每个 TC 还要声明证明策略:`replay` 用组件/页面/API/集成的稳定回放;`causal` 用已定位的首个偏差加 HAR、控制台、DOM 快照、截图或日志等受控文件证明,Core 会记录文件哈希;`observation` 用于尚不可复现、也未能证明根因的问题,只能保持阻塞并带观测计划。后两者都不能被“AI 已复现”或 Markdown 自述放行。
|
|
150
134
|
- 代码完成后,`workflow-run-acceptance-tests` 只能运行绑定到冻结 TC、且已在初始化 `.planning/toolchain.md` 发现的测试运行器命令。任何失败、未执行、人工待执行或环境阻塞用例都会阻止 CP-4。
|
|
151
135
|
- 验收不是代码完成后的临时手工检查。它是需求基线在编码前派生并冻结的完整测试集,覆盖主流程、异常、边界、权限、迁移、兼容、韧性和工作负载义务;代码只能补充执行适配信息,不能改变测试意图。
|
|
152
136
|
- 验收失败不会创建另一套人工台账,而是进入 `acceptance-remediation`:诊断 → 修复 → code-index 刷新 → 原冻结 `TC-*` 重跑。
|
package/package.json
CHANGED
|
@@ -20,6 +20,8 @@ from test_command_policy import validate_test_command
|
|
|
20
20
|
|
|
21
21
|
FEATURE_RE = re.compile(r"^[^/\\\x00-\x1f\x7f]+$")
|
|
22
22
|
AUTOMATION_MODES = {"automated", "manual", "blocked"}
|
|
23
|
+
EVIDENCE_STRATEGIES = {"replay", "causal", "observation"}
|
|
24
|
+
MAX_EVIDENCE_FILE_BYTES = 25 * 1024 * 1024
|
|
23
25
|
|
|
24
26
|
|
|
25
27
|
def utc_now() -> str:
|
|
@@ -142,6 +144,38 @@ def file_fingerprint(path: Path) -> str | None:
|
|
|
142
144
|
return digest(path.read_bytes()) if path.is_file() else None
|
|
143
145
|
|
|
144
146
|
|
|
147
|
+
def evidence_artifacts(root: Path, value: Any) -> tuple[list[dict[str, Any]], list[str]]:
|
|
148
|
+
"""Hash causal evidence from bounded project-relative files only."""
|
|
149
|
+
if not isinstance(value, list) or not value:
|
|
150
|
+
return [], ["causal verification requires non-empty evidence_files"]
|
|
151
|
+
root_real = root.resolve()
|
|
152
|
+
artifacts: list[dict[str, Any]] = []
|
|
153
|
+
errors: list[str] = []
|
|
154
|
+
for raw in value:
|
|
155
|
+
if not isinstance(raw, str) or not raw.strip() or "\x00" in raw:
|
|
156
|
+
errors.append("evidence_files entries must be non-empty relative paths")
|
|
157
|
+
continue
|
|
158
|
+
candidate = Path(raw)
|
|
159
|
+
if candidate.is_absolute() or ".." in candidate.parts:
|
|
160
|
+
errors.append(f"evidence file escapes project root: {raw}")
|
|
161
|
+
continue
|
|
162
|
+
resolved = (root_real / candidate).resolve()
|
|
163
|
+
try:
|
|
164
|
+
relative = resolved.relative_to(root_real)
|
|
165
|
+
except ValueError:
|
|
166
|
+
errors.append(f"evidence file escapes project root: {raw}")
|
|
167
|
+
continue
|
|
168
|
+
if not resolved.is_file():
|
|
169
|
+
errors.append(f"causal evidence file is missing: {raw}")
|
|
170
|
+
continue
|
|
171
|
+
size = resolved.stat().st_size
|
|
172
|
+
if size > MAX_EVIDENCE_FILE_BYTES:
|
|
173
|
+
errors.append(f"causal evidence file exceeds {MAX_EVIDENCE_FILE_BYTES} bytes: {raw}")
|
|
174
|
+
continue
|
|
175
|
+
artifacts.append({"path": relative.as_posix(), "sha256": digest(resolved.read_bytes()), "bytes": size})
|
|
176
|
+
return artifacts, errors
|
|
177
|
+
|
|
178
|
+
|
|
145
179
|
def run_http_case(source: dict[str, Any], adapter: dict[str, Any]) -> tuple[bool, int, str]:
|
|
146
180
|
execution = source["execution"]
|
|
147
181
|
request_spec = execution["request"]
|
|
@@ -248,6 +282,11 @@ def validate(root: Path, feature: str) -> tuple[dict[str, Any], dict[str, Any] |
|
|
|
248
282
|
if source is None:
|
|
249
283
|
errors.append(f"unknown TC reference: {tc_id}")
|
|
250
284
|
continue
|
|
285
|
+
strategy = source.get("evidence_strategy")
|
|
286
|
+
if strategy is not None and strategy not in EVIDENCE_STRATEGIES:
|
|
287
|
+
errors.append(f"{label}.source evidence_strategy is invalid")
|
|
288
|
+
if strategy == "observation" and source.get("automation_mode") != "blocked":
|
|
289
|
+
errors.append(f"{label}.observation source must be blocked")
|
|
251
290
|
mode = entry.get("mode")
|
|
252
291
|
if mode not in AUTOMATION_MODES:
|
|
253
292
|
errors.append(f"{label}.mode is invalid")
|
|
@@ -278,6 +317,9 @@ def validate(root: Path, feature: str) -> tuple[dict[str, Any], dict[str, Any] |
|
|
|
278
317
|
errors.append(f"{label}.adapter.setup: {setup_error}")
|
|
279
318
|
else:
|
|
280
319
|
errors.append(f"{label}.adapter kind must be command or http")
|
|
320
|
+
if strategy == "causal":
|
|
321
|
+
_artifacts, artifact_errors = evidence_artifacts(root, entry.get("evidence_files"))
|
|
322
|
+
errors.extend(f"{label}.{error}" for error in artifact_errors)
|
|
281
323
|
elif mode == "manual":
|
|
282
324
|
if not isinstance(entry.get("owner"), str) or not entry["owner"].strip() or not isinstance(entry.get("reason"), str) or not entry["reason"].strip():
|
|
283
325
|
errors.append(f"{label}.manual requires owner and reason")
|
|
@@ -303,6 +345,7 @@ def run(root: Path, feature: str, write: bool) -> dict[str, Any]:
|
|
|
303
345
|
for entry in bindings["cases"]:
|
|
304
346
|
tc_id = entry["tc_id"]
|
|
305
347
|
mode = entry["mode"]
|
|
348
|
+
strategy = cases[tc_id].get("evidence_strategy")
|
|
306
349
|
if mode == "automated":
|
|
307
350
|
started_at = utc_now()
|
|
308
351
|
adapter = entry["adapter"]
|
|
@@ -326,6 +369,13 @@ def run(root: Path, feature: str, write: bool) -> dict[str, Any]:
|
|
|
326
369
|
result = {"tc_id": tc_id, "status": "passed" if passed else "failed", "mode": mode, "adapter_kind": "http",
|
|
327
370
|
"adapter_fingerprint": digest(json.dumps(adapter, ensure_ascii=False, separators=(",", ":"))), "started_at": started_at,
|
|
328
371
|
"finished_at": utc_now(), "exit_code": exit_code, "output_sha256": digest(output)}
|
|
372
|
+
if strategy == "causal":
|
|
373
|
+
artifacts, artifact_errors = evidence_artifacts(root, entry.get("evidence_files"))
|
|
374
|
+
if artifact_errors:
|
|
375
|
+
result["status"] = "failed"
|
|
376
|
+
result["evidence_error"] = "; ".join(artifact_errors)
|
|
377
|
+
else:
|
|
378
|
+
result["evidence_artifacts"] = artifacts
|
|
329
379
|
results.append(result)
|
|
330
380
|
elif mode == "manual":
|
|
331
381
|
results.append({"tc_id": tc_id, "status": "blocked", "mode": mode, "owner": entry["owner"], "reason": entry["reason"],
|
|
@@ -554,6 +554,8 @@ def tc_candidate(api: dict[str, Any], graph: dict[str, Any], key: str, obligatio
|
|
|
554
554
|
"steps": ["发送 execution.request 中定义的 HTTP 请求"],
|
|
555
555
|
"oracle": oracle,
|
|
556
556
|
"automation_mode": "automated",
|
|
557
|
+
"evidence_strategy": "replay",
|
|
558
|
+
"replay_scope": "api",
|
|
557
559
|
"api_refs": [api["id"]],
|
|
558
560
|
"execution": execution,
|
|
559
561
|
}
|
|
@@ -96,13 +96,29 @@ module ArchitectureBrief
|
|
|
96
96
|
assessment["triggers"].each { |trigger| lines << "- `#{trigger["code"]}` — #{trigger["node_id"]}" }
|
|
97
97
|
end
|
|
98
98
|
|
|
99
|
-
lines.concat(["", "## 工作负载形态", "", "| 节点 | 状态 | 执行 | 操作 | 扇出 | 数据影响 |
|
|
99
|
+
lines.concat(["", "## 工作负载形态", "", "| 节点 | 状态 | 类型 | 一致性 | 执行 | 操作 | 扇出 | 数据影响 |", "|---|---|---|---|---|---|---|---|"])
|
|
100
100
|
if workload_shapes.empty?
|
|
101
|
-
lines << "| — | — | — | — | — | — | — |"
|
|
101
|
+
lines << "| — | — | — | — | — | — | — | — |"
|
|
102
102
|
else
|
|
103
103
|
workload_shapes.each do |node|
|
|
104
104
|
facts = node.fetch("data", {}).fetch("facts", {})
|
|
105
|
-
lines << "| #{node["id"]} | #{node["status"]} | #{text(facts["
|
|
105
|
+
lines << "| #{node["id"]} | #{node["status"]} | #{text(facts["archetype"] || "legacy")} | #{text(facts["consistency"] || "—")} | #{text(facts["execution"])} | #{text(facts["operation"])} | #{text(facts["fanout"])} | #{text(facts["data_effect"])} |"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
metrics = Array(graph["nodes"]).select { |node| node["kind"] == "metric" }
|
|
110
|
+
read_models = Array(graph["nodes"]).select { |node| node["kind"] == "read_model" }
|
|
111
|
+
lines.concat(["", "## 统计指标与读模型", "", "| 节点 | 状态 | 定义/服务对象 | 时间与修正 |", "|---|---|---|---|"])
|
|
112
|
+
if metrics.empty? && read_models.empty?
|
|
113
|
+
lines << "| — | — | — | — |"
|
|
114
|
+
else
|
|
115
|
+
metrics.each do |node|
|
|
116
|
+
data = node.fetch("data", {})
|
|
117
|
+
lines << "| #{node["id"]} | #{node["status"]} | 指标:#{text(data["name"])};粒度:#{text(data["grain"])} | #{text(data["time_basis"])} / #{text(data["timezone"])};#{text(data["correction_policy"])} |"
|
|
118
|
+
end
|
|
119
|
+
read_models.each do |node|
|
|
120
|
+
data = node.fetch("data", {})
|
|
121
|
+
lines << "| #{node["id"]} | #{node["status"]} | 读模型:#{text(data["projection_mode"])};服务:#{text(Array(data["serves"]).join(", "))} | 重建:#{text(data.dig("rebuild", "method"))};迟到数据:#{text(data["late_data_policy"])} |"
|
|
106
122
|
end
|
|
107
123
|
end
|
|
108
124
|
|
package/plugins/devflow-core/skills/workflow-orchestrator/scripts/architecture_constraints.rb
CHANGED
|
@@ -13,6 +13,10 @@ module ArchitectureConstraints
|
|
|
13
13
|
OPERATIONS = %w[read write batch_read batch_write].freeze
|
|
14
14
|
FANOUTS = %w[none bounded unbounded unknown].freeze
|
|
15
15
|
DATA_EFFECTS = %w[none update append_only migration unknown].freeze
|
|
16
|
+
ARCHETYPES = %w[transactional_command operational_query analytics_query analytics_projection batch_processing external_integration].freeze
|
|
17
|
+
DATA_ROLES = %w[source_of_truth derived_read_model external_projection].freeze
|
|
18
|
+
CONSISTENCIES = %w[strong read_your_write bounded_staleness eventual unknown].freeze
|
|
19
|
+
EVENT_TIMES = %w[required not_applicable unknown].freeze
|
|
16
20
|
METRICS = %w[peak_qps data_growth p95_latency rpo rto].freeze
|
|
17
21
|
SOURCE_STATUSES = %w[measured contracted approved_assumption unknown].freeze
|
|
18
22
|
EVIDENCE_KINDS = %w[source_file cp_record external_measurement].freeze
|
|
@@ -52,6 +56,26 @@ module ArchitectureConstraints
|
|
|
52
56
|
errors_for_enum(errors, facts, "data_effect", DATA_EFFECTS)
|
|
53
57
|
external_calls = facts["external_calls"]
|
|
54
58
|
errors << "external_calls must be an array" unless external_calls.nil? || external_calls.is_a?(Array)
|
|
59
|
+
# `archetype` is optional for pre-v5 graphs. Once supplied it is a
|
|
60
|
+
# contract, not prose: analytics shapes must say what they serve and how
|
|
61
|
+
# their time semantics behave.
|
|
62
|
+
if facts.key?("archetype")
|
|
63
|
+
errors_for_enum(errors, facts, "archetype", ARCHETYPES)
|
|
64
|
+
errors_for_enum(errors, facts, "data_role", DATA_ROLES)
|
|
65
|
+
errors_for_enum(errors, facts, "consistency", CONSISTENCIES)
|
|
66
|
+
time = facts["time_semantics"]
|
|
67
|
+
errors << "time_semantics must be an object" unless time.is_a?(Hash)
|
|
68
|
+
errors_for_enum(errors, time || {}, "event_time", EVENT_TIMES)
|
|
69
|
+
errors << "time_semantics.timezone is required" unless present?(time && time["timezone"])
|
|
70
|
+
if %w[analytics_query analytics_projection].include?(facts["archetype"])
|
|
71
|
+
refs = shape["metric_refs"]
|
|
72
|
+
errors << "analytics workloads require non-empty metric_refs" unless refs.is_a?(Array) && !refs.empty?
|
|
73
|
+
end
|
|
74
|
+
if facts["archetype"] == "analytics_projection"
|
|
75
|
+
refs = shape["read_model_refs"]
|
|
76
|
+
errors << "analytics projections require non-empty read_model_refs" unless refs.is_a?(Array) && !refs.empty?
|
|
77
|
+
end
|
|
78
|
+
end
|
|
55
79
|
source_refs = shape["source_refs"]
|
|
56
80
|
errors << "source_refs must be a non-empty array" unless source_refs.is_a?(Array) && !source_refs.empty?
|
|
57
81
|
Array(source_refs).each { |ref| errors << "source_refs must reference REQ/BEH/API" unless reference?(ref, %w[REQ BEH API]) }
|
package/plugins/devflow-core/skills/workflow-orchestrator/scripts/architecture_feasibility.rb
CHANGED
|
@@ -78,6 +78,21 @@ module ArchitectureFeasibility
|
|
|
78
78
|
data = node.fetch("data")
|
|
79
79
|
if node["kind"] == "workload_shape"
|
|
80
80
|
facts = data.fetch("facts")
|
|
81
|
+
case facts["archetype"]
|
|
82
|
+
when "analytics_query"
|
|
83
|
+
level = promote(level, "full")
|
|
84
|
+
triggers << { "code" => "ARC_ANALYTICS_QUERY", "node_id" => node["id"] }
|
|
85
|
+
when "analytics_projection"
|
|
86
|
+
level = promote(level, "full")
|
|
87
|
+
triggers << { "code" => "ARC_ANALYTICS_PROJECTION", "node_id" => node["id"] }
|
|
88
|
+
when "transactional_command"
|
|
89
|
+
level = promote(level, "lightweight")
|
|
90
|
+
triggers << { "code" => "ARC_TRANSACTIONAL_COMMAND", "node_id" => node["id"] }
|
|
91
|
+
end
|
|
92
|
+
if facts["consistency"] == "unknown"
|
|
93
|
+
level = promote(level, "lightweight")
|
|
94
|
+
unresolved << { "code" => "ARC_CONSISTENCY_UNKNOWN", "node_id" => node["id"] }
|
|
95
|
+
end
|
|
81
96
|
case facts["data_effect"]
|
|
82
97
|
when "migration"
|
|
83
98
|
level = promote(level, "full")
|
|
@@ -121,10 +136,19 @@ module ArchitectureFeasibility
|
|
|
121
136
|
end
|
|
122
137
|
end
|
|
123
138
|
|
|
139
|
+
Array(graph["nodes"]).each do |node|
|
|
140
|
+
next unless node.is_a?(Hash) && node["kind"] == "read_model" && node["status"] == "confirmed"
|
|
141
|
+
data = node.fetch("data", {})
|
|
142
|
+
if data.dig("rebuild", "method") == "unknown" || data["late_data_policy"] == "unknown"
|
|
143
|
+
level = promote(level, "lightweight")
|
|
144
|
+
unresolved << { "code" => "ARC_READ_MODEL_RECOVERY_UNKNOWN", "node_id" => node["id"] }
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
124
148
|
unless static_context["available"]
|
|
125
149
|
unresolved << { "code" => static_context["code"], "detail" => static_context["detail"] }.compact
|
|
126
150
|
end
|
|
127
|
-
workload_errors = graph_errors.grep(/\A(?:WSH|WLC)-/)
|
|
151
|
+
workload_errors = graph_errors.grep(/\A(?:WSH|WLC|METRIC|RM)-/)
|
|
128
152
|
unresolved << { "code" => "ARC_WORKLOAD_GRAPH_INVALID", "count" => workload_errors.length } if workload_errors.any?
|
|
129
153
|
|
|
130
154
|
# P2 deliberately uses no numeric threshold. A numeric constraint is a
|
|
@@ -13,16 +13,17 @@ require_relative "control_io"
|
|
|
13
13
|
require_relative "architecture_constraints"
|
|
14
14
|
|
|
15
15
|
module RequirementGraph
|
|
16
|
-
#
|
|
16
|
+
# v5 adds workload archetypes plus METRIC/RM nodes. Existing v1-v4
|
|
17
17
|
# graphs are migrated in memory and re-written only by this Core writer.
|
|
18
|
-
SCHEMA_VERSION =
|
|
19
|
-
SUPPORTED_SCHEMA_VERSIONS = [1, 2, 3, SCHEMA_VERSION].freeze
|
|
18
|
+
SCHEMA_VERSION = 5
|
|
19
|
+
SUPPORTED_SCHEMA_VERSIONS = [1, 2, 3, 4, SCHEMA_VERSION].freeze
|
|
20
20
|
ROOT = ".planning/requirements"
|
|
21
21
|
KINDS = {
|
|
22
22
|
"source" => "SRC", "code" => "CODE", "term" => "TERM", "object" => "OBJECT",
|
|
23
23
|
"behavior" => "BEH", "requirement" => "REQ", "rule" => "BR", "boundary" => "BC",
|
|
24
24
|
"slice" => "SL", "risk" => "RISK", "decision" => "DECISION", "acceptance" => "AC",
|
|
25
|
-
"api" => "API", "workload_shape" => "WSH", "workload_constraint" => "WLC", "
|
|
25
|
+
"api" => "API", "workload_shape" => "WSH", "workload_constraint" => "WLC", "metric" => "METRIC",
|
|
26
|
+
"read_model" => "RM", "test_case" => "TC"
|
|
26
27
|
}.freeze
|
|
27
28
|
VIEW_NAMES = %w[evidence definitions behavior-graph requirement-graph slices acceptance tests traceability].freeze
|
|
28
29
|
FEATURE_INVALID = /[\\\/\x00-\x1F\x7F]/
|
|
@@ -176,7 +177,7 @@ module RequirementGraph
|
|
|
176
177
|
raise ArgumentError, "node kind #{kind} is not allowed by the execution contract" if allowed_kinds && !allowed_kinds.include?(kind)
|
|
177
178
|
data = normalize_data(delta["data"])
|
|
178
179
|
node = { "id" => next_id(graph, prefix), "kind" => kind, "status" => "candidate", "data" => data }
|
|
179
|
-
candidate_errors = workload_node_errors(graph, node) + architecture_decision_errors(graph, node) + architecture_risk_errors(graph, node) + test_case_errors(graph, node)
|
|
180
|
+
candidate_errors = workload_node_errors(graph, node) + metric_node_errors(graph, node) + read_model_node_errors(graph, node) + architecture_decision_errors(graph, node) + architecture_risk_errors(graph, node) + test_case_errors(graph, node)
|
|
180
181
|
raise ArgumentError, candidate_errors.join("; ") if candidate_errors.any?
|
|
181
182
|
set_node_fingerprint!(node)
|
|
182
183
|
graph["nodes"] << node
|
|
@@ -193,12 +194,12 @@ module RequirementGraph
|
|
|
193
194
|
id = delta["id"]
|
|
194
195
|
node = Array(graph["nodes"]).find { |entry| entry.is_a?(Hash) && entry["id"] == id }
|
|
195
196
|
raise ArgumentError, "update requires a known node id" unless node
|
|
196
|
-
if (%w[workload_shape workload_constraint test_case].include?(node["kind"]) || architecture_control_node?(node)) && node["status"] == "confirmed"
|
|
197
|
-
raise ArgumentError, "confirmed workload
|
|
197
|
+
if (%w[workload_shape workload_constraint metric read_model test_case].include?(node["kind"]) || architecture_control_node?(node)) && node["status"] == "confirmed"
|
|
198
|
+
raise ArgumentError, "confirmed workload, metric/read-model, and architecture decision nodes are immutable; confirmed test cases are also immutable; add a new candidate and relink consumers"
|
|
198
199
|
end
|
|
199
200
|
data = normalize_data(delta["data"])
|
|
200
201
|
node["data"] = node.fetch("data", {}).merge(data)
|
|
201
|
-
candidate_errors = workload_node_errors(graph, node) + architecture_decision_errors(graph, node) + architecture_risk_errors(graph, node) + test_case_errors(graph, node)
|
|
202
|
+
candidate_errors = workload_node_errors(graph, node) + metric_node_errors(graph, node) + read_model_node_errors(graph, node) + architecture_decision_errors(graph, node) + architecture_risk_errors(graph, node) + test_case_errors(graph, node)
|
|
202
203
|
raise ArgumentError, candidate_errors.join("; ") if candidate_errors.any?
|
|
203
204
|
set_node_fingerprint!(node)
|
|
204
205
|
clear_stale_for!(graph, id)
|
|
@@ -221,10 +222,10 @@ module RequirementGraph
|
|
|
221
222
|
unless evidence.is_a?(Hash) && (evidence["cp_record"] || evidence["evidence_ref"])
|
|
222
223
|
raise ArgumentError, "confirm requires cp_record or evidence_ref"
|
|
223
224
|
end
|
|
224
|
-
if (%w[workload_shape workload_constraint].include?(node["kind"]) || architecture_control_node?(node)) && !confirmed_interactively
|
|
225
|
-
raise ArgumentError, "workload and architecture-control confirmation requires --confirmed-interactively from the Core confirmation dialog"
|
|
225
|
+
if (%w[workload_shape workload_constraint metric read_model].include?(node["kind"]) || architecture_control_node?(node)) && !confirmed_interactively
|
|
226
|
+
raise ArgumentError, "workload and architecture-control confirmation requires --confirmed-interactively from the Core confirmation dialog; metric/read-model candidates use the same dialog"
|
|
226
227
|
end
|
|
227
|
-
confirmation_errors = workload_confirmation_errors(graph, node, evidence) + architecture_decision_confirmation_errors(graph, node) + architecture_risk_confirmation_errors(graph, node)
|
|
228
|
+
confirmation_errors = workload_confirmation_errors(graph, node, evidence) + metric_confirmation_errors(graph, node) + read_model_confirmation_errors(graph, node) + architecture_decision_confirmation_errors(graph, node) + architecture_risk_confirmation_errors(graph, node)
|
|
228
229
|
raise ArgumentError, confirmation_errors.join("; ") if confirmation_errors.any?
|
|
229
230
|
node["status"] = "confirmed"
|
|
230
231
|
node["confirmation"] = { "origin" => "core", "evidence" => evidence }
|
|
@@ -271,8 +272,14 @@ module RequirementGraph
|
|
|
271
272
|
end
|
|
272
273
|
end
|
|
273
274
|
|
|
274
|
-
TEST_CASE_TYPES = %w[api page permission boundary resilience migration performance].freeze
|
|
275
|
+
TEST_CASE_TYPES = %w[api page permission boundary resilience migration performance analytics freshness reconciliation].freeze
|
|
275
276
|
TEST_AUTOMATION_MODES = %w[automated manual blocked].freeze
|
|
277
|
+
# A verification strategy says what would constitute trustworthy evidence.
|
|
278
|
+
# It is intentionally separate from automation_mode: a page test may be
|
|
279
|
+
# replayable yet initially manual, while an intermittent incident may be
|
|
280
|
+
# causally proven without a deterministic replay.
|
|
281
|
+
TEST_EVIDENCE_STRATEGIES = %w[replay causal observation].freeze
|
|
282
|
+
REPLAY_SCOPES = %w[component page api integration].freeze
|
|
276
283
|
|
|
277
284
|
# TC nodes are derived verification intent. They deliberately store only
|
|
278
285
|
# externally observable setup, operation and oracle; implementation paths,
|
|
@@ -282,7 +289,7 @@ module RequirementGraph
|
|
|
282
289
|
|
|
283
290
|
data = node["data"].is_a?(Hash) ? node["data"] : {}
|
|
284
291
|
errors = []
|
|
285
|
-
%w[acceptance_refs requirement_refs test_type target preconditions steps oracle automation_mode].each do |field|
|
|
292
|
+
%w[acceptance_refs requirement_refs test_type target preconditions steps oracle automation_mode evidence_strategy].each do |field|
|
|
286
293
|
errors << "#{node["id"]}: missing #{field}" unless present?(data[field])
|
|
287
294
|
end
|
|
288
295
|
errors << "#{node["id"]}: acceptance_refs must be a non-empty array" unless data["acceptance_refs"].is_a?(Array) && !data["acceptance_refs"].empty?
|
|
@@ -291,6 +298,25 @@ module RequirementGraph
|
|
|
291
298
|
errors << "#{node["id"]}: steps must be a non-empty array" unless data["steps"].is_a?(Array) && !data["steps"].empty?
|
|
292
299
|
errors << "#{node["id"]}: invalid test_type" unless TEST_CASE_TYPES.include?(data["test_type"])
|
|
293
300
|
errors << "#{node["id"]}: invalid automation_mode" unless TEST_AUTOMATION_MODES.include?(data["automation_mode"])
|
|
301
|
+
errors << "#{node["id"]}: invalid evidence_strategy" unless TEST_EVIDENCE_STRATEGIES.include?(data["evidence_strategy"])
|
|
302
|
+
case data["evidence_strategy"]
|
|
303
|
+
when "replay"
|
|
304
|
+
errors << "#{node["id"]}: replay tests require replay_scope" unless REPLAY_SCOPES.include?(data["replay_scope"])
|
|
305
|
+
when "causal"
|
|
306
|
+
refs = data["causal_evidence_refs"]
|
|
307
|
+
errors << "#{node["id"]}: causal tests require non-empty causal_evidence_refs" unless refs.is_a?(Array) && !refs.empty?
|
|
308
|
+
Array(refs).each do |ref|
|
|
309
|
+
errors << "#{node["id"]}: causal evidence ref #{ref} must reference SRC/CODE" unless refs_known?(graph, [ref], %w[SRC CODE])
|
|
310
|
+
end
|
|
311
|
+
errors << "#{node["id"]}: causal tests require first_divergence" unless present?(data["first_divergence"])
|
|
312
|
+
when "observation"
|
|
313
|
+
plan = data["observation_plan"]
|
|
314
|
+
errors << "#{node["id"]}: observation tests require observation_plan" unless plan.is_a?(Hash)
|
|
315
|
+
%w[signal owner window alert_condition].each do |field|
|
|
316
|
+
errors << "#{node["id"]}: observation_plan requires #{field}" unless plan.is_a?(Hash) && present?(plan[field])
|
|
317
|
+
end
|
|
318
|
+
errors << "#{node["id"]}: observation tests must remain blocked until Core-observed evidence exists" unless data["automation_mode"] == "blocked"
|
|
319
|
+
end
|
|
294
320
|
if data.key?("test_obligation_refs")
|
|
295
321
|
errors << "#{node["id"]}: test_obligation_refs must be a non-empty string array when present" unless data["test_obligation_refs"].is_a?(Array) && !data["test_obligation_refs"].empty? && data["test_obligation_refs"].all? { |ref| ref.is_a?(String) && ref.match?(/\AOBL-[A-Z0-9-]+\z/) }
|
|
296
322
|
end
|
|
@@ -322,7 +348,9 @@ module RequirementGraph
|
|
|
322
348
|
{
|
|
323
349
|
"api_refs" => %w[API],
|
|
324
350
|
"workload_refs" => %w[WSH WLC],
|
|
325
|
-
"risk_refs" => %w[RISK]
|
|
351
|
+
"risk_refs" => %w[RISK],
|
|
352
|
+
"metric_refs" => %w[METRIC],
|
|
353
|
+
"read_model_refs" => %w[RM]
|
|
326
354
|
}.each do |field, prefixes|
|
|
327
355
|
next unless data.key?(field)
|
|
328
356
|
|
|
@@ -504,6 +532,16 @@ module RequirementGraph
|
|
|
504
532
|
errors << "#{node["id"]}: workload source #{ref} must link with shapes_workload" if require_edges && !incoming_edge?(graph, ref, node["id"], "shapes_workload")
|
|
505
533
|
errors << "#{node["id"]}: workload source #{ref} must be confirmed" if require_confirmed_inputs && !confirmed?(graph, ref)
|
|
506
534
|
end
|
|
535
|
+
{ "metric_refs" => %w[METRIC], "read_model_refs" => %w[RM] }.each do |field, prefixes|
|
|
536
|
+
next unless data.key?(field)
|
|
537
|
+
errors << "#{node["id"]}: #{field} must be a non-empty array when present" unless data[field].is_a?(Array) && !data[field].empty?
|
|
538
|
+
Array(data[field]).each do |ref|
|
|
539
|
+
errors << "#{node["id"]}: #{field} #{ref} must exist in the graph" unless refs_known?(graph, [ref], prefixes)
|
|
540
|
+
relation = field == "metric_refs" ? "defines_metric" : "uses_read_model"
|
|
541
|
+
errors << "#{node["id"]}: #{field} #{ref} must link with #{relation}" if require_edges && !incoming_edge?(graph, ref, node["id"], relation)
|
|
542
|
+
errors << "#{node["id"]}: #{field} #{ref} must be confirmed" if require_confirmed_inputs && !confirmed?(graph, ref)
|
|
543
|
+
end
|
|
544
|
+
end
|
|
507
545
|
else
|
|
508
546
|
workload_ref = data["workload_ref"]
|
|
509
547
|
errors << "#{node["id"]}: workload_ref #{workload_ref} must exist in the graph" unless refs_known?(graph, [workload_ref], %w[BEH SL API])
|
|
@@ -528,6 +566,74 @@ module RequirementGraph
|
|
|
528
566
|
errors
|
|
529
567
|
end
|
|
530
568
|
|
|
569
|
+
# A metric is a business contract, not a free-text label on a dashboard.
|
|
570
|
+
# It deliberately has no status/ID input: those remain Core-owned.
|
|
571
|
+
def metric_node_errors(graph, node, require_edges: false, require_confirmed_inputs: false)
|
|
572
|
+
return [] unless node["kind"] == "metric"
|
|
573
|
+
|
|
574
|
+
data = node.fetch("data", {})
|
|
575
|
+
errors = []
|
|
576
|
+
%w[name grain measure inclusion_rule_refs exclusion_rule_refs dimensions time_basis timezone correction_policy owner].each do |field|
|
|
577
|
+
errors << "#{node["id"]}: metric missing #{field}" unless present?(data[field])
|
|
578
|
+
end
|
|
579
|
+
%w[inclusion_rule_refs exclusion_rule_refs dimensions].each do |field|
|
|
580
|
+
errors << "#{node["id"]}: #{field} must be a non-empty array" unless data[field].is_a?(Array) && !data[field].empty?
|
|
581
|
+
end
|
|
582
|
+
Array(data["inclusion_rule_refs"]) .concat(Array(data["exclusion_rule_refs"])).each do |ref|
|
|
583
|
+
errors << "#{node["id"]}: metric rule #{ref} must reference BR" unless refs_known?(graph, [ref], %w[BR])
|
|
584
|
+
errors << "#{node["id"]}: metric rule #{ref} must link with defines_metric" if require_edges && !incoming_edge?(graph, ref, node["id"], "defines_metric")
|
|
585
|
+
errors << "#{node["id"]}: metric rule #{ref} must be confirmed" if require_confirmed_inputs && !confirmed?(graph, ref)
|
|
586
|
+
end
|
|
587
|
+
errors.uniq
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
def metric_confirmation_errors(graph, node)
|
|
591
|
+
metric_node_errors(graph, node, require_edges: true, require_confirmed_inputs: true)
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
READ_MODEL_MODES = %w[direct_query materialized_view scheduled_aggregate event_projection warehouse].freeze
|
|
595
|
+
REBUILD_METHODS = %w[replay_from_fact full_recompute unknown].freeze
|
|
596
|
+
LATE_DATA_POLICIES = %w[recompute_window correction_event reject unknown].freeze
|
|
597
|
+
|
|
598
|
+
def read_model_node_errors(graph, node, require_edges: false, require_confirmed_inputs: false)
|
|
599
|
+
return [] unless node["kind"] == "read_model"
|
|
600
|
+
|
|
601
|
+
data = node.fetch("data", {})
|
|
602
|
+
errors = []
|
|
603
|
+
%w[source_of_truth_refs serves metric_contract_refs projection_mode rebuild late_data_policy reconciliation owner].each do |field|
|
|
604
|
+
errors << "#{node["id"]}: read model missing #{field}" unless present?(data[field])
|
|
605
|
+
end
|
|
606
|
+
%w[source_of_truth_refs serves metric_contract_refs].each do |field|
|
|
607
|
+
errors << "#{node["id"]}: #{field} must be a non-empty array" unless data[field].is_a?(Array) && !data[field].empty?
|
|
608
|
+
end
|
|
609
|
+
errors << "#{node["id"]}: invalid projection_mode" unless READ_MODEL_MODES.include?(data["projection_mode"])
|
|
610
|
+
errors << "#{node["id"]}: rebuild must be an object" unless data["rebuild"].is_a?(Hash)
|
|
611
|
+
errors << "#{node["id"]}: invalid rebuild.method" unless REBUILD_METHODS.include?(data.dig("rebuild", "method"))
|
|
612
|
+
errors << "#{node["id"]}: invalid late_data_policy" unless LATE_DATA_POLICIES.include?(data["late_data_policy"])
|
|
613
|
+
errors << "#{node["id"]}: reconciliation must be an object" unless data["reconciliation"].is_a?(Hash)
|
|
614
|
+
%w[method frequency].each { |field| errors << "#{node["id"]}: reconciliation missing #{field}" unless present?(data.dig("reconciliation", field)) }
|
|
615
|
+
Array(data["source_of_truth_refs"]).each do |ref|
|
|
616
|
+
errors << "#{node["id"]}: source #{ref} must reference BEH/OBJECT" unless refs_known?(graph, [ref], %w[BEH OBJECT])
|
|
617
|
+
errors << "#{node["id"]}: source #{ref} must link with derives_read_model" if require_edges && !incoming_edge?(graph, ref, node["id"], "derives_read_model")
|
|
618
|
+
errors << "#{node["id"]}: source #{ref} must be confirmed" if require_confirmed_inputs && !confirmed?(graph, ref)
|
|
619
|
+
end
|
|
620
|
+
Array(data["serves"]).each do |ref|
|
|
621
|
+
errors << "#{node["id"]}: serves #{ref} must reference API" unless refs_known?(graph, [ref], %w[API])
|
|
622
|
+
errors << "#{node["id"]}: serves #{ref} must link with serves_query" if require_edges && !incoming_edge?(graph, node["id"], ref, "serves_query")
|
|
623
|
+
errors << "#{node["id"]}: serves #{ref} must be confirmed" if require_confirmed_inputs && !confirmed?(graph, ref)
|
|
624
|
+
end
|
|
625
|
+
Array(data["metric_contract_refs"]).each do |ref|
|
|
626
|
+
errors << "#{node["id"]}: metric #{ref} must reference METRIC" unless refs_known?(graph, [ref], %w[METRIC])
|
|
627
|
+
errors << "#{node["id"]}: metric #{ref} must link with defines_read_model" if require_edges && !incoming_edge?(graph, ref, node["id"], "defines_read_model")
|
|
628
|
+
errors << "#{node["id"]}: metric #{ref} must be confirmed" if require_confirmed_inputs && !confirmed?(graph, ref)
|
|
629
|
+
end
|
|
630
|
+
errors.uniq
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
def read_model_confirmation_errors(graph, node)
|
|
634
|
+
read_model_node_errors(graph, node, require_edges: true, require_confirmed_inputs: true)
|
|
635
|
+
end
|
|
636
|
+
|
|
531
637
|
def architecture_option_decision?(node)
|
|
532
638
|
node["kind"] == "decision" && node.dig("data", "decision_type") == "architecture_option_comparison"
|
|
533
639
|
end
|
|
@@ -666,6 +772,14 @@ module RequirementGraph
|
|
|
666
772
|
results["requirement-graph-check"].concat(
|
|
667
773
|
workload_node_errors(graph, node, require_edges: true, require_confirmed_inputs: node["status"] == "confirmed")
|
|
668
774
|
)
|
|
775
|
+
when "metric"
|
|
776
|
+
results["requirement-graph-check"].concat(
|
|
777
|
+
metric_node_errors(graph, node, require_edges: true, require_confirmed_inputs: node["status"] == "confirmed")
|
|
778
|
+
)
|
|
779
|
+
when "read_model"
|
|
780
|
+
results["requirement-graph-check"].concat(
|
|
781
|
+
read_model_node_errors(graph, node, require_edges: true, require_confirmed_inputs: node["status"] == "confirmed")
|
|
782
|
+
)
|
|
669
783
|
when "acceptance"
|
|
670
784
|
required(results["acceptance-coverage-check"], node, %w[requirement_ref verification_types])
|
|
671
785
|
types = Array(node.dig("data", "verification_types"))
|
|
@@ -705,6 +819,13 @@ module RequirementGraph
|
|
|
705
819
|
if node["kind"] == "workload_shape" && node.dig("data", "facts", "data_effect") == "migration"
|
|
706
820
|
results["test-obligation-check"] << "#{node["id"]}: migration workload requires a migration TC with workload_refs" unless covered_by.call("migration", "workload_refs", node["id"])
|
|
707
821
|
end
|
|
822
|
+
if node["kind"] == "workload_shape" && %w[analytics_query analytics_projection].include?(node.dig("data", "facts", "archetype"))
|
|
823
|
+
results["test-obligation-check"] << "#{node["id"]}: analytics workload requires an analytics TC with workload_refs" unless covered_by.call("analytics", "workload_refs", node["id"])
|
|
824
|
+
end
|
|
825
|
+
when "metric"
|
|
826
|
+
results["test-obligation-check"] << "#{node["id"]}: metric requires a reconciliation TC with metric_refs" unless covered_by.call("reconciliation", "metric_refs", node["id"])
|
|
827
|
+
when "read_model"
|
|
828
|
+
results["test-obligation-check"] << "#{node["id"]}: read model requires a freshness TC with read_model_refs" unless covered_by.call("freshness", "read_model_refs", node["id"])
|
|
708
829
|
when "risk"
|
|
709
830
|
if architecture_option_risk?(node)
|
|
710
831
|
results["test-obligation-check"] << "#{node["id"]}: architecture risk requires a resilience TC with risk_refs" unless covered_by.call("resilience", "risk_refs", node["id"])
|
|
@@ -760,7 +881,7 @@ module RequirementGraph
|
|
|
760
881
|
when "evidence" then %w[source code]
|
|
761
882
|
when "definitions" then %w[term object rule boundary]
|
|
762
883
|
when "behavior-graph" then %w[behavior]
|
|
763
|
-
when "requirement-graph", "traceability" then %w[requirement rule boundary api workload_shape workload_constraint test_case]
|
|
884
|
+
when "requirement-graph", "traceability" then %w[requirement rule boundary api workload_shape workload_constraint metric read_model test_case]
|
|
764
885
|
when "slices" then %w[slice]
|
|
765
886
|
when "acceptance" then %w[acceptance risk decision]
|
|
766
887
|
when "tests" then %w[test_case]
|
|
@@ -43,9 +43,10 @@ The meta file must also set `traceability_status: "passed"` only after every API
|
|
|
43
43
|
1. **只定义接口契约,不谈实现细节** — 不写数据表结构、不写具体代码、不写业务流程内部逻辑
|
|
44
44
|
2. **分叉后不变更** — 一旦确定,Track A/B 都基于此契约工作。变更走 `api-supplement` 异常路径
|
|
45
45
|
3. **接口签名必须完整** — 包括 endpoint、method、请求参数(含类型约束)、响应结构(含状态码)、错误契约与行为语义(过滤、排序、权限、幂等/兼容规则)
|
|
46
|
-
4.
|
|
47
|
-
5.
|
|
48
|
-
6.
|
|
46
|
+
4. **负载职责明确** — 事务命令、运营查询和统计查询不得在同一个接口语义中混写;统计查询必须引用已确认的 `METRIC-*`,并在响应中约定 `as_of`/新鲜度语义或明确强一致依据。
|
|
47
|
+
5. **数据模型共享** — 核心业务实体定义在这里,前后端共用一个数据模型
|
|
48
|
+
6. **切片对齐** — 若 baseline 包含 `SL-*`,先读取 `skill://workflow-pipeline/references/slice-safety-guide.md`,只为当前执行切片定义 API。后置切片接口不得提前进入契约。
|
|
49
|
+
7. **既有接口优先查事实索引** — 当 `.planning/code-index/` 存在时,先用 Symbol/Relation 定位候选接口、调用方和处理器,再读取源码证据;缺失或过期时再按 navigation 和 `rg` 兜底。
|
|
49
50
|
|
|
50
51
|
## 可测试 API 契约(强制)
|
|
51
52
|
|
|
@@ -15,6 +15,8 @@ description: "Use after API contract and before frontend/backend technical speci
|
|
|
15
15
|
- WSH 使用 `workload_shape`,并连接 `REQ/BEH/API → WSH`,relation 为 `shapes_workload`。
|
|
16
16
|
- WLC 使用 `workload_constraint`,并连接 `BEH/SL/API → WLC`,relation 为 `constrains_workload`。
|
|
17
17
|
- `unknown` 只能写缺口、责任人、截止 checkpoint;不能填 target、evidence 或 assumption。
|
|
18
|
+
- 每个新增/变更的 API、BEH、Job 至少归类为 `transactional_command`、`operational_query`、`analytics_query`、`analytics_projection`、`batch_processing` 或 `external_integration`;历史节点没有 `archetype` 时按 legacy 处理,不倒逼补录。
|
|
19
|
+
- `analytics_query` 必须关联 `METRIC-*`;`analytics_projection` 还必须关联 `RM-*`。先提交指标口径(粒度、纳入/排除规则、时间字段/时区、修正规则、owner),再提交读模型(事实源、服务 API、投影/刷新方式、重建、迟到数据和对账)。
|
|
18
20
|
4. 对 `lightweight` 或 `full` 且存在实质取舍的结果,提交 `decision_type: architecture_option_comparison` 的 `DECISION` candidate:
|
|
19
21
|
- `subject_refs` 只引用 WSH/WLC;WSH 用 `informs_decision` 连到 DECISION,WLC 用 `constrains_decision` 连到 DECISION。
|
|
20
22
|
- 每个 option 使用 `option_key`,并包含 `mechanism`、非空 `fact_refs`、非空 `tradeoffs`。
|
|
@@ -31,6 +33,8 @@ description: "Use after API contract and before frontend/backend technical speci
|
|
|
31
33
|
- `assessment_status: incomplete`、`comparison_status: missing` 或 `awaiting_*` 必须原样交给下游技术方案,不能写成“评估通过”。
|
|
32
34
|
- 已确认 WSH/WLC/架构 DECISION/RISK 不可原地更新;新建 candidate、重新连边,并让 stale 传播要求消费者复核。
|
|
33
35
|
- 不替用户选择方案、接受残余风险或伪造 CP 记录。
|
|
36
|
+
- 业务事实表是唯一可审计真相源;`RM-*` 是可重建的派生读模型,不能被业务命令当作事实源回写。
|
|
37
|
+
- 不因“统计”自动要求拆库、消息队列或数仓。`direct_query`、物化视图、定时汇总、事件投影和数仓必须根据 WSH/WLC 给出有实质差异的比较与升级条件。
|
|
34
38
|
|
|
35
39
|
## 下游交接
|
|
36
40
|
|
|
@@ -48,6 +48,8 @@ allowed-tools:
|
|
|
48
48
|
|
|
49
49
|
读取 `.planning/requirements/{feature}/traceability.json`。每个设计章节必须列 `覆盖 REQ: REQ-*`;定时任务、批处理、导入和通知至少拆为“编排(扫描/触发/锁/重试)”与“业务算法(判定/计算/写入)”两个可独立验证的小节。不得把遍历全部活跃组织、发送通知、文件解析等编排事实藏在算法描述中。
|
|
50
50
|
|
|
51
|
+
当架构简报包含 `analytics_query`、`analytics_projection`、`METRIC-*` 或 `RM-*` 时,后端方案额外必须分别写明:业务事实源;指标粒度/纳入排除/时区与修正;读模型或直接聚合路径;索引/扫描范围;新鲜度;watermark/迟到数据;重建与对账。不得把统计 SQL 混进事务命令,或以“后续补统计”跳过这些字段。
|
|
52
|
+
|
|
51
53
|
根据需求基线和项目现有代码,生成聚焦于影响分析的后端技术方案。
|
|
52
54
|
|
|
53
55
|
## 核心原则
|
|
@@ -39,11 +39,15 @@ description: "Use after integration verification and before change planning to d
|
|
|
39
39
|
"preconditions": ["身份、数据和环境前置条件"],
|
|
40
40
|
"steps": ["测试操作"],
|
|
41
41
|
"oracle": "可观察的预期结果",
|
|
42
|
-
"automation_mode": "automated|manual|blocked"
|
|
42
|
+
"automation_mode": "automated|manual|blocked",
|
|
43
|
+
"evidence_strategy": "replay|causal|observation"
|
|
43
44
|
}
|
|
44
45
|
```
|
|
45
46
|
|
|
46
47
|
- `manual` 额外写 `manual_reason` 和 `owner`;`blocked` 额外写 `blocked_by` 和 `owner`。
|
|
48
|
+
- `replay`:可用稳定步骤再次触发,必须写 `replay_scope`(`component|page|api|integration`)。页面优先拆到组件、页面、接口或集成边界,不要求 AI 在真实环境盲目重试。
|
|
49
|
+
- `causal`:用于已由 HAR、控制台、DOM 快照、服务日志或请求链路证明首个偏差、但暂不能稳定重放的缺陷。必须写 `causal_evidence_refs`(已有 `SRC-*` / `CODE-*`)和 `first_divergence`;后续执行绑定还必须提供实际证据文件,Core 会哈希留档。没有这些证据不能称为“已证实”。
|
|
50
|
+
- `observation`:用于当前无法重放也无法证明根因的偶发问题。必须写 `observation_plan` 的 `signal`、`owner`、`window`、`alert_condition`,且 `automation_mode` 必须为 `blocked`。它会阻止版本收口,不能被“人工已看过”替代。
|
|
47
51
|
- 当 TC 承接结构性专项义务时,额外带对应引用并建立同样的 `verified_by` edge:`api_refs`(API→`api`)、`workload_refs`(WSH/WLC→`performance` 或 `migration`)、`risk_refs`(架构 RISK→`resilience`)。跨数据域的 REQ 必须有 `permission` TC,BC 必须有 `boundary` TC。
|
|
48
52
|
- 页面/API 的具体 selector、endpoint URL、测试数据和 runner 命令属于后续执行绑定,不得改变本步骤的步骤或 oracle。
|
|
49
53
|
- 高风险边界、权限、迁移、外部依赖和工作负载事实必须产生相应 TC;`test-obligation-check` 是机器判定,不接受“无架构影响”的模型自述。环境未准备好只能标记 blocked,不得省略。
|
|
@@ -36,6 +36,8 @@ description: "Execute the frozen TC acceptance suite after code review. Use the
|
|
|
36
36
|
- 通用自动化用例使用 `{ "kind": "command", "argv": [ ... ] }`,不使用 shell 字符串;
|
|
37
37
|
- Core 生成的 API TC 可直接使用 `{ "kind": "http", "base_url_env": "DEVFLOW_TEST_BASE_URL", "headers_env": { "Authorization": "DEVFLOW_TEST_AUTHORIZATION" } }`。请求、HTTP 状态和异常码断言来自冻结 TC 的 `execution`,绑定只能提供环境地址和鉴权,不得重写测试意图;若场景需要专用夹具,可额外配置 `{ "setup": { "argv": [ ... ] } }`,Core 会先记录并执行该受控命令,再发 HTTP 请求;
|
|
38
38
|
- 人工或环境阻塞用例必须有 owner 与原因/阻塞源。
|
|
39
|
+
- `evidence_strategy=causal` 的自动用例额外写 `evidence_files`(项目根目录内的 HAR、控制台、DOM 快照、截图或日志文件相对路径)。Core 在执行前校验文件存在且未越出项目根目录,并把内容哈希写入结果;不能只填 Markdown 描述。
|
|
40
|
+
- `evidence_strategy=observation` 只能保持 `blocked`,其观测计划是风险收口输入,不是测试通过证据。
|
|
39
41
|
5. 调用 `devflow_run_acceptance_tests(action="validate", feature=...)`。不通过时修正绑定,不得绕过。
|
|
40
42
|
6. 调用 `devflow_run_acceptance_tests(action="run", feature=...)`。该 Core 工具记录每个 TC 的命令哈希、退出码和输出摘要。
|
|
41
43
|
7. 失败、未执行或 blocked 的 TC 只能报告为 `blocked`/`failed`,不得进入版本收口。
|
package/src/constants.ts
CHANGED