@guanlin4924/devflow-pi 0.1.52 → 0.1.53

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guanlin4924/devflow-pi",
3
- "version": "0.1.52",
3
+ "version": "0.1.53",
4
4
  "type": "module",
5
5
  "description": "Devflow 专项研发 Pi,基于固定版本 OMP Runtime 组装研发状态机、代码智能、设计、实现与质量门禁。",
6
6
  "license": "UNLICENSED",
@@ -369,7 +369,7 @@ export function isRequirementDraftToolAllowed(toolName: string, input: unknown,
369
369
  return normalized === "task" && taskAgents.length > 0 && taskAgents.every(agent => PLANNING_AGENTS.has(agent));
370
370
  }
371
371
 
372
- type InternalDevflowControlResource = "contract" | "state" | "mainflow" | "requirement_graph";
372
+ type InternalDevflowControlResource = "contract" | "state" | "mainflow" | "requirement_graph" | "requirement_coverage";
373
373
 
374
374
  /** Resolve an xdev target only when all supplied target aliases agree.
375
375
  * A direct/eval caller can otherwise provide `path` for a real file plus an
@@ -395,6 +395,7 @@ export function internalDevflowControlResource(toolName: string, input: unknown)
395
395
  if (target === "xd://devflow_state") return "state";
396
396
  if (target === "xd://devflow_mainflow") return "mainflow";
397
397
  if (target === "xd://devflow_requirement_graph") return "requirement_graph";
398
+ if (target === "xd://devflow_requirement_coverage") return "requirement_coverage";
398
399
  return undefined;
399
400
  }
400
401
 
@@ -1118,6 +1119,7 @@ function stepScopedMutationOwner(toolName: string, input: unknown): string | nul
1118
1119
  if (["write", "edit", "apply_patch", "notebook_edit", "task", "bash", "shell", "exec_command"].includes(normalized)) return null;
1119
1120
  if (!normalized.startsWith("devflow_") || CONTRACT_CONTROL_TOOLS.has(normalized) || CONTRACT_READ_TOOLS.has(normalized)) return undefined;
1120
1121
  const action = inputAction(input);
1122
+ if (normalized === "devflow_requirement_coverage") return action === "validate" ? undefined : "requirement-wayfinder";
1121
1123
  if (normalized === "devflow_code_index") return action === "scan" ? "code-index-refresh" : undefined;
1122
1124
  if (normalized === "devflow_requirement_graph") {
1123
1125
  const writeContext = input && typeof input === "object" && (input as Record<string, unknown>).write === true;
@@ -2398,7 +2400,7 @@ export default function devflowCore(pi: ExtensionAPI) {
2398
2400
  recoveredApprovalState = true;
2399
2401
  }
2400
2402
  }
2401
- if (recoveredApprovalState || controlState.phase === "awaiting-approval" || controlState.phase === "waiting-user") {
2403
+ if (recoveredApprovalState || controlState.phase === "awaiting-approval" || controlState.phase === "waiting-user" || (controlState.phase === "blocked" && controlState.label === "推进校验失败")) {
2402
2404
  if (controlAction === "reject") {
2403
2405
  ctx.ui.notify("已保留当前步骤和产物。请直接说明需要修改的内容;未执行任何推进。", "info");
2404
2406
  return { handled: true };
@@ -2437,7 +2439,7 @@ export default function devflowCore(pi: ExtensionAPI) {
2437
2439
  ]);
2438
2440
  if (!result.ok) {
2439
2441
  setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "推进校验失败" });
2440
- ctx.ui.notify(`未推进:${result.stderr || result.stdout}`, "error");
2442
+ ctx.ui.notify(`未推进:${result.stderr || result.stdout}\n请补齐上述校验缺项后重新审批;重复批准不会绕过门禁。可请求重新执行当前步骤以修复产物。`, "error");
2441
2443
  return { handled: true };
2442
2444
  }
2443
2445
  const after = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
@@ -2459,6 +2461,10 @@ export default function devflowCore(pi: ExtensionAPI) {
2459
2461
  return { handled: true };
2460
2462
  }
2461
2463
  }
2464
+ if (controlAction) {
2465
+ ctx.ui.notify("当前没有可推进的审批节点。请先完成或修复当前步骤产物,再请求审批;状态未推进。", "info");
2466
+ return { handled: true };
2467
+ }
2462
2468
  const intent = classifyNaturalLanguageIntent(event.text);
2463
2469
  if (intent === "explain-state") {
2464
2470
  const inspected = await inspectWorkflow(ctx.cwd);
@@ -2826,6 +2832,20 @@ export default function devflowCore(pi: ExtensionAPI) {
2826
2832
  },
2827
2833
  });
2828
2834
 
2835
+ pi.registerTool({
2836
+ name: "devflow_requirement_coverage",
2837
+ label: "Devflow Requirement Coverage",
2838
+ description: "Generate Core source units from a requirement source file, or validate wayfinder coverage before requesting approval. Generation never creates coverage claims.",
2839
+ parameters: z.object({ action: z.enum(["generate", "validate"]), feature: z.string().min(1), source: z.string().optional() }),
2840
+ approval: args => args.action === "generate" ? "write" : "read",
2841
+ async execute(_id, params, _signal, _onUpdate, ctx) {
2842
+ return renderResult(await runRuby(ctx.cwd, "workflow-orchestrator/scripts/requirement_coverage.rb", [
2843
+ "--root", ctx.cwd, "--feature", params.feature,
2844
+ ...(params.action === "generate" ? ["--source", params.source ?? "", "--write"] : []),
2845
+ ]));
2846
+ },
2847
+ });
2848
+
2829
2849
  pi.registerTool({
2830
2850
 
2831
2851
  name: "devflow_requirement_graph",
@@ -4,6 +4,7 @@
4
4
  # Canonical wayfinder quality checker. It validates references and structured
5
5
  # coverage only; AI-provided equivalence labels are never trusted.
6
6
  require "digest"
7
+ require "date"
7
8
  require "json"
8
9
  require "optparse"
9
10
  require "yaml"
@@ -12,6 +13,33 @@ require_relative "requirement_graph"
12
13
  module RequirementCoverage
13
14
  module_function
14
15
 
16
+ def generate(root:, feature:, source:)
17
+ state = YAML.safe_load(File.read(File.join(root, ".planning/STATE.md")).split(/^---\s*$/, 3)[1], permitted_classes: [Date], aliases: false)
18
+ raise ArgumentError, "source generation requires the active wayfinder step" unless state["current_feature"] == feature && state["current_step"] == "requirement-wayfinder"
19
+ source_path = File.realpath(File.expand_path(source, root))
20
+ raise ArgumentError, "source must be a project file" unless source_path.start_with?(File.realpath(root) + File::SEPARATOR)
21
+ text = File.read(source_path)
22
+ fingerprint = sha(text)
23
+ units = text.lines.each_with_index.map do |line, index|
24
+ excerpt = line.strip
25
+ next if excerpt.empty?
26
+ id = "SU-#{sha("#{source_path.delete_prefix(File.realpath(root))}:#{index + 1}:#{excerpt}")[0, 16]}"
27
+ { "stable_unit_id" => id, "source_document_fingerprint" => fingerprint,
28
+ "excerpt" => excerpt, "excerpt_fingerprint" => sha(excerpt), "line" => index + 1,
29
+ "required_assertions" => [{ "assertion_id" => "#{id}-A1", "source_unit_id" => id,
30
+ "type" => "source_statement", "predicate" => excerpt }] }
31
+ end.compact
32
+ raise ArgumentError, "source must not be empty" if units.empty?
33
+ directory = RequirementGraph.feature_dir(root, feature)
34
+ path = File.join(directory, "source_units.yaml")
35
+ payload = { "schema_version" => 1, "produced_by" => "core.source_unitizer/v1",
36
+ "source_document" => source_path.delete_prefix(File.realpath(root) + File::SEPARATOR),
37
+ "source_document_fingerprint" => fingerprint, "units" => units }
38
+ File.write(path + ".tmp", YAML.dump(payload))
39
+ File.rename(path + ".tmp", path)
40
+ { "ok" => true, "path" => path, "unit_count" => units.length }
41
+ end
42
+
15
43
  def validate(root:, feature:)
16
44
  directory = RequirementGraph.feature_dir(root, feature)
17
45
  errors = []
@@ -165,9 +193,16 @@ if $PROGRAM_NAME == __FILE__
165
193
  OptionParser.new do |opts|
166
194
  opts.on("--root PATH") { |value| options[:root] = value }
167
195
  opts.on("--feature NAME") { |value| options[:feature] = value }
196
+ opts.on("--source PATH") { |value| options[:source] = value }
197
+ opts.on("--write") { options[:write] = true }
168
198
  end.parse!
169
199
  abort("--root and --feature are required") unless options[:root] && options[:feature]
170
- payload = RequirementCoverage.validate(root: options[:root], feature: options[:feature])
200
+ payload = if options[:write]
201
+ abort("--source is required") if options[:source].to_s.empty?
202
+ RequirementCoverage.generate(root: options[:root], feature: options[:feature], source: options[:source])
203
+ else
204
+ RequirementCoverage.validate(root: options[:root], feature: options[:feature])
205
+ end
171
206
  puts JSON.pretty_generate(payload)
172
- exit(payload["verdict"] == "allow" ? 0 : 1)
207
+ exit(payload["ok"] ? 0 : 1)
173
208
  end
@@ -409,6 +409,12 @@ steps:
409
409
  - path: ".planning/requirements/{feature}/决策地图.md"
410
410
  type: requirement-wayfinder
411
411
  required: true
412
+ - path: ".planning/requirements/{feature}/source_units.yaml"
413
+ type: requirement-source-units
414
+ required: true
415
+ - path: ".planning/requirements/{feature}/coverage.yaml"
416
+ type: requirement-coverage
417
+ required: true
412
418
  meta_output:
413
419
  path: ".planning/requirements/{feature}/决策地图.meta.json"
414
420
  fields:
@@ -13,6 +13,8 @@ Always create or update:
13
13
 
14
14
  - `.planning/requirements/{功能名}/决策地图.md`
15
15
  - `.planning/requirements/{功能名}/决策地图.meta.json`
16
+ - `.planning/requirements/{功能名}/source_units.yaml` (Core-generated)
17
+ - `.planning/requirements/{功能名}/coverage.yaml`
16
18
  - `.planning/requirements/{功能名}/wayfinder/WF-xxx.md` for each ticket
17
19
 
18
20
  `决策地图.meta.json` minimum fields:
@@ -166,8 +168,29 @@ After resolution, update the map immediately. Do not solve hidden Fog tickets be
166
168
 
167
169
  ## Phase 4 — Exit Criteria
168
170
 
171
+ ### Required Coverage Gate
172
+
173
+ Before setting ready_for_baseline, call `devflow_requirement_coverage(action="generate", feature=..., source=...)`
174
+ with the project-relative authoritative requirement source file. If the source is spread over several documents,
175
+ first consolidate the complete source with provenance; do not substitute the decision map or silently omit source text.
176
+ Core conservatively preserves every nonempty source line as an assertion; these are source statements, not automatically business rules.
177
+ Never hand-author source_units.yaml or its produced_by marker.
178
+
179
+ Read the generated units and write coverage.yaml with schema_version: 1, units and claims arrays.
180
+ Each units entry has unit_id and disposition: {kind: active}. Each claim has claim_id, source_unit_id,
181
+ assertion_ids, inherited_constraints and an evidence_ref pointing to the actual map/ticket coverage.
182
+ inherited_constraints must preserve the assertion's type and predicate (and any other constraint fields).
183
+ Review each source statement against its evidence before claiming coverage; copying constraints alone is not evidence.
184
+ Deferred/out_of_scope dispositions require real, source-fingerprint-bound approval records; never invent approval IDs.
185
+
186
+ Call `devflow_requirement_coverage(action="validate", feature=...)`. Only verdict=allow satisfies this gate.
187
+ Missing files, uncertain coverage and approval-required results must be repaired or reported as blocked,
188
+ not described as completed. Reuse existing tickets and decisions during repair.
189
+
169
190
  Exit this step to `workflow-domain-modeling` only when:
170
191
 
192
+ - The Core coverage checker returns verdict=allow.
193
+
171
194
  - Destination is still valid.
172
195
  - All L1/L2 blocking decisions are resolved.
173
196
  - Remaining L3/L4 questions are either resolved or explicitly non-blocking.
package/src/constants.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export const PRODUCT_NAME = "Devflow Pi";
2
- export const PRODUCT_VERSION = "0.1.52";
2
+ export const PRODUCT_VERSION = "0.1.53";
3
3
  export const PROJECT_SCHEMA_VERSION = 1;
4
4
  export const OMP_RUNTIME_VERSION = "17.2.7";
5
5