@guanlin4924/devflow-pi 0.1.52 → 0.1.54

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.54",
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;
@@ -1359,6 +1361,31 @@ export default function devflowCore(pi: ExtensionAPI) {
1359
1361
  return stateRevision === contract.stateRevision && graphRevision === contract.graphRevision;
1360
1362
  }
1361
1363
 
1364
+ // A successful graph mutation is already constrained by the active contract's
1365
+ // allowed kinds and the graph's compare-and-swap revision. Re-freeze the
1366
+ // Core-derived contract at that new revision so the next candidate remains in
1367
+ // the same approved step instead of being rejected as stale.
1368
+ async function rebaseActiveContractAfterGraphWrite(ctx: { cwd: string }, feature: string, graphRevision?: number): Promise<string | undefined> {
1369
+ const previous = activeContract;
1370
+ if (!previous || previous.feature !== feature) return undefined;
1371
+ const written = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/execution_contract.rb", [
1372
+ "create", "--feature", previous.feature, "--step", previous.step, "--write", "--json",
1373
+ ...(previous.stateRevision === undefined ? [] : ["--expected-state-revision", String(previous.stateRevision)]),
1374
+ ...(graphRevision === undefined ? [] : ["--expected-graph-revision", String(graphRevision)]),
1375
+ ]);
1376
+ const payload = parseJsonResult(written);
1377
+ const refreshed = payload?.contract && typeof payload.contract === "object"
1378
+ ? contractFromRecord(payload.contract as Record<string, unknown>)
1379
+ : null;
1380
+ if (!controlPlaneSucceeded(written) || !refreshed) {
1381
+ return written.stderr || written.stdout || "Core could not refresh the execution contract after graph write.";
1382
+ }
1383
+ refreshed.contractPath = typeof payload?.path === "string" ? payload.path : undefined;
1384
+ activeContract = refreshed;
1385
+ contractAgentCalls = 0;
1386
+ return undefined;
1387
+ }
1388
+
1362
1389
  async function restoreActiveContract(cwd: string): Promise<ActiveExecutionContract | undefined> {
1363
1390
  try {
1364
1391
  const state = parseJsonResult(await runRuby(cwd, "workflow-orchestrator/scripts/inspect_state.rb", ["--json"]));
@@ -2398,7 +2425,7 @@ export default function devflowCore(pi: ExtensionAPI) {
2398
2425
  recoveredApprovalState = true;
2399
2426
  }
2400
2427
  }
2401
- if (recoveredApprovalState || controlState.phase === "awaiting-approval" || controlState.phase === "waiting-user") {
2428
+ if (recoveredApprovalState || controlState.phase === "awaiting-approval" || controlState.phase === "waiting-user" || (controlState.phase === "blocked" && controlState.label === "推进校验失败")) {
2402
2429
  if (controlAction === "reject") {
2403
2430
  ctx.ui.notify("已保留当前步骤和产物。请直接说明需要修改的内容;未执行任何推进。", "info");
2404
2431
  return { handled: true };
@@ -2437,7 +2464,7 @@ export default function devflowCore(pi: ExtensionAPI) {
2437
2464
  ]);
2438
2465
  if (!result.ok) {
2439
2466
  setControlState(ctx, { intent: "workflow-execute", phase: "blocked", label: "推进校验失败" });
2440
- ctx.ui.notify(`未推进:${result.stderr || result.stdout}`, "error");
2467
+ ctx.ui.notify(`未推进:${result.stderr || result.stdout}\n请补齐上述校验缺项后重新审批;重复批准不会绕过门禁。可请求重新执行当前步骤以修复产物。`, "error");
2441
2468
  return { handled: true };
2442
2469
  }
2443
2470
  const after = await inspectWorkflowWithRegistryRecovery(ctx.cwd);
@@ -2459,6 +2486,10 @@ export default function devflowCore(pi: ExtensionAPI) {
2459
2486
  return { handled: true };
2460
2487
  }
2461
2488
  }
2489
+ if (controlAction) {
2490
+ ctx.ui.notify("当前没有可推进的审批节点。请先完成或修复当前步骤产物,再请求审批;状态未推进。", "info");
2491
+ return { handled: true };
2492
+ }
2462
2493
  const intent = classifyNaturalLanguageIntent(event.text);
2463
2494
  if (intent === "explain-state") {
2464
2495
  const inspected = await inspectWorkflow(ctx.cwd);
@@ -2826,6 +2857,20 @@ export default function devflowCore(pi: ExtensionAPI) {
2826
2857
  },
2827
2858
  });
2828
2859
 
2860
+ pi.registerTool({
2861
+ name: "devflow_requirement_coverage",
2862
+ label: "Devflow Requirement Coverage",
2863
+ description: "Generate Core source units from a requirement source file, or validate wayfinder coverage before requesting approval. Generation never creates coverage claims.",
2864
+ parameters: z.object({ action: z.enum(["generate", "validate"]), feature: z.string().min(1), source: z.string().optional() }),
2865
+ approval: args => args.action === "generate" ? "write" : "read",
2866
+ async execute(_id, params, _signal, _onUpdate, ctx) {
2867
+ return renderResult(await runRuby(ctx.cwd, "workflow-orchestrator/scripts/requirement_coverage.rb", [
2868
+ "--root", ctx.cwd, "--feature", params.feature,
2869
+ ...(params.action === "generate" ? ["--source", params.source ?? "", "--write"] : []),
2870
+ ]));
2871
+ },
2872
+ });
2873
+
2829
2874
  pi.registerTool({
2830
2875
 
2831
2876
  name: "devflow_requirement_graph",
@@ -2955,10 +3000,17 @@ export default function devflowCore(pi: ExtensionAPI) {
2955
3000
  args.push("--confirmed-interactively");
2956
3001
  }
2957
3002
  }
2958
- return renderResult(
2959
- await runRuby(ctx.cwd, "workflow-orchestrator/scripts/requirement_graph.rb", args),
2960
- { action: params.action, feature: params.feature },
2961
- );
3003
+ const result = await runRuby(ctx.cwd, "workflow-orchestrator/scripts/requirement_graph.rb", args);
3004
+ if (graphWrite && controlPlaneSucceeded(result)) {
3005
+ const revision = parseJsonResult(result)?.revision;
3006
+ const rebaseError = await rebaseActiveContractAfterGraphWrite(
3007
+ ctx,
3008
+ params.feature,
3009
+ typeof revision === "number" ? revision : undefined,
3010
+ );
3011
+ if (rebaseError) ctx.ui.notify(`图谱候选已写入,但执行合同未能刷新;后续写入将保持阻塞:${rebaseError}`, "error");
3012
+ }
3013
+ return renderResult(result, { action: params.action, feature: params.feature });
2962
3014
  },
2963
3015
  });
2964
3016
 
@@ -11,6 +11,7 @@ require_relative "control_io"
11
11
  require_relative "requirement_graph"
12
12
 
13
13
  POLICY = {
14
+ "domain-modeling" => { "checkers" => %w[requirement-graph-check], "agents" => [], "budget" => 0, "allowed_graph_delta" => %w[term object behavior rule boundary] },
14
15
  "requirement-baseline" => { "checkers" => %w[requirement-graph-check business-closure-check slice-independence-check acceptance-coverage-check change-impact-check], "agents" => [], "budget" => 0, "allowed_graph_delta" => %w[requirement rule boundary slice acceptance] },
15
16
  "api-contract" => { "checkers" => %w[requirement-graph-check contract-coverage-check api-test-schema-check change-impact-check], "agents" => [], "budget" => 0, "allowed_graph_delta" => %w[api] },
16
17
  "architecture-feasibility" => { "checkers" => %w[requirement-graph-check change-impact-check], "agents" => [], "budget" => 0, "allowed_graph_delta" => %w[workload_shape workload_constraint decision risk metric read_model] },
@@ -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.54";
3
3
  export const PROJECT_SCHEMA_VERSION = 1;
4
4
  export const OMP_RUNTIME_VERSION = "17.2.7";
5
5